diff --git a/Cargo.lock b/Cargo.lock index 4afff40..c692e2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,2359 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "comfy-table" +version = "7.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "document-features", + "parking_lot", + "rustix", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "data-breaker" version = "0.3.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "clap", + "comfy-table", + "directories", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "thiserror", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42e96ea38f49b191e08a1bab66c7ffdba24b06f9995b39a9dd60222e5b6f1da" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c7c5718134e770ee62af3b6b4a84518ec10101aad610c024b64d6ff29bb1ff" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index c316d74..ebb0e31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,19 @@ keywords = ["privacy", "data-brokers", "cli", "opt-out"] categories = ["command-line-utilities"] [dependencies] +tokio = { version = "1", features = ["full"] } +clap = { version = "4", features = ["derive"] } +rusqlite = { version = "0.32", features = ["bundled"] } +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" +directories = "6" +thiserror = "2" +anyhow = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +comfy-table = "7" +uuid = { version = "1", features = ["v4"] } +chrono = { version = "0.4", features = ["serde"] } +async-trait = "0.1" diff --git a/src/broker/dummy.rs b/src/broker/dummy.rs new file mode 100644 index 0000000..51ba17f --- /dev/null +++ b/src/broker/dummy.rs @@ -0,0 +1,95 @@ +use async_trait::async_trait; + +use super::{ + BrokerConnector, ConnectorCapabilities, DeletionStatusCheck, DeletionSubmission, FoundRecord, + PersonQuery, +}; + +/// A dummy broker connector for testing and demonstration. +/// Returns fake data — useful for verifying the CLI pipeline works end-to-end. +pub struct DummyBroker; + +#[async_trait] +impl BrokerConnector for DummyBroker { + fn id(&self) -> &str { + "dummy-broker" + } + + fn name(&self) -> &str { + "Dummy Broker" + } + + fn capabilities(&self) -> ConnectorCapabilities { + ConnectorCapabilities { + can_scan: true, + can_delete: true, + can_check_status: true, + } + } + + async fn scan(&self, query: &PersonQuery) -> anyhow::Result> { + let full_name = format!("{} {}", query.first_name, query.last_name); + + let mut records = vec![ + FoundRecord { + data_type: "name".into(), + data_value: full_name, + profile_url: Some("https://dummy-broker.example.com/profile/12345".into()), + metadata: None, + }, + FoundRecord { + data_type: "address".into(), + data_value: format!( + "123 Main St, {}, {}", + query.city.as_deref().unwrap_or("Anytown"), + query.state.as_deref().unwrap_or("CA") + ), + profile_url: Some("https://dummy-broker.example.com/profile/12345".into()), + metadata: None, + }, + ]; + + if let Some(email) = &query.email { + records.push(FoundRecord { + data_type: "email".into(), + data_value: email.clone(), + profile_url: None, + metadata: None, + }); + } + + if let Some(phone) = &query.phone { + records.push(FoundRecord { + data_type: "phone".into(), + data_value: phone.clone(), + profile_url: None, + metadata: None, + }); + } + + Ok(records) + } + + async fn request_deletion( + &self, + _query: &PersonQuery, + _records: &[FoundRecord], + ) -> anyhow::Result { + Ok(DeletionSubmission { + external_ref: format!("DUMMY-{}", uuid::Uuid::new_v4()), + message: Some("Deletion request submitted to Dummy Broker".into()), + }) + } + + async fn check_deletion_status( + &self, + external_ref: &str, + ) -> anyhow::Result { + // Always returns in_progress for the dummy + Ok(DeletionStatusCheck { + status: "in_progress".into(), + completed_at: None, + message: Some(format!("Request {external_ref} is being processed")), + }) + } +} diff --git a/src/broker/mod.rs b/src/broker/mod.rs new file mode 100644 index 0000000..f443891 --- /dev/null +++ b/src/broker/mod.rs @@ -0,0 +1,127 @@ +pub mod dummy; +pub mod registry; + +use std::collections::HashMap; +use std::sync::Arc; + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PersonQuery { + pub first_name: String, + pub last_name: String, + pub email: Option, + pub phone: Option, + pub city: Option, + pub state: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FoundRecord { + pub data_type: String, + pub data_value: String, + pub profile_url: Option, + pub metadata: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeletionSubmission { + pub external_ref: String, + pub message: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeletionStatusCheck { + pub status: String, + pub completed_at: Option, + pub message: Option, +} + +#[derive(Debug, Clone)] +pub struct ConnectorCapabilities { + pub can_scan: bool, + pub can_delete: bool, + pub can_check_status: bool, +} + +#[async_trait] +pub trait BrokerConnector: Send + Sync { + fn id(&self) -> &str; + fn name(&self) -> &str; + fn capabilities(&self) -> ConnectorCapabilities; + async fn scan(&self, query: &PersonQuery) -> anyhow::Result>; + async fn request_deletion( + &self, + query: &PersonQuery, + records: &[FoundRecord], + ) -> anyhow::Result; + async fn check_deletion_status( + &self, + external_ref: &str, + ) -> anyhow::Result; +} + +/// Build the map of all compiled-in connectors. +/// Contributors: add your connector here. +pub fn build_connector_registry() -> HashMap> { + let mut map: HashMap> = HashMap::new(); + + let dummy = Arc::new(dummy::DummyBroker); + map.insert(dummy.id().to_string(), dummy); + + map +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_connector_registry() { + let reg = build_connector_registry(); + assert!(reg.contains_key("dummy-broker")); + let dummy = reg.get("dummy-broker").unwrap(); + assert_eq!(dummy.name(), "Dummy Broker"); + assert!(dummy.capabilities().can_scan); + assert!(dummy.capabilities().can_delete); + assert!(dummy.capabilities().can_check_status); + } + + #[tokio::test] + async fn test_dummy_scan() { + let dummy = dummy::DummyBroker; + let query = PersonQuery { + first_name: "John".into(), + last_name: "Doe".into(), + email: Some("john@example.com".into()), + phone: None, + city: None, + state: None, + }; + let results = dummy.scan(&query).await.unwrap(); + assert!(!results.is_empty()); + } + + #[tokio::test] + async fn test_dummy_deletion() { + let dummy = dummy::DummyBroker; + let query = PersonQuery { + first_name: "John".into(), + last_name: "Doe".into(), + email: None, + phone: None, + city: None, + state: None, + }; + let records = dummy.scan(&query).await.unwrap(); + let submission = dummy.request_deletion(&query, &records).await.unwrap(); + assert!(!submission.external_ref.is_empty()); + + let status = dummy + .check_deletion_status(&submission.external_ref) + .await + .unwrap(); + assert_eq!(status.status, "in_progress"); + } +} diff --git a/src/broker/registry.rs b/src/broker/registry.rs new file mode 100644 index 0000000..c38d207 --- /dev/null +++ b/src/broker/registry.rs @@ -0,0 +1,48 @@ +use serde::Deserialize; + +use crate::config::REGISTRY_URL; +use crate::db::models::Broker; + +#[derive(Debug, Deserialize)] +struct RegistryBroker { + id: String, + name: String, + website: Option, + description: Option, + category: Option, + connector: Option, +} + +/// Fetch the broker registry from the remote URL and return Broker models. +pub async fn fetch_registry() -> anyhow::Result> { + let client = reqwest::Client::new(); + let resp = client + .get(REGISTRY_URL) + .header("User-Agent", "data-breaker") + .send() + .await?; + + if !resp.status().is_success() { + anyhow::bail!("Failed to fetch registry: HTTP {}", resp.status()); + } + + let registry_brokers: Vec = resp.json().await?; + let now = chrono::Utc::now().to_rfc3339(); + + let brokers = registry_brokers + .into_iter() + .map(|rb| Broker { + id: rb.id, + name: rb.name, + website: rb.website, + description: rb.description, + category: rb.category, + connector: rb.connector, + registry_updated_at: Some(now.clone()), + created_at: now.clone(), + updated_at: now.clone(), + }) + .collect(); + + Ok(brokers) +} diff --git a/src/cli/broker.rs b/src/cli/broker.rs new file mode 100644 index 0000000..27c7105 --- /dev/null +++ b/src/cli/broker.rs @@ -0,0 +1,56 @@ +use comfy_table::{Cell, Table}; + +use crate::db::Database; + +pub fn list_brokers(db: &Database, category: Option<&str>) -> anyhow::Result<()> { + let brokers = db.list_brokers(category)?; + + if brokers.is_empty() { + println!( + "No brokers found. Run `data-breaker registry update` to fetch the broker registry." + ); + return Ok(()); + } + + let mut table = Table::new(); + table.set_header(vec!["ID", "Name", "Category", "Website"]); + + for b in &brokers { + table.add_row(vec![ + Cell::new(&b.id), + Cell::new(&b.name), + Cell::new(b.category.as_deref().unwrap_or("-")), + Cell::new(b.website.as_deref().unwrap_or("-")), + ]); + } + + println!("{table}"); + Ok(()) +} + +pub fn broker_info(db: &Database, id: &str) -> anyhow::Result<()> { + let broker = db.get_broker(id)?; + match broker { + Some(b) => { + println!("ID: {}", b.id); + println!("Name: {}", b.name); + if let Some(w) = &b.website { + println!("Website: {w}"); + } + if let Some(d) = &b.description { + println!("Description: {d}"); + } + if let Some(c) = &b.category { + println!("Category: {c}"); + } + if let Some(conn) = &b.connector { + println!("Connector: {conn}"); + } + println!("Updated: {}", b.updated_at); + } + None => { + anyhow::bail!("Broker '{}' not found", id); + } + } + Ok(()) +} diff --git a/src/cli/delete.rs b/src/cli/delete.rs new file mode 100644 index 0000000..be6d38a --- /dev/null +++ b/src/cli/delete.rs @@ -0,0 +1,110 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use crate::broker::{BrokerConnector, PersonQuery}; +use crate::db::Database; +use crate::db::models::DeletionRequest; + +pub async fn delete( + db: &Database, + connectors: &HashMap>, + query: &PersonQuery, + all: bool, + broker_id: Option<&str>, + record_id: Option<&str>, +) -> anyhow::Result<()> { + let records = if let Some(rid) = record_id { + let r = db + .get_personal_record(rid)? + .ok_or_else(|| anyhow::anyhow!("Record '{}' not found", rid))?; + vec![r] + } else if let Some(bid) = broker_id { + let records = db.list_personal_records(Some(bid))?; + if records.is_empty() { + anyhow::bail!("No records found for broker '{}'", bid); + } + records + } else if all { + let records = db.list_personal_records(None)?; + if records.is_empty() { + anyhow::bail!("No records found. Run `data-breaker scan` first."); + } + records + } else { + anyhow::bail!("Specify --all, --broker , or --record "); + }; + + // Group records by broker + let mut by_broker: HashMap> = HashMap::new(); + for r in &records { + by_broker + .entry(r.broker_id.clone()) + .or_default() + .push(r.clone()); + } + + let mut submitted = 0usize; + let mut failed = 0usize; + + for (bid, broker_records) in &by_broker { + let connector = match connectors.get(bid.as_str()) { + Some(c) => c, + None => { + println!("No connector for broker '{}', skipping.", bid); + failed += broker_records.len(); + continue; + } + }; + + if !connector.capabilities().can_delete { + println!( + "Connector '{}' does not support deletion, skipping.", + connector.name() + ); + failed += broker_records.len(); + continue; + } + + let found_records: Vec<_> = broker_records + .iter() + .map(|r| crate::broker::FoundRecord { + data_type: r.data_type.clone(), + data_value: r.data_value.clone(), + profile_url: r.profile_url.clone(), + metadata: None, + }) + .collect(); + + println!("Requesting deletion from {}...", connector.name()); + match connector.request_deletion(query, &found_records).await { + Ok(submission) => { + let now = chrono::Utc::now().to_rfc3339(); + for r in broker_records { + let deletion = DeletionRequest { + id: uuid::Uuid::new_v4().to_string(), + broker_id: bid.clone(), + personal_record_id: Some(r.id.clone()), + status: "submitted".to_string(), + submitted_at: Some(now.clone()), + completed_at: None, + error_message: None, + external_ref: Some(submission.external_ref.clone()), + created_at: now.clone(), + updated_at: now.clone(), + }; + db.insert_deletion_request(&deletion)?; + } + submitted += broker_records.len(); + println!(" Submitted (ref: {})", submission.external_ref); + } + Err(e) => { + tracing::error!("Error deleting from {}: {}", bid, e); + println!(" Error: {e}"); + failed += broker_records.len(); + } + } + } + + println!("\nDeletion requests: {submitted} submitted, {failed} failed"); + Ok(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..7212fcc --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,114 @@ +pub mod broker; +pub mod delete; +pub mod registry; +pub mod report; +pub mod scan; +pub mod status; + +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command( + name = "data-breaker", + version, + about = "Break what data brokers do — automate personal data removal" +)] +pub struct Cli { + #[command(subcommand)] + pub command: Command, + + /// Increase logging verbosity (-v, -vv, -vvv) + #[arg(short, long, action = clap::ArgAction::Count, global = true)] + pub verbose: u8, +} + +#[derive(Subcommand)] +pub enum Command { + /// Manage the broker registry + Registry { + #[command(subcommand)] + command: RegistryCommand, + }, + /// List and inspect known data brokers + Broker { + #[command(subcommand)] + command: BrokerCommand, + }, + /// Scan data brokers for your personal information + Scan { + /// First name to search for + #[arg(long)] + first_name: String, + /// Last name to search for + #[arg(long)] + last_name: String, + /// Email address to search for + #[arg(long)] + email: Option, + /// Phone number to search for + #[arg(long)] + phone: Option, + /// City for location-based searches + #[arg(long)] + city: Option, + /// State for location-based searches + #[arg(long)] + state: Option, + /// Only scan specific brokers (comma-separated IDs) + #[arg(long, value_delimiter = ',')] + brokers: Vec, + }, + /// Request deletion of your personal data + Delete { + /// Delete all found records across all brokers + #[arg(long, conflicts_with_all = ["broker", "record"])] + all: bool, + /// Delete records from a specific broker + #[arg(long, conflicts_with = "record")] + broker: Option, + /// Delete a specific record by ID + #[arg(long)] + record: Option, + }, + /// Check the status of deletion requests + Status { + /// Filter by broker ID + #[arg(long)] + broker: Option, + /// Filter by status (pending, submitted, in_progress, completed, failed, rejected) + #[arg(long)] + filter: Option, + }, + /// Generate a report of findings and deletion status + Report { + /// Output format + #[arg(long, default_value = "terminal", value_parser = ["terminal", "json", "html"])] + format: String, + /// Output file path (stdout if not specified) + #[arg(long)] + output: Option, + }, +} + +#[derive(Subcommand)] +pub enum RegistryCommand { + /// Fetch the latest broker registry from GitHub + Update, + /// Show registry metadata + Info, +} + +#[derive(Subcommand)] +pub enum BrokerCommand { + /// List known data brokers + List { + /// Filter by category + #[arg(long)] + category: Option, + }, + /// Show details about a specific broker + Info { + /// Broker ID (slug) + id: String, + }, +} diff --git a/src/cli/registry.rs b/src/cli/registry.rs new file mode 100644 index 0000000..c50678a --- /dev/null +++ b/src/cli/registry.rs @@ -0,0 +1,29 @@ +use crate::db::Database; + +pub async fn update_registry(db: &Database) -> anyhow::Result<()> { + println!("Fetching broker registry..."); + let brokers = crate::broker::registry::fetch_registry().await?; + let count = brokers.len(); + + for broker in brokers { + db.upsert_broker(&broker)?; + } + + let now = chrono::Utc::now().to_rfc3339(); + db.set_registry_meta("last_fetched_at", &now)?; + + println!("Registry updated: {count} broker(s) synced."); + Ok(()) +} + +pub fn registry_info(db: &Database) -> anyhow::Result<()> { + let last_fetched = db.get_registry_meta("last_fetched_at")?; + let broker_count = db.list_brokers(None)?.len(); + + match last_fetched { + Some(ts) => println!("Last updated: {ts}"), + None => println!("Last updated: never (run `data-breaker registry update`)"), + } + println!("Brokers known: {broker_count}"); + Ok(()) +} diff --git a/src/cli/report.rs b/src/cli/report.rs new file mode 100644 index 0000000..e9e52fb --- /dev/null +++ b/src/cli/report.rs @@ -0,0 +1,26 @@ +use crate::db::Database; +use crate::report::{Report, ReportFormat}; + +pub fn generate_report(db: &Database, format: &str, output: Option<&str>) -> anyhow::Result<()> { + let report = Report::build(db)?; + + let fmt = match format { + "json" => ReportFormat::Json, + "html" => ReportFormat::Html, + _ => ReportFormat::Terminal, + }; + + let rendered = report.render(fmt)?; + + match output { + Some(path) => { + std::fs::write(path, &rendered)?; + println!("Report written to {path}"); + } + None => { + println!("{rendered}"); + } + } + + Ok(()) +} diff --git a/src/cli/scan.rs b/src/cli/scan.rs new file mode 100644 index 0000000..e83a37c --- /dev/null +++ b/src/cli/scan.rs @@ -0,0 +1,112 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use comfy_table::{Cell, Table}; + +use crate::broker::{BrokerConnector, PersonQuery}; +use crate::db::Database; +use crate::db::models::{Broker, PersonalRecord}; + +pub async fn scan( + db: &Database, + connectors: &HashMap>, + query: &PersonQuery, + broker_filter: &[String], +) -> anyhow::Result<()> { + let active_connectors: Vec<_> = if broker_filter.is_empty() { + connectors.iter().collect() + } else { + connectors + .iter() + .filter(|(id, _)| broker_filter.contains(id)) + .collect() + }; + + if active_connectors.is_empty() { + println!("No matching connectors found. Available connectors:"); + for id in connectors.keys() { + println!(" - {id}"); + } + return Ok(()); + } + + let mut total_found = 0usize; + + for (id, connector) in &active_connectors { + if !connector.capabilities().can_scan { + tracing::info!("Skipping {} (no scan capability)", id); + continue; + } + + // Ensure broker exists in DB (for FK constraint) + if db.get_broker(id)?.is_none() { + let now = chrono::Utc::now().to_rfc3339(); + db.upsert_broker(&Broker { + id: id.to_string(), + name: connector.name().to_string(), + website: None, + description: None, + category: None, + connector: Some(id.to_string()), + registry_updated_at: None, + created_at: now.clone(), + updated_at: now, + })?; + } + + println!("Scanning {}...", connector.name()); + match connector.scan(query).await { + Ok(records) => { + if records.is_empty() { + println!(" No records found."); + continue; + } + + for record in &records { + let personal_record = PersonalRecord { + id: uuid::Uuid::new_v4().to_string(), + broker_id: id.to_string(), + data_type: record.data_type.clone(), + data_value: record.data_value.clone(), + profile_url: record.profile_url.clone(), + raw_json: record + .metadata + .as_ref() + .map(|m| serde_json::to_string(m).unwrap_or_default()), + found_at: chrono::Utc::now().to_rfc3339(), + }; + db.upsert_personal_record(&personal_record)?; + } + + total_found += records.len(); + println!(" Found {} record(s).", records.len()); + } + Err(e) => { + tracing::error!("Error scanning {}: {}", id, e); + println!(" Error: {e}"); + } + } + } + + // Show summary table + let all_records = db.list_personal_records(None)?; + if !all_records.is_empty() { + println!("\n--- Scan Results ---"); + let mut table = Table::new(); + table.set_header(vec!["Broker", "Type", "Value", "Profile URL"]); + + for r in &all_records { + table.add_row(vec![ + Cell::new(&r.broker_id), + Cell::new(&r.data_type), + Cell::new(&r.data_value), + Cell::new(r.profile_url.as_deref().unwrap_or("-")), + ]); + } + + println!("{table}"); + } + + println!("\nTotal new records found this scan: {total_found}"); + Ok(()) +} diff --git a/src/cli/status.rs b/src/cli/status.rs new file mode 100644 index 0000000..b5e1975 --- /dev/null +++ b/src/cli/status.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use comfy_table::{Cell, Table}; + +use crate::broker::BrokerConnector; +use crate::db::Database; + +pub async fn status( + db: &Database, + connectors: &HashMap>, + broker_filter: Option<&str>, + status_filter: Option<&str>, +) -> anyhow::Result<()> { + let mut requests = db.list_deletion_requests(broker_filter)?; + + if let Some(filter) = status_filter { + requests.retain(|r| r.status == filter); + } + + if requests.is_empty() { + println!("No deletion requests found."); + return Ok(()); + } + + // Check for status updates on in-flight requests + for req in &mut requests { + if matches!(req.status.as_str(), "submitted" | "in_progress") + && let Some(ext_ref) = &req.external_ref + && let Some(connector) = connectors.get(&req.broker_id) + && connector.capabilities().can_check_status + { + match connector.check_deletion_status(ext_ref).await { + Ok(check) => { + if check.status != req.status { + req.status = check.status.clone(); + req.completed_at = check.completed_at.clone(); + req.error_message = check.message.clone(); + req.updated_at = chrono::Utc::now().to_rfc3339(); + db.update_deletion_request(req)?; + tracing::info!("Updated status for {} -> {}", req.id, check.status); + } + } + Err(e) => { + tracing::warn!("Could not check status for {}: {}", req.id, e); + } + } + } + } + + let mut table = Table::new(); + table.set_header(vec!["ID", "Broker", "Status", "Submitted", "External Ref"]); + + for req in &requests { + table.add_row(vec![ + Cell::new(&req.id[..8]), + Cell::new(&req.broker_id), + Cell::new(&req.status), + Cell::new(req.submitted_at.as_deref().unwrap_or("-")), + Cell::new(req.external_ref.as_deref().unwrap_or("-")), + ]); + } + + println!("{table}"); + Ok(()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5b699d0 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,18 @@ +use std::path::PathBuf; + +use directories::ProjectDirs; + +pub const REGISTRY_URL: &str = + "https://raw.githubusercontent.com/bombfork/data-breaker-registry/main/brokers.json"; + +pub fn project_dirs() -> anyhow::Result { + ProjectDirs::from("", "bombfork", "data-breaker") + .ok_or_else(|| anyhow::anyhow!("Could not determine home directory")) +} + +pub fn db_path() -> anyhow::Result { + let dirs = project_dirs()?; + let data_dir = dirs.data_dir(); + std::fs::create_dir_all(data_dir)?; + Ok(data_dir.join("data-breaker.db")) +} diff --git a/src/db/migrations.rs b/src/db/migrations.rs new file mode 100644 index 0000000..8d718c5 --- /dev/null +++ b/src/db/migrations.rs @@ -0,0 +1,66 @@ +use rusqlite::Connection; + +const MIGRATIONS: &[&str] = &[ + // Migration 1: Initial schema + "CREATE TABLE IF NOT EXISTS brokers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + website TEXT, + description TEXT, + category TEXT, + connector TEXT, + registry_updated_at TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) + ); + + CREATE TABLE IF NOT EXISTS personal_records ( + id TEXT PRIMARY KEY, + broker_id TEXT NOT NULL REFERENCES brokers(id), + data_type TEXT NOT NULL, + data_value TEXT NOT NULL, + profile_url TEXT, + raw_json TEXT, + found_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + UNIQUE(broker_id, data_type, data_value) + ); + + CREATE TABLE IF NOT EXISTS deletion_requests ( + id TEXT PRIMARY KEY, + broker_id TEXT NOT NULL REFERENCES brokers(id), + personal_record_id TEXT REFERENCES personal_records(id), + status TEXT NOT NULL DEFAULT 'pending', + submitted_at TEXT, + completed_at TEXT, + error_message TEXT, + external_ref TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) + ); + + CREATE TABLE IF NOT EXISTS registry_meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + );", +]; + +pub fn run_migrations(conn: &Connection) -> rusqlite::Result<()> { + conn.execute_batch("CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY)")?; + + let current_version: i64 = conn.query_row( + "SELECT COALESCE(MAX(version), 0) FROM _migrations", + [], + |row| row.get(0), + )?; + + for (i, sql) in MIGRATIONS.iter().enumerate() { + let version = (i + 1) as i64; + if version > current_version { + conn.execute_batch(sql)?; + conn.execute("INSERT INTO _migrations (version) VALUES (?1)", [version])?; + tracing::info!("Applied migration {version}"); + } + } + + Ok(()) +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..ad8b2ae --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,179 @@ +pub mod migrations; +pub mod models; +pub mod queries; + +use std::path::Path; +use std::sync::Mutex; + +use rusqlite::Connection; + +pub struct Database { + conn: Mutex, +} + +impl Database { + pub fn open(path: &Path) -> anyhow::Result { + let conn = Connection::open(path)?; + conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?; + migrations::run_migrations(&conn)?; + Ok(Self { + conn: Mutex::new(conn), + }) + } + + #[cfg(test)] + pub fn open_in_memory() -> anyhow::Result { + let conn = Connection::open_in_memory()?; + conn.execute_batch("PRAGMA foreign_keys=ON;")?; + migrations::run_migrations(&conn)?; + Ok(Self { + conn: Mutex::new(conn), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::models::{Broker, DeletionRequest, PersonalRecord}; + + fn test_db() -> Database { + Database::open_in_memory().expect("Failed to create test database") + } + + #[test] + fn test_migrations_run() { + let db = test_db(); + let conn = db.conn.lock().unwrap(); + // Verify tables exist + let count: i64 = conn + .query_row("SELECT COUNT(*) FROM _migrations", [], |row| row.get(0)) + .unwrap(); + assert!(count >= 1); + } + + #[test] + fn test_broker_crud() { + let db = test_db(); + let now = chrono::Utc::now().to_rfc3339(); + let broker = Broker { + id: "test-broker".into(), + name: "Test Broker".into(), + website: Some("https://example.com".into()), + description: Some("A test broker".into()), + category: Some("people-search".into()), + connector: None, + registry_updated_at: None, + created_at: now.clone(), + updated_at: now, + }; + + db.upsert_broker(&broker).unwrap(); + + let fetched = db.get_broker("test-broker").unwrap().unwrap(); + assert_eq!(fetched.name, "Test Broker"); + + let all = db.list_brokers(None).unwrap(); + assert_eq!(all.len(), 1); + + let filtered = db.list_brokers(Some("people-search")).unwrap(); + assert_eq!(filtered.len(), 1); + + let empty = db.list_brokers(Some("nonexistent")).unwrap(); + assert!(empty.is_empty()); + } + + #[test] + fn test_personal_record_crud() { + let db = test_db(); + let now = chrono::Utc::now().to_rfc3339(); + + // Need a broker first (FK) + let broker = Broker { + id: "test-broker".into(), + name: "Test".into(), + website: None, + description: None, + category: None, + connector: None, + registry_updated_at: None, + created_at: now.clone(), + updated_at: now.clone(), + }; + db.upsert_broker(&broker).unwrap(); + + let record = PersonalRecord { + id: "rec-1".into(), + broker_id: "test-broker".into(), + data_type: "name".into(), + data_value: "John Doe".into(), + profile_url: Some("https://example.com/john".into()), + raw_json: None, + found_at: now, + }; + db.upsert_personal_record(&record).unwrap(); + + let fetched = db.get_personal_record("rec-1").unwrap().unwrap(); + assert_eq!(fetched.data_value, "John Doe"); + + let all = db.list_personal_records(None).unwrap(); + assert_eq!(all.len(), 1); + + let by_broker = db.list_personal_records(Some("test-broker")).unwrap(); + assert_eq!(by_broker.len(), 1); + } + + #[test] + fn test_deletion_request_crud() { + let db = test_db(); + let now = chrono::Utc::now().to_rfc3339(); + + let broker = Broker { + id: "test-broker".into(), + name: "Test".into(), + website: None, + description: None, + category: None, + connector: None, + registry_updated_at: None, + created_at: now.clone(), + updated_at: now.clone(), + }; + db.upsert_broker(&broker).unwrap(); + + let req = DeletionRequest { + id: "del-1".into(), + broker_id: "test-broker".into(), + personal_record_id: None, + status: "submitted".into(), + submitted_at: Some(now.clone()), + completed_at: None, + error_message: None, + external_ref: Some("ref-123".into()), + created_at: now.clone(), + updated_at: now.clone(), + }; + db.insert_deletion_request(&req).unwrap(); + + let all = db.list_deletion_requests(None).unwrap(); + assert_eq!(all.len(), 1); + assert_eq!(all[0].status, "submitted"); + } + + #[test] + fn test_registry_meta() { + let db = test_db(); + assert!(db.get_registry_meta("last_fetched_at").unwrap().is_none()); + + db.set_registry_meta("last_fetched_at", "2024-01-01T00:00:00Z") + .unwrap(); + let val = db.get_registry_meta("last_fetched_at").unwrap().unwrap(); + assert_eq!(val, "2024-01-01T00:00:00Z"); + + // Update + db.set_registry_meta("last_fetched_at", "2024-06-01T00:00:00Z") + .unwrap(); + let val = db.get_registry_meta("last_fetched_at").unwrap().unwrap(); + assert_eq!(val, "2024-06-01T00:00:00Z"); + } +} diff --git a/src/db/models.rs b/src/db/models.rs new file mode 100644 index 0000000..021d129 --- /dev/null +++ b/src/db/models.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Broker { + pub id: String, + pub name: String, + pub website: Option, + pub description: Option, + pub category: Option, + pub connector: Option, + pub registry_updated_at: Option, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PersonalRecord { + pub id: String, + pub broker_id: String, + pub data_type: String, + pub data_value: String, + pub profile_url: Option, + pub raw_json: Option, + pub found_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeletionRequest { + pub id: String, + pub broker_id: String, + pub personal_record_id: Option, + pub status: String, + pub submitted_at: Option, + pub completed_at: Option, + pub error_message: Option, + pub external_ref: Option, + pub created_at: String, + pub updated_at: String, +} diff --git a/src/db/queries.rs b/src/db/queries.rs new file mode 100644 index 0000000..1b3fc97 --- /dev/null +++ b/src/db/queries.rs @@ -0,0 +1,315 @@ +use rusqlite::params; + +use super::Database; +use super::models::{Broker, DeletionRequest, PersonalRecord}; + +impl Database { + // --- Brokers --- + + pub fn upsert_broker(&self, broker: &Broker) -> anyhow::Result<()> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "INSERT INTO brokers (id, name, website, description, category, connector, registry_updated_at, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + website = excluded.website, + description = excluded.description, + category = excluded.category, + connector = excluded.connector, + registry_updated_at = excluded.registry_updated_at, + updated_at = excluded.updated_at", + params![ + broker.id, + broker.name, + broker.website, + broker.description, + broker.category, + broker.connector, + broker.registry_updated_at, + broker.created_at, + broker.updated_at, + ], + )?; + Ok(()) + } + + pub fn get_broker(&self, id: &str) -> anyhow::Result> { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT id, name, website, description, category, connector, registry_updated_at, created_at, updated_at + FROM brokers WHERE id = ?1", + )?; + let mut rows = stmt.query_map(params![id], |row| { + Ok(Broker { + id: row.get(0)?, + name: row.get(1)?, + website: row.get(2)?, + description: row.get(3)?, + category: row.get(4)?, + connector: row.get(5)?, + registry_updated_at: row.get(6)?, + created_at: row.get(7)?, + updated_at: row.get(8)?, + }) + })?; + match rows.next() { + Some(row) => Ok(Some(row?)), + None => Ok(None), + } + } + + pub fn list_brokers(&self, category: Option<&str>) -> anyhow::Result> { + let conn = self.conn.lock().unwrap(); + let mut brokers = Vec::new(); + + if let Some(cat) = category { + let mut stmt = conn.prepare( + "SELECT id, name, website, description, category, connector, registry_updated_at, created_at, updated_at + FROM brokers WHERE category = ?1 ORDER BY name", + )?; + let rows = stmt.query_map(params![cat], |row| { + Ok(Broker { + id: row.get(0)?, + name: row.get(1)?, + website: row.get(2)?, + description: row.get(3)?, + category: row.get(4)?, + connector: row.get(5)?, + registry_updated_at: row.get(6)?, + created_at: row.get(7)?, + updated_at: row.get(8)?, + }) + })?; + for row in rows { + brokers.push(row?); + } + } else { + let mut stmt = conn.prepare( + "SELECT id, name, website, description, category, connector, registry_updated_at, created_at, updated_at + FROM brokers ORDER BY name", + )?; + let rows = stmt.query_map([], |row| { + Ok(Broker { + id: row.get(0)?, + name: row.get(1)?, + website: row.get(2)?, + description: row.get(3)?, + category: row.get(4)?, + connector: row.get(5)?, + registry_updated_at: row.get(6)?, + created_at: row.get(7)?, + updated_at: row.get(8)?, + }) + })?; + for row in rows { + brokers.push(row?); + } + } + + Ok(brokers) + } + + // --- Personal Records --- + + pub fn upsert_personal_record(&self, record: &PersonalRecord) -> anyhow::Result<()> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "INSERT INTO personal_records (id, broker_id, data_type, data_value, profile_url, raw_json, found_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + ON CONFLICT(broker_id, data_type, data_value) DO UPDATE SET + profile_url = excluded.profile_url, + raw_json = excluded.raw_json, + found_at = excluded.found_at", + params![ + record.id, + record.broker_id, + record.data_type, + record.data_value, + record.profile_url, + record.raw_json, + record.found_at, + ], + )?; + Ok(()) + } + + pub fn get_personal_record(&self, id: &str) -> anyhow::Result> { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT id, broker_id, data_type, data_value, profile_url, raw_json, found_at + FROM personal_records WHERE id = ?1", + )?; + let mut rows = stmt.query_map(params![id], |row| { + Ok(PersonalRecord { + id: row.get(0)?, + broker_id: row.get(1)?, + data_type: row.get(2)?, + data_value: row.get(3)?, + profile_url: row.get(4)?, + raw_json: row.get(5)?, + found_at: row.get(6)?, + }) + })?; + match rows.next() { + Some(row) => Ok(Some(row?)), + None => Ok(None), + } + } + + pub fn list_personal_records( + &self, + broker_id: Option<&str>, + ) -> anyhow::Result> { + let conn = self.conn.lock().unwrap(); + let mut records = Vec::new(); + + if let Some(bid) = broker_id { + let mut stmt = conn.prepare( + "SELECT id, broker_id, data_type, data_value, profile_url, raw_json, found_at + FROM personal_records WHERE broker_id = ?1 ORDER BY found_at DESC", + )?; + let rows = stmt.query_map(params![bid], |row| { + Ok(PersonalRecord { + id: row.get(0)?, + broker_id: row.get(1)?, + data_type: row.get(2)?, + data_value: row.get(3)?, + profile_url: row.get(4)?, + raw_json: row.get(5)?, + found_at: row.get(6)?, + }) + })?; + for row in rows { + records.push(row?); + } + } else { + let mut stmt = conn.prepare( + "SELECT id, broker_id, data_type, data_value, profile_url, raw_json, found_at + FROM personal_records ORDER BY found_at DESC", + )?; + let rows = stmt.query_map([], |row| { + Ok(PersonalRecord { + id: row.get(0)?, + broker_id: row.get(1)?, + data_type: row.get(2)?, + data_value: row.get(3)?, + profile_url: row.get(4)?, + raw_json: row.get(5)?, + found_at: row.get(6)?, + }) + })?; + for row in rows { + records.push(row?); + } + } + + Ok(records) + } + + // --- Deletion Requests --- + + pub fn insert_deletion_request(&self, req: &DeletionRequest) -> anyhow::Result<()> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "INSERT INTO deletion_requests (id, broker_id, personal_record_id, status, submitted_at, completed_at, error_message, external_ref, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + params![ + req.id, + req.broker_id, + req.personal_record_id, + req.status, + req.submitted_at, + req.completed_at, + req.error_message, + req.external_ref, + req.created_at, + req.updated_at, + ], + )?; + Ok(()) + } + + pub fn update_deletion_request(&self, req: &DeletionRequest) -> anyhow::Result<()> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "UPDATE deletion_requests SET status = ?2, completed_at = ?3, error_message = ?4, updated_at = ?5 + WHERE id = ?1", + params![ + req.id, + req.status, + req.completed_at, + req.error_message, + req.updated_at, + ], + )?; + Ok(()) + } + + pub fn list_deletion_requests( + &self, + broker_id: Option<&str>, + ) -> anyhow::Result> { + let conn = self.conn.lock().unwrap(); + let mut requests = Vec::new(); + + if let Some(bid) = broker_id { + let mut stmt = conn.prepare( + "SELECT id, broker_id, personal_record_id, status, submitted_at, completed_at, error_message, external_ref, created_at, updated_at + FROM deletion_requests WHERE broker_id = ?1 ORDER BY created_at DESC", + )?; + let rows = stmt.query_map(params![bid], Self::map_deletion_row)?; + for row in rows { + requests.push(row?); + } + } else { + let mut stmt = conn.prepare( + "SELECT id, broker_id, personal_record_id, status, submitted_at, completed_at, error_message, external_ref, created_at, updated_at + FROM deletion_requests ORDER BY created_at DESC", + )?; + let rows = stmt.query_map([], Self::map_deletion_row)?; + for row in rows { + requests.push(row?); + } + } + + Ok(requests) + } + + fn map_deletion_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(DeletionRequest { + id: row.get(0)?, + broker_id: row.get(1)?, + personal_record_id: row.get(2)?, + status: row.get(3)?, + submitted_at: row.get(4)?, + completed_at: row.get(5)?, + error_message: row.get(6)?, + external_ref: row.get(7)?, + created_at: row.get(8)?, + updated_at: row.get(9)?, + }) + } + + // --- Registry Meta --- + + pub fn set_registry_meta(&self, key: &str, value: &str) -> anyhow::Result<()> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "INSERT INTO registry_meta (key, value) VALUES (?1, ?2) + ON CONFLICT(key) DO UPDATE SET value = excluded.value", + params![key, value], + )?; + Ok(()) + } + + pub fn get_registry_meta(&self, key: &str) -> anyhow::Result> { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare("SELECT value FROM registry_meta WHERE key = ?1")?; + let mut rows = stmt.query_map(params![key], |row| row.get(0))?; + match rows.next() { + Some(row) => Ok(Some(row?)), + None => Ok(None), + } + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..131c374 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,32 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +#[allow(dead_code)] +pub enum AppError { + #[error("Database error: {0}")] + Database(#[from] rusqlite::Error), + + #[error("Network error: {0}")] + Network(#[from] reqwest::Error), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + + #[error("Broker not found: {0}")] + BrokerNotFound(String), + + #[error("Record not found: {0}")] + RecordNotFound(String), + + #[error("No records to delete")] + NoRecordsToDelete, + + #[error("Registry error: {0}")] + Registry(String), + + #[error("Configuration error: {0}")] + Config(String), +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..c92840f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,102 @@ -fn main() { - println!("Hello, world!"); +mod broker; +mod cli; +mod config; +mod db; +mod error; +mod report; + +use clap::Parser; +use cli::{BrokerCommand, Cli, Command, RegistryCommand}; + +use crate::broker::PersonQuery; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + // Set up tracing + let filter = match cli.verbose { + 0 => "warn", + 1 => "info", + 2 => "debug", + _ => "trace", + }; + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(filter)), + ) + .with_target(false) + .init(); + + // Open database + let db_path = config::db_path()?; + let db = db::Database::open(&db_path)?; + + // Build connector registry + let connectors = broker::build_connector_registry(); + + match cli.command { + Command::Registry { command } => match command { + RegistryCommand::Update => cli::registry::update_registry(&db).await?, + RegistryCommand::Info => cli::registry::registry_info(&db)?, + }, + Command::Broker { command } => match command { + BrokerCommand::List { category } => { + cli::broker::list_brokers(&db, category.as_deref())? + } + BrokerCommand::Info { id } => cli::broker::broker_info(&db, &id)?, + }, + Command::Scan { + first_name, + last_name, + email, + phone, + city, + state, + brokers, + } => { + let query = PersonQuery { + first_name, + last_name, + email, + phone, + city, + state, + }; + cli::scan::scan(&db, &connectors, &query, &brokers).await?; + } + Command::Delete { + all, + broker: broker_id, + record, + } => { + // Build a minimal query for deletion — connectors may need it + let query = PersonQuery { + first_name: String::new(), + last_name: String::new(), + email: None, + phone: None, + city: None, + state: None, + }; + cli::delete::delete( + &db, + &connectors, + &query, + all, + broker_id.as_deref(), + record.as_deref(), + ) + .await?; + } + Command::Status { broker, filter } => { + cli::status::status(&db, &connectors, broker.as_deref(), filter.as_deref()).await?; + } + Command::Report { format, output } => { + cli::report::generate_report(&db, &format, output.as_deref())?; + } + } + + Ok(()) } diff --git a/src/report/html.rs b/src/report/html.rs new file mode 100644 index 0000000..5279ac4 --- /dev/null +++ b/src/report/html.rs @@ -0,0 +1,98 @@ +use super::Report; + +pub fn render(report: &Report) -> anyhow::Result { + let mut html = String::new(); + + html.push_str("\n\n\n"); + html.push_str("\n"); + html.push_str("\n"); + html.push_str("Data Breaker Report\n"); + html.push_str("\n"); + html.push_str("\n\n"); + + html.push_str(&format!( + "

