auth dns over atproto

initial commit

this contains the first implementation of the appview, dns, and
verification services, along with some documentation on how to
use it.

seiso.moe a0362181

+8110
+6
.gitignore
··· 1 + # builds 2 + target/ 3 + result 4 + 5 + # testing 6 + data/
+3906
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "ahash" 7 + version = "0.8.12" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 10 + dependencies = [ 11 + "cfg-if", 12 + "once_cell", 13 + "version_check", 14 + "zerocopy", 15 + ] 16 + 17 + [[package]] 18 + name = "aho-corasick" 19 + version = "1.1.4" 20 + source = "registry+https://github.com/rust-lang/crates.io-index" 21 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 22 + dependencies = [ 23 + "memchr", 24 + ] 25 + 26 + [[package]] 27 + name = "allocator-api2" 28 + version = "0.2.21" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 31 + 32 + [[package]] 33 + name = "android_system_properties" 34 + version = "0.1.5" 35 + source = "registry+https://github.com/rust-lang/crates.io-index" 36 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 37 + dependencies = [ 38 + "libc", 39 + ] 40 + 41 + [[package]] 42 + name = "anyhow" 43 + version = "1.0.100" 44 + source = "registry+https://github.com/rust-lang/crates.io-index" 45 + checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 46 + 47 + [[package]] 48 + name = "async-lock" 49 + version = "3.4.2" 50 + source = "registry+https://github.com/rust-lang/crates.io-index" 51 + checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" 52 + dependencies = [ 53 + "event-listener", 54 + "event-listener-strategy", 55 + "pin-project-lite", 56 + ] 57 + 58 + [[package]] 59 + name = "async-trait" 60 + version = "0.1.89" 61 + source = "registry+https://github.com/rust-lang/crates.io-index" 62 + checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 63 + dependencies = [ 64 + "proc-macro2", 65 + "quote", 66 + "syn", 67 + ] 68 + 69 + [[package]] 70 + name = "atoi" 71 + version = "2.0.0" 72 + source = "registry+https://github.com/rust-lang/crates.io-index" 73 + checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 74 + dependencies = [ 75 + "num-traits", 76 + ] 77 + 78 + [[package]] 79 + name = "atomic-waker" 80 + version = "1.1.2" 81 + source = "registry+https://github.com/rust-lang/crates.io-index" 82 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 83 + 84 + [[package]] 85 + name = "atrium-api" 86 + version = "0.25.7" 87 + source = "registry+https://github.com/rust-lang/crates.io-index" 88 + checksum = "1f182d9437cd447ed87eca75540151653e332d6753a2a4749d72c0f15aa1f179" 89 + dependencies = [ 90 + "atrium-common", 91 + "atrium-xrpc", 92 + "chrono", 93 + "http", 94 + "ipld-core", 95 + "langtag", 96 + "regex", 97 + "serde", 98 + "serde_bytes", 99 + "serde_json", 100 + "thiserror 1.0.69", 101 + "tokio", 102 + "trait-variant", 103 + ] 104 + 105 + [[package]] 106 + name = "atrium-common" 107 + version = "0.1.3" 108 + source = "registry+https://github.com/rust-lang/crates.io-index" 109 + checksum = "eff94b4ce3e9ba11d8bda83674e75ccaca281d5251ec3816d03e6bb23583ff4f" 110 + dependencies = [ 111 + "dashmap", 112 + "lru", 113 + "moka", 114 + "thiserror 1.0.69", 115 + "tokio", 116 + "trait-variant", 117 + "web-time", 118 + ] 119 + 120 + [[package]] 121 + name = "atrium-lex" 122 + version = "0.1.0" 123 + source = "git+https://github.com/sugyan/atrium.git?rev=f162f815a04b5ecb0421b390d521c883c41d5f75#f162f815a04b5ecb0421b390d521c883c41d5f75" 124 + dependencies = [ 125 + "serde", 126 + "serde_repr", 127 + "serde_with", 128 + ] 129 + 130 + [[package]] 131 + name = "atrium-xrpc" 132 + version = "0.12.4" 133 + source = "registry+https://github.com/rust-lang/crates.io-index" 134 + checksum = "944b35cc08732d40ddbb3356be9e38d11aed4b4c40c33f5b0f235e0650eff296" 135 + dependencies = [ 136 + "http", 137 + "serde", 138 + "serde_html_form", 139 + "serde_json", 140 + "thiserror 1.0.69", 141 + "trait-variant", 142 + ] 143 + 144 + [[package]] 145 + name = "autocfg" 146 + version = "1.5.0" 147 + source = "registry+https://github.com/rust-lang/crates.io-index" 148 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 149 + 150 + [[package]] 151 + name = "aws-lc-rs" 152 + version = "1.15.4" 153 + source = "registry+https://github.com/rust-lang/crates.io-index" 154 + checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" 155 + dependencies = [ 156 + "aws-lc-sys", 157 + "zeroize", 158 + ] 159 + 160 + [[package]] 161 + name = "aws-lc-sys" 162 + version = "0.37.0" 163 + source = "registry+https://github.com/rust-lang/crates.io-index" 164 + checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" 165 + dependencies = [ 166 + "cc", 167 + "cmake", 168 + "dunce", 169 + "fs_extra", 170 + ] 171 + 172 + [[package]] 173 + name = "axum" 174 + version = "0.8.8" 175 + source = "registry+https://github.com/rust-lang/crates.io-index" 176 + checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" 177 + dependencies = [ 178 + "axum-core", 179 + "bytes", 180 + "form_urlencoded", 181 + "futures-util", 182 + "http", 183 + "http-body", 184 + "http-body-util", 185 + "hyper", 186 + "hyper-util", 187 + "itoa", 188 + "matchit", 189 + "memchr", 190 + "mime", 191 + "percent-encoding", 192 + "pin-project-lite", 193 + "serde_core", 194 + "serde_json", 195 + "serde_path_to_error", 196 + "serde_urlencoded", 197 + "sync_wrapper", 198 + "tokio", 199 + "tower", 200 + "tower-layer", 201 + "tower-service", 202 + "tracing", 203 + ] 204 + 205 + [[package]] 206 + name = "axum-core" 207 + version = "0.5.6" 208 + source = "registry+https://github.com/rust-lang/crates.io-index" 209 + checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" 210 + dependencies = [ 211 + "bytes", 212 + "futures-core", 213 + "http", 214 + "http-body", 215 + "http-body-util", 216 + "mime", 217 + "pin-project-lite", 218 + "sync_wrapper", 219 + "tower-layer", 220 + "tower-service", 221 + "tracing", 222 + ] 223 + 224 + [[package]] 225 + name = "base-x" 226 + version = "0.2.11" 227 + source = "registry+https://github.com/rust-lang/crates.io-index" 228 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 229 + 230 + [[package]] 231 + name = "base256emoji" 232 + version = "1.0.2" 233 + source = "registry+https://github.com/rust-lang/crates.io-index" 234 + checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" 235 + dependencies = [ 236 + "const-str", 237 + "match-lookup", 238 + ] 239 + 240 + [[package]] 241 + name = "base64" 242 + version = "0.13.1" 243 + source = "registry+https://github.com/rust-lang/crates.io-index" 244 + checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 245 + 246 + [[package]] 247 + name = "base64" 248 + version = "0.22.1" 249 + source = "registry+https://github.com/rust-lang/crates.io-index" 250 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 251 + 252 + [[package]] 253 + name = "base64ct" 254 + version = "1.8.3" 255 + source = "registry+https://github.com/rust-lang/crates.io-index" 256 + checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 257 + 258 + [[package]] 259 + name = "bitflags" 260 + version = "2.10.0" 261 + source = "registry+https://github.com/rust-lang/crates.io-index" 262 + checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 263 + dependencies = [ 264 + "serde_core", 265 + ] 266 + 267 + [[package]] 268 + name = "block-buffer" 269 + version = "0.10.4" 270 + source = "registry+https://github.com/rust-lang/crates.io-index" 271 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 272 + dependencies = [ 273 + "generic-array", 274 + ] 275 + 276 + [[package]] 277 + name = "bumpalo" 278 + version = "3.19.1" 279 + source = "registry+https://github.com/rust-lang/crates.io-index" 280 + checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" 281 + 282 + [[package]] 283 + name = "byteorder" 284 + version = "1.5.0" 285 + source = "registry+https://github.com/rust-lang/crates.io-index" 286 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 287 + 288 + [[package]] 289 + name = "bytes" 290 + version = "1.11.0" 291 + source = "registry+https://github.com/rust-lang/crates.io-index" 292 + checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 293 + 294 + [[package]] 295 + name = "cc" 296 + version = "1.2.55" 297 + source = "registry+https://github.com/rust-lang/crates.io-index" 298 + checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" 299 + dependencies = [ 300 + "find-msvc-tools", 301 + "jobserver", 302 + "libc", 303 + "shlex", 304 + ] 305 + 306 + [[package]] 307 + name = "cfg-if" 308 + version = "1.0.4" 309 + source = "registry+https://github.com/rust-lang/crates.io-index" 310 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 311 + 312 + [[package]] 313 + name = "chrono" 314 + version = "0.4.43" 315 + source = "registry+https://github.com/rust-lang/crates.io-index" 316 + checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" 317 + dependencies = [ 318 + "iana-time-zone", 319 + "js-sys", 320 + "num-traits", 321 + "serde", 322 + "wasm-bindgen", 323 + "windows-link", 324 + ] 325 + 326 + [[package]] 327 + name = "cid" 328 + version = "0.11.1" 329 + source = "registry+https://github.com/rust-lang/crates.io-index" 330 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 331 + dependencies = [ 332 + "core2", 333 + "multibase", 334 + "multihash", 335 + "serde", 336 + "serde_bytes", 337 + "unsigned-varint", 338 + ] 339 + 340 + [[package]] 341 + name = "cmake" 342 + version = "0.1.57" 343 + source = "registry+https://github.com/rust-lang/crates.io-index" 344 + checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" 345 + dependencies = [ 346 + "cc", 347 + ] 348 + 349 + [[package]] 350 + name = "concurrent-queue" 351 + version = "2.5.0" 352 + source = "registry+https://github.com/rust-lang/crates.io-index" 353 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 354 + dependencies = [ 355 + "crossbeam-utils", 356 + ] 357 + 358 + [[package]] 359 + name = "const-oid" 360 + version = "0.9.6" 361 + source = "registry+https://github.com/rust-lang/crates.io-index" 362 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 363 + 364 + [[package]] 365 + name = "const-str" 366 + version = "0.4.3" 367 + source = "registry+https://github.com/rust-lang/crates.io-index" 368 + checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 369 + 370 + [[package]] 371 + name = "core-foundation" 372 + version = "0.9.4" 373 + source = "registry+https://github.com/rust-lang/crates.io-index" 374 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 375 + dependencies = [ 376 + "core-foundation-sys", 377 + "libc", 378 + ] 379 + 380 + [[package]] 381 + name = "core-foundation" 382 + version = "0.10.1" 383 + source = "registry+https://github.com/rust-lang/crates.io-index" 384 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 385 + dependencies = [ 386 + "core-foundation-sys", 387 + "libc", 388 + ] 389 + 390 + [[package]] 391 + name = "core-foundation-sys" 392 + version = "0.8.7" 393 + source = "registry+https://github.com/rust-lang/crates.io-index" 394 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 395 + 396 + [[package]] 397 + name = "core2" 398 + version = "0.4.0" 399 + source = "registry+https://github.com/rust-lang/crates.io-index" 400 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 401 + dependencies = [ 402 + "memchr", 403 + ] 404 + 405 + [[package]] 406 + name = "cpufeatures" 407 + version = "0.2.17" 408 + source = "registry+https://github.com/rust-lang/crates.io-index" 409 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 410 + dependencies = [ 411 + "libc", 412 + ] 413 + 414 + [[package]] 415 + name = "crc" 416 + version = "3.4.0" 417 + source = "registry+https://github.com/rust-lang/crates.io-index" 418 + checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" 419 + dependencies = [ 420 + "crc-catalog", 421 + ] 422 + 423 + [[package]] 424 + name = "crc-catalog" 425 + version = "2.4.0" 426 + source = "registry+https://github.com/rust-lang/crates.io-index" 427 + checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 428 + 429 + [[package]] 430 + name = "critical-section" 431 + version = "1.2.0" 432 + source = "registry+https://github.com/rust-lang/crates.io-index" 433 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 434 + 435 + [[package]] 436 + name = "crossbeam-channel" 437 + version = "0.5.15" 438 + source = "registry+https://github.com/rust-lang/crates.io-index" 439 + checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 440 + dependencies = [ 441 + "crossbeam-utils", 442 + ] 443 + 444 + [[package]] 445 + name = "crossbeam-epoch" 446 + version = "0.9.18" 447 + source = "registry+https://github.com/rust-lang/crates.io-index" 448 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 449 + dependencies = [ 450 + "crossbeam-utils", 451 + ] 452 + 453 + [[package]] 454 + name = "crossbeam-queue" 455 + version = "0.3.12" 456 + source = "registry+https://github.com/rust-lang/crates.io-index" 457 + checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 458 + dependencies = [ 459 + "crossbeam-utils", 460 + ] 461 + 462 + [[package]] 463 + name = "crossbeam-utils" 464 + version = "0.8.21" 465 + source = "registry+https://github.com/rust-lang/crates.io-index" 466 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 467 + 468 + [[package]] 469 + name = "crypto-common" 470 + version = "0.1.7" 471 + source = "registry+https://github.com/rust-lang/crates.io-index" 472 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 473 + dependencies = [ 474 + "generic-array", 475 + "typenum", 476 + ] 477 + 478 + [[package]] 479 + name = "darling" 480 + version = "0.20.11" 481 + source = "registry+https://github.com/rust-lang/crates.io-index" 482 + checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 483 + dependencies = [ 484 + "darling_core", 485 + "darling_macro", 486 + ] 487 + 488 + [[package]] 489 + name = "darling_core" 490 + version = "0.20.11" 491 + source = "registry+https://github.com/rust-lang/crates.io-index" 492 + checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 493 + dependencies = [ 494 + "fnv", 495 + "ident_case", 496 + "proc-macro2", 497 + "quote", 498 + "strsim", 499 + "syn", 500 + ] 501 + 502 + [[package]] 503 + name = "darling_macro" 504 + version = "0.20.11" 505 + source = "registry+https://github.com/rust-lang/crates.io-index" 506 + checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 507 + dependencies = [ 508 + "darling_core", 509 + "quote", 510 + "syn", 511 + ] 512 + 513 + [[package]] 514 + name = "dashmap" 515 + version = "6.1.0" 516 + source = "registry+https://github.com/rust-lang/crates.io-index" 517 + checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 518 + dependencies = [ 519 + "cfg-if", 520 + "crossbeam-utils", 521 + "hashbrown 0.14.5", 522 + "lock_api", 523 + "once_cell", 524 + "parking_lot_core", 525 + ] 526 + 527 + [[package]] 528 + name = "data-encoding" 529 + version = "2.10.0" 530 + source = "registry+https://github.com/rust-lang/crates.io-index" 531 + checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" 532 + 533 + [[package]] 534 + name = "data-encoding-macro" 535 + version = "0.1.19" 536 + source = "registry+https://github.com/rust-lang/crates.io-index" 537 + checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" 538 + dependencies = [ 539 + "data-encoding", 540 + "data-encoding-macro-internal", 541 + ] 542 + 543 + [[package]] 544 + name = "data-encoding-macro-internal" 545 + version = "0.1.17" 546 + source = "registry+https://github.com/rust-lang/crates.io-index" 547 + checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" 548 + dependencies = [ 549 + "data-encoding", 550 + "syn", 551 + ] 552 + 553 + [[package]] 554 + name = "der" 555 + version = "0.7.10" 556 + source = "registry+https://github.com/rust-lang/crates.io-index" 557 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 558 + dependencies = [ 559 + "const-oid", 560 + "pem-rfc7468", 561 + "zeroize", 562 + ] 563 + 564 + [[package]] 565 + name = "deranged" 566 + version = "0.5.5" 567 + source = "registry+https://github.com/rust-lang/crates.io-index" 568 + checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 569 + dependencies = [ 570 + "powerfmt", 571 + "serde_core", 572 + ] 573 + 574 + [[package]] 575 + name = "digest" 576 + version = "0.10.7" 577 + source = "registry+https://github.com/rust-lang/crates.io-index" 578 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 579 + dependencies = [ 580 + "block-buffer", 581 + "const-oid", 582 + "crypto-common", 583 + "subtle", 584 + ] 585 + 586 + [[package]] 587 + name = "displaydoc" 588 + version = "0.2.5" 589 + source = "registry+https://github.com/rust-lang/crates.io-index" 590 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 591 + dependencies = [ 592 + "proc-macro2", 593 + "quote", 594 + "syn", 595 + ] 596 + 597 + [[package]] 598 + name = "dotenvy" 599 + version = "0.15.7" 600 + source = "registry+https://github.com/rust-lang/crates.io-index" 601 + checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 602 + 603 + [[package]] 604 + name = "dunce" 605 + version = "1.0.5" 606 + source = "registry+https://github.com/rust-lang/crates.io-index" 607 + checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 608 + 609 + [[package]] 610 + name = "either" 611 + version = "1.15.0" 612 + source = "registry+https://github.com/rust-lang/crates.io-index" 613 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 614 + dependencies = [ 615 + "serde", 616 + ] 617 + 618 + [[package]] 619 + name = "enum-as-inner" 620 + version = "0.6.1" 621 + source = "registry+https://github.com/rust-lang/crates.io-index" 622 + checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 623 + dependencies = [ 624 + "heck 0.5.0", 625 + "proc-macro2", 626 + "quote", 627 + "syn", 628 + ] 629 + 630 + [[package]] 631 + name = "equivalent" 632 + version = "1.0.2" 633 + source = "registry+https://github.com/rust-lang/crates.io-index" 634 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 635 + 636 + [[package]] 637 + name = "errno" 638 + version = "0.3.14" 639 + source = "registry+https://github.com/rust-lang/crates.io-index" 640 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 641 + dependencies = [ 642 + "libc", 643 + "windows-sys 0.61.2", 644 + ] 645 + 646 + [[package]] 647 + name = "esquema-codegen" 648 + version = "0.1.0" 649 + source = "git+https://github.com/fatfingers23/esquema.git?branch=main#9ef00b9d631b746bd6396fb46ba255eb9360e43f" 650 + dependencies = [ 651 + "atrium-lex", 652 + "heck 0.4.1", 653 + "itertools", 654 + "prettyplease", 655 + "proc-macro2", 656 + "quote", 657 + "serde", 658 + "serde_json", 659 + "syn", 660 + ] 661 + 662 + [[package]] 663 + name = "etcetera" 664 + version = "0.8.0" 665 + source = "registry+https://github.com/rust-lang/crates.io-index" 666 + checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 667 + dependencies = [ 668 + "cfg-if", 669 + "home", 670 + "windows-sys 0.48.0", 671 + ] 672 + 673 + [[package]] 674 + name = "event-listener" 675 + version = "5.4.1" 676 + source = "registry+https://github.com/rust-lang/crates.io-index" 677 + checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 678 + dependencies = [ 679 + "concurrent-queue", 680 + "parking", 681 + "pin-project-lite", 682 + ] 683 + 684 + [[package]] 685 + name = "event-listener-strategy" 686 + version = "0.5.4" 687 + source = "registry+https://github.com/rust-lang/crates.io-index" 688 + checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" 689 + dependencies = [ 690 + "event-listener", 691 + "pin-project-lite", 692 + ] 693 + 694 + [[package]] 695 + name = "fastrand" 696 + version = "2.3.0" 697 + source = "registry+https://github.com/rust-lang/crates.io-index" 698 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 699 + 700 + [[package]] 701 + name = "find-msvc-tools" 702 + version = "0.1.9" 703 + source = "registry+https://github.com/rust-lang/crates.io-index" 704 + checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" 705 + 706 + [[package]] 707 + name = "flume" 708 + version = "0.11.1" 709 + source = "registry+https://github.com/rust-lang/crates.io-index" 710 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 711 + dependencies = [ 712 + "futures-core", 713 + "futures-sink", 714 + "spin", 715 + ] 716 + 717 + [[package]] 718 + name = "fnv" 719 + version = "1.0.7" 720 + source = "registry+https://github.com/rust-lang/crates.io-index" 721 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 722 + 723 + [[package]] 724 + name = "foldhash" 725 + version = "0.1.5" 726 + source = "registry+https://github.com/rust-lang/crates.io-index" 727 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 728 + 729 + [[package]] 730 + name = "foldhash" 731 + version = "0.2.0" 732 + source = "registry+https://github.com/rust-lang/crates.io-index" 733 + checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 734 + 735 + [[package]] 736 + name = "foreign-types" 737 + version = "0.3.2" 738 + source = "registry+https://github.com/rust-lang/crates.io-index" 739 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 740 + dependencies = [ 741 + "foreign-types-shared", 742 + ] 743 + 744 + [[package]] 745 + name = "foreign-types-shared" 746 + version = "0.1.1" 747 + source = "registry+https://github.com/rust-lang/crates.io-index" 748 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 749 + 750 + [[package]] 751 + name = "form_urlencoded" 752 + version = "1.2.2" 753 + source = "registry+https://github.com/rust-lang/crates.io-index" 754 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 755 + dependencies = [ 756 + "percent-encoding", 757 + ] 758 + 759 + [[package]] 760 + name = "fs_extra" 761 + version = "1.3.0" 762 + source = "registry+https://github.com/rust-lang/crates.io-index" 763 + checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 764 + 765 + [[package]] 766 + name = "futures-channel" 767 + version = "0.3.31" 768 + source = "registry+https://github.com/rust-lang/crates.io-index" 769 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 770 + dependencies = [ 771 + "futures-core", 772 + "futures-sink", 773 + ] 774 + 775 + [[package]] 776 + name = "futures-core" 777 + version = "0.3.31" 778 + source = "registry+https://github.com/rust-lang/crates.io-index" 779 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 780 + 781 + [[package]] 782 + name = "futures-executor" 783 + version = "0.3.31" 784 + source = "registry+https://github.com/rust-lang/crates.io-index" 785 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 786 + dependencies = [ 787 + "futures-core", 788 + "futures-task", 789 + "futures-util", 790 + ] 791 + 792 + [[package]] 793 + name = "futures-intrusive" 794 + version = "0.5.0" 795 + source = "registry+https://github.com/rust-lang/crates.io-index" 796 + checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 797 + dependencies = [ 798 + "futures-core", 799 + "lock_api", 800 + "parking_lot", 801 + ] 802 + 803 + [[package]] 804 + name = "futures-io" 805 + version = "0.3.31" 806 + source = "registry+https://github.com/rust-lang/crates.io-index" 807 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 808 + 809 + [[package]] 810 + name = "futures-macro" 811 + version = "0.3.31" 812 + source = "registry+https://github.com/rust-lang/crates.io-index" 813 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 814 + dependencies = [ 815 + "proc-macro2", 816 + "quote", 817 + "syn", 818 + ] 819 + 820 + [[package]] 821 + name = "futures-sink" 822 + version = "0.3.31" 823 + source = "registry+https://github.com/rust-lang/crates.io-index" 824 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 825 + 826 + [[package]] 827 + name = "futures-task" 828 + version = "0.3.31" 829 + source = "registry+https://github.com/rust-lang/crates.io-index" 830 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 831 + 832 + [[package]] 833 + name = "futures-util" 834 + version = "0.3.31" 835 + source = "registry+https://github.com/rust-lang/crates.io-index" 836 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 837 + dependencies = [ 838 + "futures-core", 839 + "futures-io", 840 + "futures-macro", 841 + "futures-sink", 842 + "futures-task", 843 + "memchr", 844 + "pin-project-lite", 845 + "pin-utils", 846 + "slab", 847 + ] 848 + 849 + [[package]] 850 + name = "generic-array" 851 + version = "0.14.7" 852 + source = "registry+https://github.com/rust-lang/crates.io-index" 853 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 854 + dependencies = [ 855 + "typenum", 856 + "version_check", 857 + ] 858 + 859 + [[package]] 860 + name = "getrandom" 861 + version = "0.2.17" 862 + source = "registry+https://github.com/rust-lang/crates.io-index" 863 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 864 + dependencies = [ 865 + "cfg-if", 866 + "libc", 867 + "wasi", 868 + ] 869 + 870 + [[package]] 871 + name = "getrandom" 872 + version = "0.3.4" 873 + source = "registry+https://github.com/rust-lang/crates.io-index" 874 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 875 + dependencies = [ 876 + "cfg-if", 877 + "libc", 878 + "r-efi", 879 + "wasip2", 880 + ] 881 + 882 + [[package]] 883 + name = "h2" 884 + version = "0.4.13" 885 + source = "registry+https://github.com/rust-lang/crates.io-index" 886 + checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" 887 + dependencies = [ 888 + "atomic-waker", 889 + "bytes", 890 + "fnv", 891 + "futures-core", 892 + "futures-sink", 893 + "http", 894 + "indexmap 2.13.0", 895 + "slab", 896 + "tokio", 897 + "tokio-util", 898 + "tracing", 899 + ] 900 + 901 + [[package]] 902 + name = "hashbrown" 903 + version = "0.12.3" 904 + source = "registry+https://github.com/rust-lang/crates.io-index" 905 + checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 906 + 907 + [[package]] 908 + name = "hashbrown" 909 + version = "0.14.5" 910 + source = "registry+https://github.com/rust-lang/crates.io-index" 911 + checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 912 + 913 + [[package]] 914 + name = "hashbrown" 915 + version = "0.15.5" 916 + source = "registry+https://github.com/rust-lang/crates.io-index" 917 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 918 + dependencies = [ 919 + "allocator-api2", 920 + "equivalent", 921 + "foldhash 0.1.5", 922 + ] 923 + 924 + [[package]] 925 + name = "hashbrown" 926 + version = "0.16.1" 927 + source = "registry+https://github.com/rust-lang/crates.io-index" 928 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 929 + dependencies = [ 930 + "foldhash 0.2.0", 931 + ] 932 + 933 + [[package]] 934 + name = "hashlink" 935 + version = "0.10.0" 936 + source = "registry+https://github.com/rust-lang/crates.io-index" 937 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 938 + dependencies = [ 939 + "hashbrown 0.15.5", 940 + ] 941 + 942 + [[package]] 943 + name = "heck" 944 + version = "0.4.1" 945 + source = "registry+https://github.com/rust-lang/crates.io-index" 946 + checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 947 + 948 + [[package]] 949 + name = "heck" 950 + version = "0.5.0" 951 + source = "registry+https://github.com/rust-lang/crates.io-index" 952 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 953 + 954 + [[package]] 955 + name = "hex" 956 + version = "0.4.3" 957 + source = "registry+https://github.com/rust-lang/crates.io-index" 958 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 959 + 960 + [[package]] 961 + name = "hickory-proto" 962 + version = "0.25.2" 963 + source = "registry+https://github.com/rust-lang/crates.io-index" 964 + checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" 965 + dependencies = [ 966 + "async-trait", 967 + "cfg-if", 968 + "data-encoding", 969 + "enum-as-inner", 970 + "futures-channel", 971 + "futures-io", 972 + "futures-util", 973 + "idna", 974 + "ipnet", 975 + "once_cell", 976 + "rand 0.9.2", 977 + "ring", 978 + "serde", 979 + "thiserror 2.0.18", 980 + "tinyvec", 981 + "tokio", 982 + "tracing", 983 + "url", 984 + ] 985 + 986 + [[package]] 987 + name = "hickory-resolver" 988 + version = "0.25.2" 989 + source = "registry+https://github.com/rust-lang/crates.io-index" 990 + checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" 991 + dependencies = [ 992 + "cfg-if", 993 + "futures-util", 994 + "hickory-proto", 995 + "ipconfig", 996 + "moka", 997 + "once_cell", 998 + "parking_lot", 999 + "rand 0.9.2", 1000 + "resolv-conf", 1001 + "smallvec", 1002 + "thiserror 2.0.18", 1003 + "tokio", 1004 + "tracing", 1005 + ] 1006 + 1007 + [[package]] 1008 + name = "hickory-server" 1009 + version = "0.25.2" 1010 + source = "registry+https://github.com/rust-lang/crates.io-index" 1011 + checksum = "d53e5fe811b941c74ee46b8818228bfd2bc2688ba276a0eaeb0f2c95ea3b2585" 1012 + dependencies = [ 1013 + "async-trait", 1014 + "bytes", 1015 + "cfg-if", 1016 + "data-encoding", 1017 + "enum-as-inner", 1018 + "futures-util", 1019 + "hickory-proto", 1020 + "ipnet", 1021 + "prefix-trie", 1022 + "serde", 1023 + "thiserror 2.0.18", 1024 + "time", 1025 + "tokio", 1026 + "tokio-util", 1027 + "tracing", 1028 + ] 1029 + 1030 + [[package]] 1031 + name = "hkdf" 1032 + version = "0.12.4" 1033 + source = "registry+https://github.com/rust-lang/crates.io-index" 1034 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 1035 + dependencies = [ 1036 + "hmac", 1037 + ] 1038 + 1039 + [[package]] 1040 + name = "hmac" 1041 + version = "0.12.1" 1042 + source = "registry+https://github.com/rust-lang/crates.io-index" 1043 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 1044 + dependencies = [ 1045 + "digest", 1046 + ] 1047 + 1048 + [[package]] 1049 + name = "home" 1050 + version = "0.5.12" 1051 + source = "registry+https://github.com/rust-lang/crates.io-index" 1052 + checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" 1053 + dependencies = [ 1054 + "windows-sys 0.61.2", 1055 + ] 1056 + 1057 + [[package]] 1058 + name = "http" 1059 + version = "1.4.0" 1060 + source = "registry+https://github.com/rust-lang/crates.io-index" 1061 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 1062 + dependencies = [ 1063 + "bytes", 1064 + "itoa", 1065 + ] 1066 + 1067 + [[package]] 1068 + name = "http-body" 1069 + version = "1.0.1" 1070 + source = "registry+https://github.com/rust-lang/crates.io-index" 1071 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1072 + dependencies = [ 1073 + "bytes", 1074 + "http", 1075 + ] 1076 + 1077 + [[package]] 1078 + name = "http-body-util" 1079 + version = "0.1.3" 1080 + source = "registry+https://github.com/rust-lang/crates.io-index" 1081 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 1082 + dependencies = [ 1083 + "bytes", 1084 + "futures-core", 1085 + "http", 1086 + "http-body", 1087 + "pin-project-lite", 1088 + ] 1089 + 1090 + [[package]] 1091 + name = "httparse" 1092 + version = "1.10.1" 1093 + source = "registry+https://github.com/rust-lang/crates.io-index" 1094 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 1095 + 1096 + [[package]] 1097 + name = "httpdate" 1098 + version = "1.0.3" 1099 + source = "registry+https://github.com/rust-lang/crates.io-index" 1100 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1101 + 1102 + [[package]] 1103 + name = "hyper" 1104 + version = "1.8.1" 1105 + source = "registry+https://github.com/rust-lang/crates.io-index" 1106 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 1107 + dependencies = [ 1108 + "atomic-waker", 1109 + "bytes", 1110 + "futures-channel", 1111 + "futures-core", 1112 + "h2", 1113 + "http", 1114 + "http-body", 1115 + "httparse", 1116 + "httpdate", 1117 + "itoa", 1118 + "pin-project-lite", 1119 + "pin-utils", 1120 + "smallvec", 1121 + "tokio", 1122 + "want", 1123 + ] 1124 + 1125 + [[package]] 1126 + name = "hyper-rustls" 1127 + version = "0.27.7" 1128 + source = "registry+https://github.com/rust-lang/crates.io-index" 1129 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1130 + dependencies = [ 1131 + "http", 1132 + "hyper", 1133 + "hyper-util", 1134 + "rustls", 1135 + "rustls-native-certs", 1136 + "rustls-pki-types", 1137 + "tokio", 1138 + "tokio-rustls", 1139 + "tower-service", 1140 + ] 1141 + 1142 + [[package]] 1143 + name = "hyper-util" 1144 + version = "0.1.19" 1145 + source = "registry+https://github.com/rust-lang/crates.io-index" 1146 + checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" 1147 + dependencies = [ 1148 + "base64 0.22.1", 1149 + "bytes", 1150 + "futures-channel", 1151 + "futures-core", 1152 + "futures-util", 1153 + "http", 1154 + "http-body", 1155 + "hyper", 1156 + "ipnet", 1157 + "libc", 1158 + "percent-encoding", 1159 + "pin-project-lite", 1160 + "socket2 0.6.2", 1161 + "tokio", 1162 + "tower-service", 1163 + "tracing", 1164 + ] 1165 + 1166 + [[package]] 1167 + name = "iana-time-zone" 1168 + version = "0.1.65" 1169 + source = "registry+https://github.com/rust-lang/crates.io-index" 1170 + checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" 1171 + dependencies = [ 1172 + "android_system_properties", 1173 + "core-foundation-sys", 1174 + "iana-time-zone-haiku", 1175 + "js-sys", 1176 + "log", 1177 + "wasm-bindgen", 1178 + "windows-core", 1179 + ] 1180 + 1181 + [[package]] 1182 + name = "iana-time-zone-haiku" 1183 + version = "0.1.2" 1184 + source = "registry+https://github.com/rust-lang/crates.io-index" 1185 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1186 + dependencies = [ 1187 + "cc", 1188 + ] 1189 + 1190 + [[package]] 1191 + name = "icu_collections" 1192 + version = "2.1.1" 1193 + source = "registry+https://github.com/rust-lang/crates.io-index" 1194 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 1195 + dependencies = [ 1196 + "displaydoc", 1197 + "potential_utf", 1198 + "yoke", 1199 + "zerofrom", 1200 + "zerovec", 1201 + ] 1202 + 1203 + [[package]] 1204 + name = "icu_locale_core" 1205 + version = "2.1.1" 1206 + source = "registry+https://github.com/rust-lang/crates.io-index" 1207 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 1208 + dependencies = [ 1209 + "displaydoc", 1210 + "litemap", 1211 + "tinystr", 1212 + "writeable", 1213 + "zerovec", 1214 + ] 1215 + 1216 + [[package]] 1217 + name = "icu_normalizer" 1218 + version = "2.1.1" 1219 + source = "registry+https://github.com/rust-lang/crates.io-index" 1220 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 1221 + dependencies = [ 1222 + "icu_collections", 1223 + "icu_normalizer_data", 1224 + "icu_properties", 1225 + "icu_provider", 1226 + "smallvec", 1227 + "zerovec", 1228 + ] 1229 + 1230 + [[package]] 1231 + name = "icu_normalizer_data" 1232 + version = "2.1.1" 1233 + source = "registry+https://github.com/rust-lang/crates.io-index" 1234 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 1235 + 1236 + [[package]] 1237 + name = "icu_properties" 1238 + version = "2.1.2" 1239 + source = "registry+https://github.com/rust-lang/crates.io-index" 1240 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 1241 + dependencies = [ 1242 + "icu_collections", 1243 + "icu_locale_core", 1244 + "icu_properties_data", 1245 + "icu_provider", 1246 + "zerotrie", 1247 + "zerovec", 1248 + ] 1249 + 1250 + [[package]] 1251 + name = "icu_properties_data" 1252 + version = "2.1.2" 1253 + source = "registry+https://github.com/rust-lang/crates.io-index" 1254 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 1255 + 1256 + [[package]] 1257 + name = "icu_provider" 1258 + version = "2.1.1" 1259 + source = "registry+https://github.com/rust-lang/crates.io-index" 1260 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 1261 + dependencies = [ 1262 + "displaydoc", 1263 + "icu_locale_core", 1264 + "writeable", 1265 + "yoke", 1266 + "zerofrom", 1267 + "zerotrie", 1268 + "zerovec", 1269 + ] 1270 + 1271 + [[package]] 1272 + name = "ident_case" 1273 + version = "1.0.1" 1274 + source = "registry+https://github.com/rust-lang/crates.io-index" 1275 + checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 1276 + 1277 + [[package]] 1278 + name = "idna" 1279 + version = "1.1.0" 1280 + source = "registry+https://github.com/rust-lang/crates.io-index" 1281 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 1282 + dependencies = [ 1283 + "idna_adapter", 1284 + "smallvec", 1285 + "utf8_iter", 1286 + ] 1287 + 1288 + [[package]] 1289 + name = "idna_adapter" 1290 + version = "1.2.1" 1291 + source = "registry+https://github.com/rust-lang/crates.io-index" 1292 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1293 + dependencies = [ 1294 + "icu_normalizer", 1295 + "icu_properties", 1296 + ] 1297 + 1298 + [[package]] 1299 + name = "indexmap" 1300 + version = "1.9.3" 1301 + source = "registry+https://github.com/rust-lang/crates.io-index" 1302 + checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 1303 + dependencies = [ 1304 + "autocfg", 1305 + "hashbrown 0.12.3", 1306 + "serde", 1307 + ] 1308 + 1309 + [[package]] 1310 + name = "indexmap" 1311 + version = "2.13.0" 1312 + source = "registry+https://github.com/rust-lang/crates.io-index" 1313 + checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" 1314 + dependencies = [ 1315 + "equivalent", 1316 + "hashbrown 0.16.1", 1317 + ] 1318 + 1319 + [[package]] 1320 + name = "ipconfig" 1321 + version = "0.3.2" 1322 + source = "registry+https://github.com/rust-lang/crates.io-index" 1323 + checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" 1324 + dependencies = [ 1325 + "socket2 0.5.10", 1326 + "widestring", 1327 + "windows-sys 0.48.0", 1328 + "winreg", 1329 + ] 1330 + 1331 + [[package]] 1332 + name = "ipld-core" 1333 + version = "0.4.2" 1334 + source = "registry+https://github.com/rust-lang/crates.io-index" 1335 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 1336 + dependencies = [ 1337 + "cid", 1338 + "serde", 1339 + "serde_bytes", 1340 + ] 1341 + 1342 + [[package]] 1343 + name = "ipnet" 1344 + version = "2.11.0" 1345 + source = "registry+https://github.com/rust-lang/crates.io-index" 1346 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1347 + dependencies = [ 1348 + "serde", 1349 + ] 1350 + 1351 + [[package]] 1352 + name = "iri-string" 1353 + version = "0.7.10" 1354 + source = "registry+https://github.com/rust-lang/crates.io-index" 1355 + checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" 1356 + dependencies = [ 1357 + "memchr", 1358 + "serde", 1359 + ] 1360 + 1361 + [[package]] 1362 + name = "itertools" 1363 + version = "0.10.5" 1364 + source = "registry+https://github.com/rust-lang/crates.io-index" 1365 + checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 1366 + dependencies = [ 1367 + "either", 1368 + ] 1369 + 1370 + [[package]] 1371 + name = "itoa" 1372 + version = "1.0.17" 1373 + source = "registry+https://github.com/rust-lang/crates.io-index" 1374 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 1375 + 1376 + [[package]] 1377 + name = "jobserver" 1378 + version = "0.1.34" 1379 + source = "registry+https://github.com/rust-lang/crates.io-index" 1380 + checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 1381 + dependencies = [ 1382 + "getrandom 0.3.4", 1383 + "libc", 1384 + ] 1385 + 1386 + [[package]] 1387 + name = "js-sys" 1388 + version = "0.3.85" 1389 + source = "registry+https://github.com/rust-lang/crates.io-index" 1390 + checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" 1391 + dependencies = [ 1392 + "once_cell", 1393 + "wasm-bindgen", 1394 + ] 1395 + 1396 + [[package]] 1397 + name = "langtag" 1398 + version = "0.3.4" 1399 + source = "registry+https://github.com/rust-lang/crates.io-index" 1400 + checksum = "ed60c85f254d6ae8450cec15eedd921efbc4d1bdf6fcf6202b9a58b403f6f805" 1401 + dependencies = [ 1402 + "serde", 1403 + ] 1404 + 1405 + [[package]] 1406 + name = "lazy_static" 1407 + version = "1.5.0" 1408 + source = "registry+https://github.com/rust-lang/crates.io-index" 1409 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1410 + dependencies = [ 1411 + "spin", 1412 + ] 1413 + 1414 + [[package]] 1415 + name = "libc" 1416 + version = "0.2.180" 1417 + source = "registry+https://github.com/rust-lang/crates.io-index" 1418 + checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" 1419 + 1420 + [[package]] 1421 + name = "libm" 1422 + version = "0.2.16" 1423 + source = "registry+https://github.com/rust-lang/crates.io-index" 1424 + checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" 1425 + 1426 + [[package]] 1427 + name = "libredox" 1428 + version = "0.1.12" 1429 + source = "registry+https://github.com/rust-lang/crates.io-index" 1430 + checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" 1431 + dependencies = [ 1432 + "bitflags", 1433 + "libc", 1434 + "redox_syscall 0.7.0", 1435 + ] 1436 + 1437 + [[package]] 1438 + name = "libsqlite3-sys" 1439 + version = "0.30.1" 1440 + source = "registry+https://github.com/rust-lang/crates.io-index" 1441 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 1442 + dependencies = [ 1443 + "cc", 1444 + "pkg-config", 1445 + "vcpkg", 1446 + ] 1447 + 1448 + [[package]] 1449 + name = "linux-raw-sys" 1450 + version = "0.11.0" 1451 + source = "registry+https://github.com/rust-lang/crates.io-index" 1452 + checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 1453 + 1454 + [[package]] 1455 + name = "litemap" 1456 + version = "0.8.1" 1457 + source = "registry+https://github.com/rust-lang/crates.io-index" 1458 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 1459 + 1460 + [[package]] 1461 + name = "lock_api" 1462 + version = "0.4.14" 1463 + source = "registry+https://github.com/rust-lang/crates.io-index" 1464 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 1465 + dependencies = [ 1466 + "scopeguard", 1467 + ] 1468 + 1469 + [[package]] 1470 + name = "log" 1471 + version = "0.4.29" 1472 + source = "registry+https://github.com/rust-lang/crates.io-index" 1473 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 1474 + 1475 + [[package]] 1476 + name = "lru" 1477 + version = "0.12.5" 1478 + source = "registry+https://github.com/rust-lang/crates.io-index" 1479 + checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 1480 + dependencies = [ 1481 + "hashbrown 0.15.5", 1482 + ] 1483 + 1484 + [[package]] 1485 + name = "match-lookup" 1486 + version = "0.1.2" 1487 + source = "registry+https://github.com/rust-lang/crates.io-index" 1488 + checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" 1489 + dependencies = [ 1490 + "proc-macro2", 1491 + "quote", 1492 + "syn", 1493 + ] 1494 + 1495 + [[package]] 1496 + name = "matchers" 1497 + version = "0.2.0" 1498 + source = "registry+https://github.com/rust-lang/crates.io-index" 1499 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 1500 + dependencies = [ 1501 + "regex-automata", 1502 + ] 1503 + 1504 + [[package]] 1505 + name = "matchit" 1506 + version = "0.8.4" 1507 + source = "registry+https://github.com/rust-lang/crates.io-index" 1508 + checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 1509 + 1510 + [[package]] 1511 + name = "md-5" 1512 + version = "0.10.6" 1513 + source = "registry+https://github.com/rust-lang/crates.io-index" 1514 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 1515 + dependencies = [ 1516 + "cfg-if", 1517 + "digest", 1518 + ] 1519 + 1520 + [[package]] 1521 + name = "memchr" 1522 + version = "2.7.6" 1523 + source = "registry+https://github.com/rust-lang/crates.io-index" 1524 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 1525 + 1526 + [[package]] 1527 + name = "metrics" 1528 + version = "0.24.3" 1529 + source = "registry+https://github.com/rust-lang/crates.io-index" 1530 + checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" 1531 + dependencies = [ 1532 + "ahash", 1533 + "portable-atomic", 1534 + ] 1535 + 1536 + [[package]] 1537 + name = "metrics-exporter-prometheus" 1538 + version = "0.18.1" 1539 + source = "registry+https://github.com/rust-lang/crates.io-index" 1540 + checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda" 1541 + dependencies = [ 1542 + "base64 0.22.1", 1543 + "http-body-util", 1544 + "hyper", 1545 + "hyper-rustls", 1546 + "hyper-util", 1547 + "indexmap 2.13.0", 1548 + "ipnet", 1549 + "metrics", 1550 + "metrics-util", 1551 + "quanta", 1552 + "rustls", 1553 + "thiserror 2.0.18", 1554 + "tokio", 1555 + "tracing", 1556 + ] 1557 + 1558 + [[package]] 1559 + name = "metrics-util" 1560 + version = "0.20.1" 1561 + source = "registry+https://github.com/rust-lang/crates.io-index" 1562 + checksum = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4" 1563 + dependencies = [ 1564 + "crossbeam-epoch", 1565 + "crossbeam-utils", 1566 + "hashbrown 0.16.1", 1567 + "metrics", 1568 + "quanta", 1569 + "rand 0.9.2", 1570 + "rand_xoshiro", 1571 + "sketches-ddsketch", 1572 + ] 1573 + 1574 + [[package]] 1575 + name = "mime" 1576 + version = "0.3.17" 1577 + source = "registry+https://github.com/rust-lang/crates.io-index" 1578 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1579 + 1580 + [[package]] 1581 + name = "mio" 1582 + version = "1.1.1" 1583 + source = "registry+https://github.com/rust-lang/crates.io-index" 1584 + checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 1585 + dependencies = [ 1586 + "libc", 1587 + "wasi", 1588 + "windows-sys 0.61.2", 1589 + ] 1590 + 1591 + [[package]] 1592 + name = "moka" 1593 + version = "0.12.13" 1594 + source = "registry+https://github.com/rust-lang/crates.io-index" 1595 + checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" 1596 + dependencies = [ 1597 + "async-lock", 1598 + "crossbeam-channel", 1599 + "crossbeam-epoch", 1600 + "crossbeam-utils", 1601 + "equivalent", 1602 + "event-listener", 1603 + "futures-util", 1604 + "parking_lot", 1605 + "portable-atomic", 1606 + "smallvec", 1607 + "tagptr", 1608 + "uuid", 1609 + ] 1610 + 1611 + [[package]] 1612 + name = "multibase" 1613 + version = "0.9.2" 1614 + source = "registry+https://github.com/rust-lang/crates.io-index" 1615 + checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" 1616 + dependencies = [ 1617 + "base-x", 1618 + "base256emoji", 1619 + "data-encoding", 1620 + "data-encoding-macro", 1621 + ] 1622 + 1623 + [[package]] 1624 + name = "multihash" 1625 + version = "0.19.3" 1626 + source = "registry+https://github.com/rust-lang/crates.io-index" 1627 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1628 + dependencies = [ 1629 + "core2", 1630 + "serde", 1631 + "unsigned-varint", 1632 + ] 1633 + 1634 + [[package]] 1635 + name = "native-tls" 1636 + version = "0.2.14" 1637 + source = "registry+https://github.com/rust-lang/crates.io-index" 1638 + checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 1639 + dependencies = [ 1640 + "libc", 1641 + "log", 1642 + "openssl", 1643 + "openssl-probe 0.1.6", 1644 + "openssl-sys", 1645 + "schannel", 1646 + "security-framework 2.11.1", 1647 + "security-framework-sys", 1648 + "tempfile", 1649 + ] 1650 + 1651 + [[package]] 1652 + name = "nu-ansi-term" 1653 + version = "0.50.3" 1654 + source = "registry+https://github.com/rust-lang/crates.io-index" 1655 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 1656 + dependencies = [ 1657 + "windows-sys 0.61.2", 1658 + ] 1659 + 1660 + [[package]] 1661 + name = "num-bigint-dig" 1662 + version = "0.8.6" 1663 + source = "registry+https://github.com/rust-lang/crates.io-index" 1664 + checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" 1665 + dependencies = [ 1666 + "lazy_static", 1667 + "libm", 1668 + "num-integer", 1669 + "num-iter", 1670 + "num-traits", 1671 + "rand 0.8.5", 1672 + "smallvec", 1673 + "zeroize", 1674 + ] 1675 + 1676 + [[package]] 1677 + name = "num-conv" 1678 + version = "0.2.0" 1679 + source = "registry+https://github.com/rust-lang/crates.io-index" 1680 + checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" 1681 + 1682 + [[package]] 1683 + name = "num-integer" 1684 + version = "0.1.46" 1685 + source = "registry+https://github.com/rust-lang/crates.io-index" 1686 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1687 + dependencies = [ 1688 + "num-traits", 1689 + ] 1690 + 1691 + [[package]] 1692 + name = "num-iter" 1693 + version = "0.1.45" 1694 + source = "registry+https://github.com/rust-lang/crates.io-index" 1695 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 1696 + dependencies = [ 1697 + "autocfg", 1698 + "num-integer", 1699 + "num-traits", 1700 + ] 1701 + 1702 + [[package]] 1703 + name = "num-traits" 1704 + version = "0.2.19" 1705 + source = "registry+https://github.com/rust-lang/crates.io-index" 1706 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1707 + dependencies = [ 1708 + "autocfg", 1709 + "libm", 1710 + ] 1711 + 1712 + [[package]] 1713 + name = "once_cell" 1714 + version = "1.21.3" 1715 + source = "registry+https://github.com/rust-lang/crates.io-index" 1716 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1717 + dependencies = [ 1718 + "critical-section", 1719 + "portable-atomic", 1720 + ] 1721 + 1722 + [[package]] 1723 + name = "onis-appview" 1724 + version = "0.1.0" 1725 + dependencies = [ 1726 + "anyhow", 1727 + "axum", 1728 + "chrono", 1729 + "futures-util", 1730 + "metrics", 1731 + "onis-common", 1732 + "serde", 1733 + "serde_json", 1734 + "sqlx", 1735 + "tokio", 1736 + "tokio-tungstenite", 1737 + "tracing", 1738 + "tracing-subscriber", 1739 + ] 1740 + 1741 + [[package]] 1742 + name = "onis-common" 1743 + version = "0.1.0" 1744 + dependencies = [ 1745 + "atrium-api", 1746 + "axum", 1747 + "esquema-codegen", 1748 + "metrics-exporter-prometheus", 1749 + "serde", 1750 + "serde_json", 1751 + "sqlx", 1752 + "thiserror 2.0.18", 1753 + "tokio", 1754 + "toml", 1755 + "tracing", 1756 + ] 1757 + 1758 + [[package]] 1759 + name = "onis-dns" 1760 + version = "0.1.0" 1761 + dependencies = [ 1762 + "anyhow", 1763 + "async-trait", 1764 + "axum", 1765 + "hickory-proto", 1766 + "hickory-server", 1767 + "metrics", 1768 + "onis-common", 1769 + "reqwest", 1770 + "serde", 1771 + "serde_json", 1772 + "sqlx", 1773 + "tokio", 1774 + "tracing", 1775 + "tracing-subscriber", 1776 + ] 1777 + 1778 + [[package]] 1779 + name = "onis-verify" 1780 + version = "0.1.0" 1781 + dependencies = [ 1782 + "anyhow", 1783 + "axum", 1784 + "chrono", 1785 + "hickory-resolver", 1786 + "metrics", 1787 + "onis-common", 1788 + "reqwest", 1789 + "serde", 1790 + "serde_json", 1791 + "sqlx", 1792 + "tokio", 1793 + "tracing", 1794 + "tracing-subscriber", 1795 + ] 1796 + 1797 + [[package]] 1798 + name = "openssl" 1799 + version = "0.10.75" 1800 + source = "registry+https://github.com/rust-lang/crates.io-index" 1801 + checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 1802 + dependencies = [ 1803 + "bitflags", 1804 + "cfg-if", 1805 + "foreign-types", 1806 + "libc", 1807 + "once_cell", 1808 + "openssl-macros", 1809 + "openssl-sys", 1810 + ] 1811 + 1812 + [[package]] 1813 + name = "openssl-macros" 1814 + version = "0.1.1" 1815 + source = "registry+https://github.com/rust-lang/crates.io-index" 1816 + checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1817 + dependencies = [ 1818 + "proc-macro2", 1819 + "quote", 1820 + "syn", 1821 + ] 1822 + 1823 + [[package]] 1824 + name = "openssl-probe" 1825 + version = "0.1.6" 1826 + source = "registry+https://github.com/rust-lang/crates.io-index" 1827 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1828 + 1829 + [[package]] 1830 + name = "openssl-probe" 1831 + version = "0.2.1" 1832 + source = "registry+https://github.com/rust-lang/crates.io-index" 1833 + checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" 1834 + 1835 + [[package]] 1836 + name = "openssl-sys" 1837 + version = "0.9.111" 1838 + source = "registry+https://github.com/rust-lang/crates.io-index" 1839 + checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" 1840 + dependencies = [ 1841 + "cc", 1842 + "libc", 1843 + "pkg-config", 1844 + "vcpkg", 1845 + ] 1846 + 1847 + [[package]] 1848 + name = "parking" 1849 + version = "2.2.1" 1850 + source = "registry+https://github.com/rust-lang/crates.io-index" 1851 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1852 + 1853 + [[package]] 1854 + name = "parking_lot" 1855 + version = "0.12.5" 1856 + source = "registry+https://github.com/rust-lang/crates.io-index" 1857 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 1858 + dependencies = [ 1859 + "lock_api", 1860 + "parking_lot_core", 1861 + ] 1862 + 1863 + [[package]] 1864 + name = "parking_lot_core" 1865 + version = "0.9.12" 1866 + source = "registry+https://github.com/rust-lang/crates.io-index" 1867 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 1868 + dependencies = [ 1869 + "cfg-if", 1870 + "libc", 1871 + "redox_syscall 0.5.18", 1872 + "smallvec", 1873 + "windows-link", 1874 + ] 1875 + 1876 + [[package]] 1877 + name = "pem-rfc7468" 1878 + version = "0.7.0" 1879 + source = "registry+https://github.com/rust-lang/crates.io-index" 1880 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1881 + dependencies = [ 1882 + "base64ct", 1883 + ] 1884 + 1885 + [[package]] 1886 + name = "percent-encoding" 1887 + version = "2.3.2" 1888 + source = "registry+https://github.com/rust-lang/crates.io-index" 1889 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1890 + 1891 + [[package]] 1892 + name = "pin-project-lite" 1893 + version = "0.2.16" 1894 + source = "registry+https://github.com/rust-lang/crates.io-index" 1895 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1896 + 1897 + [[package]] 1898 + name = "pin-utils" 1899 + version = "0.1.0" 1900 + source = "registry+https://github.com/rust-lang/crates.io-index" 1901 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1902 + 1903 + [[package]] 1904 + name = "pkcs1" 1905 + version = "0.7.5" 1906 + source = "registry+https://github.com/rust-lang/crates.io-index" 1907 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1908 + dependencies = [ 1909 + "der", 1910 + "pkcs8", 1911 + "spki", 1912 + ] 1913 + 1914 + [[package]] 1915 + name = "pkcs8" 1916 + version = "0.10.2" 1917 + source = "registry+https://github.com/rust-lang/crates.io-index" 1918 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1919 + dependencies = [ 1920 + "der", 1921 + "spki", 1922 + ] 1923 + 1924 + [[package]] 1925 + name = "pkg-config" 1926 + version = "0.3.32" 1927 + source = "registry+https://github.com/rust-lang/crates.io-index" 1928 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1929 + 1930 + [[package]] 1931 + name = "portable-atomic" 1932 + version = "1.13.1" 1933 + source = "registry+https://github.com/rust-lang/crates.io-index" 1934 + checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" 1935 + 1936 + [[package]] 1937 + name = "potential_utf" 1938 + version = "0.1.4" 1939 + source = "registry+https://github.com/rust-lang/crates.io-index" 1940 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 1941 + dependencies = [ 1942 + "zerovec", 1943 + ] 1944 + 1945 + [[package]] 1946 + name = "powerfmt" 1947 + version = "0.2.0" 1948 + source = "registry+https://github.com/rust-lang/crates.io-index" 1949 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1950 + 1951 + [[package]] 1952 + name = "ppv-lite86" 1953 + version = "0.2.21" 1954 + source = "registry+https://github.com/rust-lang/crates.io-index" 1955 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1956 + dependencies = [ 1957 + "zerocopy", 1958 + ] 1959 + 1960 + [[package]] 1961 + name = "prefix-trie" 1962 + version = "0.7.0" 1963 + source = "registry+https://github.com/rust-lang/crates.io-index" 1964 + checksum = "85cf4c7c25f1dd66c76b451e9041a8cfce26e4ca754934fa7aed8d5a59a01d20" 1965 + dependencies = [ 1966 + "ipnet", 1967 + "num-traits", 1968 + ] 1969 + 1970 + [[package]] 1971 + name = "prettyplease" 1972 + version = "0.2.37" 1973 + source = "registry+https://github.com/rust-lang/crates.io-index" 1974 + checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 1975 + dependencies = [ 1976 + "proc-macro2", 1977 + "syn", 1978 + ] 1979 + 1980 + [[package]] 1981 + name = "proc-macro2" 1982 + version = "1.0.106" 1983 + source = "registry+https://github.com/rust-lang/crates.io-index" 1984 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 1985 + dependencies = [ 1986 + "unicode-ident", 1987 + ] 1988 + 1989 + [[package]] 1990 + name = "quanta" 1991 + version = "0.12.6" 1992 + source = "registry+https://github.com/rust-lang/crates.io-index" 1993 + checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" 1994 + dependencies = [ 1995 + "crossbeam-utils", 1996 + "libc", 1997 + "once_cell", 1998 + "raw-cpuid", 1999 + "wasi", 2000 + "web-sys", 2001 + "winapi", 2002 + ] 2003 + 2004 + [[package]] 2005 + name = "quote" 2006 + version = "1.0.44" 2007 + source = "registry+https://github.com/rust-lang/crates.io-index" 2008 + checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" 2009 + dependencies = [ 2010 + "proc-macro2", 2011 + ] 2012 + 2013 + [[package]] 2014 + name = "r-efi" 2015 + version = "5.3.0" 2016 + source = "registry+https://github.com/rust-lang/crates.io-index" 2017 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 2018 + 2019 + [[package]] 2020 + name = "rand" 2021 + version = "0.8.5" 2022 + source = "registry+https://github.com/rust-lang/crates.io-index" 2023 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 2024 + dependencies = [ 2025 + "libc", 2026 + "rand_chacha 0.3.1", 2027 + "rand_core 0.6.4", 2028 + ] 2029 + 2030 + [[package]] 2031 + name = "rand" 2032 + version = "0.9.2" 2033 + source = "registry+https://github.com/rust-lang/crates.io-index" 2034 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 2035 + dependencies = [ 2036 + "rand_chacha 0.9.0", 2037 + "rand_core 0.9.5", 2038 + ] 2039 + 2040 + [[package]] 2041 + name = "rand_chacha" 2042 + version = "0.3.1" 2043 + source = "registry+https://github.com/rust-lang/crates.io-index" 2044 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 2045 + dependencies = [ 2046 + "ppv-lite86", 2047 + "rand_core 0.6.4", 2048 + ] 2049 + 2050 + [[package]] 2051 + name = "rand_chacha" 2052 + version = "0.9.0" 2053 + source = "registry+https://github.com/rust-lang/crates.io-index" 2054 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 2055 + dependencies = [ 2056 + "ppv-lite86", 2057 + "rand_core 0.9.5", 2058 + ] 2059 + 2060 + [[package]] 2061 + name = "rand_core" 2062 + version = "0.6.4" 2063 + source = "registry+https://github.com/rust-lang/crates.io-index" 2064 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 2065 + dependencies = [ 2066 + "getrandom 0.2.17", 2067 + ] 2068 + 2069 + [[package]] 2070 + name = "rand_core" 2071 + version = "0.9.5" 2072 + source = "registry+https://github.com/rust-lang/crates.io-index" 2073 + checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" 2074 + dependencies = [ 2075 + "getrandom 0.3.4", 2076 + ] 2077 + 2078 + [[package]] 2079 + name = "rand_xoshiro" 2080 + version = "0.7.0" 2081 + source = "registry+https://github.com/rust-lang/crates.io-index" 2082 + checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" 2083 + dependencies = [ 2084 + "rand_core 0.9.5", 2085 + ] 2086 + 2087 + [[package]] 2088 + name = "raw-cpuid" 2089 + version = "11.6.0" 2090 + source = "registry+https://github.com/rust-lang/crates.io-index" 2091 + checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" 2092 + dependencies = [ 2093 + "bitflags", 2094 + ] 2095 + 2096 + [[package]] 2097 + name = "redox_syscall" 2098 + version = "0.5.18" 2099 + source = "registry+https://github.com/rust-lang/crates.io-index" 2100 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 2101 + dependencies = [ 2102 + "bitflags", 2103 + ] 2104 + 2105 + [[package]] 2106 + name = "redox_syscall" 2107 + version = "0.7.0" 2108 + source = "registry+https://github.com/rust-lang/crates.io-index" 2109 + checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" 2110 + dependencies = [ 2111 + "bitflags", 2112 + ] 2113 + 2114 + [[package]] 2115 + name = "regex" 2116 + version = "1.12.2" 2117 + source = "registry+https://github.com/rust-lang/crates.io-index" 2118 + checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 2119 + dependencies = [ 2120 + "aho-corasick", 2121 + "memchr", 2122 + "regex-automata", 2123 + "regex-syntax", 2124 + ] 2125 + 2126 + [[package]] 2127 + name = "regex-automata" 2128 + version = "0.4.13" 2129 + source = "registry+https://github.com/rust-lang/crates.io-index" 2130 + checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 2131 + dependencies = [ 2132 + "aho-corasick", 2133 + "memchr", 2134 + "regex-syntax", 2135 + ] 2136 + 2137 + [[package]] 2138 + name = "regex-syntax" 2139 + version = "0.8.8" 2140 + source = "registry+https://github.com/rust-lang/crates.io-index" 2141 + checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 2142 + 2143 + [[package]] 2144 + name = "reqwest" 2145 + version = "0.13.1" 2146 + source = "registry+https://github.com/rust-lang/crates.io-index" 2147 + checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" 2148 + dependencies = [ 2149 + "base64 0.22.1", 2150 + "bytes", 2151 + "futures-core", 2152 + "http", 2153 + "http-body", 2154 + "http-body-util", 2155 + "hyper", 2156 + "hyper-util", 2157 + "js-sys", 2158 + "log", 2159 + "percent-encoding", 2160 + "pin-project-lite", 2161 + "serde", 2162 + "serde_json", 2163 + "sync_wrapper", 2164 + "tokio", 2165 + "tower", 2166 + "tower-http", 2167 + "tower-service", 2168 + "url", 2169 + "wasm-bindgen", 2170 + "wasm-bindgen-futures", 2171 + "web-sys", 2172 + ] 2173 + 2174 + [[package]] 2175 + name = "resolv-conf" 2176 + version = "0.7.6" 2177 + source = "registry+https://github.com/rust-lang/crates.io-index" 2178 + checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" 2179 + 2180 + [[package]] 2181 + name = "ring" 2182 + version = "0.17.14" 2183 + source = "registry+https://github.com/rust-lang/crates.io-index" 2184 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 2185 + dependencies = [ 2186 + "cc", 2187 + "cfg-if", 2188 + "getrandom 0.2.17", 2189 + "libc", 2190 + "untrusted", 2191 + "windows-sys 0.52.0", 2192 + ] 2193 + 2194 + [[package]] 2195 + name = "rsa" 2196 + version = "0.9.10" 2197 + source = "registry+https://github.com/rust-lang/crates.io-index" 2198 + checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" 2199 + dependencies = [ 2200 + "const-oid", 2201 + "digest", 2202 + "num-bigint-dig", 2203 + "num-integer", 2204 + "num-traits", 2205 + "pkcs1", 2206 + "pkcs8", 2207 + "rand_core 0.6.4", 2208 + "signature", 2209 + "spki", 2210 + "subtle", 2211 + "zeroize", 2212 + ] 2213 + 2214 + [[package]] 2215 + name = "rustix" 2216 + version = "1.1.3" 2217 + source = "registry+https://github.com/rust-lang/crates.io-index" 2218 + checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 2219 + dependencies = [ 2220 + "bitflags", 2221 + "errno", 2222 + "libc", 2223 + "linux-raw-sys", 2224 + "windows-sys 0.61.2", 2225 + ] 2226 + 2227 + [[package]] 2228 + name = "rustls" 2229 + version = "0.23.36" 2230 + source = "registry+https://github.com/rust-lang/crates.io-index" 2231 + checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" 2232 + dependencies = [ 2233 + "aws-lc-rs", 2234 + "once_cell", 2235 + "rustls-pki-types", 2236 + "rustls-webpki", 2237 + "subtle", 2238 + "zeroize", 2239 + ] 2240 + 2241 + [[package]] 2242 + name = "rustls-native-certs" 2243 + version = "0.8.3" 2244 + source = "registry+https://github.com/rust-lang/crates.io-index" 2245 + checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" 2246 + dependencies = [ 2247 + "openssl-probe 0.2.1", 2248 + "rustls-pki-types", 2249 + "schannel", 2250 + "security-framework 3.5.1", 2251 + ] 2252 + 2253 + [[package]] 2254 + name = "rustls-pki-types" 2255 + version = "1.14.0" 2256 + source = "registry+https://github.com/rust-lang/crates.io-index" 2257 + checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" 2258 + dependencies = [ 2259 + "zeroize", 2260 + ] 2261 + 2262 + [[package]] 2263 + name = "rustls-webpki" 2264 + version = "0.103.9" 2265 + source = "registry+https://github.com/rust-lang/crates.io-index" 2266 + checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" 2267 + dependencies = [ 2268 + "aws-lc-rs", 2269 + "ring", 2270 + "rustls-pki-types", 2271 + "untrusted", 2272 + ] 2273 + 2274 + [[package]] 2275 + name = "rustversion" 2276 + version = "1.0.22" 2277 + source = "registry+https://github.com/rust-lang/crates.io-index" 2278 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 2279 + 2280 + [[package]] 2281 + name = "ryu" 2282 + version = "1.0.22" 2283 + source = "registry+https://github.com/rust-lang/crates.io-index" 2284 + checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 2285 + 2286 + [[package]] 2287 + name = "schannel" 2288 + version = "0.1.28" 2289 + source = "registry+https://github.com/rust-lang/crates.io-index" 2290 + checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 2291 + dependencies = [ 2292 + "windows-sys 0.61.2", 2293 + ] 2294 + 2295 + [[package]] 2296 + name = "scopeguard" 2297 + version = "1.2.0" 2298 + source = "registry+https://github.com/rust-lang/crates.io-index" 2299 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2300 + 2301 + [[package]] 2302 + name = "security-framework" 2303 + version = "2.11.1" 2304 + source = "registry+https://github.com/rust-lang/crates.io-index" 2305 + checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2306 + dependencies = [ 2307 + "bitflags", 2308 + "core-foundation 0.9.4", 2309 + "core-foundation-sys", 2310 + "libc", 2311 + "security-framework-sys", 2312 + ] 2313 + 2314 + [[package]] 2315 + name = "security-framework" 2316 + version = "3.5.1" 2317 + source = "registry+https://github.com/rust-lang/crates.io-index" 2318 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 2319 + dependencies = [ 2320 + "bitflags", 2321 + "core-foundation 0.10.1", 2322 + "core-foundation-sys", 2323 + "libc", 2324 + "security-framework-sys", 2325 + ] 2326 + 2327 + [[package]] 2328 + name = "security-framework-sys" 2329 + version = "2.15.0" 2330 + source = "registry+https://github.com/rust-lang/crates.io-index" 2331 + checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 2332 + dependencies = [ 2333 + "core-foundation-sys", 2334 + "libc", 2335 + ] 2336 + 2337 + [[package]] 2338 + name = "serde" 2339 + version = "1.0.228" 2340 + source = "registry+https://github.com/rust-lang/crates.io-index" 2341 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 2342 + dependencies = [ 2343 + "serde_core", 2344 + "serde_derive", 2345 + ] 2346 + 2347 + [[package]] 2348 + name = "serde_bytes" 2349 + version = "0.11.19" 2350 + source = "registry+https://github.com/rust-lang/crates.io-index" 2351 + checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 2352 + dependencies = [ 2353 + "serde", 2354 + "serde_core", 2355 + ] 2356 + 2357 + [[package]] 2358 + name = "serde_core" 2359 + version = "1.0.228" 2360 + source = "registry+https://github.com/rust-lang/crates.io-index" 2361 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 2362 + dependencies = [ 2363 + "serde_derive", 2364 + ] 2365 + 2366 + [[package]] 2367 + name = "serde_derive" 2368 + version = "1.0.228" 2369 + source = "registry+https://github.com/rust-lang/crates.io-index" 2370 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 2371 + dependencies = [ 2372 + "proc-macro2", 2373 + "quote", 2374 + "syn", 2375 + ] 2376 + 2377 + [[package]] 2378 + name = "serde_html_form" 2379 + version = "0.2.8" 2380 + source = "registry+https://github.com/rust-lang/crates.io-index" 2381 + checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 2382 + dependencies = [ 2383 + "form_urlencoded", 2384 + "indexmap 2.13.0", 2385 + "itoa", 2386 + "ryu", 2387 + "serde_core", 2388 + ] 2389 + 2390 + [[package]] 2391 + name = "serde_json" 2392 + version = "1.0.149" 2393 + source = "registry+https://github.com/rust-lang/crates.io-index" 2394 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 2395 + dependencies = [ 2396 + "itoa", 2397 + "memchr", 2398 + "serde", 2399 + "serde_core", 2400 + "zmij", 2401 + ] 2402 + 2403 + [[package]] 2404 + name = "serde_path_to_error" 2405 + version = "0.1.20" 2406 + source = "registry+https://github.com/rust-lang/crates.io-index" 2407 + checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" 2408 + dependencies = [ 2409 + "itoa", 2410 + "serde", 2411 + "serde_core", 2412 + ] 2413 + 2414 + [[package]] 2415 + name = "serde_repr" 2416 + version = "0.1.20" 2417 + source = "registry+https://github.com/rust-lang/crates.io-index" 2418 + checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" 2419 + dependencies = [ 2420 + "proc-macro2", 2421 + "quote", 2422 + "syn", 2423 + ] 2424 + 2425 + [[package]] 2426 + name = "serde_spanned" 2427 + version = "1.0.4" 2428 + source = "registry+https://github.com/rust-lang/crates.io-index" 2429 + checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" 2430 + dependencies = [ 2431 + "serde_core", 2432 + ] 2433 + 2434 + [[package]] 2435 + name = "serde_urlencoded" 2436 + version = "0.7.1" 2437 + source = "registry+https://github.com/rust-lang/crates.io-index" 2438 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 2439 + dependencies = [ 2440 + "form_urlencoded", 2441 + "itoa", 2442 + "ryu", 2443 + "serde", 2444 + ] 2445 + 2446 + [[package]] 2447 + name = "serde_with" 2448 + version = "2.3.3" 2449 + source = "registry+https://github.com/rust-lang/crates.io-index" 2450 + checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" 2451 + dependencies = [ 2452 + "base64 0.13.1", 2453 + "chrono", 2454 + "hex", 2455 + "indexmap 1.9.3", 2456 + "serde", 2457 + "serde_json", 2458 + "serde_with_macros", 2459 + "time", 2460 + ] 2461 + 2462 + [[package]] 2463 + name = "serde_with_macros" 2464 + version = "2.3.3" 2465 + source = "registry+https://github.com/rust-lang/crates.io-index" 2466 + checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" 2467 + dependencies = [ 2468 + "darling", 2469 + "proc-macro2", 2470 + "quote", 2471 + "syn", 2472 + ] 2473 + 2474 + [[package]] 2475 + name = "sha1" 2476 + version = "0.10.6" 2477 + source = "registry+https://github.com/rust-lang/crates.io-index" 2478 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 2479 + dependencies = [ 2480 + "cfg-if", 2481 + "cpufeatures", 2482 + "digest", 2483 + ] 2484 + 2485 + [[package]] 2486 + name = "sha2" 2487 + version = "0.10.9" 2488 + source = "registry+https://github.com/rust-lang/crates.io-index" 2489 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 2490 + dependencies = [ 2491 + "cfg-if", 2492 + "cpufeatures", 2493 + "digest", 2494 + ] 2495 + 2496 + [[package]] 2497 + name = "sharded-slab" 2498 + version = "0.1.7" 2499 + source = "registry+https://github.com/rust-lang/crates.io-index" 2500 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 2501 + dependencies = [ 2502 + "lazy_static", 2503 + ] 2504 + 2505 + [[package]] 2506 + name = "shlex" 2507 + version = "1.3.0" 2508 + source = "registry+https://github.com/rust-lang/crates.io-index" 2509 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 2510 + 2511 + [[package]] 2512 + name = "signal-hook-registry" 2513 + version = "1.4.8" 2514 + source = "registry+https://github.com/rust-lang/crates.io-index" 2515 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 2516 + dependencies = [ 2517 + "errno", 2518 + "libc", 2519 + ] 2520 + 2521 + [[package]] 2522 + name = "signature" 2523 + version = "2.2.0" 2524 + source = "registry+https://github.com/rust-lang/crates.io-index" 2525 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 2526 + dependencies = [ 2527 + "digest", 2528 + "rand_core 0.6.4", 2529 + ] 2530 + 2531 + [[package]] 2532 + name = "sketches-ddsketch" 2533 + version = "0.3.0" 2534 + source = "registry+https://github.com/rust-lang/crates.io-index" 2535 + checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" 2536 + 2537 + [[package]] 2538 + name = "slab" 2539 + version = "0.4.12" 2540 + source = "registry+https://github.com/rust-lang/crates.io-index" 2541 + checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" 2542 + 2543 + [[package]] 2544 + name = "smallvec" 2545 + version = "1.15.1" 2546 + source = "registry+https://github.com/rust-lang/crates.io-index" 2547 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 2548 + dependencies = [ 2549 + "serde", 2550 + ] 2551 + 2552 + [[package]] 2553 + name = "socket2" 2554 + version = "0.5.10" 2555 + source = "registry+https://github.com/rust-lang/crates.io-index" 2556 + checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 2557 + dependencies = [ 2558 + "libc", 2559 + "windows-sys 0.52.0", 2560 + ] 2561 + 2562 + [[package]] 2563 + name = "socket2" 2564 + version = "0.6.2" 2565 + source = "registry+https://github.com/rust-lang/crates.io-index" 2566 + checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" 2567 + dependencies = [ 2568 + "libc", 2569 + "windows-sys 0.60.2", 2570 + ] 2571 + 2572 + [[package]] 2573 + name = "spin" 2574 + version = "0.9.8" 2575 + source = "registry+https://github.com/rust-lang/crates.io-index" 2576 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 2577 + dependencies = [ 2578 + "lock_api", 2579 + ] 2580 + 2581 + [[package]] 2582 + name = "spki" 2583 + version = "0.7.3" 2584 + source = "registry+https://github.com/rust-lang/crates.io-index" 2585 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 2586 + dependencies = [ 2587 + "base64ct", 2588 + "der", 2589 + ] 2590 + 2591 + [[package]] 2592 + name = "sqlx" 2593 + version = "0.8.6" 2594 + source = "registry+https://github.com/rust-lang/crates.io-index" 2595 + checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" 2596 + dependencies = [ 2597 + "sqlx-core", 2598 + "sqlx-macros", 2599 + "sqlx-mysql", 2600 + "sqlx-postgres", 2601 + "sqlx-sqlite", 2602 + ] 2603 + 2604 + [[package]] 2605 + name = "sqlx-core" 2606 + version = "0.8.6" 2607 + source = "registry+https://github.com/rust-lang/crates.io-index" 2608 + checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" 2609 + dependencies = [ 2610 + "base64 0.22.1", 2611 + "bytes", 2612 + "crc", 2613 + "crossbeam-queue", 2614 + "either", 2615 + "event-listener", 2616 + "futures-core", 2617 + "futures-intrusive", 2618 + "futures-io", 2619 + "futures-util", 2620 + "hashbrown 0.15.5", 2621 + "hashlink", 2622 + "indexmap 2.13.0", 2623 + "log", 2624 + "memchr", 2625 + "once_cell", 2626 + "percent-encoding", 2627 + "serde", 2628 + "serde_json", 2629 + "sha2", 2630 + "smallvec", 2631 + "thiserror 2.0.18", 2632 + "tokio", 2633 + "tokio-stream", 2634 + "tracing", 2635 + "url", 2636 + ] 2637 + 2638 + [[package]] 2639 + name = "sqlx-macros" 2640 + version = "0.8.6" 2641 + source = "registry+https://github.com/rust-lang/crates.io-index" 2642 + checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" 2643 + dependencies = [ 2644 + "proc-macro2", 2645 + "quote", 2646 + "sqlx-core", 2647 + "sqlx-macros-core", 2648 + "syn", 2649 + ] 2650 + 2651 + [[package]] 2652 + name = "sqlx-macros-core" 2653 + version = "0.8.6" 2654 + source = "registry+https://github.com/rust-lang/crates.io-index" 2655 + checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" 2656 + dependencies = [ 2657 + "dotenvy", 2658 + "either", 2659 + "heck 0.5.0", 2660 + "hex", 2661 + "once_cell", 2662 + "proc-macro2", 2663 + "quote", 2664 + "serde", 2665 + "serde_json", 2666 + "sha2", 2667 + "sqlx-core", 2668 + "sqlx-mysql", 2669 + "sqlx-postgres", 2670 + "sqlx-sqlite", 2671 + "syn", 2672 + "tokio", 2673 + "url", 2674 + ] 2675 + 2676 + [[package]] 2677 + name = "sqlx-mysql" 2678 + version = "0.8.6" 2679 + source = "registry+https://github.com/rust-lang/crates.io-index" 2680 + checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" 2681 + dependencies = [ 2682 + "atoi", 2683 + "base64 0.22.1", 2684 + "bitflags", 2685 + "byteorder", 2686 + "bytes", 2687 + "crc", 2688 + "digest", 2689 + "dotenvy", 2690 + "either", 2691 + "futures-channel", 2692 + "futures-core", 2693 + "futures-io", 2694 + "futures-util", 2695 + "generic-array", 2696 + "hex", 2697 + "hkdf", 2698 + "hmac", 2699 + "itoa", 2700 + "log", 2701 + "md-5", 2702 + "memchr", 2703 + "once_cell", 2704 + "percent-encoding", 2705 + "rand 0.8.5", 2706 + "rsa", 2707 + "serde", 2708 + "sha1", 2709 + "sha2", 2710 + "smallvec", 2711 + "sqlx-core", 2712 + "stringprep", 2713 + "thiserror 2.0.18", 2714 + "tracing", 2715 + "whoami", 2716 + ] 2717 + 2718 + [[package]] 2719 + name = "sqlx-postgres" 2720 + version = "0.8.6" 2721 + source = "registry+https://github.com/rust-lang/crates.io-index" 2722 + checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" 2723 + dependencies = [ 2724 + "atoi", 2725 + "base64 0.22.1", 2726 + "bitflags", 2727 + "byteorder", 2728 + "crc", 2729 + "dotenvy", 2730 + "etcetera", 2731 + "futures-channel", 2732 + "futures-core", 2733 + "futures-util", 2734 + "hex", 2735 + "hkdf", 2736 + "hmac", 2737 + "home", 2738 + "itoa", 2739 + "log", 2740 + "md-5", 2741 + "memchr", 2742 + "once_cell", 2743 + "rand 0.8.5", 2744 + "serde", 2745 + "serde_json", 2746 + "sha2", 2747 + "smallvec", 2748 + "sqlx-core", 2749 + "stringprep", 2750 + "thiserror 2.0.18", 2751 + "tracing", 2752 + "whoami", 2753 + ] 2754 + 2755 + [[package]] 2756 + name = "sqlx-sqlite" 2757 + version = "0.8.6" 2758 + source = "registry+https://github.com/rust-lang/crates.io-index" 2759 + checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 2760 + dependencies = [ 2761 + "atoi", 2762 + "flume", 2763 + "futures-channel", 2764 + "futures-core", 2765 + "futures-executor", 2766 + "futures-intrusive", 2767 + "futures-util", 2768 + "libsqlite3-sys", 2769 + "log", 2770 + "percent-encoding", 2771 + "serde", 2772 + "serde_urlencoded", 2773 + "sqlx-core", 2774 + "thiserror 2.0.18", 2775 + "tracing", 2776 + "url", 2777 + ] 2778 + 2779 + [[package]] 2780 + name = "stable_deref_trait" 2781 + version = "1.2.1" 2782 + source = "registry+https://github.com/rust-lang/crates.io-index" 2783 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 2784 + 2785 + [[package]] 2786 + name = "stringprep" 2787 + version = "0.1.5" 2788 + source = "registry+https://github.com/rust-lang/crates.io-index" 2789 + checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 2790 + dependencies = [ 2791 + "unicode-bidi", 2792 + "unicode-normalization", 2793 + "unicode-properties", 2794 + ] 2795 + 2796 + [[package]] 2797 + name = "strsim" 2798 + version = "0.11.1" 2799 + source = "registry+https://github.com/rust-lang/crates.io-index" 2800 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 2801 + 2802 + [[package]] 2803 + name = "subtle" 2804 + version = "2.6.1" 2805 + source = "registry+https://github.com/rust-lang/crates.io-index" 2806 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2807 + 2808 + [[package]] 2809 + name = "syn" 2810 + version = "2.0.114" 2811 + source = "registry+https://github.com/rust-lang/crates.io-index" 2812 + checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" 2813 + dependencies = [ 2814 + "proc-macro2", 2815 + "quote", 2816 + "unicode-ident", 2817 + ] 2818 + 2819 + [[package]] 2820 + name = "sync_wrapper" 2821 + version = "1.0.2" 2822 + source = "registry+https://github.com/rust-lang/crates.io-index" 2823 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2824 + dependencies = [ 2825 + "futures-core", 2826 + ] 2827 + 2828 + [[package]] 2829 + name = "synstructure" 2830 + version = "0.13.2" 2831 + source = "registry+https://github.com/rust-lang/crates.io-index" 2832 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 2833 + dependencies = [ 2834 + "proc-macro2", 2835 + "quote", 2836 + "syn", 2837 + ] 2838 + 2839 + [[package]] 2840 + name = "tagptr" 2841 + version = "0.2.0" 2842 + source = "registry+https://github.com/rust-lang/crates.io-index" 2843 + checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 2844 + 2845 + [[package]] 2846 + name = "tempfile" 2847 + version = "3.24.0" 2848 + source = "registry+https://github.com/rust-lang/crates.io-index" 2849 + checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" 2850 + dependencies = [ 2851 + "fastrand", 2852 + "getrandom 0.3.4", 2853 + "once_cell", 2854 + "rustix", 2855 + "windows-sys 0.61.2", 2856 + ] 2857 + 2858 + [[package]] 2859 + name = "thiserror" 2860 + version = "1.0.69" 2861 + source = "registry+https://github.com/rust-lang/crates.io-index" 2862 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2863 + dependencies = [ 2864 + "thiserror-impl 1.0.69", 2865 + ] 2866 + 2867 + [[package]] 2868 + name = "thiserror" 2869 + version = "2.0.18" 2870 + source = "registry+https://github.com/rust-lang/crates.io-index" 2871 + checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" 2872 + dependencies = [ 2873 + "thiserror-impl 2.0.18", 2874 + ] 2875 + 2876 + [[package]] 2877 + name = "thiserror-impl" 2878 + version = "1.0.69" 2879 + source = "registry+https://github.com/rust-lang/crates.io-index" 2880 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2881 + dependencies = [ 2882 + "proc-macro2", 2883 + "quote", 2884 + "syn", 2885 + ] 2886 + 2887 + [[package]] 2888 + name = "thiserror-impl" 2889 + version = "2.0.18" 2890 + source = "registry+https://github.com/rust-lang/crates.io-index" 2891 + checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" 2892 + dependencies = [ 2893 + "proc-macro2", 2894 + "quote", 2895 + "syn", 2896 + ] 2897 + 2898 + [[package]] 2899 + name = "thread_local" 2900 + version = "1.1.9" 2901 + source = "registry+https://github.com/rust-lang/crates.io-index" 2902 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 2903 + dependencies = [ 2904 + "cfg-if", 2905 + ] 2906 + 2907 + [[package]] 2908 + name = "time" 2909 + version = "0.3.46" 2910 + source = "registry+https://github.com/rust-lang/crates.io-index" 2911 + checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" 2912 + dependencies = [ 2913 + "deranged", 2914 + "itoa", 2915 + "num-conv", 2916 + "powerfmt", 2917 + "serde_core", 2918 + "time-core", 2919 + "time-macros", 2920 + ] 2921 + 2922 + [[package]] 2923 + name = "time-core" 2924 + version = "0.1.8" 2925 + source = "registry+https://github.com/rust-lang/crates.io-index" 2926 + checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" 2927 + 2928 + [[package]] 2929 + name = "time-macros" 2930 + version = "0.2.26" 2931 + source = "registry+https://github.com/rust-lang/crates.io-index" 2932 + checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" 2933 + dependencies = [ 2934 + "num-conv", 2935 + "time-core", 2936 + ] 2937 + 2938 + [[package]] 2939 + name = "tinystr" 2940 + version = "0.8.2" 2941 + source = "registry+https://github.com/rust-lang/crates.io-index" 2942 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 2943 + dependencies = [ 2944 + "displaydoc", 2945 + "zerovec", 2946 + ] 2947 + 2948 + [[package]] 2949 + name = "tinyvec" 2950 + version = "1.10.0" 2951 + source = "registry+https://github.com/rust-lang/crates.io-index" 2952 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 2953 + dependencies = [ 2954 + "tinyvec_macros", 2955 + ] 2956 + 2957 + [[package]] 2958 + name = "tinyvec_macros" 2959 + version = "0.1.1" 2960 + source = "registry+https://github.com/rust-lang/crates.io-index" 2961 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2962 + 2963 + [[package]] 2964 + name = "tokio" 2965 + version = "1.49.0" 2966 + source = "registry+https://github.com/rust-lang/crates.io-index" 2967 + checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" 2968 + dependencies = [ 2969 + "bytes", 2970 + "libc", 2971 + "mio", 2972 + "parking_lot", 2973 + "pin-project-lite", 2974 + "signal-hook-registry", 2975 + "socket2 0.6.2", 2976 + "tokio-macros", 2977 + "windows-sys 0.61.2", 2978 + ] 2979 + 2980 + [[package]] 2981 + name = "tokio-macros" 2982 + version = "2.6.0" 2983 + source = "registry+https://github.com/rust-lang/crates.io-index" 2984 + checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 2985 + dependencies = [ 2986 + "proc-macro2", 2987 + "quote", 2988 + "syn", 2989 + ] 2990 + 2991 + [[package]] 2992 + name = "tokio-native-tls" 2993 + version = "0.3.1" 2994 + source = "registry+https://github.com/rust-lang/crates.io-index" 2995 + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2996 + dependencies = [ 2997 + "native-tls", 2998 + "tokio", 2999 + ] 3000 + 3001 + [[package]] 3002 + name = "tokio-rustls" 3003 + version = "0.26.4" 3004 + source = "registry+https://github.com/rust-lang/crates.io-index" 3005 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 3006 + dependencies = [ 3007 + "rustls", 3008 + "tokio", 3009 + ] 3010 + 3011 + [[package]] 3012 + name = "tokio-stream" 3013 + version = "0.1.18" 3014 + source = "registry+https://github.com/rust-lang/crates.io-index" 3015 + checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" 3016 + dependencies = [ 3017 + "futures-core", 3018 + "pin-project-lite", 3019 + "tokio", 3020 + ] 3021 + 3022 + [[package]] 3023 + name = "tokio-tungstenite" 3024 + version = "0.28.0" 3025 + source = "registry+https://github.com/rust-lang/crates.io-index" 3026 + checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" 3027 + dependencies = [ 3028 + "futures-util", 3029 + "log", 3030 + "native-tls", 3031 + "tokio", 3032 + "tokio-native-tls", 3033 + "tungstenite", 3034 + ] 3035 + 3036 + [[package]] 3037 + name = "tokio-util" 3038 + version = "0.7.18" 3039 + source = "registry+https://github.com/rust-lang/crates.io-index" 3040 + checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" 3041 + dependencies = [ 3042 + "bytes", 3043 + "futures-core", 3044 + "futures-sink", 3045 + "pin-project-lite", 3046 + "tokio", 3047 + ] 3048 + 3049 + [[package]] 3050 + name = "toml" 3051 + version = "0.9.11+spec-1.1.0" 3052 + source = "registry+https://github.com/rust-lang/crates.io-index" 3053 + checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" 3054 + dependencies = [ 3055 + "indexmap 2.13.0", 3056 + "serde_core", 3057 + "serde_spanned", 3058 + "toml_datetime", 3059 + "toml_parser", 3060 + "toml_writer", 3061 + "winnow", 3062 + ] 3063 + 3064 + [[package]] 3065 + name = "toml_datetime" 3066 + version = "0.7.5+spec-1.1.0" 3067 + source = "registry+https://github.com/rust-lang/crates.io-index" 3068 + checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" 3069 + dependencies = [ 3070 + "serde_core", 3071 + ] 3072 + 3073 + [[package]] 3074 + name = "toml_parser" 3075 + version = "1.0.6+spec-1.1.0" 3076 + source = "registry+https://github.com/rust-lang/crates.io-index" 3077 + checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" 3078 + dependencies = [ 3079 + "winnow", 3080 + ] 3081 + 3082 + [[package]] 3083 + name = "toml_writer" 3084 + version = "1.0.6+spec-1.1.0" 3085 + source = "registry+https://github.com/rust-lang/crates.io-index" 3086 + checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" 3087 + 3088 + [[package]] 3089 + name = "tower" 3090 + version = "0.5.3" 3091 + source = "registry+https://github.com/rust-lang/crates.io-index" 3092 + checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" 3093 + dependencies = [ 3094 + "futures-core", 3095 + "futures-util", 3096 + "pin-project-lite", 3097 + "sync_wrapper", 3098 + "tokio", 3099 + "tower-layer", 3100 + "tower-service", 3101 + "tracing", 3102 + ] 3103 + 3104 + [[package]] 3105 + name = "tower-http" 3106 + version = "0.6.8" 3107 + source = "registry+https://github.com/rust-lang/crates.io-index" 3108 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 3109 + dependencies = [ 3110 + "bitflags", 3111 + "bytes", 3112 + "futures-util", 3113 + "http", 3114 + "http-body", 3115 + "iri-string", 3116 + "pin-project-lite", 3117 + "tower", 3118 + "tower-layer", 3119 + "tower-service", 3120 + ] 3121 + 3122 + [[package]] 3123 + name = "tower-layer" 3124 + version = "0.3.3" 3125 + source = "registry+https://github.com/rust-lang/crates.io-index" 3126 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 3127 + 3128 + [[package]] 3129 + name = "tower-service" 3130 + version = "0.3.3" 3131 + source = "registry+https://github.com/rust-lang/crates.io-index" 3132 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 3133 + 3134 + [[package]] 3135 + name = "tracing" 3136 + version = "0.1.44" 3137 + source = "registry+https://github.com/rust-lang/crates.io-index" 3138 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 3139 + dependencies = [ 3140 + "log", 3141 + "pin-project-lite", 3142 + "tracing-attributes", 3143 + "tracing-core", 3144 + ] 3145 + 3146 + [[package]] 3147 + name = "tracing-attributes" 3148 + version = "0.1.31" 3149 + source = "registry+https://github.com/rust-lang/crates.io-index" 3150 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 3151 + dependencies = [ 3152 + "proc-macro2", 3153 + "quote", 3154 + "syn", 3155 + ] 3156 + 3157 + [[package]] 3158 + name = "tracing-core" 3159 + version = "0.1.36" 3160 + source = "registry+https://github.com/rust-lang/crates.io-index" 3161 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 3162 + dependencies = [ 3163 + "once_cell", 3164 + "valuable", 3165 + ] 3166 + 3167 + [[package]] 3168 + name = "tracing-log" 3169 + version = "0.2.0" 3170 + source = "registry+https://github.com/rust-lang/crates.io-index" 3171 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 3172 + dependencies = [ 3173 + "log", 3174 + "once_cell", 3175 + "tracing-core", 3176 + ] 3177 + 3178 + [[package]] 3179 + name = "tracing-serde" 3180 + version = "0.2.0" 3181 + source = "registry+https://github.com/rust-lang/crates.io-index" 3182 + checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" 3183 + dependencies = [ 3184 + "serde", 3185 + "tracing-core", 3186 + ] 3187 + 3188 + [[package]] 3189 + name = "tracing-subscriber" 3190 + version = "0.3.22" 3191 + source = "registry+https://github.com/rust-lang/crates.io-index" 3192 + checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" 3193 + dependencies = [ 3194 + "matchers", 3195 + "nu-ansi-term", 3196 + "once_cell", 3197 + "regex-automata", 3198 + "serde", 3199 + "serde_json", 3200 + "sharded-slab", 3201 + "smallvec", 3202 + "thread_local", 3203 + "tracing", 3204 + "tracing-core", 3205 + "tracing-log", 3206 + "tracing-serde", 3207 + ] 3208 + 3209 + [[package]] 3210 + name = "trait-variant" 3211 + version = "0.1.2" 3212 + source = "registry+https://github.com/rust-lang/crates.io-index" 3213 + checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" 3214 + dependencies = [ 3215 + "proc-macro2", 3216 + "quote", 3217 + "syn", 3218 + ] 3219 + 3220 + [[package]] 3221 + name = "try-lock" 3222 + version = "0.2.5" 3223 + source = "registry+https://github.com/rust-lang/crates.io-index" 3224 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 3225 + 3226 + [[package]] 3227 + name = "tungstenite" 3228 + version = "0.28.0" 3229 + source = "registry+https://github.com/rust-lang/crates.io-index" 3230 + checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" 3231 + dependencies = [ 3232 + "bytes", 3233 + "data-encoding", 3234 + "http", 3235 + "httparse", 3236 + "log", 3237 + "native-tls", 3238 + "rand 0.9.2", 3239 + "sha1", 3240 + "thiserror 2.0.18", 3241 + "utf-8", 3242 + ] 3243 + 3244 + [[package]] 3245 + name = "typenum" 3246 + version = "1.19.0" 3247 + source = "registry+https://github.com/rust-lang/crates.io-index" 3248 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 3249 + 3250 + [[package]] 3251 + name = "unicode-bidi" 3252 + version = "0.3.18" 3253 + source = "registry+https://github.com/rust-lang/crates.io-index" 3254 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 3255 + 3256 + [[package]] 3257 + name = "unicode-ident" 3258 + version = "1.0.22" 3259 + source = "registry+https://github.com/rust-lang/crates.io-index" 3260 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 3261 + 3262 + [[package]] 3263 + name = "unicode-normalization" 3264 + version = "0.1.25" 3265 + source = "registry+https://github.com/rust-lang/crates.io-index" 3266 + checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" 3267 + dependencies = [ 3268 + "tinyvec", 3269 + ] 3270 + 3271 + [[package]] 3272 + name = "unicode-properties" 3273 + version = "0.1.4" 3274 + source = "registry+https://github.com/rust-lang/crates.io-index" 3275 + checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 3276 + 3277 + [[package]] 3278 + name = "unsigned-varint" 3279 + version = "0.8.0" 3280 + source = "registry+https://github.com/rust-lang/crates.io-index" 3281 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 3282 + 3283 + [[package]] 3284 + name = "untrusted" 3285 + version = "0.9.0" 3286 + source = "registry+https://github.com/rust-lang/crates.io-index" 3287 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 3288 + 3289 + [[package]] 3290 + name = "url" 3291 + version = "2.5.8" 3292 + source = "registry+https://github.com/rust-lang/crates.io-index" 3293 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 3294 + dependencies = [ 3295 + "form_urlencoded", 3296 + "idna", 3297 + "percent-encoding", 3298 + "serde", 3299 + "serde_derive", 3300 + ] 3301 + 3302 + [[package]] 3303 + name = "utf-8" 3304 + version = "0.7.6" 3305 + source = "registry+https://github.com/rust-lang/crates.io-index" 3306 + checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 3307 + 3308 + [[package]] 3309 + name = "utf8_iter" 3310 + version = "1.0.4" 3311 + source = "registry+https://github.com/rust-lang/crates.io-index" 3312 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 3313 + 3314 + [[package]] 3315 + name = "uuid" 3316 + version = "1.20.0" 3317 + source = "registry+https://github.com/rust-lang/crates.io-index" 3318 + checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" 3319 + dependencies = [ 3320 + "getrandom 0.3.4", 3321 + "js-sys", 3322 + "wasm-bindgen", 3323 + ] 3324 + 3325 + [[package]] 3326 + name = "valuable" 3327 + version = "0.1.1" 3328 + source = "registry+https://github.com/rust-lang/crates.io-index" 3329 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 3330 + 3331 + [[package]] 3332 + name = "vcpkg" 3333 + version = "0.2.15" 3334 + source = "registry+https://github.com/rust-lang/crates.io-index" 3335 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 3336 + 3337 + [[package]] 3338 + name = "version_check" 3339 + version = "0.9.5" 3340 + source = "registry+https://github.com/rust-lang/crates.io-index" 3341 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3342 + 3343 + [[package]] 3344 + name = "want" 3345 + version = "0.3.1" 3346 + source = "registry+https://github.com/rust-lang/crates.io-index" 3347 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 3348 + dependencies = [ 3349 + "try-lock", 3350 + ] 3351 + 3352 + [[package]] 3353 + name = "wasi" 3354 + version = "0.11.1+wasi-snapshot-preview1" 3355 + source = "registry+https://github.com/rust-lang/crates.io-index" 3356 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 3357 + 3358 + [[package]] 3359 + name = "wasip2" 3360 + version = "1.0.2+wasi-0.2.9" 3361 + source = "registry+https://github.com/rust-lang/crates.io-index" 3362 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 3363 + dependencies = [ 3364 + "wit-bindgen", 3365 + ] 3366 + 3367 + [[package]] 3368 + name = "wasite" 3369 + version = "0.1.0" 3370 + source = "registry+https://github.com/rust-lang/crates.io-index" 3371 + checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 3372 + 3373 + [[package]] 3374 + name = "wasm-bindgen" 3375 + version = "0.2.108" 3376 + source = "registry+https://github.com/rust-lang/crates.io-index" 3377 + checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" 3378 + dependencies = [ 3379 + "cfg-if", 3380 + "once_cell", 3381 + "rustversion", 3382 + "wasm-bindgen-macro", 3383 + "wasm-bindgen-shared", 3384 + ] 3385 + 3386 + [[package]] 3387 + name = "wasm-bindgen-futures" 3388 + version = "0.4.58" 3389 + source = "registry+https://github.com/rust-lang/crates.io-index" 3390 + checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" 3391 + dependencies = [ 3392 + "cfg-if", 3393 + "futures-util", 3394 + "js-sys", 3395 + "once_cell", 3396 + "wasm-bindgen", 3397 + "web-sys", 3398 + ] 3399 + 3400 + [[package]] 3401 + name = "wasm-bindgen-macro" 3402 + version = "0.2.108" 3403 + source = "registry+https://github.com/rust-lang/crates.io-index" 3404 + checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" 3405 + dependencies = [ 3406 + "quote", 3407 + "wasm-bindgen-macro-support", 3408 + ] 3409 + 3410 + [[package]] 3411 + name = "wasm-bindgen-macro-support" 3412 + version = "0.2.108" 3413 + source = "registry+https://github.com/rust-lang/crates.io-index" 3414 + checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" 3415 + dependencies = [ 3416 + "bumpalo", 3417 + "proc-macro2", 3418 + "quote", 3419 + "syn", 3420 + "wasm-bindgen-shared", 3421 + ] 3422 + 3423 + [[package]] 3424 + name = "wasm-bindgen-shared" 3425 + version = "0.2.108" 3426 + source = "registry+https://github.com/rust-lang/crates.io-index" 3427 + checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" 3428 + dependencies = [ 3429 + "unicode-ident", 3430 + ] 3431 + 3432 + [[package]] 3433 + name = "web-sys" 3434 + version = "0.3.85" 3435 + source = "registry+https://github.com/rust-lang/crates.io-index" 3436 + checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" 3437 + dependencies = [ 3438 + "js-sys", 3439 + "wasm-bindgen", 3440 + ] 3441 + 3442 + [[package]] 3443 + name = "web-time" 3444 + version = "1.1.0" 3445 + source = "registry+https://github.com/rust-lang/crates.io-index" 3446 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 3447 + dependencies = [ 3448 + "js-sys", 3449 + "wasm-bindgen", 3450 + ] 3451 + 3452 + [[package]] 3453 + name = "whoami" 3454 + version = "1.6.1" 3455 + source = "registry+https://github.com/rust-lang/crates.io-index" 3456 + checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" 3457 + dependencies = [ 3458 + "libredox", 3459 + "wasite", 3460 + ] 3461 + 3462 + [[package]] 3463 + name = "widestring" 3464 + version = "1.2.1" 3465 + source = "registry+https://github.com/rust-lang/crates.io-index" 3466 + checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" 3467 + 3468 + [[package]] 3469 + name = "winapi" 3470 + version = "0.3.9" 3471 + source = "registry+https://github.com/rust-lang/crates.io-index" 3472 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 3473 + dependencies = [ 3474 + "winapi-i686-pc-windows-gnu", 3475 + "winapi-x86_64-pc-windows-gnu", 3476 + ] 3477 + 3478 + [[package]] 3479 + name = "winapi-i686-pc-windows-gnu" 3480 + version = "0.4.0" 3481 + source = "registry+https://github.com/rust-lang/crates.io-index" 3482 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 3483 + 3484 + [[package]] 3485 + name = "winapi-x86_64-pc-windows-gnu" 3486 + version = "0.4.0" 3487 + source = "registry+https://github.com/rust-lang/crates.io-index" 3488 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 3489 + 3490 + [[package]] 3491 + name = "windows-core" 3492 + version = "0.62.2" 3493 + source = "registry+https://github.com/rust-lang/crates.io-index" 3494 + checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 3495 + dependencies = [ 3496 + "windows-implement", 3497 + "windows-interface", 3498 + "windows-link", 3499 + "windows-result", 3500 + "windows-strings", 3501 + ] 3502 + 3503 + [[package]] 3504 + name = "windows-implement" 3505 + version = "0.60.2" 3506 + source = "registry+https://github.com/rust-lang/crates.io-index" 3507 + checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 3508 + dependencies = [ 3509 + "proc-macro2", 3510 + "quote", 3511 + "syn", 3512 + ] 3513 + 3514 + [[package]] 3515 + name = "windows-interface" 3516 + version = "0.59.3" 3517 + source = "registry+https://github.com/rust-lang/crates.io-index" 3518 + checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 3519 + dependencies = [ 3520 + "proc-macro2", 3521 + "quote", 3522 + "syn", 3523 + ] 3524 + 3525 + [[package]] 3526 + name = "windows-link" 3527 + version = "0.2.1" 3528 + source = "registry+https://github.com/rust-lang/crates.io-index" 3529 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 3530 + 3531 + [[package]] 3532 + name = "windows-result" 3533 + version = "0.4.1" 3534 + source = "registry+https://github.com/rust-lang/crates.io-index" 3535 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 3536 + dependencies = [ 3537 + "windows-link", 3538 + ] 3539 + 3540 + [[package]] 3541 + name = "windows-strings" 3542 + version = "0.5.1" 3543 + source = "registry+https://github.com/rust-lang/crates.io-index" 3544 + checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 3545 + dependencies = [ 3546 + "windows-link", 3547 + ] 3548 + 3549 + [[package]] 3550 + name = "windows-sys" 3551 + version = "0.48.0" 3552 + source = "registry+https://github.com/rust-lang/crates.io-index" 3553 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 3554 + dependencies = [ 3555 + "windows-targets 0.48.5", 3556 + ] 3557 + 3558 + [[package]] 3559 + name = "windows-sys" 3560 + version = "0.52.0" 3561 + source = "registry+https://github.com/rust-lang/crates.io-index" 3562 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3563 + dependencies = [ 3564 + "windows-targets 0.52.6", 3565 + ] 3566 + 3567 + [[package]] 3568 + name = "windows-sys" 3569 + version = "0.60.2" 3570 + source = "registry+https://github.com/rust-lang/crates.io-index" 3571 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 3572 + dependencies = [ 3573 + "windows-targets 0.53.5", 3574 + ] 3575 + 3576 + [[package]] 3577 + name = "windows-sys" 3578 + version = "0.61.2" 3579 + source = "registry+https://github.com/rust-lang/crates.io-index" 3580 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 3581 + dependencies = [ 3582 + "windows-link", 3583 + ] 3584 + 3585 + [[package]] 3586 + name = "windows-targets" 3587 + version = "0.48.5" 3588 + source = "registry+https://github.com/rust-lang/crates.io-index" 3589 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 3590 + dependencies = [ 3591 + "windows_aarch64_gnullvm 0.48.5", 3592 + "windows_aarch64_msvc 0.48.5", 3593 + "windows_i686_gnu 0.48.5", 3594 + "windows_i686_msvc 0.48.5", 3595 + "windows_x86_64_gnu 0.48.5", 3596 + "windows_x86_64_gnullvm 0.48.5", 3597 + "windows_x86_64_msvc 0.48.5", 3598 + ] 3599 + 3600 + [[package]] 3601 + name = "windows-targets" 3602 + version = "0.52.6" 3603 + source = "registry+https://github.com/rust-lang/crates.io-index" 3604 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3605 + dependencies = [ 3606 + "windows_aarch64_gnullvm 0.52.6", 3607 + "windows_aarch64_msvc 0.52.6", 3608 + "windows_i686_gnu 0.52.6", 3609 + "windows_i686_gnullvm 0.52.6", 3610 + "windows_i686_msvc 0.52.6", 3611 + "windows_x86_64_gnu 0.52.6", 3612 + "windows_x86_64_gnullvm 0.52.6", 3613 + "windows_x86_64_msvc 0.52.6", 3614 + ] 3615 + 3616 + [[package]] 3617 + name = "windows-targets" 3618 + version = "0.53.5" 3619 + source = "registry+https://github.com/rust-lang/crates.io-index" 3620 + checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 3621 + dependencies = [ 3622 + "windows-link", 3623 + "windows_aarch64_gnullvm 0.53.1", 3624 + "windows_aarch64_msvc 0.53.1", 3625 + "windows_i686_gnu 0.53.1", 3626 + "windows_i686_gnullvm 0.53.1", 3627 + "windows_i686_msvc 0.53.1", 3628 + "windows_x86_64_gnu 0.53.1", 3629 + "windows_x86_64_gnullvm 0.53.1", 3630 + "windows_x86_64_msvc 0.53.1", 3631 + ] 3632 + 3633 + [[package]] 3634 + name = "windows_aarch64_gnullvm" 3635 + version = "0.48.5" 3636 + source = "registry+https://github.com/rust-lang/crates.io-index" 3637 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 3638 + 3639 + [[package]] 3640 + name = "windows_aarch64_gnullvm" 3641 + version = "0.52.6" 3642 + source = "registry+https://github.com/rust-lang/crates.io-index" 3643 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3644 + 3645 + [[package]] 3646 + name = "windows_aarch64_gnullvm" 3647 + version = "0.53.1" 3648 + source = "registry+https://github.com/rust-lang/crates.io-index" 3649 + checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 3650 + 3651 + [[package]] 3652 + name = "windows_aarch64_msvc" 3653 + version = "0.48.5" 3654 + source = "registry+https://github.com/rust-lang/crates.io-index" 3655 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 3656 + 3657 + [[package]] 3658 + name = "windows_aarch64_msvc" 3659 + version = "0.52.6" 3660 + source = "registry+https://github.com/rust-lang/crates.io-index" 3661 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3662 + 3663 + [[package]] 3664 + name = "windows_aarch64_msvc" 3665 + version = "0.53.1" 3666 + source = "registry+https://github.com/rust-lang/crates.io-index" 3667 + checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 3668 + 3669 + [[package]] 3670 + name = "windows_i686_gnu" 3671 + version = "0.48.5" 3672 + source = "registry+https://github.com/rust-lang/crates.io-index" 3673 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 3674 + 3675 + [[package]] 3676 + name = "windows_i686_gnu" 3677 + version = "0.52.6" 3678 + source = "registry+https://github.com/rust-lang/crates.io-index" 3679 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3680 + 3681 + [[package]] 3682 + name = "windows_i686_gnu" 3683 + version = "0.53.1" 3684 + source = "registry+https://github.com/rust-lang/crates.io-index" 3685 + checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 3686 + 3687 + [[package]] 3688 + name = "windows_i686_gnullvm" 3689 + version = "0.52.6" 3690 + source = "registry+https://github.com/rust-lang/crates.io-index" 3691 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 3692 + 3693 + [[package]] 3694 + name = "windows_i686_gnullvm" 3695 + version = "0.53.1" 3696 + source = "registry+https://github.com/rust-lang/crates.io-index" 3697 + checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 3698 + 3699 + [[package]] 3700 + name = "windows_i686_msvc" 3701 + version = "0.48.5" 3702 + source = "registry+https://github.com/rust-lang/crates.io-index" 3703 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 3704 + 3705 + [[package]] 3706 + name = "windows_i686_msvc" 3707 + version = "0.52.6" 3708 + source = "registry+https://github.com/rust-lang/crates.io-index" 3709 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3710 + 3711 + [[package]] 3712 + name = "windows_i686_msvc" 3713 + version = "0.53.1" 3714 + source = "registry+https://github.com/rust-lang/crates.io-index" 3715 + checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 3716 + 3717 + [[package]] 3718 + name = "windows_x86_64_gnu" 3719 + version = "0.48.5" 3720 + source = "registry+https://github.com/rust-lang/crates.io-index" 3721 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 3722 + 3723 + [[package]] 3724 + name = "windows_x86_64_gnu" 3725 + version = "0.52.6" 3726 + source = "registry+https://github.com/rust-lang/crates.io-index" 3727 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3728 + 3729 + [[package]] 3730 + name = "windows_x86_64_gnu" 3731 + version = "0.53.1" 3732 + source = "registry+https://github.com/rust-lang/crates.io-index" 3733 + checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 3734 + 3735 + [[package]] 3736 + name = "windows_x86_64_gnullvm" 3737 + version = "0.48.5" 3738 + source = "registry+https://github.com/rust-lang/crates.io-index" 3739 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 3740 + 3741 + [[package]] 3742 + name = "windows_x86_64_gnullvm" 3743 + version = "0.52.6" 3744 + source = "registry+https://github.com/rust-lang/crates.io-index" 3745 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3746 + 3747 + [[package]] 3748 + name = "windows_x86_64_gnullvm" 3749 + version = "0.53.1" 3750 + source = "registry+https://github.com/rust-lang/crates.io-index" 3751 + checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 3752 + 3753 + [[package]] 3754 + name = "windows_x86_64_msvc" 3755 + version = "0.48.5" 3756 + source = "registry+https://github.com/rust-lang/crates.io-index" 3757 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 3758 + 3759 + [[package]] 3760 + name = "windows_x86_64_msvc" 3761 + version = "0.52.6" 3762 + source = "registry+https://github.com/rust-lang/crates.io-index" 3763 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3764 + 3765 + [[package]] 3766 + name = "windows_x86_64_msvc" 3767 + version = "0.53.1" 3768 + source = "registry+https://github.com/rust-lang/crates.io-index" 3769 + checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 3770 + 3771 + [[package]] 3772 + name = "winnow" 3773 + version = "0.7.14" 3774 + source = "registry+https://github.com/rust-lang/crates.io-index" 3775 + checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" 3776 + 3777 + [[package]] 3778 + name = "winreg" 3779 + version = "0.50.0" 3780 + source = "registry+https://github.com/rust-lang/crates.io-index" 3781 + checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 3782 + dependencies = [ 3783 + "cfg-if", 3784 + "windows-sys 0.48.0", 3785 + ] 3786 + 3787 + [[package]] 3788 + name = "wit-bindgen" 3789 + version = "0.51.0" 3790 + source = "registry+https://github.com/rust-lang/crates.io-index" 3791 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 3792 + 3793 + [[package]] 3794 + name = "writeable" 3795 + version = "0.6.2" 3796 + source = "registry+https://github.com/rust-lang/crates.io-index" 3797 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 3798 + 3799 + [[package]] 3800 + name = "yoke" 3801 + version = "0.8.1" 3802 + source = "registry+https://github.com/rust-lang/crates.io-index" 3803 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 3804 + dependencies = [ 3805 + "stable_deref_trait", 3806 + "yoke-derive", 3807 + "zerofrom", 3808 + ] 3809 + 3810 + [[package]] 3811 + name = "yoke-derive" 3812 + version = "0.8.1" 3813 + source = "registry+https://github.com/rust-lang/crates.io-index" 3814 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 3815 + dependencies = [ 3816 + "proc-macro2", 3817 + "quote", 3818 + "syn", 3819 + "synstructure", 3820 + ] 3821 + 3822 + [[package]] 3823 + name = "zerocopy" 3824 + version = "0.8.37" 3825 + source = "registry+https://github.com/rust-lang/crates.io-index" 3826 + checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" 3827 + dependencies = [ 3828 + "zerocopy-derive", 3829 + ] 3830 + 3831 + [[package]] 3832 + name = "zerocopy-derive" 3833 + version = "0.8.37" 3834 + source = "registry+https://github.com/rust-lang/crates.io-index" 3835 + checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" 3836 + dependencies = [ 3837 + "proc-macro2", 3838 + "quote", 3839 + "syn", 3840 + ] 3841 + 3842 + [[package]] 3843 + name = "zerofrom" 3844 + version = "0.1.6" 3845 + source = "registry+https://github.com/rust-lang/crates.io-index" 3846 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 3847 + dependencies = [ 3848 + "zerofrom-derive", 3849 + ] 3850 + 3851 + [[package]] 3852 + name = "zerofrom-derive" 3853 + version = "0.1.6" 3854 + source = "registry+https://github.com/rust-lang/crates.io-index" 3855 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 3856 + dependencies = [ 3857 + "proc-macro2", 3858 + "quote", 3859 + "syn", 3860 + "synstructure", 3861 + ] 3862 + 3863 + [[package]] 3864 + name = "zeroize" 3865 + version = "1.8.2" 3866 + source = "registry+https://github.com/rust-lang/crates.io-index" 3867 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 3868 + 3869 + [[package]] 3870 + name = "zerotrie" 3871 + version = "0.2.3" 3872 + source = "registry+https://github.com/rust-lang/crates.io-index" 3873 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 3874 + dependencies = [ 3875 + "displaydoc", 3876 + "yoke", 3877 + "zerofrom", 3878 + ] 3879 + 3880 + [[package]] 3881 + name = "zerovec" 3882 + version = "0.11.5" 3883 + source = "registry+https://github.com/rust-lang/crates.io-index" 3884 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 3885 + dependencies = [ 3886 + "yoke", 3887 + "zerofrom", 3888 + "zerovec-derive", 3889 + ] 3890 + 3891 + [[package]] 3892 + name = "zerovec-derive" 3893 + version = "0.11.2" 3894 + source = "registry+https://github.com/rust-lang/crates.io-index" 3895 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 3896 + dependencies = [ 3897 + "proc-macro2", 3898 + "quote", 3899 + "syn", 3900 + ] 3901 + 3902 + [[package]] 3903 + name = "zmij" 3904 + version = "1.0.18" 3905 + source = "registry+https://github.com/rust-lang/crates.io-index" 3906 + checksum = "1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214"
+51
Cargo.toml
··· 1 + [workspace] 2 + resolver = "2" 3 + members = [ 4 + "onis-common", 5 + "onis-appview", 6 + "onis-dns", 7 + "onis-verify", 8 + ] 9 + 10 + [workspace.metadata.crane] 11 + name = "onis" 12 + 13 + [workspace.package] 14 + version = "0.1.0" 15 + edition = "2024" 16 + license = "MIT" 17 + repository = "https://github.com/blu/onis" 18 + 19 + [workspace.lints.clippy] 20 + unwrap_used = "deny" 21 + expect_used = "deny" 22 + panic = "deny" 23 + todo = "deny" 24 + indexing_slicing = "deny" 25 + unwrap_in_result = "deny" 26 + pedantic = "warn" 27 + 28 + [workspace.dependencies] 29 + onis-common = { path = "onis-common" } 30 + serde = { version = "1.0.228", features = ["derive"] } 31 + serde_json = "1.0.149" 32 + tracing = "0.1.44" 33 + tracing-subscriber = { version = "0.3.22", features = ["json", "env-filter"] } 34 + sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] } 35 + anyhow = "1.0.100" 36 + thiserror = "2.0.18" 37 + tokio = { version = "1.49.0", features = ["full"] } 38 + tokio-tungstenite = { version = "0.28", features = ["native-tls"] } 39 + futures-util = "0.3.31" 40 + async-trait = "0.1.89" 41 + atrium-api = "0.25.7" 42 + axum = "0.8.8" 43 + reqwest = { version = "0.13.1", default-features = false, features = ["json"] } 44 + chrono = "0.4.43" 45 + toml = "0.9.8" 46 + hickory-server = "0.25.1" 47 + hickory-proto = "0.25.1" 48 + hickory-resolver = "0.25.1" 49 + metrics = "0.24" 50 + metrics-exporter-prometheus = "0.18" 51 +
+172
README.md
··· 1 + # onis 2 + 3 + decentralized dns over atproto :3 4 + 5 + [pdsls](https://pdsls.dev) is the easiest way to manage your records currently. 6 + 7 + needed: 8 + - atproto account 9 + - domain 10 + - access to the domain's current NS settings 11 + 12 + ## 1. declare your zone 13 + 14 + create a `systems.kiri.zone` record on your PDS: 15 + 16 + ```json 17 + { 18 + "$type": "systems.kiri.zone", 19 + "domain": "example.com" 20 + } 21 + ``` 22 + 23 + ## 2. set up verification 24 + 25 + onis needs to confirm you actually control the domain two things are checked: 26 + 27 + **a) NS delegation** 28 + 29 + ``` 30 + ns1.kiri.systems 31 + ns2.kiri.systems 32 + ``` 33 + 34 + if you are running this yourself it should match your `expected_ns` config 35 + 36 + **b) TXT ownership proof** 37 + 38 + ``` 39 + _onis-verify.example.com. TXT "did:plc:your-did-here" 40 + ``` 41 + 42 + once both of these are passing your domain is verified :D 43 + 44 + ## 3. publish dns records 45 + 46 + you will also need to copy the TXT `_onis-verify.example.com` verification check to a `systems.kiri.dns#txtRecord` as well. 47 + 48 + **A record:** 49 + 50 + ```json 51 + { 52 + "$type": "systems.kiri.dns", 53 + "domain": "example.com", 54 + "ttl": 300, 55 + "record": { 56 + "$type": "systems.kiri.dns#aRecord", 57 + "address": "93.184.216.34" 58 + } 59 + } 60 + ``` 61 + 62 + **AAAA record:** 63 + 64 + ```json 65 + { 66 + "$type": "systems.kiri.dns", 67 + "domain": "example.com", 68 + "ttl": 300, 69 + "record": { 70 + "$type": "systems.kiri.dns#aaaaRecord", 71 + "address": "2001:db8::1" 72 + } 73 + } 74 + ``` 75 + 76 + **CNAME record:** 77 + 78 + ```json 79 + { 80 + "$type": "systems.kiri.dns", 81 + "domain": "www.example.com", 82 + "ttl": 300, 83 + "record": { 84 + "$type": "systems.kiri.dns#cnameRecord", 85 + "cname": "example.com" 86 + } 87 + } 88 + ``` 89 + 90 + **MX record:** 91 + 92 + ```json 93 + { 94 + "$type": "systems.kiri.dns", 95 + "domain": "example.com", 96 + "ttl": 300, 97 + "record": { 98 + "$type": "systems.kiri.dns#mxRecord", 99 + "preference": 10, 100 + "exchange": "mail.example.com" 101 + } 102 + } 103 + ``` 104 + 105 + **TXT record:** 106 + 107 + ```json 108 + { 109 + "$type": "systems.kiri.dns", 110 + "domain": "example.com", 111 + "ttl": 300, 112 + "record": { 113 + "$type": "systems.kiri.dns#txtRecord", 114 + "values": ["v=spf1 include:example.com ~all"] 115 + } 116 + } 117 + ``` 118 + 119 + **SRV record:** 120 + 121 + ```json 122 + { 123 + "$type": "systems.kiri.dns", 124 + "domain": "_sip._tcp.example.com", 125 + "ttl": 300, 126 + "record": { 127 + "$type": "systems.kiri.dns#srvRecord", 128 + "priority": 10, 129 + "weight": 60, 130 + "port": 5060, 131 + "target": "sip.example.com" 132 + } 133 + } 134 + ``` 135 + 136 + `ttl` is optional on all records — if you leave it out, the zone's SOA minimum is used (default 300s). 137 + *WARNING* => on ns1.kiri.systems and ns2.kiri.systems this is a floor of 60 seconds. 138 + 139 + ## full example 140 + 141 + Here's what a real setup looks like. User `did:plc:adtzorbhmmjbzxsl2y4vqlqs` setting up `blu.red`: 142 + 143 + 144 + **zone declaration** (`systems.kiri.zone` collection): 145 + https://pds.ls/at://did:plc:adtzorbhmmjbzxsl2y4vqlqs/systems.kiri.zone/3mdr6rm2trm2y 146 + 147 + ```json 148 + { 149 + "$type": "systems.kiri.zone", 150 + "domain": "blu.red" 151 + } 152 + ``` 153 + 154 + 155 + **verification TXT** (`systems.kiri.dns` collection): 156 + https://pds.ls/at://did:plc:adtzorbhmmjbzxsl2y4vqlqs/systems.kiri.dns/3mdt7enp5nu2y 157 + 158 + ```json 159 + { 160 + "$type": "systems.kiri.dns", 161 + "domain": "_onis-verify.blu.red", 162 + "record": { 163 + "$type": "systems.kiri.dns#txtRecord", 164 + "values": [ 165 + "did:plc:adtzorbhmmjbzxsl2y4vqlqs" 166 + ] 167 + } 168 + } 169 + ``` 170 + ## License 171 + 172 + MIT
+77
flake.lock
··· 1 + { 2 + "nodes": { 3 + "crane": { 4 + "locked": { 5 + "lastModified": 1769737823, 6 + "narHash": "sha256-DrBaNpZ+sJ4stXm+0nBX7zqZT9t9P22zbk6m5YhQxS4=", 7 + "owner": "ipetkov", 8 + "repo": "crane", 9 + "rev": "b2f45c3830aa96b7456a4c4bc327d04d7a43e1ba", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "ipetkov", 14 + "repo": "crane", 15 + "type": "github" 16 + } 17 + }, 18 + "flake-utils": { 19 + "inputs": { 20 + "systems": "systems" 21 + }, 22 + "locked": { 23 + "lastModified": 1731533236, 24 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 25 + "owner": "numtide", 26 + "repo": "flake-utils", 27 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 28 + "type": "github" 29 + }, 30 + "original": { 31 + "owner": "numtide", 32 + "repo": "flake-utils", 33 + "type": "github" 34 + } 35 + }, 36 + "nixpkgs": { 37 + "locked": { 38 + "lastModified": 1769740369, 39 + "narHash": "sha256-xKPyJoMoXfXpDM5DFDZDsi9PHArf2k5BJjvReYXoFpM=", 40 + "owner": "NixOS", 41 + "repo": "nixpkgs", 42 + "rev": "6308c3b21396534d8aaeac46179c14c439a89b8a", 43 + "type": "github" 44 + }, 45 + "original": { 46 + "owner": "NixOS", 47 + "ref": "nixpkgs-unstable", 48 + "repo": "nixpkgs", 49 + "type": "github" 50 + } 51 + }, 52 + "root": { 53 + "inputs": { 54 + "crane": "crane", 55 + "flake-utils": "flake-utils", 56 + "nixpkgs": "nixpkgs" 57 + } 58 + }, 59 + "systems": { 60 + "locked": { 61 + "lastModified": 1681028828, 62 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 63 + "owner": "nix-systems", 64 + "repo": "default", 65 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 66 + "type": "github" 67 + }, 68 + "original": { 69 + "owner": "nix-systems", 70 + "repo": "default", 71 + "type": "github" 72 + } 73 + } 74 + }, 75 + "root": "root", 76 + "version": 7 77 + }
+411
flake.nix
··· 1 + { 2 + description = "onis — decentralized DNS over ATProto"; 3 + 4 + inputs = { 5 + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 + crane.url = "github:ipetkov/crane"; 7 + flake-utils.url = "github:numtide/flake-utils"; 8 + }; 9 + 10 + outputs = { self, nixpkgs, crane, flake-utils, ... }: 11 + let 12 + perSystem = flake-utils.lib.eachSystem [ "x86_64-linux" ] (system: 13 + let 14 + pkgs = import nixpkgs { 15 + inherit system; 16 + }; 17 + 18 + craneLib = crane.mkLib pkgs; 19 + 20 + commonArgs = { 21 + src = craneLib.cleanCargoSource ./.; 22 + 23 + buildInputs = with pkgs; [ 24 + openssl 25 + ]; 26 + 27 + nativeBuildInputs = with pkgs; [ 28 + pkg-config 29 + ]; 30 + }; 31 + 32 + cargoArtifacts = craneLib.buildDepsOnly commonArgs; 33 + 34 + onis-appview = craneLib.buildPackage (commonArgs // { 35 + inherit cargoArtifacts; 36 + cargoExtraArgs = "--bin onis-appview"; 37 + }); 38 + 39 + onis-dns = craneLib.buildPackage (commonArgs // { 40 + inherit cargoArtifacts; 41 + cargoExtraArgs = "--bin onis-dns"; 42 + }); 43 + 44 + onis-verify = craneLib.buildPackage (commonArgs // { 45 + inherit cargoArtifacts; 46 + cargoExtraArgs = "--bin onis-verify"; 47 + }); 48 + in 49 + { 50 + packages = { 51 + inherit onis-appview onis-dns onis-verify; 52 + default = pkgs.symlinkJoin { 53 + name = "onis"; 54 + paths = [ onis-appview onis-dns onis-verify ]; 55 + }; 56 + }; 57 + 58 + checks = { 59 + onis-clippy = craneLib.cargoClippy (commonArgs // { 60 + inherit cargoArtifacts; 61 + cargoClippyExtraArgs = "-- --deny warnings"; 62 + }); 63 + 64 + onis-fmt = craneLib.cargoFmt { 65 + src = commonArgs.src; 66 + }; 67 + 68 + onis-test = craneLib.cargoNextest (commonArgs // { 69 + inherit cargoArtifacts; 70 + }); 71 + }; 72 + 73 + devShells.default = craneLib.devShell { 74 + checks = self.checks.${system}; 75 + 76 + packages = with pkgs; [ 77 + clippy 78 + rust-analyzer 79 + sqlx-cli 80 + cargo-watch 81 + pkg-config 82 + openssl 83 + sqlite 84 + dig 85 + ]; 86 + }; 87 + } 88 + ); 89 + in 90 + perSystem // { 91 + 92 + nixosModules.default = { config, lib, pkgs, ... }: 93 + let 94 + inherit (lib) mkEnableOption mkOption types mkIf mkMerge; 95 + cfg = config.services.onis; 96 + settingsFormat = pkgs.formats.toml { }; 97 + 98 + configFile = settingsFormat.generate "onis.toml" { 99 + appview = { 100 + inherit (cfg.appview) bind tap_url tap_acks tap_reconnect_delay index_path db_dir; 101 + database = { 102 + inherit (cfg.appview.database) busy_timeout user_max_connections index_max_connections; 103 + }; 104 + }; 105 + dns = { 106 + inherit (cfg.dns) appview_url bind port tcp_timeout ttl_floor slow_query_threshold_ms ns metrics_bind; 107 + soa = { 108 + inherit (cfg.dns.soa) ttl refresh retry expire minimum mname rname; 109 + }; 110 + }; 111 + verify = { 112 + inherit (cfg.verify) appview_url bind port check_interval recheck_interval expected_ns nameservers dns_port; 113 + }; 114 + }; 115 + in 116 + { 117 + options.services.onis = { 118 + 119 + # ----------------------------------------------------------------- 120 + # Appview 121 + # ----------------------------------------------------------------- 122 + appview = { 123 + enable = mkEnableOption "onis appview service"; 124 + 125 + package = mkOption { 126 + type = types.package; 127 + default = self.packages.${pkgs.system}.onis-appview; 128 + defaultText = lib.literalExpression "self.packages.\${pkgs.system}.onis-appview"; 129 + description = "The onis-appview package to use."; 130 + }; 131 + 132 + bind = mkOption { 133 + type = types.str; 134 + default = "localhost:3000"; 135 + description = "Address and port for the appview HTTP server."; 136 + }; 137 + 138 + tap_url = mkOption { 139 + type = types.str; 140 + default = "ws://localhost:2480/channel"; 141 + description = "WebSocket URL for the TAP firehose."; 142 + }; 143 + 144 + tap_acks = mkOption { 145 + type = types.bool; 146 + default = true; 147 + description = "Whether to acknowledge TAP messages."; 148 + }; 149 + 150 + tap_reconnect_delay = mkOption { 151 + type = types.int; 152 + default = 5; 153 + description = "Seconds to wait before reconnecting after a TAP connection error."; 154 + }; 155 + 156 + index_path = mkOption { 157 + type = types.str; 158 + default = "/var/lib/onis-appview/index.db"; 159 + description = "Path to the shared index SQLite database."; 160 + }; 161 + 162 + db_dir = mkOption { 163 + type = types.str; 164 + default = "/var/lib/onis-appview/dbs"; 165 + description = "Directory for per-user SQLite databases."; 166 + }; 167 + 168 + database = { 169 + busy_timeout = mkOption { 170 + type = types.int; 171 + default = 5; 172 + description = "Seconds to wait when the database is locked."; 173 + }; 174 + 175 + user_max_connections = mkOption { 176 + type = types.int; 177 + default = 5; 178 + description = "Max connections for per-user database pools."; 179 + }; 180 + 181 + index_max_connections = mkOption { 182 + type = types.int; 183 + default = 10; 184 + description = "Max connections for the shared index database pool."; 185 + }; 186 + }; 187 + }; 188 + 189 + # ----------------------------------------------------------------- 190 + # DNS 191 + # ----------------------------------------------------------------- 192 + dns = { 193 + enable = mkEnableOption "onis DNS server"; 194 + 195 + package = mkOption { 196 + type = types.package; 197 + default = self.packages.${pkgs.system}.onis-dns; 198 + defaultText = lib.literalExpression "self.packages.\${pkgs.system}.onis-dns"; 199 + description = "The onis-dns package to use."; 200 + }; 201 + 202 + appview_url = mkOption { 203 + type = types.str; 204 + default = "http://localhost:3000"; 205 + description = "URL of the onis appview API."; 206 + }; 207 + 208 + bind = mkOption { 209 + type = types.str; 210 + default = "0.0.0.0"; 211 + description = "Address for the DNS server to listen on."; 212 + }; 213 + 214 + port = mkOption { 215 + type = types.port; 216 + default = 53; 217 + description = "Port for the DNS server."; 218 + }; 219 + 220 + tcp_timeout = mkOption { 221 + type = types.int; 222 + default = 30; 223 + description = "Seconds before a TCP connection times out."; 224 + }; 225 + 226 + ttl_floor = mkOption { 227 + type = types.int; 228 + default = 60; 229 + description = "Minimum TTL enforced on all DNS responses."; 230 + }; 231 + 232 + slow_query_threshold_ms = mkOption { 233 + type = types.int; 234 + default = 50; 235 + description = "Log a warning for queries slower than this (milliseconds)."; 236 + }; 237 + 238 + ns = mkOption { 239 + type = types.listOf types.str; 240 + default = [ "ns1.example.com." "ns2.example.com." ]; 241 + description = "NS records to serve for all zones (fully qualified, trailing dot)."; 242 + }; 243 + 244 + metrics_bind = mkOption { 245 + type = types.str; 246 + default = "0.0.0.0:9100"; 247 + description = "Address and port for the DNS metrics HTTP server."; 248 + }; 249 + 250 + soa = { 251 + ttl = mkOption { 252 + type = types.int; 253 + default = 3600; 254 + description = "SOA record TTL in seconds."; 255 + }; 256 + 257 + refresh = mkOption { 258 + type = types.int; 259 + default = 3600; 260 + description = "SOA refresh interval in seconds."; 261 + }; 262 + 263 + retry = mkOption { 264 + type = types.int; 265 + default = 900; 266 + description = "SOA retry interval in seconds."; 267 + }; 268 + 269 + expire = mkOption { 270 + type = types.int; 271 + default = 604800; 272 + description = "SOA expire interval in seconds."; 273 + }; 274 + 275 + minimum = mkOption { 276 + type = types.int; 277 + default = 300; 278 + description = "SOA minimum (negative cache) TTL in seconds."; 279 + }; 280 + 281 + mname = mkOption { 282 + type = types.str; 283 + default = "ns1.example.com."; 284 + description = "SOA MNAME — primary nameserver, fully qualified."; 285 + }; 286 + 287 + rname = mkOption { 288 + type = types.str; 289 + default = "admin.example.com."; 290 + description = "SOA RNAME — admin email in DNS format, fully qualified."; 291 + }; 292 + }; 293 + }; 294 + 295 + # ----------------------------------------------------------------- 296 + # Verify 297 + # ----------------------------------------------------------------- 298 + verify = { 299 + enable = mkEnableOption "onis verify service"; 300 + 301 + package = mkOption { 302 + type = types.package; 303 + default = self.packages.${pkgs.system}.onis-verify; 304 + defaultText = lib.literalExpression "self.packages.\${pkgs.system}.onis-verify"; 305 + description = "The onis-verify package to use."; 306 + }; 307 + 308 + appview_url = mkOption { 309 + type = types.str; 310 + default = "http://localhost:3000"; 311 + description = "URL of the onis appview API."; 312 + }; 313 + 314 + bind = mkOption { 315 + type = types.str; 316 + default = "0.0.0.0"; 317 + description = "Address for the verify HTTP server to listen on."; 318 + }; 319 + 320 + port = mkOption { 321 + type = types.port; 322 + default = 3001; 323 + description = "Port for the verify HTTP server."; 324 + }; 325 + 326 + check_interval = mkOption { 327 + type = types.int; 328 + default = 60; 329 + description = "Seconds between scheduled verification runs."; 330 + }; 331 + 332 + recheck_interval = mkOption { 333 + type = types.int; 334 + default = 3600; 335 + description = "Seconds a zone must be stale before rechecking."; 336 + }; 337 + 338 + expected_ns = mkOption { 339 + type = types.listOf types.str; 340 + default = [ "ns1.example.com" "ns2.example.com" ]; 341 + description = "Expected NS records that indicate correct delegation."; 342 + }; 343 + 344 + nameservers = mkOption { 345 + type = types.listOf types.str; 346 + default = [ ]; 347 + description = "Optional custom resolver IP addresses."; 348 + }; 349 + 350 + dns_port = mkOption { 351 + type = types.port; 352 + default = 53; 353 + description = "Port used when resolving against custom nameservers."; 354 + }; 355 + }; 356 + }; 357 + 358 + config = mkMerge [ 359 + (mkIf cfg.appview.enable { 360 + systemd.services.onis-appview = { 361 + description = "onis appview — ATProto DNS appview"; 362 + wantedBy = [ "multi-user.target" ]; 363 + after = [ "network.target" ]; 364 + environment.ONIS_CONFIG = "${configFile}"; 365 + serviceConfig = { 366 + ExecStart = "${cfg.appview.package}/bin/onis-appview"; 367 + DynamicUser = true; 368 + StateDirectory = "onis-appview"; 369 + Restart = "on-failure"; 370 + RestartSec = 5; 371 + }; 372 + }; 373 + }) 374 + 375 + (mkIf cfg.dns.enable { 376 + systemd.services.onis-dns = { 377 + description = "onis DNS — authoritative DNS server"; 378 + wantedBy = [ "multi-user.target" ]; 379 + after = [ "network.target" ]; 380 + environment.ONIS_CONFIG = "${configFile}"; 381 + serviceConfig = { 382 + ExecStart = "${cfg.dns.package}/bin/onis-dns"; 383 + DynamicUser = true; 384 + StateDirectory = "onis-dns"; 385 + Restart = "on-failure"; 386 + RestartSec = 5; 387 + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 388 + }; 389 + }; 390 + }) 391 + 392 + (mkIf cfg.verify.enable { 393 + systemd.services.onis-verify = { 394 + description = "onis verify — DNS delegation checker"; 395 + wantedBy = [ "multi-user.target" ]; 396 + after = [ "network.target" ]; 397 + environment.ONIS_CONFIG = "${configFile}"; 398 + serviceConfig = { 399 + ExecStart = "${cfg.verify.package}/bin/onis-verify"; 400 + DynamicUser = true; 401 + StateDirectory = "onis-verify"; 402 + Restart = "on-failure"; 403 + RestartSec = 5; 404 + }; 405 + }; 406 + }) 407 + ]; 408 + }; 409 + 410 + }; 411 + }
+185
lexicons/systems/kiri/dns.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "systems.kiri.dns", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "description": "A DNS record managed through ATProto.", 9 + "record": { 10 + "type": "object", 11 + "required": ["domain", "record"], 12 + "properties": { 13 + "domain": { 14 + "type": "string", 15 + "description": "Fully qualified domain name, lowercase, no trailing dot.", 16 + "maxLength": 253 17 + }, 18 + "ttl": { 19 + "type": "integer", 20 + "description": "Time to live in seconds. Optional — falls back to SOA minimum for the zone if omitted. Server enforces a 60s floor.", 21 + "minimum": 0, 22 + "maximum": 2147483647 23 + }, 24 + "note": { 25 + "type": "string", 26 + "description": "Attached note for informational reasons, not used by dns.", 27 + "maxGraphemes": 256 28 + }, 29 + "record": { 30 + "type": "union", 31 + "description": "The DNS record data.", 32 + "refs": [ 33 + "#aRecord", 34 + "#aaaaRecord", 35 + "#cnameRecord", 36 + "#mxRecord", 37 + "#txtRecord", 38 + "#srvRecord", 39 + "#soaRecord" 40 + ] 41 + } 42 + } 43 + } 44 + }, 45 + 46 + "aRecord": { 47 + "type": "object", 48 + "description": "A 32 bit Internet address.", 49 + "required": ["address"], 50 + "properties": { 51 + "address": { 52 + "type": "string", 53 + "description": "IPv4 address in dotted-decimal notation.", 54 + "maxLength": 15 55 + } 56 + } 57 + }, 58 + 59 + "aaaaRecord": { 60 + "type": "object", 61 + "description": "An 128 bit Internet address.", 62 + "required": ["address"], 63 + "properties": { 64 + "address": { 65 + "type": "string", 66 + "description": "IPv6 address in standard notation.", 67 + "maxLength": 45 68 + } 69 + } 70 + }, 71 + 72 + "cnameRecord": { 73 + "type": "object", 74 + "description": "A domain name which specifies the canonical or primary name for the owner.", 75 + "required": ["cname"], 76 + "properties": { 77 + "cname": { 78 + "type": "string", 79 + "description": "The canonical domain name.", 80 + "maxLength": 253 81 + } 82 + } 83 + }, 84 + 85 + "mxRecord": { 86 + "type": "object", 87 + "description": "A mail exchange record.", 88 + "required": ["preference", "exchange"], 89 + "properties": { 90 + "preference": { 91 + "type": "integer", 92 + "description": "Priority value. Lower is preferred.", 93 + "minimum": 0, 94 + "maximum": 65535 95 + }, 96 + "exchange": { 97 + "type": "string", 98 + "description": "The mail server hostname.", 99 + "maxLength": 253 100 + } 101 + } 102 + }, 103 + 104 + "txtRecord": { 105 + "type": "object", 106 + "description": "A text record. TXT-DATA is one or more character strings.", 107 + "required": ["values"], 108 + "properties": { 109 + "values": { 110 + "type": "array", 111 + "description": "One or more character strings. Each string has a 255-byte max on the wire.", 112 + "items": { 113 + "type": "string", 114 + "maxLength": 255 115 + }, 116 + "minLength": 1, 117 + "maxLength": 16 118 + } 119 + } 120 + }, 121 + 122 + "srvRecord": { 123 + "type": "object", 124 + "description": "A service locator record.", 125 + "required": ["priority", "weight", "port", "target"], 126 + "properties": { 127 + "priority": { 128 + "type": "integer", 129 + "minimum": 0, 130 + "maximum": 65535 131 + }, 132 + "weight": { 133 + "type": "integer", 134 + "minimum": 0, 135 + "maximum": 65535 136 + }, 137 + "port": { 138 + "type": "integer", 139 + "minimum": 0, 140 + "maximum": 65535 141 + }, 142 + "target": { 143 + "type": "string", 144 + "description": "The target hostname.", 145 + "maxLength": 253 146 + } 147 + } 148 + }, 149 + 150 + "soaRecord": { 151 + "type": "object", 152 + "description": "Start of authority. Optional — system generates defaults if absent. Serial is also generated.", 153 + "required": ["mname", "rname", "refresh", "retry", "expire", "minimum"], 154 + "properties": { 155 + "mname": { 156 + "type": "string", 157 + "description": "Primary nameserver.", 158 + "maxLength": 253 159 + }, 160 + "rname": { 161 + "type": "string", 162 + "description": "Responsible person email in DNS format (e.g. admin.example.com).", 163 + "maxLength": 253 164 + }, 165 + "refresh": { 166 + "type": "integer", 167 + "minimum": 0 168 + }, 169 + "retry": { 170 + "type": "integer", 171 + "minimum": 0 172 + }, 173 + "expire": { 174 + "type": "integer", 175 + "minimum": 0 176 + }, 177 + "minimum": { 178 + "type": "integer", 179 + "description": "Minimum TTL / negative cache TTL. Also used as the fallback TTL for records in this zone that don't specify one.", 180 + "minimum": 0 181 + } 182 + } 183 + } 184 + } 185 + }
+26
lexicons/systems/kiri/zone.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "systems.kiri.zone", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "required": ["domain"], 11 + "properties": { 12 + "domain": { 13 + "type": "string", 14 + "description": "The zone apex domain. Lowercase, no trailing dot.", 15 + "maxLength": 253 16 + }, 17 + "note": { 18 + "type": "string", 19 + "description": "Attached note for informational reasons, not used by dns.", 20 + "maxGraphemes": 256 21 + } 22 + } 23 + } 24 + } 25 + } 26 + }
+13
migrations/index/001_init.sql
··· 1 + -- Shared reverse index schema 2 + 3 + CREATE TABLE IF NOT EXISTS zone_index ( 4 + zone TEXT NOT NULL, 5 + did TEXT NOT NULL, 6 + verified INTEGER NOT NULL DEFAULT 0, 7 + first_seen INTEGER NOT NULL, 8 + last_verified INTEGER, 9 + PRIMARY KEY (zone, did) 10 + ); 11 + 12 + CREATE INDEX IF NOT EXISTS idx_zone_index_did 13 + ON zone_index(did);
+22
migrations/user/001_init.sql
··· 1 + -- Per-DID database schema 2 + 3 + CREATE TABLE IF NOT EXISTS records ( 4 + rkey TEXT PRIMARY KEY, 5 + domain TEXT NOT NULL, 6 + record_type TEXT NOT NULL, 7 + data TEXT NOT NULL, -- JSON-encoded full DnsRecord (domain + ttl + record) 8 + created_at INTEGER NOT NULL, 9 + updated_at INTEGER NOT NULL 10 + ); 11 + 12 + CREATE TABLE IF NOT EXISTS zones ( 13 + rkey TEXT PRIMARY KEY, 14 + domain TEXT NOT NULL UNIQUE, 15 + created_at INTEGER NOT NULL 16 + ); 17 + 18 + CREATE INDEX IF NOT EXISTS idx_records_domain 19 + ON records(domain); 20 + 21 + CREATE INDEX IF NOT EXISTS idx_records_domain_type 22 + ON records(domain, record_type);
+26
onis-appview/Cargo.toml
··· 1 + [package] 2 + name = "onis-appview" 3 + version.workspace = true 4 + edition.workspace = true 5 + 6 + [lints] 7 + workspace = true 8 + 9 + [[bin]] 10 + name = "onis-appview" 11 + path = "src/main.rs" 12 + 13 + [dependencies] 14 + onis-common.workspace = true 15 + serde.workspace = true 16 + serde_json.workspace = true 17 + sqlx.workspace = true 18 + tokio.workspace = true 19 + tracing.workspace = true 20 + tracing-subscriber.workspace = true 21 + anyhow.workspace = true 22 + axum.workspace = true 23 + tokio-tungstenite.workspace = true 24 + futures-util.workspace = true 25 + chrono.workspace = true 26 + metrics.workspace = true
+298
onis-appview/src/api.rs
··· 1 + use std::sync::Arc; 2 + use std::time::Instant; 3 + 4 + use axum::{ 5 + Json, Router, 6 + extract::{Query, Request, State}, 7 + http::StatusCode, 8 + middleware::{self, Next}, 9 + response::IntoResponse, 10 + routing::{get, put}, 11 + }; 12 + use serde::{Deserialize, Serialize}; 13 + 14 + use crate::materializer::{AppState, domain_ancestors}; 15 + 16 + pub fn router(state: Arc<AppState>) -> Router { 17 + let metrics_router = onis_common::metrics::router(state.metrics_handle.clone()); 18 + 19 + Router::new() 20 + .route("/v1/resolve", get(resolve)) 21 + .route("/v1/health", get(health)) 22 + .route("/v1/zones", get(zones)) 23 + .route("/v1/zones/stale", get(stale_zones)) 24 + .route("/v1/verification", put(verification)) 25 + .layer(middleware::from_fn(track_request_duration)) 26 + .with_state(state) 27 + .merge(metrics_router) 28 + } 29 + 30 + async fn track_request_duration(request: Request, next: Next) -> impl IntoResponse { 31 + let path = request.uri().path().to_owned(); 32 + let start = Instant::now(); 33 + let response = next.run(request).await; 34 + metrics::histogram!("appview_api_request_duration_seconds", "path" => path) 35 + .record(start.elapsed().as_secs_f64()); 36 + response 37 + } 38 + 39 + // --------------------------------------------------------------------------- 40 + // GET /v1/resolve?name={name}&type={type} 41 + // --------------------------------------------------------------------------- 42 + 43 + #[derive(Deserialize)] 44 + struct ResolveParams { 45 + name: String, 46 + #[serde(rename = "type")] 47 + record_type: Option<String>, 48 + } 49 + 50 + #[derive(Serialize)] 51 + struct ResolveResponse { 52 + zone: Option<String>, 53 + verified: bool, 54 + records: Vec<serde_json::Value>, 55 + name_exists: bool, 56 + } 57 + 58 + /// Walk up the domain tree to find the matching zone in zone_index. 59 + async fn find_zone( 60 + index: &sqlx::SqlitePool, 61 + name: &str, 62 + ) -> Result<Option<(String, String, bool)>, sqlx::Error> { 63 + for candidate in domain_ancestors(name) { 64 + let row: Option<(String, i64)> = 65 + sqlx::query_as( 66 + "SELECT did, verified FROM zone_index WHERE zone = ? ORDER BY verified DESC, first_seen ASC LIMIT 1" 67 + ) 68 + .bind(&candidate) 69 + .fetch_optional(index) 70 + .await?; 71 + if let Some((did, verified)) = row { 72 + return Ok(Some((candidate, did, verified != 0))); 73 + } 74 + } 75 + Ok(None) 76 + } 77 + 78 + async fn resolve( 79 + State(state): State<Arc<AppState>>, 80 + Query(params): Query<ResolveParams>, 81 + ) -> Result<Json<ResolveResponse>, StatusCode> { 82 + let name = params.name.to_lowercase(); 83 + 84 + let zone_match = find_zone(&state.index, &name) 85 + .await 86 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 87 + 88 + let (zone, did, verified) = match zone_match { 89 + Some(z) => z, 90 + None => { 91 + return Ok(Json(ResolveResponse { 92 + zone: None, 93 + verified: false, 94 + records: vec![], 95 + name_exists: false, 96 + })); 97 + } 98 + }; 99 + 100 + if !verified { 101 + return Ok(Json(ResolveResponse { 102 + zone: Some(zone), 103 + verified: false, 104 + records: vec![], 105 + name_exists: false, 106 + })); 107 + } 108 + 109 + let user_db = state 110 + .get_user_db(&did) 111 + .await 112 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 113 + 114 + let records: Vec<(String,)> = if let Some(ref rt) = params.record_type { 115 + sqlx::query_as( 116 + "SELECT data FROM records WHERE domain = ? AND record_type = ?", 117 + ) 118 + .bind(&name) 119 + .bind(rt) 120 + .fetch_all(&user_db) 121 + .await 122 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? 123 + } else { 124 + sqlx::query_as("SELECT data FROM records WHERE domain = ?") 125 + .bind(&name) 126 + .fetch_all(&user_db) 127 + .await 128 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? 129 + }; 130 + 131 + let name_exists: Option<(i64,)> = 132 + sqlx::query_as("SELECT 1 FROM records WHERE domain = ? LIMIT 1") 133 + .bind(&name) 134 + .fetch_optional(&user_db) 135 + .await 136 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 137 + 138 + let records: Vec<serde_json::Value> = records 139 + .into_iter() 140 + .filter_map(|(data,)| serde_json::from_str(&data).ok()) 141 + .collect(); 142 + 143 + Ok(Json(ResolveResponse { 144 + zone: Some(zone), 145 + verified: true, 146 + records, 147 + name_exists: name_exists.is_some(), 148 + })) 149 + } 150 + 151 + // --------------------------------------------------------------------------- 152 + // GET /v1/health 153 + // --------------------------------------------------------------------------- 154 + 155 + #[derive(Serialize)] 156 + struct HealthResponse { 157 + status: String, 158 + zones: i64, 159 + users: i64, 160 + } 161 + 162 + async fn health( 163 + State(state): State<Arc<AppState>>, 164 + ) -> Result<Json<HealthResponse>, StatusCode> { 165 + let (zones,): (i64,) = 166 + sqlx::query_as("SELECT COUNT(*) FROM zone_index") 167 + .fetch_one(&state.index) 168 + .await 169 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 170 + 171 + let (users,): (i64,) = 172 + sqlx::query_as("SELECT COUNT(DISTINCT did) FROM zone_index") 173 + .fetch_one(&state.index) 174 + .await 175 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 176 + 177 + Ok(Json(HealthResponse { 178 + status: "ok".to_string(), 179 + zones, 180 + users, 181 + })) 182 + } 183 + 184 + // --------------------------------------------------------------------------- 185 + // GET /v1/zones?did={did} 186 + // --------------------------------------------------------------------------- 187 + 188 + #[derive(Deserialize)] 189 + struct ZonesParams { 190 + did: String, 191 + } 192 + 193 + #[derive(Serialize, sqlx::FromRow)] 194 + struct ZoneEntry { 195 + zone: String, 196 + verified: bool, 197 + } 198 + 199 + #[derive(Serialize)] 200 + struct ZonesResponse { 201 + zones: Vec<ZoneEntry>, 202 + } 203 + 204 + async fn zones( 205 + State(state): State<Arc<AppState>>, 206 + Query(params): Query<ZonesParams>, 207 + ) -> Result<Json<ZonesResponse>, StatusCode> { 208 + let zones: Vec<ZoneEntry> = 209 + sqlx::query_as("SELECT zone, verified FROM zone_index WHERE did = ?") 210 + .bind(&params.did) 211 + .fetch_all(&state.index) 212 + .await 213 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 214 + 215 + Ok(Json(ZonesResponse { zones })) 216 + } 217 + 218 + // --------------------------------------------------------------------------- 219 + // GET /v1/zones/stale?checked_before={unix_timestamp} 220 + // --------------------------------------------------------------------------- 221 + 222 + #[derive(Deserialize)] 223 + struct StaleZonesParams { 224 + checked_before: i64, 225 + } 226 + 227 + #[derive(Serialize, sqlx::FromRow)] 228 + struct StaleZoneEntry { 229 + zone: String, 230 + did: String, 231 + verified: bool, 232 + first_seen: i64, 233 + last_verified: Option<i64>, 234 + } 235 + 236 + #[derive(Serialize)] 237 + struct StaleZonesResponse { 238 + zones: Vec<StaleZoneEntry>, 239 + } 240 + 241 + async fn stale_zones( 242 + State(state): State<Arc<AppState>>, 243 + Query(params): Query<StaleZonesParams>, 244 + ) -> Result<Json<StaleZonesResponse>, StatusCode> { 245 + let zones: Vec<StaleZoneEntry> = sqlx::query_as( 246 + "SELECT zone, did, verified, first_seen, last_verified FROM zone_index \ 247 + WHERE last_verified < ? OR last_verified IS NULL", 248 + ) 249 + .bind(params.checked_before) 250 + .fetch_all(&state.index) 251 + .await 252 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 253 + 254 + Ok(Json(StaleZonesResponse { zones })) 255 + } 256 + 257 + // --------------------------------------------------------------------------- 258 + // PUT /v1/verification 259 + // --------------------------------------------------------------------------- 260 + 261 + #[derive(Deserialize)] 262 + struct VerificationBody { 263 + zone: String, 264 + did: String, 265 + verified: bool, 266 + } 267 + 268 + async fn verification( 269 + State(state): State<Arc<AppState>>, 270 + Json(body): Json<VerificationBody>, 271 + ) -> Result<StatusCode, StatusCode> { 272 + let now = chrono::Utc::now().timestamp(); 273 + let verified_int: i64 = if body.verified { 1 } else { 0 }; 274 + 275 + let result = sqlx::query( 276 + "UPDATE zone_index SET verified = ?, last_verified = ? WHERE zone = ? AND did = ?", 277 + ) 278 + .bind(verified_int) 279 + .bind(now) 280 + .bind(&body.zone) 281 + .bind(&body.did) 282 + .execute(&state.index) 283 + .await 284 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 285 + 286 + if result.rows_affected() == 0 { 287 + return Err(StatusCode::NOT_FOUND); 288 + } 289 + 290 + tracing::info!( 291 + zone = %body.zone, 292 + did = %body.did, 293 + verified = body.verified, 294 + "verification status updated" 295 + ); 296 + 297 + Ok(StatusCode::NO_CONTENT) 298 + }
+123
onis-appview/src/main.rs
··· 1 + use std::path::PathBuf; 2 + use std::sync::Arc; 3 + 4 + use anyhow::Result; 5 + use onis_common::config::OnisConfig; 6 + use tracing_subscriber::EnvFilter; 7 + 8 + mod api; 9 + mod materializer; 10 + mod tap; 11 + 12 + use materializer::AppState; 13 + use tap::TapConsumer; 14 + 15 + #[tokio::main] 16 + async fn main() -> Result<()> { 17 + tracing_subscriber::fmt() 18 + .with_env_filter(EnvFilter::from_default_env()) 19 + .json() 20 + .init(); 21 + 22 + let metrics_handle = onis_common::metrics::init() 23 + .map_err(|e| anyhow::anyhow!("failed to install metrics recorder: {e}"))?; 24 + 25 + metrics::describe_counter!( 26 + "appview_firehose_events_total", 27 + "Total firehose events processed" 28 + ); 29 + metrics::describe_counter!( 30 + "appview_records_skipped_no_zone_total", 31 + "DNS records skipped due to missing zone declaration" 32 + ); 33 + metrics::describe_histogram!( 34 + "appview_sqlite_write_duration_seconds", 35 + "SQLite write duration" 36 + ); 37 + metrics::describe_histogram!( 38 + "appview_api_request_duration_seconds", 39 + "API request duration" 40 + ); 41 + metrics::describe_gauge!("appview_zones_total", "Total declared zones"); 42 + metrics::describe_gauge!("appview_users_total", "Total unique DIDs with zones"); 43 + 44 + let config = OnisConfig::load()?; 45 + let cfg = &config.appview; 46 + 47 + tracing::info!( 48 + bind = %cfg.bind, 49 + tap_url = %cfg.tap_url, 50 + "onis-appview starting" 51 + ); 52 + 53 + let state = Arc::new( 54 + AppState::new( 55 + &PathBuf::from(&cfg.index_path), 56 + PathBuf::from(&cfg.db_dir), 57 + cfg.database.clone(), 58 + metrics_handle, 59 + ) 60 + .await?, 61 + ); 62 + 63 + let api_bind = cfg.bind.clone(); 64 + let api_state = state.clone(); 65 + let api_handle = tokio::spawn(async move { 66 + let app = api::router(api_state); 67 + let listener = tokio::net::TcpListener::bind(&api_bind).await.unwrap(); 68 + tracing::info!("API listening on {api_bind}"); 69 + axum::serve(listener, app).await.unwrap(); 70 + }); 71 + 72 + let consumer = TapConsumer::new( 73 + cfg.tap_url.clone(), 74 + cfg.tap_acks, 75 + cfg.tap_reconnect_delay, 76 + ); 77 + let tap_state = state.clone(); 78 + let tap_handle = tokio::spawn(async move { 79 + consumer 80 + .run(|event| { 81 + let s = tap_state.clone(); 82 + async move { materializer::handle_event(s, event).await } 83 + }) 84 + .await 85 + .unwrap(); 86 + }); 87 + 88 + let gauge_state = state.clone(); 89 + let gauge_handle = tokio::spawn(async move { 90 + update_gauges(gauge_state).await; 91 + }); 92 + 93 + tokio::select! { 94 + _ = api_handle => tracing::error!("API server exited unexpectedly"), 95 + _ = tap_handle => tracing::error!("tap consumer exited unexpectedly"), 96 + _ = gauge_handle => tracing::error!("gauge updater exited unexpectedly"), 97 + } 98 + 99 + Ok(()) 100 + } 101 + 102 + /// Periodically query zone/user counts and update Prometheus gauges. 103 + async fn update_gauges(state: Arc<AppState>) { 104 + loop { 105 + tokio::time::sleep(std::time::Duration::from_secs(30)).await; 106 + 107 + let zones: Result<(i64,), _> = 108 + sqlx::query_as("SELECT COUNT(*) FROM zone_index") 109 + .fetch_one(&state.index) 110 + .await; 111 + let users: Result<(i64,), _> = 112 + sqlx::query_as("SELECT COUNT(DISTINCT did) FROM zone_index") 113 + .fetch_one(&state.index) 114 + .await; 115 + 116 + if let Ok((count,)) = zones { 117 + metrics::gauge!("appview_zones_total").set(count as f64); 118 + } 119 + if let Ok((count,)) = users { 120 + metrics::gauge!("appview_users_total").set(count as f64); 121 + } 122 + } 123 + }
+339
onis-appview/src/materializer.rs
··· 1 + use std::path::PathBuf; 2 + use std::sync::Arc; 3 + use std::time::Instant; 4 + 5 + use anyhow::{Context, Result}; 6 + use onis_common::metrics::PrometheusHandle; 7 + use sqlx::SqlitePool; 8 + use tokio::sync::RwLock; 9 + 10 + use onis_common::config::DatabaseConfig; 11 + use onis_common::db; 12 + 13 + use crate::tap::{RecordAction, TapEvent, TapRecordEvent}; 14 + 15 + pub struct AppState { 16 + /// Reverse index: domain → DID, verification status 17 + pub index: SqlitePool, 18 + /// Base directory for per-DID SQLite databases 19 + pub db_dir: PathBuf, 20 + /// Database pool configuration 21 + pub db_config: DatabaseConfig, 22 + /// Cache of open per-DID database pools 23 + pub user_dbs: RwLock<std::collections::HashMap<String, SqlitePool>>, 24 + /// Prometheus metrics handle for /metrics endpoint 25 + pub metrics_handle: PrometheusHandle, 26 + } 27 + 28 + impl AppState { 29 + pub async fn new( 30 + index_path: &std::path::Path, 31 + db_dir: PathBuf, 32 + db_config: DatabaseConfig, 33 + metrics_handle: PrometheusHandle, 34 + ) -> Result<Self> { 35 + let index = db::open_index_db(index_path, &db_config).await?; 36 + Ok(Self { 37 + index, 38 + db_dir, 39 + db_config, 40 + user_dbs: RwLock::new(std::collections::HashMap::new()), 41 + metrics_handle, 42 + }) 43 + } 44 + 45 + /// Get or create a per-DID database pool. 46 + pub async fn get_user_db(&self, did: &str) -> Result<SqlitePool> { 47 + { 48 + let dbs = self.user_dbs.read().await; 49 + if let Some(pool) = dbs.get(did) { 50 + return Ok(pool.clone()); 51 + } 52 + } 53 + 54 + let path = db::did_to_db_path(&self.db_dir, did); 55 + let pool = db::open_user_db(&path, &self.db_config).await?; 56 + 57 + let mut dbs = self.user_dbs.write().await; 58 + dbs.insert(did.to_string(), pool.clone()); 59 + Ok(pool) 60 + } 61 + } 62 + 63 + /// Look up the domain for a record by rkey, then delete the row. 64 + /// Returns the domain if the record existed. 65 + async fn delete_by_rkey(pool: &SqlitePool, table: &str, rkey: &str) -> Result<Option<String>> { 66 + let select = format!("SELECT domain FROM {table} WHERE rkey = ?"); 67 + let domain: Option<(String,)> = sqlx::query_as(&select) 68 + .bind(rkey) 69 + .fetch_optional(pool) 70 + .await?; 71 + 72 + let delete = format!("DELETE FROM {table} WHERE rkey = ?"); 73 + sqlx::query(&delete) 74 + .bind(rkey) 75 + .execute(pool) 76 + .await 77 + .with_context(|| format!("failed to delete from {table}"))?; 78 + 79 + Ok(domain.map(|(d,)| d)) 80 + } 81 + 82 + /// Yield each candidate zone for a domain by walking up the tree. 83 + /// e.g. "a.b.example.com" → ["a.b.example.com", "b.example.com", "example.com"] 84 + pub(crate) fn domain_ancestors(domain: &str) -> impl Iterator<Item = String> + '_ { 85 + let parts: Vec<&str> = domain.split('.').collect(); 86 + (0..parts.len().saturating_sub(1)).map(move |i| parts[i..].join(".")) 87 + } 88 + 89 + /// Check if the user has a declared zone that covers the given domain. 90 + async fn has_zone_for(pool: &SqlitePool, domain: &str) -> Result<bool> { 91 + for candidate in domain_ancestors(domain) { 92 + let found: Option<(i64,)> = 93 + sqlx::query_as("SELECT 1 FROM zones WHERE domain = ?") 94 + .bind(&candidate) 95 + .fetch_optional(pool) 96 + .await?; 97 + if found.is_some() { 98 + return Ok(true); 99 + } 100 + } 101 + Ok(false) 102 + } 103 + 104 + pub async fn handle_event(state: Arc<AppState>, event: TapEvent) -> Result<()> { 105 + match event.event_type.as_str() { 106 + "record" => { 107 + let rec = event.record.context("record event missing record field")?; 108 + let collection = match rec.collection.as_str() { 109 + "systems.kiri.zone" => "zone", 110 + "systems.kiri.dns" => "dns", 111 + _ => return Ok(()), 112 + }; 113 + let action = match rec.action { 114 + RecordAction::Create => "create", 115 + RecordAction::Update => "update", 116 + RecordAction::Delete => "delete", 117 + }; 118 + 119 + let result = match collection { 120 + "zone" => handle_zone_event(state, rec).await, 121 + _ => handle_record_event(state, rec).await, 122 + }; 123 + 124 + let status = if result.is_ok() { "success" } else { "error" }; 125 + metrics::counter!( 126 + "appview_firehose_events_total", 127 + "collection" => collection, 128 + "action" => action, 129 + "status" => status, 130 + ) 131 + .increment(1); 132 + 133 + result 134 + } 135 + "identity" => { 136 + if let Some(ident) = event.identity { 137 + tracing::debug!( 138 + did = %ident.did, 139 + handle = %ident.handle, 140 + active = ident.is_active, 141 + "identity event" 142 + ); 143 + } 144 + Ok(()) 145 + } 146 + other => { 147 + tracing::debug!("ignoring tap event type: {other}"); 148 + Ok(()) 149 + } 150 + } 151 + } 152 + 153 + async fn handle_record_event(state: Arc<AppState>, event: TapRecordEvent) -> Result<()> { 154 + let did = &event.did; 155 + let rkey = &event.rkey; 156 + 157 + match event.action { 158 + RecordAction::Create | RecordAction::Update => { 159 + let record_value = event 160 + .record 161 + .context("create/update event missing record payload")?; 162 + 163 + let domain = record_value 164 + .get("domain") 165 + .and_then(|v| v.as_str()) 166 + .context("record missing domain field")? 167 + .to_lowercase(); 168 + 169 + let record_type = record_value 170 + .get("record") 171 + .and_then(|v| v.get("$type")) 172 + .and_then(|v| v.as_str()) 173 + .context("record missing $type")?; 174 + 175 + let type_name = record_type 176 + .strip_prefix("systems.kiri.dns#") 177 + .context("unexpected $type prefix")? 178 + .trim_end_matches("Record"); 179 + 180 + let user_db = state.get_user_db(did).await?; 181 + 182 + if !has_zone_for(&user_db, &domain).await? { 183 + tracing::warn!( 184 + did = %did, 185 + domain = %domain, 186 + "record for {domain} has no matching zone, skipping" 187 + ); 188 + metrics::counter!("appview_records_skipped_no_zone_total").increment(1); 189 + return Ok(()); 190 + } 191 + 192 + let data = serde_json::to_string(&record_value)?; 193 + let now = chrono::Utc::now().timestamp(); 194 + 195 + let write_start = Instant::now(); 196 + sqlx::query( 197 + "INSERT INTO records (rkey, domain, record_type, data, created_at, updated_at) 198 + VALUES (?, ?, ?, ?, ?, ?) 199 + ON CONFLICT(rkey) DO UPDATE SET 200 + domain = excluded.domain, 201 + record_type = excluded.record_type, 202 + data = excluded.data, 203 + updated_at = excluded.updated_at", 204 + ) 205 + .bind(rkey) 206 + .bind(&domain) 207 + .bind(type_name) 208 + .bind(&data) 209 + .bind(now) 210 + .bind(now) 211 + .execute(&user_db) 212 + .await 213 + .context("failed to upsert record")?; 214 + metrics::histogram!("appview_sqlite_write_duration_seconds") 215 + .record(write_start.elapsed().as_secs_f64()); 216 + 217 + tracing::info!( 218 + did = %did, 219 + rkey = %rkey, 220 + domain = %domain, 221 + record_type = %type_name, 222 + live = event.live, 223 + "record upserted" 224 + ); 225 + } 226 + 227 + RecordAction::Delete => { 228 + let Ok(user_db) = state.get_user_db(did).await else { 229 + tracing::debug!(did = %did, rkey = %rkey, "delete for unknown DID, skipping"); 230 + return Ok(()); 231 + }; 232 + 233 + let write_start = Instant::now(); 234 + let domain = delete_by_rkey(&user_db, "records", rkey).await?; 235 + metrics::histogram!("appview_sqlite_write_duration_seconds") 236 + .record(write_start.elapsed().as_secs_f64()); 237 + 238 + if let Some(domain) = domain { 239 + tracing::info!( 240 + did = %did, 241 + rkey = %rkey, 242 + domain = %domain, 243 + live = event.live, 244 + "record deleted" 245 + ); 246 + } 247 + } 248 + } 249 + 250 + Ok(()) 251 + } 252 + 253 + async fn handle_zone_event(state: Arc<AppState>, event: TapRecordEvent) -> Result<()> { 254 + let did = &event.did; 255 + let rkey = &event.rkey; 256 + 257 + match event.action { 258 + RecordAction::Create | RecordAction::Update => { 259 + let record_value = event 260 + .record 261 + .context("create/update zone event missing record payload")?; 262 + 263 + let domain = record_value 264 + .get("domain") 265 + .and_then(|v| v.as_str()) 266 + .context("zone record missing domain field")? 267 + .to_lowercase(); 268 + 269 + let now = chrono::Utc::now().timestamp(); 270 + let user_db = state.get_user_db(did).await?; 271 + 272 + let write_start = Instant::now(); 273 + sqlx::query( 274 + "INSERT INTO zones (rkey, domain, created_at) 275 + VALUES (?, ?, ?) 276 + ON CONFLICT(rkey) DO UPDATE SET domain = excluded.domain", 277 + ) 278 + .bind(rkey) 279 + .bind(&domain) 280 + .bind(now) 281 + .execute(&user_db) 282 + .await 283 + .context("failed to upsert zone")?; 284 + 285 + sqlx::query( 286 + "INSERT INTO zone_index (zone, did, first_seen) 287 + VALUES (?, ?, ?) 288 + ON CONFLICT(zone, did) DO NOTHING", 289 + ) 290 + .bind(&domain) 291 + .bind(did) 292 + .bind(now) 293 + .execute(&state.index) 294 + .await 295 + .context("failed to update zone index")?; 296 + metrics::histogram!("appview_sqlite_write_duration_seconds") 297 + .record(write_start.elapsed().as_secs_f64()); 298 + 299 + tracing::info!( 300 + did = %did, 301 + rkey = %rkey, 302 + domain = %domain, 303 + live = event.live, 304 + "zone upserted" 305 + ); 306 + } 307 + 308 + RecordAction::Delete => { 309 + let Ok(user_db) = state.get_user_db(did).await else { 310 + tracing::debug!(did = %did, rkey = %rkey, "zone delete for unknown DID, skipping"); 311 + return Ok(()); 312 + }; 313 + 314 + let write_start = Instant::now(); 315 + let domain = delete_by_rkey(&user_db, "zones", rkey).await?; 316 + 317 + if let Some(domain) = domain { 318 + sqlx::query("DELETE FROM zone_index WHERE zone = ? AND did = ?") 319 + .bind(&domain) 320 + .bind(did) 321 + .execute(&state.index) 322 + .await 323 + .context("failed to delete from zone index")?; 324 + metrics::histogram!("appview_sqlite_write_duration_seconds") 325 + .record(write_start.elapsed().as_secs_f64()); 326 + 327 + tracing::info!( 328 + did = %did, 329 + rkey = %rkey, 330 + domain = %domain, 331 + live = event.live, 332 + "zone deleted" 333 + ); 334 + } 335 + } 336 + } 337 + 338 + Ok(()) 339 + }
+160
onis-appview/src/tap.rs
··· 1 + use anyhow::{Context, Result}; 2 + use futures_util::{SinkExt, StreamExt}; 3 + use serde::Deserialize; 4 + use tokio_tungstenite::{connect_async, tungstenite::Message}; 5 + 6 + /// A single event received from the TAP firehose websocket. 7 + #[derive(Debug, Deserialize)] 8 + pub struct TapEvent { 9 + /// Monotonically increasing event sequence number. 10 + pub id: u64, 11 + /// Event kind, e.g. `"record"` or `"identity"`. 12 + #[serde(rename = "type")] 13 + pub event_type: String, 14 + /// Present when event_type == "record". 15 + pub record: Option<TapRecordEvent>, 16 + /// Present when event_type == "identity". 17 + pub identity: Option<TapIdentityEvent>, 18 + } 19 + 20 + /// Payload for a TAP record event (create, update, or delete). 21 + #[derive(Debug, Deserialize)] 22 + pub struct TapRecordEvent { 23 + /// Whether this event comes from the live stream (true) or backfill (false). 24 + pub live: bool, 25 + /// DID of the repository that owns this record. 26 + pub did: String, 27 + /// Repository revision (commit CID) that produced this event. 28 + pub rev: String, 29 + /// Lexicon collection NSID, e.g. `"systems.kiri.zone"`. 30 + pub collection: String, 31 + /// Record key within the collection. 32 + pub rkey: String, 33 + /// The action performed on the record. 34 + pub action: RecordAction, 35 + /// The full record value; present on create and update, absent on delete. 36 + pub record: Option<serde_json::Value>, 37 + /// Content hash (CID) of the record; present on create and update. 38 + pub cid: Option<String>, 39 + } 40 + 41 + /// The type of mutation performed on a record. 42 + #[derive(Debug, Deserialize, PartialEq)] 43 + #[serde(rename_all = "lowercase")] 44 + pub enum RecordAction { 45 + /// A new record was created. 46 + Create, 47 + /// An existing record was updated. 48 + Update, 49 + /// A record was deleted. 50 + Delete, 51 + } 52 + 53 + /// Payload for a TAP identity event (handle or status change). 54 + #[derive(Debug, Deserialize)] 55 + pub struct TapIdentityEvent { 56 + /// DID of the identity that changed. 57 + pub did: String, 58 + /// Current handle for this identity. 59 + pub handle: String, 60 + /// Whether the account is currently active. 61 + pub is_active: bool, 62 + /// Account status string, e.g. `"active"`, `"deactivated"`. 63 + pub status: String, 64 + } 65 + 66 + /// Persistent WebSocket consumer for the TAP firehose. 67 + /// 68 + /// Connects to the relay, deserializes events, dispatches them to a handler, 69 + /// and sends acknowledgements. Automatically reconnects on failure. 70 + pub struct TapConsumer { 71 + /// WebSocket URL of the TAP firehose endpoint. 72 + url: String, 73 + /// Whether to send ack messages after processing each event. 74 + acks_enabled: bool, 75 + /// Delay before reconnecting after a connection error. 76 + reconnect_delay: std::time::Duration, 77 + } 78 + 79 + impl TapConsumer { 80 + pub fn new(url: String, acks_enabled: bool, reconnect_delay_secs: u64) -> Self { 81 + Self { 82 + url, 83 + acks_enabled, 84 + reconnect_delay: std::time::Duration::from_secs(reconnect_delay_secs), 85 + } 86 + } 87 + 88 + pub async fn run<F, Fut>(&self, handler: F) -> Result<()> 89 + where 90 + F: Fn(TapEvent) -> Fut + Send + Sync, 91 + Fut: std::future::Future<Output = Result<()>> + Send, 92 + { 93 + loop { 94 + match self.connect_and_consume(&handler).await { 95 + Ok(()) => { 96 + tracing::info!("tap connection closed cleanly, reconnecting..."); 97 + } 98 + Err(e) => { 99 + tracing::error!( 100 + delay_secs = self.reconnect_delay.as_secs(), 101 + "tap connection error: {e:#}, reconnecting..." 102 + ); 103 + tokio::time::sleep(self.reconnect_delay).await; 104 + } 105 + } 106 + } 107 + } 108 + 109 + async fn connect_and_consume<F, Fut>(&self, handler: &F) -> Result<()> 110 + where 111 + F: Fn(TapEvent) -> Fut + Send + Sync, 112 + Fut: std::future::Future<Output = Result<()>> + Send, 113 + { 114 + let (ws, _) = connect_async(&self.url) 115 + .await 116 + .context("failed to connect to tap")?; 117 + 118 + tracing::info!("connected to tap at {}", self.url); 119 + 120 + let (mut sink, mut stream) = ws.split(); 121 + 122 + while let Some(msg) = stream.next().await { 123 + let msg = msg.context("websocket read error")?; 124 + 125 + let text = match msg { 126 + Message::Text(t) => t, 127 + Message::Ping(_) => continue, 128 + Message::Pong(_) => continue, 129 + Message::Close(_) => { 130 + tracing::info!("tap sent close frame"); 131 + break; 132 + } 133 + _ => continue, 134 + }; 135 + 136 + let event: TapEvent = match serde_json::from_str(&text) { 137 + Ok(e) => e, 138 + Err(e) => { 139 + tracing::warn!("failed to deserialize tap event: {e}, raw: {text}"); 140 + continue; 141 + } 142 + }; 143 + 144 + let event_id = event.id; 145 + 146 + if let Err(e) = handler(event).await { 147 + tracing::error!("error handling tap event {event_id}: {e:#}"); 148 + } 149 + 150 + if self.acks_enabled { 151 + let ack = serde_json::json!({ "type": "ack", "id": event_id }); 152 + sink.send(Message::Text(ack.to_string().into())) 153 + .await 154 + .context("failed to send ack")?; 155 + } 156 + } 157 + 158 + Ok(()) 159 + } 160 + }
+22
onis-common/Cargo.toml
··· 1 + [package] 2 + name = "onis-common" 3 + version.workspace = true 4 + edition.workspace = true 5 + 6 + [lints] 7 + workspace = true 8 + 9 + [dependencies] 10 + serde.workspace = true 11 + serde_json.workspace = true 12 + sqlx.workspace = true 13 + tokio.workspace = true 14 + thiserror.workspace = true 15 + tracing.workspace = true 16 + toml.workspace = true 17 + atrium-api.workspace = true 18 + axum.workspace = true 19 + metrics-exporter-prometheus.workspace = true 20 + 21 + [build-dependencies] 22 + esquema-codegen = { git = "https://github.com/fatfingers23/esquema.git", branch = "main" }
+16
onis-common/build.rs
··· 1 + use std::fs; 2 + use std::path::PathBuf; 3 + 4 + use esquema_codegen::genapi; 5 + 6 + fn main() { 7 + let lex_dir = PathBuf::from("../lexicons"); 8 + let out_dir = PathBuf::from("src"); 9 + 10 + fs::create_dir_all(&out_dir.join("lexicons")).unwrap(); 11 + 12 + println!("cargo:rerun-if-changed=../lexicons/"); 13 + 14 + let module_name = Some("lexicons".to_string()); 15 + let _ = genapi(&lex_dir, &out_dir, &module_name).unwrap(); 16 + }
+304
onis-common/src/config.rs
··· 1 + use std::net::IpAddr; 2 + use std::path::Path; 3 + 4 + use serde::Deserialize; 5 + use thiserror::Error; 6 + 7 + #[derive(Debug, Error)] 8 + pub enum ConfigError { 9 + #[error("io error: {0}")] 10 + Io(#[from] std::io::Error), 11 + #[error("toml parse error: {0}")] 12 + Toml(#[from] toml::de::Error), 13 + } 14 + 15 + /// Top-level config covering all onis services. 16 + /// 17 + /// Load from a TOML file with [`OnisConfig::load`]. Every field has a default, 18 + /// so an empty (or missing) file produces a usable config. 19 + #[derive(Debug, Deserialize)] 20 + #[serde(default)] 21 + pub struct OnisConfig { 22 + /// Configuration for the appview service. 23 + pub appview: AppviewConfig, 24 + /// Configuration for the DNS server. 25 + pub dns: DnsConfig, 26 + /// Configuration for the verification service. 27 + pub verify: VerifyConfig, 28 + } 29 + 30 + impl OnisConfig { 31 + /// Load config from `ONIS_CONFIG` env var path, or `onis.toml` in the 32 + /// current directory. Returns defaults if the file does not exist. 33 + pub fn load() -> Result<Self, ConfigError> { 34 + let path = std::env::var("ONIS_CONFIG").unwrap_or_else(|_| "onis.toml".to_string()); 35 + let path = Path::new(&path); 36 + 37 + if path.exists() { 38 + let content = std::fs::read_to_string(path)?; 39 + Ok(toml::from_str(&content)?) 40 + } else { 41 + Ok(Self::default()) 42 + } 43 + } 44 + } 45 + 46 + impl Default for OnisConfig { 47 + fn default() -> Self { 48 + Self { 49 + appview: AppviewConfig::default(), 50 + dns: DnsConfig::default(), 51 + verify: VerifyConfig::default(), 52 + } 53 + } 54 + } 55 + 56 + #[derive(Debug, Deserialize)] 57 + #[serde(default)] 58 + pub struct AppviewConfig { 59 + /// Address and port for the appview HTTP server. 60 + pub bind: String, 61 + /// WebSocket URL for the TAP firehose. 62 + pub tap_url: String, 63 + /// Whether to acknowledge TAP messages. 64 + pub tap_acks: bool, 65 + /// Seconds to wait before reconnecting after a TAP connection error. 66 + pub tap_reconnect_delay: u64, 67 + /// Path to the shared zone index SQLite database. 68 + pub index_path: String, 69 + /// Directory for per-DID SQLite databases. 70 + pub db_dir: String, 71 + /// Database pool configuration. 72 + pub database: DatabaseConfig, 73 + } 74 + 75 + impl Default for AppviewConfig { 76 + fn default() -> Self { 77 + Self { 78 + bind: "0.0.0.0:3000".to_string(), 79 + tap_url: "ws://localhost:2480/channel".to_string(), 80 + tap_acks: true, 81 + tap_reconnect_delay: 5, 82 + index_path: "./data/index.db".to_string(), 83 + db_dir: "./data/dbs".to_string(), 84 + database: DatabaseConfig::default(), 85 + } 86 + } 87 + } 88 + 89 + #[derive(Debug, Clone, Deserialize)] 90 + #[serde(default)] 91 + pub struct DatabaseConfig { 92 + /// Seconds to wait when the database is locked. 93 + pub busy_timeout: u64, 94 + /// Max connections for per-user database pools. 95 + pub user_max_connections: u32, 96 + /// Max connections for the shared index database pool. 97 + pub index_max_connections: u32, 98 + } 99 + 100 + impl Default for DatabaseConfig { 101 + fn default() -> Self { 102 + Self { 103 + busy_timeout: 5, 104 + user_max_connections: 5, 105 + index_max_connections: 10, 106 + } 107 + } 108 + } 109 + 110 + #[derive(Debug, Deserialize)] 111 + #[serde(default)] 112 + pub struct DnsConfig { 113 + /// URL of the appview API. 114 + pub appview_url: String, 115 + /// Address for the DNS server to listen on. 116 + pub bind: String, 117 + /// Port for the DNS server. 118 + pub port: u16, 119 + /// Seconds before a TCP connection times out. 120 + pub tcp_timeout: u64, 121 + /// Minimum TTL enforced on all DNS responses. 122 + pub ttl_floor: u32, 123 + /// Log a warning for queries slower than this (milliseconds). 124 + pub slow_query_threshold_ms: u64, 125 + /// SOA record defaults for zones without a user-published SOA. 126 + pub soa: SoaConfig, 127 + /// NS records to serve for all zones (fully qualified, trailing dot). 128 + pub ns: Vec<String>, 129 + /// Bind address for the metrics HTTP server (e.g. "0.0.0.0:9100"). 130 + pub metrics_bind: String, 131 + } 132 + 133 + impl Default for DnsConfig { 134 + fn default() -> Self { 135 + Self { 136 + appview_url: "http://localhost:3000".to_string(), 137 + bind: "0.0.0.0".to_string(), 138 + port: 5353, 139 + tcp_timeout: 30, 140 + ttl_floor: 60, 141 + slow_query_threshold_ms: 50, 142 + soa: SoaConfig::default(), 143 + ns: vec![ 144 + "ns1.kiri.systems.".to_string(), 145 + "ns2.kiri.systems.".to_string(), 146 + ], 147 + metrics_bind: "0.0.0.0:9100".to_string(), 148 + } 149 + } 150 + } 151 + 152 + #[derive(Debug, Deserialize)] 153 + #[serde(default)] 154 + pub struct SoaConfig { 155 + /// SOA record TTL in seconds. 156 + pub ttl: u32, 157 + /// SOA refresh interval in seconds. 158 + pub refresh: i32, 159 + /// SOA retry interval in seconds. 160 + pub retry: i32, 161 + /// SOA expire interval in seconds. 162 + pub expire: i32, 163 + /// SOA minimum (negative cache) TTL in seconds. 164 + pub minimum: u32, 165 + /// SOA MNAME (primary nameserver, fully qualified). 166 + pub mname: String, 167 + /// SOA RNAME (admin email in DNS format, fully qualified). 168 + pub rname: String, 169 + } 170 + 171 + impl Default for SoaConfig { 172 + fn default() -> Self { 173 + Self { 174 + ttl: 3600, 175 + refresh: 3600, 176 + retry: 900, 177 + expire: 604800, 178 + minimum: 300, 179 + mname: "ns1.kiri.systems.".to_string(), 180 + rname: "admin.kiri.systems.".to_string(), 181 + } 182 + } 183 + } 184 + 185 + #[derive(Debug, Deserialize)] 186 + #[serde(default)] 187 + pub struct VerifyConfig { 188 + /// Onis appview to call. 189 + pub appview_url: String, 190 + /// Address to start listening on for api. 191 + pub bind: String, 192 + /// Port used for api. 193 + pub port: u16, 194 + /// Seconds between scheduled verification runs. 195 + pub check_interval: u64, 196 + /// Seconds a zone must be stale before rechecking. 197 + pub recheck_interval: i64, 198 + /// Expected NS records that indicate correct delegation. 199 + pub expected_ns: Vec<String>, 200 + /// Optional custom resolver IP addresses. 201 + pub nameservers: Vec<String>, 202 + /// Port used when resolving against custom nameservers. 203 + pub dns_port: u16, 204 + } 205 + 206 + impl Default for VerifyConfig { 207 + fn default() -> Self { 208 + Self { 209 + appview_url: "http://localhost:3000".to_string(), 210 + bind: "0.0.0.0".to_string(), 211 + port: 3001, 212 + check_interval: 60, 213 + recheck_interval: 3600, 214 + expected_ns: vec![ 215 + "curitiba.ns.porkbun.com".to_string(), 216 + "maceio.ns.porkbun.com".to_string(), 217 + ], 218 + nameservers: vec![], 219 + dns_port: 53, 220 + } 221 + } 222 + } 223 + 224 + impl VerifyConfig { 225 + /// Parse the nameservers list into IP addresses. 226 + /// Returns None if the list is empty. 227 + pub fn parse_nameservers(&self) -> Result<Option<Vec<IpAddr>>, std::net::AddrParseError> { 228 + if self.nameservers.is_empty() { 229 + return Ok(None); 230 + } 231 + let addrs: Result<Vec<IpAddr>, _> = self 232 + .nameservers 233 + .iter() 234 + .map(|s| s.parse()) 235 + .collect(); 236 + addrs.map(Some) 237 + } 238 + } 239 + 240 + #[cfg(test)] 241 + #[allow(clippy::unwrap_used)] 242 + mod tests { 243 + use super::*; 244 + 245 + #[test] 246 + fn parse_nameservers_empty_returns_none() { 247 + let config = VerifyConfig::default(); 248 + let result = config.parse_nameservers().unwrap(); 249 + assert_eq!(result, None); 250 + } 251 + 252 + #[test] 253 + fn parse_nameservers_valid_ipv4() { 254 + let config = VerifyConfig { 255 + nameservers: vec!["1.1.1.1".to_string(), "8.8.8.8".to_string()], 256 + ..Default::default() 257 + }; 258 + let result = config.parse_nameservers().unwrap().unwrap(); 259 + assert_eq!(result.len(), 2); 260 + assert_eq!(result, vec![ 261 + "1.1.1.1".parse::<IpAddr>().unwrap(), 262 + "8.8.8.8".parse::<IpAddr>().unwrap(), 263 + ]); 264 + } 265 + 266 + #[test] 267 + fn parse_nameservers_valid_ipv6() { 268 + let config = VerifyConfig { 269 + nameservers: vec!["2001:4860:4860::8888".to_string()], 270 + ..Default::default() 271 + }; 272 + let result = config.parse_nameservers().unwrap().unwrap(); 273 + assert_eq!(result.len(), 1); 274 + assert_eq!(result, vec!["2001:4860:4860::8888".parse::<IpAddr>().unwrap()]); 275 + } 276 + 277 + #[test] 278 + fn parse_nameservers_mixed_v4_v6() { 279 + let config = VerifyConfig { 280 + nameservers: vec!["1.1.1.1".to_string(), "::1".to_string()], 281 + ..Default::default() 282 + }; 283 + let result = config.parse_nameservers().unwrap().unwrap(); 284 + assert_eq!(result.len(), 2); 285 + } 286 + 287 + #[test] 288 + fn parse_nameservers_invalid_returns_err() { 289 + let config = VerifyConfig { 290 + nameservers: vec!["not-an-ip".to_string()], 291 + ..Default::default() 292 + }; 293 + assert!(config.parse_nameservers().is_err()); 294 + } 295 + 296 + #[test] 297 + fn parse_nameservers_one_invalid_fails_all() { 298 + let config = VerifyConfig { 299 + nameservers: vec!["1.1.1.1".to_string(), "bad".to_string()], 300 + ..Default::default() 301 + }; 302 + assert!(config.parse_nameservers().is_err()); 303 + } 304 + }
+183
onis-common/src/db.rs
··· 1 + //! SQLite database helpers for onis using sqlx. 2 + //! 3 + //! Two database types: 4 + //! - Per-DID databases: one per user, stores their DNS records 5 + //! - Reverse index: shared database mapping domains → DIDs + verification status 6 + //! 7 + //! Migrations live in: 8 + //! migrations/user/ — per-DID database migrations 9 + //! migrations/index/ — reverse index migrations 10 + 11 + use std::path::Path; 12 + 13 + use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; 14 + use thiserror::Error; 15 + 16 + use crate::config::DatabaseConfig; 17 + 18 + #[derive(Debug, Error)] 19 + pub enum DbError { 20 + #[error("sqlx error: {0}")] 21 + Sqlx(#[from] sqlx::Error), 22 + #[error("migrate error: {0}")] 23 + Migrate(#[from] sqlx::migrate::MigrateError), 24 + #[error("io error: {0}")] 25 + Io(#[from] std::io::Error), 26 + } 27 + 28 + /// Opens (or creates) a per-DID SQLite database. 29 + /// 30 + /// Runs migrations from `migrations/user/` on first open. 31 + pub async fn open_user_db(path: &Path, db_config: &DatabaseConfig) -> Result<SqlitePool, DbError> { 32 + if let Some(parent) = path.parent() { 33 + tokio::fs::create_dir_all(parent).await?; 34 + } 35 + 36 + let opts = SqliteConnectOptions::new() 37 + .filename(path) 38 + .create_if_missing(true) 39 + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) 40 + .busy_timeout(std::time::Duration::from_secs(db_config.busy_timeout)); 41 + 42 + let pool = SqlitePoolOptions::new() 43 + .max_connections(db_config.user_max_connections) 44 + .connect_with(opts) 45 + .await?; 46 + 47 + let migrator = sqlx::migrate!("../migrations/user"); 48 + tracing::info!("migrations to run: {}", migrator.migrations.len()); 49 + migrator.run(&pool).await?; 50 + 51 + Ok(pool) 52 + } 53 + 54 + /// Opens (or creates) the shared reverse index database. 55 + /// 56 + /// Runs migrations from `migrations/index/` on first open. 57 + pub async fn open_index_db(path: &Path, db_config: &DatabaseConfig) -> Result<SqlitePool, DbError> { 58 + if let Some(parent) = path.parent() { 59 + tokio::fs::create_dir_all(parent).await?; 60 + } 61 + 62 + let opts = SqliteConnectOptions::new() 63 + .filename(path) 64 + .create_if_missing(true) 65 + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) 66 + .busy_timeout(std::time::Duration::from_secs(db_config.busy_timeout)); 67 + 68 + let pool = SqlitePoolOptions::new() 69 + .max_connections(db_config.index_max_connections) 70 + .connect_with(opts) 71 + .await?; 72 + 73 + sqlx::migrate!("../migrations/index").run(&pool).await?; 74 + 75 + Ok(pool) 76 + } 77 + 78 + /// Converts a DID to a filesystem-safe path for its database. 79 + /// 80 + /// e.g. `did:plc:adtzorbhmmjbzxsl2y4vqlqs` → `{base}/ad/tz/did_plc_adtzorbhmmjbzxsl2y4vqlqs.db` 81 + /// e.g. `did:web:vim.sh` → `{base}/vi/m./did_web_vim.sh.db` 82 + pub fn did_to_db_path(base: &Path, did: &str) -> std::path::PathBuf { 83 + let safe = did.replace(':', "_"); 84 + 85 + // use first 4 chars after "did_(plc|web)_" for sharding 86 + // XXX: this will break if another did method is added 87 + // thats method is longer than 3 characters. 88 + let shard = if safe.len() > 8 { 89 + &safe[8..] 90 + } else { 91 + &safe 92 + }; 93 + 94 + let (a, b) = if shard.len() >= 4 { 95 + (&shard[..2], &shard[2..4]) 96 + } else { 97 + ("xx", "xx") 98 + }; 99 + base.join(a).join(b).join(format!("{safe}.db")) 100 + } 101 + 102 + #[cfg(test)] 103 + mod tests { 104 + use super::*; 105 + use std::path::PathBuf; 106 + 107 + #[test] 108 + fn did_plc_standard() { 109 + let base = PathBuf::from("/data/dbs"); 110 + let path = did_to_db_path(&base, "did:plc:adtzorbhmmjbzxsl2y4vqlqs"); 111 + assert_eq!( 112 + path, 113 + PathBuf::from("/data/dbs/ad/tz/did_plc_adtzorbhmmjbzxsl2y4vqlqs.db") 114 + ); 115 + } 116 + 117 + #[test] 118 + fn did_web_domain() { 119 + let base = PathBuf::from("/data/dbs"); 120 + let path = did_to_db_path(&base, "did:web:example.com"); 121 + assert_eq!( 122 + path, 123 + PathBuf::from("/data/dbs/ex/am/did_web_example.com.db") 124 + ); 125 + } 126 + 127 + #[test] 128 + fn did_web_short_domain() { 129 + let base = PathBuf::from("/data/dbs"); 130 + let path = did_to_db_path(&base, "did:web:vim.sh"); 131 + assert_eq!( 132 + path, 133 + PathBuf::from("/data/dbs/vi/m./did_web_vim.sh.db") 134 + ); 135 + } 136 + 137 + #[test] 138 + fn did_web_subdomain() { 139 + let base = PathBuf::from("/data/dbs"); 140 + let path = did_to_db_path(&base, "did:web:sub.example.com"); 141 + assert_eq!( 142 + path, 143 + PathBuf::from("/data/dbs/su/b./did_web_sub.example.com.db") 144 + ); 145 + } 146 + 147 + #[test] 148 + fn did_web_very_short_falls_back() { 149 + let base = PathBuf::from("/data/dbs"); 150 + let path = did_to_db_path(&base, "did:web:a.b"); 151 + assert_eq!( 152 + path, 153 + PathBuf::from("/data/dbs/xx/xx/did_web_a.b.db") 154 + ); 155 + } 156 + 157 + #[test] 158 + fn did_plc_short_falls_back() { 159 + let base = PathBuf::from("/data/dbs"); 160 + let path = did_to_db_path(&base, "did:plc:abc"); 161 + assert_eq!( 162 + path, 163 + PathBuf::from("/data/dbs/xx/xx/did_plc_abc.db") 164 + ); 165 + } 166 + 167 + #[test] 168 + fn different_dids_produce_different_paths() { 169 + let base = PathBuf::from("/data/dbs"); 170 + let a = did_to_db_path(&base, "did:plc:aaaa1111bbbb2222"); 171 + let b = did_to_db_path(&base, "did:plc:cccc3333dddd4444"); 172 + assert_ne!(a, b); 173 + } 174 + 175 + #[test] 176 + fn same_identifier_different_method_produces_different_paths() { 177 + let base = PathBuf::from("/data/dbs"); 178 + let plc = did_to_db_path(&base, "did:plc:example.com"); 179 + let web = did_to_db_path(&base, "did:web:example.com"); 180 + assert_ne!(plc, web); 181 + assert_eq!(plc.parent(), web.parent()); 182 + } 183 + }
+3
onis-common/src/lexicons/mod.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + pub mod record; 3 + pub mod systems;
+35
onis-common/src/lexicons/record.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!A collection of known record types. 3 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 4 + #[serde(tag = "$type")] 5 + pub enum KnownRecord { 6 + #[serde(rename = "systems.kiri.dns")] 7 + LexiconsSystemsKiriDns(Box<crate::lexicons::systems::kiri::dns::Record>), 8 + #[serde(rename = "systems.kiri.zone")] 9 + LexiconsSystemsKiriZone(Box<crate::lexicons::systems::kiri::zone::Record>), 10 + } 11 + impl From<crate::lexicons::systems::kiri::dns::Record> for KnownRecord { 12 + fn from(record: crate::lexicons::systems::kiri::dns::Record) -> Self { 13 + KnownRecord::LexiconsSystemsKiriDns(Box::new(record)) 14 + } 15 + } 16 + impl From<crate::lexicons::systems::kiri::dns::RecordData> for KnownRecord { 17 + fn from(record_data: crate::lexicons::systems::kiri::dns::RecordData) -> Self { 18 + KnownRecord::LexiconsSystemsKiriDns(Box::new(record_data.into())) 19 + } 20 + } 21 + impl From<crate::lexicons::systems::kiri::zone::Record> for KnownRecord { 22 + fn from(record: crate::lexicons::systems::kiri::zone::Record) -> Self { 23 + KnownRecord::LexiconsSystemsKiriZone(Box::new(record)) 24 + } 25 + } 26 + impl From<crate::lexicons::systems::kiri::zone::RecordData> for KnownRecord { 27 + fn from(record_data: crate::lexicons::systems::kiri::zone::RecordData) -> Self { 28 + KnownRecord::LexiconsSystemsKiriZone(Box::new(record_data.into())) 29 + } 30 + } 31 + impl Into<atrium_api::types::Unknown> for KnownRecord { 32 + fn into(self) -> atrium_api::types::Unknown { 33 + atrium_api::types::TryIntoUnknown::try_into_unknown(&self).unwrap() 34 + } 35 + }
+3
onis-common/src/lexicons/systems.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `systems` namespace. 3 + pub mod kiri;
+16
onis-common/src/lexicons/systems/kiri.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `systems.kiri` namespace. 3 + pub mod dns; 4 + pub mod zone; 5 + #[derive(Debug)] 6 + pub struct Dns; 7 + impl atrium_api::types::Collection for Dns { 8 + const NSID: &'static str = "systems.kiri.dns"; 9 + type Record = dns::Record; 10 + } 11 + #[derive(Debug)] 12 + pub struct Zone; 13 + impl atrium_api::types::Collection for Zone { 14 + const NSID: &'static str = "systems.kiri.zone"; 15 + type Record = zone::Record; 16 + }
+109
onis-common/src/lexicons/systems/kiri/dns.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `systems.kiri.dns` namespace. 3 + use atrium_api::types::TryFromUnknown; 4 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 5 + #[serde(rename_all = "camelCase")] 6 + pub struct RecordData { 7 + ///Fully qualified domain name, lowercase, no trailing dot. 8 + pub domain: String, 9 + ///Attached note for informational reasons, not used by dns. 10 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 11 + pub note: core::option::Option<String>, 12 + ///The DNS record data. 13 + pub record: atrium_api::types::Union<RecordRecordRefs>, 14 + ///Time to live in seconds. Optional — falls back to SOA minimum for the zone if omitted. Server enforces a 60s floor. 15 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 16 + pub ttl: core::option::Option<atrium_api::types::LimitedU32<2147483647u32>>, 17 + } 18 + pub type Record = atrium_api::types::Object<RecordData>; 19 + impl From<atrium_api::types::Unknown> for RecordData { 20 + fn from(value: atrium_api::types::Unknown) -> Self { 21 + Self::try_from_unknown(value).unwrap() 22 + } 23 + } 24 + ///A 32 bit Internet address. 25 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 26 + #[serde(rename_all = "camelCase")] 27 + pub struct ARecordData { 28 + ///IPv4 address in dotted-decimal notation. 29 + pub address: String, 30 + } 31 + pub type ARecord = atrium_api::types::Object<ARecordData>; 32 + ///An 128 bit Internet address. 33 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 34 + #[serde(rename_all = "camelCase")] 35 + pub struct AaaaRecordData { 36 + ///IPv6 address in standard notation. 37 + pub address: String, 38 + } 39 + pub type AaaaRecord = atrium_api::types::Object<AaaaRecordData>; 40 + ///A domain name which specifies the canonical or primary name for the owner. 41 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 42 + #[serde(rename_all = "camelCase")] 43 + pub struct CnameRecordData { 44 + ///The canonical domain name. 45 + pub cname: String, 46 + } 47 + pub type CnameRecord = atrium_api::types::Object<CnameRecordData>; 48 + ///A mail exchange record. 49 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 50 + #[serde(rename_all = "camelCase")] 51 + pub struct MxRecordData { 52 + ///The mail server hostname. 53 + pub exchange: String, 54 + ///Priority value. Lower is preferred. 55 + pub preference: u16, 56 + } 57 + pub type MxRecord = atrium_api::types::Object<MxRecordData>; 58 + ///Start of authority. Optional — system generates defaults if absent. Serial is also generated. 59 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 60 + #[serde(rename_all = "camelCase")] 61 + pub struct SoaRecordData { 62 + pub expire: usize, 63 + ///Minimum TTL / negative cache TTL. Also used as the fallback TTL for records in this zone that don't specify one. 64 + pub minimum: usize, 65 + ///Primary nameserver. 66 + pub mname: String, 67 + pub refresh: usize, 68 + pub retry: usize, 69 + ///Responsible person email in DNS format (e.g. admin.example.com). 70 + pub rname: String, 71 + } 72 + pub type SoaRecord = atrium_api::types::Object<SoaRecordData>; 73 + ///A service locator record. 74 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 75 + #[serde(rename_all = "camelCase")] 76 + pub struct SrvRecordData { 77 + pub port: u16, 78 + pub priority: u16, 79 + ///The target hostname. 80 + pub target: String, 81 + pub weight: u16, 82 + } 83 + pub type SrvRecord = atrium_api::types::Object<SrvRecordData>; 84 + ///A text record. TXT-DATA is one or more character strings. 85 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 86 + #[serde(rename_all = "camelCase")] 87 + pub struct TxtRecordData { 88 + ///One or more character strings. Each string has a 255-byte max on the wire. 89 + pub values: Vec<String>, 90 + } 91 + pub type TxtRecord = atrium_api::types::Object<TxtRecordData>; 92 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 93 + #[serde(tag = "$type")] 94 + pub enum RecordRecordRefs { 95 + #[serde(rename = "systems.kiri.dns#aRecord")] 96 + ARecord(Box<ARecord>), 97 + #[serde(rename = "systems.kiri.dns#aaaaRecord")] 98 + AaaaRecord(Box<AaaaRecord>), 99 + #[serde(rename = "systems.kiri.dns#cnameRecord")] 100 + CnameRecord(Box<CnameRecord>), 101 + #[serde(rename = "systems.kiri.dns#mxRecord")] 102 + MxRecord(Box<MxRecord>), 103 + #[serde(rename = "systems.kiri.dns#txtRecord")] 104 + TxtRecord(Box<TxtRecord>), 105 + #[serde(rename = "systems.kiri.dns#srvRecord")] 106 + SrvRecord(Box<SrvRecord>), 107 + #[serde(rename = "systems.kiri.dns#soaRecord")] 108 + SoaRecord(Box<SoaRecord>), 109 + }
+18
onis-common/src/lexicons/systems/kiri/zone.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `systems.kiri.zone` namespace. 3 + use atrium_api::types::TryFromUnknown; 4 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 5 + #[serde(rename_all = "camelCase")] 6 + pub struct RecordData { 7 + ///The zone apex domain. Lowercase, no trailing dot. 8 + pub domain: String, 9 + ///Attached note for informational reasons, not used by dns. 10 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 11 + pub note: core::option::Option<String>, 12 + } 13 + pub type Record = atrium_api::types::Object<RecordData>; 14 + impl From<atrium_api::types::Unknown> for RecordData { 15 + fn from(value: atrium_api::types::Unknown) -> Self { 16 + Self::try_from_unknown(value).unwrap() 17 + } 18 + }
+4
onis-common/src/lib.rs
··· 1 + pub mod config; 2 + pub mod db; 3 + pub mod lexicons; 4 + pub mod metrics;
+30
onis-common/src/metrics.rs
··· 1 + use axum::extract::State; 2 + use axum::http::StatusCode; 3 + use axum::response::IntoResponse; 4 + use axum::routing::get; 5 + use axum::Router; 6 + 7 + pub use metrics_exporter_prometheus::PrometheusHandle; 8 + 9 + use metrics_exporter_prometheus::PrometheusBuilder; 10 + 11 + /// Initialize the global metrics recorder and return a handle for rendering. 12 + pub fn init() -> Result<PrometheusHandle, metrics_exporter_prometheus::BuildError> { 13 + PrometheusBuilder::new().install_recorder() 14 + } 15 + 16 + /// Axum handler that renders all registered metrics in Prometheus exposition format. 17 + pub async fn metrics_handler(State(handle): State<PrometheusHandle>) -> impl IntoResponse { 18 + ( 19 + StatusCode::OK, 20 + [("content-type", "text/plain; version=0.0.4; charset=utf-8")], 21 + handle.render(), 22 + ) 23 + } 24 + 25 + /// Returns a minimal axum router with just `GET /metrics`. 26 + pub fn router(handle: PrometheusHandle) -> Router { 27 + Router::new() 28 + .route("/metrics", get(metrics_handler)) 29 + .with_state(handle) 30 + }
+27
onis-dns/Cargo.toml
··· 1 + [package] 2 + name = "onis-dns" 3 + version.workspace = true 4 + edition.workspace = true 5 + 6 + [lints] 7 + workspace = true 8 + 9 + [[bin]] 10 + name = "onis-dns" 11 + path = "src/main.rs" 12 + 13 + [dependencies] 14 + onis-common.workspace = true 15 + serde.workspace = true 16 + serde_json.workspace = true 17 + sqlx.workspace = true 18 + tokio.workspace = true 19 + tracing.workspace = true 20 + tracing-subscriber.workspace = true 21 + anyhow.workspace = true 22 + hickory-server.workspace = true 23 + hickory-proto.workspace = true 24 + reqwest.workspace = true 25 + async-trait.workspace = true 26 + axum.workspace = true 27 + metrics.workspace = true
+44
onis-dns/src/client.rs
··· 1 + use serde::Deserialize; 2 + 3 + /// HTTP client for querying the onis appview resolve API. 4 + #[derive(Clone)] 5 + pub struct AppviewClient { 6 + /// Underlying HTTP client. 7 + client: reqwest::Client, 8 + /// Base URL of the appview, e.g. `"http://localhost:3000"`. 9 + base_url: String, 10 + } 11 + 12 + /// Response from the appview `GET /v1/resolve` endpoint. 13 + #[derive(Debug, Deserialize)] 14 + pub struct ResolveResponse { 15 + /// The zone that covers the queried name, if any. 16 + pub zone: Option<String>, 17 + /// Whether the zone has been verified via DNS delegation. 18 + pub verified: bool, 19 + /// Matching DNS records as raw JSON values. 20 + pub records: Vec<serde_json::Value>, 21 + /// Whether any records exist for the exact queried name. 22 + pub name_exists: bool, 23 + } 24 + 25 + impl AppviewClient { 26 + pub fn new(base_url: String) -> Self { 27 + Self { 28 + client: reqwest::Client::new(), 29 + base_url, 30 + } 31 + } 32 + 33 + pub async fn resolve( 34 + &self, 35 + name: &str, 36 + record_type: Option<&str>, 37 + ) -> Result<ResolveResponse, reqwest::Error> { 38 + let mut url = format!("{}/v1/resolve?name={}", self.base_url, name); 39 + if let Some(rt) = record_type { 40 + url.push_str(&format!("&type={}", rt)); 41 + } 42 + self.client.get(&url).send().await?.json().await 43 + } 44 + }
+852
onis-dns/src/handler.rs
··· 1 + use std::net::{Ipv4Addr, Ipv6Addr}; 2 + use std::str::FromStr; 3 + use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; 4 + 5 + use async_trait::async_trait; 6 + use hickory_proto::op::{Header, MessageType, OpCode, ResponseCode}; 7 + use hickory_proto::rr::rdata::{A, AAAA, CNAME, MX, NS, SOA, SRV, TXT}; 8 + use hickory_proto::rr::{LowerName, Name, RData, Record, RecordType}; 9 + use hickory_server::authority::MessageResponseBuilder; 10 + use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo}; 11 + use tracing::{debug, info, warn}; 12 + 13 + use onis_common::config::DnsConfig; 14 + 15 + use crate::client::AppviewClient; 16 + 17 + /// Authoritative DNS request handler backed by the onis appview. 18 + /// 19 + /// Translates incoming DNS queries into appview resolve calls and converts 20 + /// the JSON records back into wire-format DNS responses. 21 + pub struct OnisHandler { 22 + /// HTTP client for querying the appview resolve API. 23 + client: AppviewClient, 24 + /// Minimum TTL enforced on all outgoing DNS records. 25 + ttl_floor: u32, 26 + /// Queries exceeding this duration are logged as slow. 27 + slow_query_threshold: Duration, 28 + /// TTL used for synthesized SOA records. 29 + soa_ttl: u32, 30 + /// SOA refresh interval in seconds. 31 + soa_refresh: i32, 32 + /// SOA retry interval in seconds. 33 + soa_retry: i32, 34 + /// SOA expire interval in seconds. 35 + soa_expire: i32, 36 + /// SOA minimum (negative cache) TTL in seconds. 37 + soa_minimum: u32, 38 + /// SOA MNAME — primary nameserver. 39 + soa_mname: Name, 40 + /// SOA RNAME — admin contact in DNS format. 41 + soa_rname: Name, 42 + /// NS records served for all zones. 43 + ns_records: Vec<Name>, 44 + } 45 + 46 + impl OnisHandler { 47 + pub fn new(client: AppviewClient, config: &DnsConfig) -> Self { 48 + let ns_records: Vec<Name> = config 49 + .ns 50 + .iter() 51 + .filter_map(|ns| Name::from_str(ns).ok()) 52 + .collect(); 53 + 54 + Self { 55 + client, 56 + ttl_floor: config.ttl_floor, 57 + slow_query_threshold: Duration::from_millis(config.slow_query_threshold_ms), 58 + soa_ttl: config.soa.ttl, 59 + soa_refresh: config.soa.refresh, 60 + soa_retry: config.soa.retry, 61 + soa_expire: config.soa.expire, 62 + soa_minimum: config.soa.minimum, 63 + soa_mname: Name::from_str(&config.soa.mname).unwrap_or_else(|_| Name::root()), 64 + soa_rname: Name::from_str(&config.soa.rname).unwrap_or_else(|_| Name::root()), 65 + ns_records, 66 + } 67 + } 68 + 69 + fn enforce_ttl(&self, ttl: u32) -> u32 { 70 + ttl.max(self.ttl_floor) 71 + } 72 + 73 + fn name_to_domain(name: &LowerName) -> String { 74 + name.to_string().trim_end_matches('.').to_string() 75 + } 76 + 77 + fn map_record_type(rtype: RecordType) -> Option<&'static str> { 78 + match rtype { 79 + RecordType::A => Some("a"), 80 + RecordType::AAAA => Some("aaaa"), 81 + RecordType::CNAME => Some("cname"), 82 + RecordType::MX => Some("mx"), 83 + RecordType::TXT => Some("txt"), 84 + RecordType::SRV => Some("srv"), 85 + RecordType::SOA => Some("soa"), 86 + _ => None, 87 + } 88 + } 89 + 90 + fn to_fqdn(name: &str) -> Option<Name> { 91 + Name::from_str(&format!("{name}.")).ok() 92 + } 93 + 94 + fn now_serial() -> u32 { 95 + SystemTime::now() 96 + .duration_since(UNIX_EPOCH) 97 + .unwrap_or_default() 98 + .as_secs() as u32 99 + } 100 + 101 + fn synthesize_soa(&self, zone: &str) -> Record { 102 + let name = Self::to_fqdn(zone).unwrap_or_else(Name::root); 103 + Record::from_rdata( 104 + name, 105 + self.enforce_ttl(self.soa_ttl), 106 + RData::SOA(SOA::new( 107 + self.soa_mname.clone(), 108 + self.soa_rname.clone(), 109 + Self::now_serial(), 110 + self.soa_refresh, 111 + self.soa_retry, 112 + self.soa_expire, 113 + self.soa_minimum, 114 + )), 115 + ) 116 + } 117 + 118 + fn synthesize_ns(&self, zone: &str) -> Vec<Record> { 119 + let name = Self::to_fqdn(zone).unwrap_or_else(Name::root); 120 + let ttl = self.enforce_ttl(self.soa_ttl); 121 + self.ns_records 122 + .iter() 123 + .map(|ns| { 124 + Record::from_rdata(name.clone(), ttl, RData::NS(NS(ns.clone()))) 125 + }) 126 + .collect() 127 + } 128 + 129 + fn convert_record(&self, value: &serde_json::Value) -> Option<Record> { 130 + let domain = value.get("domain")?.as_str()?; 131 + let name = Self::to_fqdn(domain)?; 132 + 133 + let ttl_raw = value 134 + .get("ttl") 135 + .and_then(|v| v.as_u64()) 136 + .unwrap_or(self.soa_minimum as u64); 137 + let ttl = self.enforce_ttl(ttl_raw as u32); 138 + 139 + let record = value.get("record")?; 140 + let rtype = record.get("$type")?.as_str()?; 141 + 142 + let rdata: RData = match rtype { 143 + "systems.kiri.dns#aRecord" => { 144 + let addr: Ipv4Addr = record.get("address")?.as_str()?.parse().ok()?; 145 + RData::A(A(addr)) 146 + } 147 + "systems.kiri.dns#aaaaRecord" => { 148 + let addr: Ipv6Addr = record.get("address")?.as_str()?.parse().ok()?; 149 + RData::AAAA(AAAA(addr)) 150 + } 151 + "systems.kiri.dns#cnameRecord" => { 152 + let target = record.get("cname")?.as_str()?; 153 + RData::CNAME(CNAME(Self::to_fqdn(target)?)) 154 + } 155 + "systems.kiri.dns#mxRecord" => { 156 + let pref = record.get("preference")?.as_u64()? as u16; 157 + let exchange = record.get("exchange")?.as_str()?; 158 + RData::MX(MX::new( 159 + pref, 160 + Self::to_fqdn(exchange)?, 161 + )) 162 + } 163 + "systems.kiri.dns#txtRecord" => { 164 + let values = record.get("values")?.as_array()?; 165 + let strings: Vec<String> = values 166 + .iter() 167 + .filter_map(|v| v.as_str()) 168 + .map(String::from) 169 + .collect(); 170 + RData::TXT(TXT::new(strings)) 171 + } 172 + "systems.kiri.dns#srvRecord" => { 173 + let priority = record.get("priority")?.as_u64()? as u16; 174 + let weight = record.get("weight")?.as_u64()? as u16; 175 + let port = record.get("port")?.as_u64()? as u16; 176 + let target = record.get("target")?.as_str()?; 177 + RData::SRV(SRV::new( 178 + priority, 179 + weight, 180 + port, 181 + Self::to_fqdn(target)?, 182 + )) 183 + } 184 + "systems.kiri.dns#soaRecord" => { 185 + let mname = record.get("mname")?.as_str()?; 186 + let rname = record.get("rname")?.as_str()?; 187 + let refresh = record.get("refresh")?.as_u64()? as i32; 188 + let retry = record.get("retry")?.as_u64()? as i32; 189 + let expire = record.get("expire")?.as_u64()? as i32; 190 + let minimum = record.get("minimum")?.as_u64()? as u32; 191 + RData::SOA(SOA::new( 192 + Self::to_fqdn(mname)?, 193 + Self::to_fqdn(rname)?, 194 + Self::now_serial(), 195 + refresh, 196 + retry, 197 + expire, 198 + minimum, 199 + )) 200 + } 201 + _ => return None, 202 + }; 203 + 204 + Some(Record::from_rdata(name, ttl, rdata)) 205 + } 206 + 207 + /// Get the SOA record for a zone, synthesizing if the user hasn't published one. 208 + async fn get_soa(&self, zone: &str) -> Record { 209 + if let Ok(resp) = self.client.resolve(zone, Some("soa")).await { 210 + for val in &resp.records { 211 + if let Some(record) = self.convert_record(val) { 212 + return record; 213 + } 214 + } 215 + } 216 + self.synthesize_soa(zone) 217 + } 218 + 219 + fn response_header(request: &Request, rcode: ResponseCode) -> Header { 220 + let mut header = Header::new(); 221 + header.set_id(request.header().id()); 222 + header.set_message_type(MessageType::Response); 223 + header.set_op_code(OpCode::Query); 224 + header.set_authoritative(true); 225 + header.set_recursion_desired(request.header().recursion_desired()); 226 + header.set_recursion_available(false); 227 + header.set_response_code(rcode); 228 + header 229 + } 230 + 231 + fn error_response_info(rcode: ResponseCode) -> ResponseInfo { 232 + let mut header = Header::new(); 233 + header.set_response_code(rcode); 234 + header.into() 235 + } 236 + 237 + async fn send_error<R: ResponseHandler>( 238 + request: &Request, 239 + response_handle: &mut R, 240 + start: &Instant, 241 + qtype_str: &str, 242 + rcode: ResponseCode, 243 + rcode_label: &str, 244 + ) -> ResponseInfo { 245 + let builder = MessageResponseBuilder::from_message_request(request); 246 + let response = builder.error_msg(request.header(), rcode); 247 + record_query_metrics(start, qtype_str, rcode_label); 248 + response_handle 249 + .send_response(response) 250 + .await 251 + .unwrap_or_else(|e| { 252 + warn!(error = %e, rcode = rcode_label, "failed to send error response"); 253 + Self::error_response_info(ResponseCode::ServFail) 254 + }) 255 + } 256 + 257 + async fn send_dns_response<R: ResponseHandler>( 258 + &self, 259 + request: &Request, 260 + response_handle: &mut R, 261 + start: &Instant, 262 + domain: &str, 263 + qtype_str: &str, 264 + rcode: ResponseCode, 265 + rcode_label: &str, 266 + answers: &[Record], 267 + nameservers: &[Record], 268 + authority: &[Record], 269 + ) -> ResponseInfo { 270 + let header = Self::response_header(request, rcode); 271 + let builder = MessageResponseBuilder::from_message_request(request); 272 + let response = builder.build( 273 + header, 274 + answers.iter(), 275 + nameservers.iter(), 276 + authority.iter(), 277 + std::iter::empty(), 278 + ); 279 + 280 + let elapsed = start.elapsed(); 281 + info!( 282 + domain = %domain, 283 + qtype = %qtype_str, 284 + rcode = rcode_label, 285 + answers = answers.len(), 286 + duration_ms = elapsed.as_millis() as u64, 287 + src = %request.src(), 288 + "query complete" 289 + ); 290 + 291 + if elapsed > self.slow_query_threshold { 292 + warn!( 293 + domain = %domain, 294 + qtype = %qtype_str, 295 + duration_ms = elapsed.as_millis() as u64, 296 + "slow query" 297 + ); 298 + } 299 + 300 + record_query_metrics(start, qtype_str, rcode_label); 301 + response_handle 302 + .send_response(response) 303 + .await 304 + .unwrap_or_else(|e| { 305 + warn!(error = %e, "failed to send DNS response"); 306 + Self::error_response_info(ResponseCode::ServFail) 307 + }) 308 + } 309 + } 310 + 311 + #[async_trait] 312 + impl RequestHandler for OnisHandler { 313 + async fn handle_request<R: ResponseHandler>( 314 + &self, 315 + request: &Request, 316 + mut response_handle: R, 317 + ) -> ResponseInfo { 318 + let start = Instant::now(); 319 + 320 + // Only handle standard queries 321 + if request.header().message_type() != MessageType::Query 322 + || request.header().op_code() != OpCode::Query 323 + { 324 + return Self::send_error( 325 + request, &mut response_handle, &start, 326 + "UNKNOWN", ResponseCode::NotImp, "NOTIMP", 327 + ).await; 328 + } 329 + 330 + let queries = request.queries(); 331 + if queries.is_empty() { 332 + return Self::send_error( 333 + request, &mut response_handle, &start, 334 + "UNKNOWN", ResponseCode::FormErr, "FORMERR", 335 + ).await; 336 + } 337 + 338 + let query = &queries[0]; 339 + let qname = query.name(); 340 + let qtype = query.query_type(); 341 + let qtype_str = format!("{qtype:?}"); 342 + let domain = Self::name_to_domain(qname); 343 + 344 + debug!( 345 + domain = %domain, 346 + qtype = ?qtype, 347 + src = %request.src(), 348 + "query" 349 + ); 350 + 351 + // Resolve against the appview — single call, appview walks the zone tree 352 + let resp = match self.client.resolve(&domain, None).await { 353 + Ok(r) => r, 354 + Err(e) => { 355 + warn!(error = %e, domain = %domain, "appview request failed"); 356 + return Self::send_error( 357 + request, &mut response_handle, &start, 358 + &qtype_str, ResponseCode::ServFail, "SERVFAIL", 359 + ).await; 360 + } 361 + }; 362 + 363 + // No zone found or zone not verified → REFUSED 364 + let zone = match resp.zone { 365 + Some(ref z) if resp.verified => z.clone(), 366 + _ => { 367 + debug!(domain = %domain, "no verified zone, refusing"); 368 + return self.send_dns_response( 369 + request, &mut response_handle, &start, 370 + &domain, &qtype_str, 371 + ResponseCode::Refused, "REFUSED", 372 + &[], &[], &[], 373 + ).await; 374 + } 375 + }; 376 + 377 + let soa_record = self.get_soa(&zone).await; 378 + let ns_records = self.synthesize_ns(&zone); 379 + 380 + // Handle NS queries — always return auto-generated NS records 381 + if qtype == RecordType::NS { 382 + return self.send_dns_response( 383 + request, &mut response_handle, &start, 384 + &domain, &qtype_str, 385 + ResponseCode::NoError, "NOERROR", 386 + &ns_records, &[], std::slice::from_ref(&soa_record), 387 + ).await; 388 + } 389 + 390 + // Handle SOA queries 391 + if qtype == RecordType::SOA { 392 + return self.send_dns_response( 393 + request, &mut response_handle, &start, 394 + &domain, &qtype_str, 395 + ResponseCode::NoError, "NOERROR", 396 + std::slice::from_ref(&soa_record), &ns_records, &[], 397 + ).await; 398 + } 399 + 400 + // Map query type to appview type string 401 + let type_str = match Self::map_record_type(qtype) { 402 + Some(t) => t, 403 + None => { 404 + // Unsupported type for a zone we serve → NODATA 405 + return self.send_dns_response( 406 + request, &mut response_handle, &start, 407 + &domain, &qtype_str, 408 + ResponseCode::NoError, "NOERROR/NODATA", 409 + &[], &[], std::slice::from_ref(&soa_record), 410 + ).await; 411 + } 412 + }; 413 + 414 + // Query the appview API for the specific record type 415 + let typed_resp = match self.client.resolve(&domain, Some(type_str)).await { 416 + Ok(r) => r, 417 + Err(e) => { 418 + warn!(error = %e, domain = %domain, "appview typed request failed"); 419 + return Self::send_error( 420 + request, &mut response_handle, &start, 421 + &qtype_str, ResponseCode::ServFail, "SERVFAIL", 422 + ).await; 423 + } 424 + }; 425 + 426 + // Convert appview records to DNS records 427 + let answers: Vec<Record> = typed_resp 428 + .records 429 + .iter() 430 + .filter_map(|v| self.convert_record(v)) 431 + .collect(); 432 + 433 + if answers.is_empty() { 434 + let (rcode, label) = if resp.name_exists { 435 + (ResponseCode::NoError, "NOERROR/NODATA") 436 + } else { 437 + (ResponseCode::NXDomain, "NXDOMAIN") 438 + }; 439 + self.send_dns_response( 440 + request, &mut response_handle, &start, 441 + &domain, &qtype_str, 442 + rcode, label, 443 + &[], &[], std::slice::from_ref(&soa_record), 444 + ).await 445 + } else { 446 + self.send_dns_response( 447 + request, &mut response_handle, &start, 448 + &domain, &qtype_str, 449 + ResponseCode::NoError, "NOERROR", 450 + &answers, &ns_records, &[], 451 + ).await 452 + } 453 + } 454 + } 455 + 456 + fn record_query_metrics(start: &Instant, qtype: &str, rcode: &str) { 457 + metrics::counter!("dns_queries_total", "qtype" => qtype.to_owned(), "rcode" => rcode.to_owned()) 458 + .increment(1); 459 + metrics::histogram!("dns_query_duration_seconds") 460 + .record(start.elapsed().as_secs_f64()); 461 + } 462 + 463 + #[cfg(test)] 464 + #[allow(clippy::unwrap_used)] 465 + mod tests { 466 + use std::net::{Ipv4Addr, Ipv6Addr}; 467 + use std::str::FromStr; 468 + 469 + use hickory_proto::rr::{LowerName, Name, RData, RecordType}; 470 + use serde_json::json; 471 + 472 + use crate::client::AppviewClient; 473 + use onis_common::config::DnsConfig; 474 + 475 + use super::OnisHandler; 476 + 477 + fn test_handler() -> OnisHandler { 478 + let client = AppviewClient::new("http://test:3000".to_string()); 479 + let config = DnsConfig::default(); 480 + OnisHandler::new(client, &config) 481 + } 482 + 483 + #[test] 484 + fn enforce_ttl_below_floor() { 485 + let handler = test_handler(); 486 + assert_eq!(handler.enforce_ttl(10), 60); 487 + } 488 + 489 + #[test] 490 + fn enforce_ttl_above_floor() { 491 + let handler = test_handler(); 492 + assert_eq!(handler.enforce_ttl(300), 300); 493 + } 494 + 495 + #[test] 496 + fn enforce_ttl_at_floor() { 497 + let handler = test_handler(); 498 + assert_eq!(handler.enforce_ttl(60), 60); 499 + } 500 + 501 + #[test] 502 + fn enforce_ttl_zero() { 503 + let handler = test_handler(); 504 + assert_eq!(handler.enforce_ttl(0), 60); 505 + } 506 + 507 + #[test] 508 + fn name_to_domain_strips_trailing_dot() { 509 + let name = LowerName::from(Name::from_str("example.com.").unwrap()); 510 + assert_eq!(OnisHandler::name_to_domain(&name), "example.com"); 511 + } 512 + 513 + #[test] 514 + fn name_to_domain_subdomain() { 515 + let name = LowerName::from(Name::from_str("sub.example.com.").unwrap()); 516 + assert_eq!(OnisHandler::name_to_domain(&name), "sub.example.com"); 517 + } 518 + 519 + #[test] 520 + fn name_to_domain_root() { 521 + let name = LowerName::from(Name::root()); 522 + assert_eq!(OnisHandler::name_to_domain(&name), ""); 523 + } 524 + 525 + #[test] 526 + fn map_record_type_supported() { 527 + assert_eq!(OnisHandler::map_record_type(RecordType::A), Some("a")); 528 + assert_eq!(OnisHandler::map_record_type(RecordType::AAAA), Some("aaaa")); 529 + assert_eq!(OnisHandler::map_record_type(RecordType::CNAME), Some("cname")); 530 + assert_eq!(OnisHandler::map_record_type(RecordType::MX), Some("mx")); 531 + assert_eq!(OnisHandler::map_record_type(RecordType::TXT), Some("txt")); 532 + assert_eq!(OnisHandler::map_record_type(RecordType::SRV), Some("srv")); 533 + assert_eq!(OnisHandler::map_record_type(RecordType::SOA), Some("soa")); 534 + } 535 + 536 + #[test] 537 + fn map_record_type_unsupported_returns_none() { 538 + assert_eq!(OnisHandler::map_record_type(RecordType::NS), None); 539 + assert_eq!(OnisHandler::map_record_type(RecordType::PTR), None); 540 + } 541 + 542 + #[test] 543 + fn convert_a_record() { 544 + let handler = test_handler(); 545 + let value = json!({ 546 + "domain": "example.com", 547 + "ttl": 300, 548 + "record": { 549 + "$type": "systems.kiri.dns#aRecord", 550 + "address": "1.2.3.4" 551 + } 552 + }); 553 + let record = handler.convert_record(&value).unwrap(); 554 + assert_eq!(record.name(), &Name::from_str("example.com.").unwrap()); 555 + assert_eq!(record.ttl(), 300); 556 + match record.data() { 557 + RData::A(a) => assert_eq!(a.0, Ipv4Addr::new(1, 2, 3, 4)), 558 + other => unreachable!("expected A record, got {other:?}"), 559 + } 560 + } 561 + 562 + #[test] 563 + fn convert_aaaa_record() { 564 + let handler = test_handler(); 565 + let value = json!({ 566 + "domain": "example.com", 567 + "ttl": 300, 568 + "record": { 569 + "$type": "systems.kiri.dns#aaaaRecord", 570 + "address": "2001:db8::1" 571 + } 572 + }); 573 + let record = handler.convert_record(&value).unwrap(); 574 + match record.data() { 575 + RData::AAAA(aaaa) => { 576 + assert_eq!(aaaa.0, Ipv6Addr::from_str("2001:db8::1").unwrap()); 577 + } 578 + other => unreachable!("expected AAAA record, got {other:?}"), 579 + } 580 + } 581 + 582 + #[test] 583 + fn convert_cname_record() { 584 + let handler = test_handler(); 585 + let value = json!({ 586 + "domain": "www.example.com", 587 + "ttl": 300, 588 + "record": { 589 + "$type": "systems.kiri.dns#cnameRecord", 590 + "cname": "example.com" 591 + } 592 + }); 593 + let record = handler.convert_record(&value).unwrap(); 594 + assert_eq!(record.name(), &Name::from_str("www.example.com.").unwrap()); 595 + match record.data() { 596 + RData::CNAME(cname) => { 597 + assert_eq!(cname.0, Name::from_str("example.com.").unwrap()); 598 + } 599 + other => unreachable!("expected CNAME record, got {other:?}"), 600 + } 601 + } 602 + 603 + #[test] 604 + fn convert_mx_record() { 605 + let handler = test_handler(); 606 + let value = json!({ 607 + "domain": "example.com", 608 + "ttl": 300, 609 + "record": { 610 + "$type": "systems.kiri.dns#mxRecord", 611 + "preference": 10, 612 + "exchange": "mail.example.com" 613 + } 614 + }); 615 + let record = handler.convert_record(&value).unwrap(); 616 + match record.data() { 617 + RData::MX(mx) => { 618 + assert_eq!(mx.preference(), 10); 619 + assert_eq!( 620 + mx.exchange(), 621 + &Name::from_str("mail.example.com.").unwrap() 622 + ); 623 + } 624 + other => unreachable!("expected MX record, got {other:?}"), 625 + } 626 + } 627 + 628 + #[test] 629 + fn convert_txt_record() { 630 + let handler = test_handler(); 631 + let value = json!({ 632 + "domain": "example.com", 633 + "ttl": 300, 634 + "record": { 635 + "$type": "systems.kiri.dns#txtRecord", 636 + "values": ["v=spf1 include:example.com ~all"] 637 + } 638 + }); 639 + let record = handler.convert_record(&value).unwrap(); 640 + match record.data() { 641 + RData::TXT(txt) => { 642 + let text: String = txt 643 + .txt_data() 644 + .iter() 645 + .map(|d| String::from_utf8_lossy(d).to_string()) 646 + .collect::<Vec<_>>() 647 + .join(""); 648 + assert_eq!(text, "v=spf1 include:example.com ~all"); 649 + } 650 + other => unreachable!("expected TXT record, got {other:?}"), 651 + } 652 + } 653 + 654 + #[test] 655 + fn convert_srv_record() { 656 + let handler = test_handler(); 657 + let value = json!({ 658 + "domain": "_sip._tcp.example.com", 659 + "ttl": 300, 660 + "record": { 661 + "$type": "systems.kiri.dns#srvRecord", 662 + "priority": 10, 663 + "weight": 60, 664 + "port": 5060, 665 + "target": "sip.example.com" 666 + } 667 + }); 668 + let record = handler.convert_record(&value).unwrap(); 669 + match record.data() { 670 + RData::SRV(srv) => { 671 + assert_eq!(srv.priority(), 10); 672 + assert_eq!(srv.weight(), 60); 673 + assert_eq!(srv.port(), 5060); 674 + assert_eq!(srv.target(), &Name::from_str("sip.example.com.").unwrap()); 675 + } 676 + other => unreachable!("expected SRV record, got {other:?}"), 677 + } 678 + } 679 + 680 + #[test] 681 + fn convert_soa_record() { 682 + let handler = test_handler(); 683 + let value = json!({ 684 + "domain": "example.com", 685 + "ttl": 3600, 686 + "record": { 687 + "$type": "systems.kiri.dns#soaRecord", 688 + "mname": "ns1.example.com", 689 + "rname": "admin.example.com", 690 + "refresh": 3600, 691 + "retry": 900, 692 + "expire": 604800, 693 + "minimum": 300 694 + } 695 + }); 696 + let record = handler.convert_record(&value).unwrap(); 697 + match record.data() { 698 + RData::SOA(soa) => { 699 + assert_eq!(soa.mname(), &Name::from_str("ns1.example.com.").unwrap()); 700 + assert_eq!(soa.rname(), &Name::from_str("admin.example.com.").unwrap()); 701 + assert_eq!(soa.refresh(), 3600); 702 + assert_eq!(soa.retry(), 900); 703 + assert_eq!(soa.expire(), 604800); 704 + assert_eq!(soa.minimum(), 300); 705 + } 706 + other => unreachable!("expected SOA record, got {other:?}"), 707 + } 708 + } 709 + 710 + #[test] 711 + fn convert_record_missing_domain_returns_none() { 712 + let handler = test_handler(); 713 + let value = json!({ 714 + "record": { 715 + "$type": "systems.kiri.dns#aRecord", 716 + "address": "1.2.3.4" 717 + } 718 + }); 719 + assert!(handler.convert_record(&value).is_none()); 720 + } 721 + 722 + #[test] 723 + fn convert_record_missing_record_returns_none() { 724 + let handler = test_handler(); 725 + let value = json!({ 726 + "domain": "example.com" 727 + }); 728 + assert!(handler.convert_record(&value).is_none()); 729 + } 730 + 731 + #[test] 732 + fn convert_record_missing_type_returns_none() { 733 + let handler = test_handler(); 734 + let value = json!({ 735 + "domain": "example.com", 736 + "record": { 737 + "address": "1.2.3.4" 738 + } 739 + }); 740 + assert!(handler.convert_record(&value).is_none()); 741 + } 742 + 743 + #[test] 744 + fn convert_record_unknown_type_returns_none() { 745 + let handler = test_handler(); 746 + let value = json!({ 747 + "domain": "example.com", 748 + "record": { 749 + "$type": "systems.kiri.dns#ptrRecord", 750 + "name": "1.2.3.4.in-addr.arpa" 751 + } 752 + }); 753 + assert!(handler.convert_record(&value).is_none()); 754 + } 755 + 756 + #[test] 757 + fn convert_record_invalid_ip_returns_none() { 758 + let handler = test_handler(); 759 + let value = json!({ 760 + "domain": "example.com", 761 + "record": { 762 + "$type": "systems.kiri.dns#aRecord", 763 + "address": "not-an-ip" 764 + } 765 + }); 766 + assert!(handler.convert_record(&value).is_none()); 767 + } 768 + 769 + #[test] 770 + fn convert_record_ttl_defaults_to_soa_minimum() { 771 + let handler = test_handler(); 772 + let value = json!({ 773 + "domain": "example.com", 774 + "record": { 775 + "$type": "systems.kiri.dns#aRecord", 776 + "address": "1.2.3.4" 777 + } 778 + }); 779 + let record = handler.convert_record(&value).unwrap(); 780 + // Default soa_minimum is 300, above the ttl_floor of 60 781 + assert_eq!(record.ttl(), 300); 782 + } 783 + 784 + #[test] 785 + fn convert_record_ttl_enforces_floor() { 786 + let handler = test_handler(); 787 + let value = json!({ 788 + "domain": "example.com", 789 + "ttl": 10, 790 + "record": { 791 + "$type": "systems.kiri.dns#aRecord", 792 + "address": "1.2.3.4" 793 + } 794 + }); 795 + let record = handler.convert_record(&value).unwrap(); 796 + assert_eq!(record.ttl(), 60); 797 + } 798 + 799 + #[test] 800 + fn synthesize_soa_uses_config_defaults() { 801 + let handler = test_handler(); 802 + let record = handler.synthesize_soa("example.com"); 803 + assert_eq!(record.name(), &Name::from_str("example.com.").unwrap()); 804 + assert_eq!(record.ttl(), 3600); 805 + match record.data() { 806 + RData::SOA(soa) => { 807 + assert_eq!(soa.mname(), &Name::from_str("ns1.kiri.systems.").unwrap()); 808 + assert_eq!(soa.rname(), &Name::from_str("admin.kiri.systems.").unwrap()); 809 + assert_eq!(soa.refresh(), 3600); 810 + assert_eq!(soa.retry(), 900); 811 + assert_eq!(soa.expire(), 604800); 812 + assert_eq!(soa.minimum(), 300); 813 + } 814 + other => unreachable!("expected SOA record, got {other:?}"), 815 + } 816 + } 817 + 818 + #[test] 819 + fn synthesize_ns_returns_configured_nameservers() { 820 + let handler = test_handler(); 821 + let records = handler.synthesize_ns("example.com"); 822 + assert_eq!(records.len(), 2); 823 + let names: Vec<String> = records 824 + .iter() 825 + .map(|r| match r.data() { 826 + RData::NS(ns) => ns.0.to_string(), 827 + other => unreachable!("expected NS record, got {other:?}"), 828 + }) 829 + .collect(); 830 + assert!(names.contains(&"ns1.kiri.systems.".to_string())); 831 + assert!(names.contains(&"ns2.kiri.systems.".to_string())); 832 + } 833 + 834 + #[test] 835 + fn synthesize_ns_uses_zone_name() { 836 + let handler = test_handler(); 837 + let records = handler.synthesize_ns("example.com"); 838 + let expected = Name::from_str("example.com.").unwrap(); 839 + for record in &records { 840 + assert_eq!(record.name(), &expected); 841 + } 842 + } 843 + 844 + #[test] 845 + fn synthesize_ns_ttl_matches_soa() { 846 + let handler = test_handler(); 847 + let records = handler.synthesize_ns("example.com"); 848 + for record in &records { 849 + assert_eq!(record.ttl(), 3600); 850 + } 851 + } 852 + }
+67
onis-dns/src/main.rs
··· 1 + use std::time::Duration; 2 + 3 + use anyhow::Result; 4 + use hickory_server::ServerFuture; 5 + use onis_common::config::OnisConfig; 6 + use tokio::net::{TcpListener, UdpSocket}; 7 + use tracing_subscriber::EnvFilter; 8 + 9 + mod client; 10 + mod handler; 11 + 12 + #[tokio::main] 13 + async fn main() -> Result<()> { 14 + tracing_subscriber::fmt() 15 + .with_env_filter(EnvFilter::from_default_env()) 16 + .json() 17 + .init(); 18 + 19 + let metrics_handle = onis_common::metrics::init() 20 + .map_err(|e| anyhow::anyhow!("failed to install metrics recorder: {e}"))?; 21 + 22 + metrics::describe_counter!("dns_queries_total", "Total DNS queries"); 23 + metrics::describe_histogram!( 24 + "dns_query_duration_seconds", 25 + "DNS query duration" 26 + ); 27 + 28 + let config = OnisConfig::load()?; 29 + let cfg = &config.dns; 30 + 31 + tracing::info!( 32 + appview_url = %cfg.appview_url, 33 + port = cfg.port, 34 + "onis-dns starting" 35 + ); 36 + 37 + let client = client::AppviewClient::new(cfg.appview_url.clone()); 38 + let handler = handler::OnisHandler::new(client, cfg); 39 + 40 + let mut server = ServerFuture::new(handler); 41 + 42 + let udp_addr = format!("{}:{}", cfg.bind, cfg.port); 43 + let tcp_addr = format!("{}:{}", cfg.bind, cfg.port); 44 + 45 + let udp = UdpSocket::bind(&udp_addr).await?; 46 + let tcp = TcpListener::bind(&tcp_addr).await?; 47 + 48 + tracing::info!("listening on UDP and TCP {udp_addr}"); 49 + 50 + server.register_socket(udp); 51 + server.register_listener(tcp, Duration::from_secs(cfg.tcp_timeout)); 52 + 53 + let metrics_bind = cfg.metrics_bind.clone(); 54 + let metrics_server = tokio::spawn(async move { 55 + let app = onis_common::metrics::router(metrics_handle); 56 + let listener = tokio::net::TcpListener::bind(&metrics_bind).await.unwrap(); 57 + tracing::info!("metrics listening on {metrics_bind}"); 58 + axum::serve(listener, app).await.unwrap(); 59 + }); 60 + 61 + tokio::select! { 62 + _ = tokio::signal::ctrl_c() => tracing::info!("shutting down"), 63 + _ = metrics_server => tracing::error!("metrics server exited unexpectedly"), 64 + } 65 + 66 + Ok(()) 67 + }
+26
onis-verify/Cargo.toml
··· 1 + [package] 2 + name = "onis-verify" 3 + version.workspace = true 4 + edition.workspace = true 5 + 6 + [lints] 7 + workspace = true 8 + 9 + [[bin]] 10 + name = "onis-verify" 11 + path = "src/main.rs" 12 + 13 + [dependencies] 14 + onis-common.workspace = true 15 + serde.workspace = true 16 + serde_json.workspace = true 17 + sqlx.workspace = true 18 + tokio.workspace = true 19 + tracing.workspace = true 20 + tracing-subscriber.workspace = true 21 + anyhow.workspace = true 22 + hickory-resolver.workspace = true 23 + reqwest.workspace = true 24 + axum.workspace = true 25 + chrono.workspace = true 26 + metrics.workspace = true
+103
onis-verify/src/api.rs
··· 1 + use std::sync::Arc; 2 + use std::time::Instant; 3 + 4 + use axum::{Json, Router, extract::State, http::StatusCode, routing::post}; 5 + use onis_common::metrics::PrometheusHandle; 6 + use serde::{Deserialize, Serialize}; 7 + 8 + use crate::checker::Checker; 9 + use crate::client::AppviewClient; 10 + 11 + /// Shared state for the verification HTTP API. 12 + pub struct VerifyState { 13 + /// DNS delegation checker used for on-demand and scheduled verification. 14 + pub checker: Checker, 15 + /// HTTP client for communicating verification results to the appview. 16 + pub client: AppviewClient, 17 + /// Prometheus metrics handle for the `/metrics` endpoint. 18 + pub metrics_handle: PrometheusHandle, 19 + } 20 + 21 + pub fn router(state: Arc<VerifyState>) -> Router { 22 + let metrics_router = onis_common::metrics::router(state.metrics_handle.clone()); 23 + 24 + Router::new() 25 + .route("/verify", post(verify)) 26 + .with_state(state) 27 + .merge(metrics_router) 28 + } 29 + 30 + /// Request body for the `POST /verify` endpoint. 31 + #[derive(Deserialize)] 32 + struct VerifyRequest { 33 + /// Domain name of the zone to verify. 34 + zone: String, 35 + /// DID of the zone owner. 36 + did: String, 37 + } 38 + 39 + /// Response body returned by the `POST /verify` endpoint. 40 + #[derive(Serialize)] 41 + struct VerifyResponse { 42 + /// Domain name of the zone that was checked. 43 + zone: String, 44 + /// Whether the zone passed delegation verification. 45 + verified: bool, 46 + /// NS records found for the zone during the check. 47 + ns_records: Vec<String>, 48 + /// Whether a matching TXT proof record was found. 49 + txt_match: bool, 50 + } 51 + 52 + async fn verify( 53 + State(state): State<Arc<VerifyState>>, 54 + Json(body): Json<VerifyRequest>, 55 + ) -> Result<Json<VerifyResponse>, StatusCode> { 56 + let start = Instant::now(); 57 + 58 + let result = state 59 + .checker 60 + .check_zone(&body.zone, &body.did) 61 + .await 62 + .map_err(|e| { 63 + tracing::error!(zone = %body.zone, error = %e, "on-demand verification failed"); 64 + metrics::counter!("verification_checks_total", "result" => "error").increment(1); 65 + metrics::histogram!("verification_check_duration_seconds") 66 + .record(start.elapsed().as_secs_f64()); 67 + StatusCode::INTERNAL_SERVER_ERROR 68 + })?; 69 + 70 + let result_label = if result.verified { "verified" } else { "unverified" }; 71 + metrics::counter!("verification_checks_total", "result" => result_label).increment(1); 72 + metrics::histogram!("verification_check_duration_seconds") 73 + .record(start.elapsed().as_secs_f64()); 74 + 75 + if let Err(e) = state 76 + .client 77 + .set_verification(&body.zone, &body.did, result.verified) 78 + .await 79 + { 80 + tracing::error!( 81 + zone = %body.zone, 82 + did = %body.did, 83 + error = %e, 84 + "failed to update verification status" 85 + ); 86 + return Err(StatusCode::INTERNAL_SERVER_ERROR); 87 + } 88 + 89 + tracing::info!( 90 + zone = %body.zone, 91 + did = %body.did, 92 + verified = result.verified, 93 + ns = ?result.ns_records, 94 + "on-demand verification complete" 95 + ); 96 + 97 + Ok(Json(VerifyResponse { 98 + zone: body.zone, 99 + verified: result.verified, 100 + ns_records: result.ns_records, 101 + txt_match: result.txt_match, 102 + })) 103 + }
+158
onis-verify/src/checker.rs
··· 1 + use std::net::IpAddr; 2 + 3 + use anyhow::{Context, Result}; 4 + use hickory_resolver::config::{NameServerConfigGroup, ResolverConfig}; 5 + use hickory_resolver::name_server::TokioConnectionProvider; 6 + use hickory_resolver::TokioResolver; 7 + 8 + pub struct Checker { 9 + expected_ns: Vec<String>, 10 + nameservers: Option<Vec<IpAddr>>, 11 + dns_port: u16, 12 + } 13 + 14 + pub struct CheckResult { 15 + pub verified: bool, 16 + pub ns_records: Vec<String>, 17 + pub txt_match: bool, 18 + } 19 + 20 + impl Checker { 21 + pub fn new(expected_ns: Vec<String>, nameservers: Option<Vec<IpAddr>>, dns_port: u16) -> Self { 22 + let expected_ns = expected_ns 23 + .into_iter() 24 + .map(|ns| ns.to_lowercase().trim_end_matches('.').to_string()) 25 + .collect(); 26 + 27 + Self { 28 + expected_ns, 29 + nameservers, 30 + dns_port, 31 + } 32 + } 33 + 34 + fn build_resolver(&self) -> Result<TokioResolver> { 35 + let resolver = match &self.nameservers { 36 + Some(addrs) => { 37 + let ns_group = NameServerConfigGroup::from_ips_clear(addrs, self.dns_port, true); 38 + let config = ResolverConfig::from_parts(None, vec![], ns_group); 39 + TokioResolver::builder_with_config(config, TokioConnectionProvider::default()) 40 + .build() 41 + } 42 + None => TokioResolver::builder_tokio() 43 + .context("failed to read system resolver config")? 44 + .build(), 45 + }; 46 + 47 + Ok(resolver) 48 + } 49 + 50 + pub async fn check_zone(&self, zone: &str, did: &str) -> Result<CheckResult> { 51 + let resolver = self.build_resolver()?; 52 + 53 + let fqdn = if zone.ends_with('.') { 54 + zone.to_string() 55 + } else { 56 + format!("{zone}.") 57 + }; 58 + 59 + let ns_lookup = resolver 60 + .ns_lookup(&fqdn) 61 + .await 62 + .context("NS lookup failed")?; 63 + 64 + let ns_records: Vec<String> = ns_lookup 65 + .iter() 66 + .map(|ns| ns.to_string().to_lowercase().trim_end_matches('.').to_string()) 67 + .collect(); 68 + 69 + tracing::debug!( 70 + zone = %zone, 71 + ns = ?ns_records, 72 + "NS lookup result" 73 + ); 74 + 75 + let ns_valid = self.expected_ns.iter().all(|expected| { 76 + ns_records.iter().any(|actual| actual == expected) 77 + }); 78 + 79 + let txt_name = format!("_onis-verify.{fqdn}"); 80 + let txt_match = match resolver.txt_lookup(&txt_name).await { 81 + Ok(txt_lookup) => txt_lookup.iter().any(|txt| { 82 + let value: String = txt 83 + .txt_data() 84 + .iter() 85 + .map(|d| String::from_utf8_lossy(d)) 86 + .collect(); 87 + value.trim() == did 88 + }), 89 + Err(e) => { 90 + tracing::debug!( 91 + zone = %zone, 92 + name = %txt_name, 93 + error = %e, 94 + "TXT ownership lookup failed or no record found" 95 + ); 96 + false 97 + } 98 + }; 99 + 100 + tracing::debug!( 101 + zone = %zone, 102 + ns_valid, 103 + txt_match, 104 + did = %did, 105 + "verification check result" 106 + ); 107 + 108 + let verified = ns_valid && txt_match; 109 + 110 + Ok(CheckResult { 111 + verified, 112 + ns_records, 113 + txt_match, 114 + }) 115 + } 116 + } 117 + 118 + #[cfg(test)] 119 + mod tests { 120 + use super::*; 121 + 122 + #[test] 123 + fn new_normalizes_trailing_dots() { 124 + let checker = Checker::new( 125 + vec![ 126 + "ns1.example.com.".to_string(), 127 + "ns2.example.com.".to_string(), 128 + ], 129 + None, 130 + 53, 131 + ); 132 + assert_eq!(checker.expected_ns, vec!["ns1.example.com", "ns2.example.com"]); 133 + } 134 + 135 + #[test] 136 + fn new_normalizes_to_lowercase() { 137 + let checker = Checker::new(vec!["NS1.Example.COM".to_string()], None, 53); 138 + assert_eq!(checker.expected_ns, vec!["ns1.example.com"]); 139 + } 140 + 141 + #[test] 142 + fn new_normalizes_case_and_dots() { 143 + let checker = Checker::new(vec!["NS1.Example.COM.".to_string()], None, 53); 144 + assert_eq!(checker.expected_ns, vec!["ns1.example.com"]); 145 + } 146 + 147 + #[test] 148 + fn new_empty_expected_ns() { 149 + let checker = Checker::new(vec![], None, 53); 150 + assert!(checker.expected_ns.is_empty()); 151 + } 152 + 153 + #[test] 154 + fn new_preserves_dns_port() { 155 + let checker = Checker::new(vec![], None, 5353); 156 + assert_eq!(checker.dns_port, 5353); 157 + } 158 + }
+93
onis-verify/src/client.rs
··· 1 + use anyhow::{Context, Result}; 2 + use serde::{Deserialize, Serialize}; 3 + 4 + /// HTTP client for communicating with the onis appview API. 5 + #[derive(Clone)] 6 + pub struct AppviewClient { 7 + /// Underlying HTTP client. 8 + client: reqwest::Client, 9 + /// Base URL of the appview, e.g. `"http://localhost:3000"`. 10 + base_url: String, 11 + } 12 + 13 + /// A zone that is due for re-verification, returned by the appview stale zones endpoint. 14 + #[derive(Debug, Deserialize)] 15 + pub struct StaleZoneEntry { 16 + /// Domain name of the zone. 17 + pub zone: String, 18 + /// DID of the zone owner. 19 + pub did: String, 20 + /// Current verification status. 21 + pub verified: bool, 22 + /// Unix timestamp when the zone was first seen. 23 + pub first_seen: i64, 24 + /// Unix timestamp of the last successful verification, if any. 25 + pub last_verified: Option<i64>, 26 + } 27 + 28 + /// Wire format for the `GET /v1/zones/stale` response. 29 + #[derive(Debug, Deserialize)] 30 + struct StaleZonesResponse { 31 + /// List of zones needing re-verification. 32 + zones: Vec<StaleZoneEntry>, 33 + } 34 + 35 + /// Request body for `PUT /v1/verification`. 36 + #[derive(Serialize)] 37 + struct VerificationBody { 38 + /// Domain name of the zone. 39 + zone: String, 40 + /// DID of the zone owner. 41 + did: String, 42 + /// Whether the zone passed delegation verification. 43 + verified: bool, 44 + } 45 + 46 + impl AppviewClient { 47 + pub fn new(base_url: String) -> Self { 48 + Self { 49 + client: reqwest::Client::new(), 50 + base_url, 51 + } 52 + } 53 + 54 + pub async fn get_stale_zones(&self, checked_before: i64) -> Result<Vec<StaleZoneEntry>> { 55 + let url = format!( 56 + "{}/v1/zones/stale?checked_before={}", 57 + self.base_url, checked_before 58 + ); 59 + 60 + let body: StaleZonesResponse = self 61 + .client 62 + .get(&url) 63 + .send() 64 + .await 65 + .context("failed to fetch stale zones")? 66 + .error_for_status() 67 + .context("stale zones request failed")? 68 + .json() 69 + .await 70 + .context("failed to deserialize stale zones response")?; 71 + 72 + Ok(body.zones) 73 + } 74 + 75 + pub async fn set_verification(&self, zone: &str, did: &str, verified: bool) -> Result<()> { 76 + let url = format!("{}/v1/verification", self.base_url); 77 + 78 + self.client 79 + .put(&url) 80 + .json(&VerificationBody { 81 + zone: zone.to_string(), 82 + did: did.to_string(), 83 + verified, 84 + }) 85 + .send() 86 + .await 87 + .context("failed to set verification")? 88 + .error_for_status() 89 + .with_context(|| format!("set verification for {zone} failed"))?; 90 + 91 + Ok(()) 92 + } 93 + }
+182
onis-verify/src/main.rs
··· 1 + use std::sync::Arc; 2 + use std::time::{Duration, Instant}; 3 + 4 + use anyhow::Result; 5 + use onis_common::config::OnisConfig; 6 + use tracing_subscriber::EnvFilter; 7 + 8 + mod api; 9 + mod checker; 10 + mod client; 11 + 12 + use checker::Checker; 13 + use client::AppviewClient; 14 + 15 + #[tokio::main] 16 + async fn main() -> Result<()> { 17 + tracing_subscriber::fmt() 18 + .with_env_filter(EnvFilter::from_default_env()) 19 + .json() 20 + .init(); 21 + 22 + let metrics_handle = onis_common::metrics::init() 23 + .map_err(|e| anyhow::anyhow!("failed to install metrics recorder: {e}"))?; 24 + 25 + metrics::describe_counter!( 26 + "verification_checks_total", 27 + "Total verification checks" 28 + ); 29 + metrics::describe_histogram!( 30 + "verification_check_duration_seconds", 31 + "Verification check duration" 32 + ); 33 + metrics::describe_gauge!( 34 + "verification_zones_verified", 35 + "Number of verified zones" 36 + ); 37 + metrics::describe_gauge!( 38 + "verification_zones_unverified", 39 + "Number of unverified zones" 40 + ); 41 + 42 + let config = OnisConfig::load()?; 43 + let cfg = &config.verify; 44 + 45 + let nameservers = cfg.parse_nameservers() 46 + .map_err(|e| anyhow::anyhow!("invalid nameserver address: {e}"))?; 47 + 48 + tracing::info!( 49 + appview_url = %cfg.appview_url, 50 + port = cfg.port, 51 + check_interval = cfg.check_interval, 52 + recheck_interval = cfg.recheck_interval, 53 + expected_ns = ?cfg.expected_ns, 54 + "onis-verify starting" 55 + ); 56 + 57 + let client = AppviewClient::new(cfg.appview_url.clone()); 58 + let checker = Checker::new(cfg.expected_ns.clone(), nameservers, cfg.dns_port); 59 + 60 + let state = Arc::new(api::VerifyState { 61 + checker, 62 + client, 63 + metrics_handle, 64 + }); 65 + 66 + let api_bind = format!("{}:{}", cfg.bind, cfg.port); 67 + let api_state = state.clone(); 68 + let api_handle = tokio::spawn(async move { 69 + let app = api::router(api_state); 70 + let listener = tokio::net::TcpListener::bind(&api_bind).await.unwrap(); 71 + tracing::info!("verify API listening on {api_bind}"); 72 + axum::serve(listener, app).await.unwrap(); 73 + }); 74 + 75 + let check_interval = Duration::from_secs(cfg.check_interval); 76 + let recheck_interval = cfg.recheck_interval; 77 + let scheduler_state = state.clone(); 78 + let scheduler_handle = tokio::spawn(async move { 79 + run_scheduler(scheduler_state, check_interval, recheck_interval).await; 80 + }); 81 + 82 + tokio::select! { 83 + _ = api_handle => tracing::error!("verify API exited unexpectedly"), 84 + _ = scheduler_handle => tracing::error!("scheduler exited unexpectedly"), 85 + _ = tokio::signal::ctrl_c() => tracing::info!("shutting down"), 86 + } 87 + 88 + Ok(()) 89 + } 90 + 91 + async fn run_scheduler(state: Arc<api::VerifyState>, interval: Duration, recheck_interval: i64) { 92 + loop { 93 + tokio::time::sleep(interval).await; 94 + 95 + let now = chrono::Utc::now().timestamp(); 96 + let stale_threshold = now - recheck_interval; 97 + 98 + let zones = match state.client.get_stale_zones(stale_threshold).await { 99 + Ok(z) => z, 100 + Err(e) => { 101 + tracing::error!(error = %e, "failed to fetch stale zones"); 102 + continue; 103 + } 104 + }; 105 + 106 + if zones.is_empty() { 107 + tracing::debug!("no stale zones to check"); 108 + continue; 109 + } 110 + 111 + tracing::info!(count = zones.len(), "checking stale zones"); 112 + 113 + let mut verified_count: u64 = 0; 114 + let mut unverified_count: u64 = 0; 115 + 116 + for zone_entry in &zones { 117 + let check_start = Instant::now(); 118 + 119 + let result = match state.checker.check_zone(&zone_entry.zone, &zone_entry.did).await { 120 + Ok(r) => r, 121 + Err(e) => { 122 + tracing::warn!( 123 + zone = %zone_entry.zone, 124 + did = %zone_entry.did, 125 + error = %e, 126 + "verification check failed" 127 + ); 128 + metrics::counter!("verification_checks_total", "result" => "error") 129 + .increment(1); 130 + metrics::histogram!("verification_check_duration_seconds") 131 + .record(check_start.elapsed().as_secs_f64()); 132 + continue; 133 + } 134 + }; 135 + 136 + let result_label = if result.verified { "verified" } else { "unverified" }; 137 + metrics::counter!("verification_checks_total", "result" => result_label).increment(1); 138 + metrics::histogram!("verification_check_duration_seconds") 139 + .record(check_start.elapsed().as_secs_f64()); 140 + 141 + if result.verified { 142 + verified_count += 1; 143 + } else { 144 + unverified_count += 1; 145 + } 146 + 147 + if let Err(e) = state 148 + .client 149 + .set_verification(&zone_entry.zone, &zone_entry.did, result.verified) 150 + .await 151 + { 152 + tracing::error!( 153 + zone = %zone_entry.zone, 154 + did = %zone_entry.did, 155 + error = %e, 156 + "failed to update verification status" 157 + ); 158 + continue; 159 + } 160 + 161 + if result.verified != zone_entry.verified { 162 + tracing::info!( 163 + zone = %zone_entry.zone, 164 + did = %zone_entry.did, 165 + old = zone_entry.verified, 166 + new = result.verified, 167 + ns = ?result.ns_records, 168 + "verification status changed" 169 + ); 170 + } else { 171 + tracing::debug!( 172 + zone = %zone_entry.zone, 173 + verified = result.verified, 174 + "verification status unchanged" 175 + ); 176 + } 177 + } 178 + 179 + metrics::gauge!("verification_zones_verified").set(verified_count as f64); 180 + metrics::gauge!("verification_zones_unverified").set(unverified_count as f64); 181 + } 182 + }