Data Breaker Report

\n

Generated: {}

\n", + report.generated_at + )); + + // Summary cards + html.push_str("
\n"); + write_stat(&mut html, "Brokers Tracked", report.summary.total_brokers); + write_stat(&mut html, "Records Found", report.summary.total_records); + write_stat( + &mut html, + "Deletions Submitted", + report.summary.deletions_submitted, + ); + write_stat( + &mut html, + "Deletions Completed", + report.summary.deletions_completed, + ); + write_stat( + &mut html, + "Deletions Failed", + report.summary.deletions_failed, + ); + html.push_str("
\n"); + + // Records table + if !report.records.is_empty() { + html.push_str("

Personal Records Found

\n"); + html.push_str("\n\n\n"); + for r in &report.records { + html.push_str(&format!( + "\n", + escape_html(&r.broker_id), + escape_html(&r.data_type), + escape_html(&r.data_value), + escape_html(&r.found_at), + )); + } + html.push_str("
BrokerTypeValueFound At
{}{}{}{}
\n"); + } + + // Deletion requests table + if !report.deletion_requests.is_empty() { + html.push_str("

Deletion Requests

\n"); + html.push_str("\n\n\n"); + for req in &report.deletion_requests { + html.push_str(&format!( + "\n", + escape_html(&req.id[..8]), + escape_html(&req.broker_id), + escape_html(&req.status), + escape_html(req.submitted_at.as_deref().unwrap_or("-")), + )); + } + html.push_str("
IDBrokerStatusSubmitted
{}{}{}{}
\n"); + } + + html.push_str("\n\n"); + + Ok(html) +} + +fn write_stat(html: &mut String, label: &str, value: usize) { + html.push_str(&format!( + "
{value}
{label}
\n" + )); +} + +fn escape_html(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} diff --git a/src/report/json.rs b/src/report/json.rs new file mode 100644 index 0000000..64008c5 --- /dev/null +++ b/src/report/json.rs @@ -0,0 +1,5 @@ +use super::Report; + +pub fn render(report: &Report) -> anyhow::Result { + Ok(serde_json::to_string_pretty(report)?) +} diff --git a/src/report/mod.rs b/src/report/mod.rs new file mode 100644 index 0000000..5c62152 --- /dev/null +++ b/src/report/mod.rs @@ -0,0 +1,80 @@ +pub mod html; +pub mod json; +pub mod terminal; + +use serde::Serialize; + +use crate::db::Database; +use crate::db::models::{Broker, DeletionRequest, PersonalRecord}; + +pub enum ReportFormat { + Terminal, + Json, + Html, +} + +#[derive(Debug, Serialize)] +pub struct Report { + pub generated_at: String, + pub brokers: Vec, + pub records: Vec, + pub deletion_requests: Vec, + pub summary: ReportSummary, +} + +#[derive(Debug, Serialize)] +pub struct ReportSummary { + pub total_brokers: usize, + pub total_records: usize, + pub total_deletions: usize, + pub deletions_pending: usize, + pub deletions_submitted: usize, + pub deletions_completed: usize, + pub deletions_failed: usize, +} + +impl Report { + pub fn build(db: &Database) -> anyhow::Result { + let brokers = db.list_brokers(None)?; + let records = db.list_personal_records(None)?; + let deletion_requests = db.list_deletion_requests(None)?; + + let summary = ReportSummary { + total_brokers: brokers.len(), + total_records: records.len(), + total_deletions: deletion_requests.len(), + deletions_pending: deletion_requests + .iter() + .filter(|r| r.status == "pending") + .count(), + deletions_submitted: deletion_requests + .iter() + .filter(|r| r.status == "submitted") + .count(), + deletions_completed: deletion_requests + .iter() + .filter(|r| r.status == "completed") + .count(), + deletions_failed: deletion_requests + .iter() + .filter(|r| r.status == "failed" || r.status == "rejected") + .count(), + }; + + Ok(Self { + generated_at: chrono::Utc::now().to_rfc3339(), + brokers, + records, + deletion_requests, + summary, + }) + } + + pub fn render(&self, format: ReportFormat) -> anyhow::Result { + match format { + ReportFormat::Terminal => terminal::render(self), + ReportFormat::Json => json::render(self), + ReportFormat::Html => html::render(self), + } + } +} diff --git a/src/report/terminal.rs b/src/report/terminal.rs new file mode 100644 index 0000000..229083e --- /dev/null +++ b/src/report/terminal.rs @@ -0,0 +1,80 @@ +use comfy_table::{Cell, Table}; + +use super::Report; + +pub fn render(report: &Report) -> anyhow::Result { + let mut output = String::new(); + + output.push_str(&format!( + "=== Data Breaker Report ({}) ===\n\n", + report.generated_at + )); + + // Summary + output.push_str("--- Summary ---\n"); + output.push_str(&format!( + "Brokers tracked: {}\n", + report.summary.total_brokers + )); + output.push_str(&format!( + "Records found: {}\n", + report.summary.total_records + )); + output.push_str(&format!( + "Deletion requests: {}\n", + report.summary.total_deletions + )); + output.push_str(&format!( + " Pending: {}\n", + report.summary.deletions_pending + )); + output.push_str(&format!( + " Submitted: {}\n", + report.summary.deletions_submitted + )); + output.push_str(&format!( + " Completed: {}\n", + report.summary.deletions_completed + )); + output.push_str(&format!( + " Failed/Rejected: {}\n", + report.summary.deletions_failed + )); + + // Records table + if !report.records.is_empty() { + output.push_str("\n--- Personal Records Found ---\n"); + let mut table = Table::new(); + table.set_header(vec!["Broker", "Type", "Value", "Found At"]); + for r in &report.records { + table.add_row(vec![ + Cell::new(&r.broker_id), + Cell::new(&r.data_type), + Cell::new(&r.data_value), + Cell::new(&r.found_at), + ]); + } + output.push_str(&table.to_string()); + output.push('\n'); + } + + // Deletion requests table + if !report.deletion_requests.is_empty() { + output.push_str("\n--- Deletion Requests ---\n"); + let mut table = Table::new(); + table.set_header(vec!["ID", "Broker", "Status", "Submitted", "External Ref"]); + for req in &report.deletion_requests { + table.add_row(vec![ + Cell::new(&req.id[..8]), + Cell::new(&req.broker_id), + Cell::new(&req.status), + Cell::new(req.submitted_at.as_deref().unwrap_or("-")), + Cell::new(req.external_ref.as_deref().unwrap_or("-")), + ]); + } + output.push_str(&table.to_string()); + output.push('\n'); + } + + Ok(output) +}