diff --git a/.gitignore b/.gitignore index 0704a0eb..7156a1fc 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,4 @@ node_modules .rustup /config/ local_data -.env \ No newline at end of file +.env diff --git a/Cargo.lock b/Cargo.lock index c6a6af89..ce3a9030 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,7 +91,7 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "async-bb8-diesel" version = "0.3.0" -source = "git+https://github.com/oxidecomputer/async-bb8-diesel#18baf49aca5f72bf441951b11669c6c3c3affe70" +source = "git+https://github.com/oxidecomputer/async-bb8-diesel#f049570ff89080b2385c637af4c0e614b0a124a4" dependencies = [ "async-trait", "bb8", @@ -157,6 +157,453 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c456581cb3c77fafcc8c67204a70680d40b61112d6da78c77bd31d945b65f1b5" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.4.0", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c635c2dc792cb4a11ce1a4f392a925340d1bdf499289b5ec1ec6810954eb43f5" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.122.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c2ca0cba97e8e279eb6c0b2d0aa10db5959000e602ab2b7c02de6b85d4c19b" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "http-body 1.0.1", + "lru", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.93.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcb38bb33fc0a11f1ffc3e3e85669e0a11a37690b86f77e75306d8f369146a0" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ada8ffbea7bd1be1f53df1dadb0f8fdb04badb13185b3321b929d1ee3caad09" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6443ccadc777095d5ed13e21f5c364878c9f5bad4e35187a6cdbd863b0afcad" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "p256 0.11.1", + "percent-encoding", + "ring", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.64.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf418858f9f3edd228acb8759d77394fed7531cce78d02bdda499025368439" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b9c7354a3b13c66f60fe4616d6d1969c9fd36b1b5333a5dfb3ee716b33c588" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fb0abf49ff0cab20fd31ac1215ed7ce0ea92286ba09e2854b42ba5cabe7525" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.8.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.36", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base16ct" version = "0.2.0" @@ -169,6 +616,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -225,13 +682,23 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "camino" version = "1.2.2" @@ -243,11 +710,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -279,9 +748,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -289,9 +758,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -301,9 +770,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -317,6 +786,15 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -330,7 +808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" dependencies = [ "async-trait", - "convert_case", + "convert_case 0.6.0", "json5", "pathdiff", "ron", @@ -378,6 +856,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -423,6 +910,33 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc-fast" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" +dependencies = [ + "crc", + "digest", + "rustversion", + "spin 0.10.0", +] + [[package]] name = "crc32c" version = "0.6.8" @@ -432,6 +946,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -453,6 +976,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -557,6 +1092,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21" +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der" version = "0.7.10" @@ -716,9 +1261,9 @@ checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "dropshot" -version = "0.16.6" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d0df98c06659ab85a454f32dc36ca5dbc6500bd2a58f25ede4dc1f1d478904e" +checksum = "d69fd85c8dfc67252d02f260595f6b62b5abceb1b88b4b9722369d27936e5fa4" dependencies = [ "async-stream", "async-trait", @@ -733,7 +1278,7 @@ dependencies = [ "hostname 0.4.2", "http 1.4.0", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "indexmap 2.13.0", "multer", @@ -767,7 +1312,7 @@ dependencies = [ [[package]] name = "dropshot-authorization-header" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/v-api#5ea039de839c4a88943961e8bbfb298781671755" +source = "git+https://github.com/oxidecomputer/v-api#21d760b210429e23df76a1f6bf691c94b3de5488" dependencies = [ "async-trait", "base64", @@ -786,7 +1331,7 @@ dependencies = [ "hex", "hmac", "http 1.4.0", - "hyper", + "hyper 1.8.1", "schemars 0.8.22", "serde", "serde_json", @@ -797,9 +1342,9 @@ dependencies = [ [[package]] name = "dropshot_endpoint" -version = "0.16.6" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e53aef8838e0e341485590738ab180a6dceff3565ffcb198d5f365fea650378" +checksum = "67d106478e4a4782556981d028a667f41c4845cdaa6e2d3a9f58c5d15e725401" dependencies = [ "heck", "proc-macro2", @@ -824,24 +1369,42 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -859,8 +1422,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", ] [[package]] @@ -886,23 +1449,43 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest", - "ff", + "ff 0.13.1", "generic-array", - "group", + "group 0.13.0", "hkdf", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -958,6 +1541,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ff" version = "0.13.1" @@ -976,9 +1569,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" @@ -1013,6 +1606,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -1156,7 +1755,7 @@ dependencies = [ "chrono", "http 1.4.0", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "mime", "percent-encoding", @@ -1176,8 +1775,8 @@ checksum = "cef55117f2b1d40e56f2fd26161a2e93c9d7899be9389a9d4aa59308168e880c" dependencies = [ "chrono", "google-apis-common", - "hyper", - "hyper-rustls", + "hyper 1.8.1", + "hyper-rustls 0.27.7", "hyper-util", "mime", "serde", @@ -1196,8 +1795,8 @@ checksum = "30742c5730cad187ffb8922f1a91766830676ec6b714489fcdb89e9b9385fd7c" dependencies = [ "chrono", "google-apis-common", - "hyper", - "hyper-rustls", + "hyper 1.8.1", + "hyper-rustls 0.27.7", "hyper-util", "mime", "serde", @@ -1216,8 +1815,8 @@ checksum = "0d454a68994bbdae724ec6a3129a23ec2a86ef29f8ae9e92bb3a3578c193284c" dependencies = [ "chrono", "google-apis-common", - "hyper", - "hyper-rustls", + "hyper 1.8.1", + "hyper-rustls 0.27.7", "hyper-util", "mime", "serde", @@ -1251,17 +1850,47 @@ dependencies = [ "web-time", ] +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.13" @@ -1410,6 +2039,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1429,7 +2069,7 @@ dependencies = [ "bytes", "futures-core", "http 1.4.0", - "http-body", + "http-body 1.0.1", "pin-project-lite", ] @@ -1445,6 +2085,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.8.1" @@ -1455,9 +2119,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", + "h2 0.4.13", "http 1.4.0", - "http-body", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1468,6 +2132,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.7" @@ -1475,7 +2154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.4.0", - "hyper", + "hyper 1.8.1", "hyper-util", "rustls 0.23.36", "rustls-native-certs", @@ -1488,23 +2167,22 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", - "http-body", - "hyper", + "http-body 1.0.1", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.5.10", "system-configuration", "tokio", "tower-layer", @@ -1515,9 +2193,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1737,6 +2415,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -1775,16 +2463,16 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.2.0" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64", "ed25519-dalek", "getrandom 0.2.17", "hmac", "js-sys", - "p256", + "p256 0.13.2", "p384", "pem", "rand 0.8.5", @@ -1792,7 +2480,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "signature", + "signature 2.2.0", "simple_asn1", ] @@ -1802,7 +2490,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -1857,6 +2545,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1907,11 +2604,11 @@ dependencies = [ [[package]] name = "meilisearch-index-setting-macro" -version = "0.28.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420f67f5943a0236eea7f199720cc465e806c48978d9b0fdc1fb62eceaee7556" +checksum = "e0795d177129fed792fc789cf07d4647bb9e3939409fbe01764805c060afcd0e" dependencies = [ - "convert_case", + "convert_case 0.8.0", "proc-macro2", "quote", "structmeta", @@ -1920,25 +2617,28 @@ dependencies = [ [[package]] name = "meilisearch-sdk" -version = "0.28.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2325355c73c96667178c09675389cfa7afc2382d5aa0e0d34d0cf29793d89090" +checksum = "e4a20b5a4215a39c66854d14cbba97f615e84c49925d68629c05c42a5a85dac1" dependencies = [ "async-trait", "bytes", "either", - "futures", + "futures-channel", + "futures-core", "futures-io", + "futures-util", "iso8601", - "jsonwebtoken 9.3.1", + "jsonwebtoken 10.3.0", "log", "meilisearch-index-setting-macro", "pin-project-lite", "reqwest", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.18", "time", + "tokio", "uuid", "wasm-bindgen-futures", "web-sys", @@ -1996,9 +2696,9 @@ dependencies = [ [[package]] name = "minijinja" -version = "2.14.0" +version = "2.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ea9ac0a51fb5112607099560fdf0f90366ab088a2a9e6e8ae176794e9806aa" +checksum = "b479616bb6f0779fb0f3964246beda02d4b01144e1b0d5519616e012ccc2a245" dependencies = [ "memo-map", "self_cell", @@ -2055,7 +2755,7 @@ dependencies = [ "httparse", "memchr", "mime", - "spin", + "spin 0.9.8", "version_check", ] @@ -2274,20 +2974,37 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "owo-colors" version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2", +] + [[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "primeorder", "sha2", ] @@ -2298,8 +3015,8 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "primeorder", "sha2", ] @@ -2480,9 +3197,19 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", ] [[package]] @@ -2491,8 +3218,8 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", ] [[package]] @@ -2503,9 +3230,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -2574,7 +3301,7 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.13.8", ] [[package]] @@ -2680,7 +3407,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.36", - "socket2", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -2717,7 +3444,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -2878,9 +3605,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2890,20 +3617,26 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "regress" @@ -2925,12 +3658,12 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "h2", + "h2 0.4.13", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.8.1", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", @@ -2996,7 +3729,7 @@ dependencies = [ "futures", "getrandom 0.2.17", "http 1.4.0", - "hyper", + "hyper 1.8.1", "parking_lot 0.11.2", "reqwest", "reqwest-middleware", @@ -3049,6 +3782,17 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -3078,8 +3822,8 @@ dependencies = [ "dropshot-verified-body", "hex", "http 1.4.0", - "hyper", - "jsonwebtoken 10.2.0", + "hyper 1.8.1", + "jsonwebtoken 10.3.0", "meilisearch-sdk", "minijinja", "mockall", @@ -3098,6 +3842,7 @@ dependencies = [ "rfd-data", "rfd-github", "rfd-model", + "rfd-secret", "ring", "rsa", "schemars 0.8.22", @@ -3136,7 +3881,7 @@ dependencies = [ "dirs 6.0.0", "futures", "itertools", - "jsonwebtoken 10.2.0", + "jsonwebtoken 10.3.0", "oauth2", "owo-colors", "progenitor-client", @@ -3217,6 +3962,8 @@ name = "rfd-processor" version = "0.12.3" dependencies = [ "async-trait", + "aws-config", + "aws-sdk-s3", "base64", "chrono", "config", @@ -3230,6 +3977,7 @@ dependencies = [ "md-5", "meilisearch-sdk", "mime_guess", + "mockall", "newtype-uuid", "octorust", "parse-rfd", @@ -3241,6 +3989,7 @@ dependencies = [ "rfd-data", "rfd-github", "rfd-model", + "rfd-secret", "rsa", "serde", "tap", @@ -3267,6 +4016,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "rfd-secret" +version = "0.1.0" +dependencies = [ + "serde", + "tempfile", + "thiserror 2.0.18", + "toml 0.9.11+spec-1.1.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -3307,11 +4066,11 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sha2", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] @@ -3367,6 +4126,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.22.4" @@ -3387,6 +4158,7 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -3426,6 +4198,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.102.8" @@ -3443,6 +4225,7 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3517,9 +4300,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3545,22 +4328,46 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + [[package]] name = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.10", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -3745,7 +4552,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.2.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -3824,6 +4631,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "2.2.0" @@ -3854,9 +4671,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slog" @@ -3926,6 +4743,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.2" @@ -3942,6 +4769,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spinning_top" version = "0.3.0" @@ -3951,6 +4784,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -3958,7 +4801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", ] [[package]] @@ -4035,9 +4878,9 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags 2.10.0", "core-foundation 0.9.4", @@ -4231,7 +5074,7 @@ dependencies = [ "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -4247,6 +5090,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.25.0" @@ -4399,7 +5252,7 @@ dependencies = [ "bytes", "futures-util", "http 1.4.0", - "http-body", + "http-body 1.0.1", "iri-string", "pin-project-lite", "tower", @@ -4646,6 +5499,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4673,7 +5532,7 @@ dependencies = [ [[package]] name = "v-api" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/v-api#5ea039de839c4a88943961e8bbfb298781671755" +source = "git+https://github.com/oxidecomputer/v-api#21d760b210429e23df76a1f6bf691c94b3de5488" dependencies = [ "async-trait", "base64", @@ -4688,8 +5547,8 @@ dependencies = [ "hex", "http 1.4.0", "http-body-util", - "hyper", - "jsonwebtoken 10.2.0", + "hyper 1.8.1", + "jsonwebtoken 10.3.0", "newtype-uuid", "oauth2", "partial-struct", @@ -4718,7 +5577,7 @@ dependencies = [ [[package]] name = "v-api-installer" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/v-api#5ea039de839c4a88943961e8bbfb298781671755" +source = "git+https://github.com/oxidecomputer/v-api#21d760b210429e23df76a1f6bf691c94b3de5488" dependencies = [ "diesel", "diesel_migrations", @@ -4727,7 +5586,7 @@ dependencies = [ [[package]] name = "v-api-permission-derive" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/v-api#5ea039de839c4a88943961e8bbfb298781671755" +source = "git+https://github.com/oxidecomputer/v-api#21d760b210429e23df76a1f6bf691c94b3de5488" dependencies = [ "heck", "proc-macro2", @@ -4738,7 +5597,7 @@ dependencies = [ [[package]] name = "v-model" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/v-api#5ea039de839c4a88943961e8bbfb298781671755" +source = "git+https://github.com/oxidecomputer/v-api#21d760b210429e23df76a1f6bf691c94b3de5488" dependencies = [ "async-bb8-diesel", "async-trait", @@ -4773,6 +5632,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "waitgroup" version = "0.1.2" @@ -4925,9 +5790,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -5210,6 +6075,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xtask" version = "0.0.0" @@ -5280,8 +6151,8 @@ dependencies = [ "base64", "http 1.4.0", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.8.1", + "hyper-rustls 0.27.7", "hyper-util", "log", "percent-encoding", @@ -5297,18 +6168,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.34" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.34" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", @@ -5377,6 +6248,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml index b74c6c61..74f71a52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,15 +9,19 @@ members = [ "rfd-model", "rfd-processor", "rfd-sdk", + "rfd-secret", "trace-request", "xtask" ] +exclude = ["rfd-kube-init"] resolver = "2" [workspace.dependencies] anyhow = "1.0.100" async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", version = "0.3" } async-trait = "0.1.89" +aws-config = "1.8.13" +aws-sdk-s3 = "1.99.0" base64 = "0.22" bb8 = "0.9" chrono = "0.4.43" @@ -40,7 +44,8 @@ http = "1.4.0" hyper = "1.8.1" itertools = "0.13.0" jsonwebtoken = { version = "10.2", features = ["rust_crypto"] } -meilisearch-sdk = "0.28.0" +meilisearch-sdk = { version = "0.32.0", default-features = false, features = ["reqwest", "tls", "jwt_rust_crypto"] } +k8s-openapi = { version = "0.27", features = ["v1_32"] } md-5 = "0.10.6" mime_guess = "2.0.5" minijinja = { version = "2.14", features = ["loader"] } @@ -77,6 +82,7 @@ slog = "2.8.2" slog-async = "2.8.0" tabwriter = "1.4.1" tap = "1.0.1" +tempfile = "3" textwrap = "0.16.2" thiserror = "2" tokio = { version = "1.49.0", default-features = false, features = ["rt-multi-thread", "macros"] } @@ -117,3 +123,6 @@ lto = "thin" # v-api-installer = { path = "../v-api/v-api-installer" } # v-model = { path = "../v-api/v-model" } # v-api-permission-derive = { path = "../v-api/v-api-permission-derive" } + +[profile.release] +debug = 1 \ No newline at end of file diff --git a/Containerfile b/Containerfile new file mode 100644 index 00000000..2ed6a893 --- /dev/null +++ b/Containerfile @@ -0,0 +1,92 @@ +# syntax=docker/dockerfile:1 +# Build stage +FROM docker.io/rust:1-trixie AS builder + +ARG DIESEL_VERSION=v2.3.5 +ARG NODE_VERSION=v24.13.0 + +WORKDIR /app + +# Install build dependencies for diesel/postgres +RUN apt-get update && apt-get install -y \ + libpq-dev \ + pkg-config + +# Copy workspace files +COPY Cargo.toml Cargo.lock rust-toolchain.toml ./ + +# Copy all workspace members +COPY parse-rfd ./parse-rfd +COPY rfd-api ./rfd-api +COPY rfd-cli ./rfd-cli +COPY rfd-data ./rfd-data +COPY rfd-github ./rfd-github +COPY rfd-kube-init ./rfd-kube-init +COPY rfd-installer ./rfd-installer +COPY rfd-model ./rfd-model +COPY rfd-processor ./rfd-processor +COPY rfd-sdk ./rfd-sdk +COPY rfd-secret ./rfd-secret +COPY trace-request ./trace-request +COPY xtask ./xtask + +ENV CARGO_HOME=/data/cargo + +# Build workspace binaries in release mode +RUN cargo build --release \ + --package rfd-api \ + --package rfd-processor \ + --package rfd-cli \ + --package rfd-installer + +# Build rfd-kube-init separately (excluded from workspace to avoid feature conflicts) +RUN cd rfd-kube-init && cargo build --release + +# Download diesel tool for migrations +WORKDIR /tmp +RUN curl -L --output-dir /tmp -O "https://github.com/diesel-rs/diesel/releases/download/${DIESEL_VERSION}/diesel_cli-x86_64-unknown-linux-gnu.tar.xz" \ + && curl -L --output-dir /tmp -O "https://github.com/diesel-rs/diesel/releases/download/${DIESEL_VERSION}/diesel_cli-x86_64-unknown-linux-gnu.tar.xz.sha256" \ + && sha256sum diesel_cli-x86_64-unknown-linux-gnu.tar.xz.sha256 && tar --strip-components=1 -xJvf diesel_cli-x86_64-unknown-linux-gnu.tar.xz + +# Node for search indec +RUN curl -L --output-dir /tmp -O "https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.xz" \ + && curl -L --output-dir /tmp -o node.SHASUMS256.txt "https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt" \ + && sha256sum node.SHASUMS256.txt && tar --strip-components=1 -xJvf "node-${NODE_VERSION}-linux-x64.tar.xz" + +# Runtime stage +FROM docker.io/debian:trixie-slim + +# Install runtime dependencies and tini +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libpq5 \ + tini \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd --create-home --user-group rfd \ + && mkdir /home/rfd/db + +# Copy binaries from builder +COPY --from=builder /app/target/release/rfd-api /usr/local/bin/ +COPY --from=builder /app/target/release/rfd-processor /usr/local/bin/ +COPY --from=builder /app/target/release/rfd-cli /usr/local/bin/ +COPY --from=builder /app/target/release/rfd-installer /usr/local/bin/ +COPY --from=builder /app/rfd-kube-init/target/release/rfd-kube-init /usr/local/bin/ + +# Database migrations for diesel +COPY --from=builder /tmp/diesel /usr/local/bin/ +# COPY --from=builder /app/rfd-model/diesel.toml /home/rfd/db/ +COPY --from=builder /app/rfd-model/migrations/ /home/rfd/db/migrations/ + +# Node for search indexing +COPY --from=builder /tmp/bin/node /usr/local/bin/ + +# Create non-root user +USER rfd +WORKDIR /home/rfd + +# Use tini as entrypoint for proper signal handling +ENTRYPOINT ["/usr/bin/tini", "--"] + +# Default to running rfd-api, can be overridden with CMD +CMD ["rfd-api"] diff --git a/README.md b/README.md index d34868a8..96b09f6d 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,54 @@ The RFD API backend services expect to run against a Postgres database. Running the API requires setting up a configuration file as outlined in `config.example.toml`. +### Initialization + +The `/init` endpoint is used to bootstrap the RFD API with an initial OAuth client. This is designed for automated deployment scenarios where an OAuth client needs to be created before any authenticated access is possible. + +#### Usage + +The endpoint requires no authentication and can only be called **once**. After successful initialization, subsequent calls will fail with a 409 Conflict status. + +```bash +curl -X POST http://localhost:8080/init \ + -H "Content-Type: application/json" \ + -d '{ + "redirect_uris": [ + "https://app.example.com/callback", + "http://localhost:3000/callback" + ] + }' +``` + +#### Response + +On success, the endpoint returns the created OAuth client credentials. **The client secret is only returned in this response and cannot be retrieved later.** + +```json +{ + "client_id": "01234567-89ab-cdef-0123-456789abcdef", + "secret": "rfd_abc123def456ghi789jkl012mno345pqr678stu901vwx234yz", + "redirect_uris": [ + "https://app.example.com/callback", + "http://localhost:3000/callback" + ] +} +``` + +#### Re-initialization + +To re-initialize the system (e.g., for testing or recovery), a human must manually delete the initialization record from the database: + +```sql +-- Delete the initialization record to allow /init to be called again +DELETE FROM initialization; + +-- Optionally also delete existing OAuth clients +DELETE FROM oauth_client_redirect_uri; +DELETE FROM oauth_client_secret; +DELETE FROM oauth_client; +``` + ### Processor Dependencies diff --git a/SETUP.md b/SETUP.md index 13a3119b..2416b351 100644 --- a/SETUP.md +++ b/SETUP.md @@ -107,3 +107,42 @@ cargo run -p rfd-cli --features local-dev The processor has multiple jobs that are able to be run, and configuration is only required for jobs that are going to be run. The `actions` key defines the jobs that should be run. By default all jobs are disabled. In this this mode the processor will only construct a database of RFDs. + +##### Static Asset Storage + +The processor can copy images and other static assets extracted from RFDs to cloud storage. Both +Google Cloud Storage (GCS) and Amazon S3 (or S3-compatible services) are supported. Multiple +storage backends can be configured simultaneously, and assets will be pushed to all of them. + +To enable this feature, add `CopyImagesToStorage` to the `actions` list and configure at least one +storage backend. + +**Google Cloud Storage (GCS)** + +```toml +[[gcs_storage]] +bucket = "your-bucket-name" +``` + +GCS uses GCP Application Default Credentials for authentication. Configure credentials using one of: +- `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to a service account key file +- Instance metadata (when running on GCP Compute Engine, GKE, etc.) +- `gcloud auth application-default login` (for local development) + +**Amazon S3** + +```toml +[[s3_storage]] +bucket = "your-bucket-name" +region = "us-west-2" +# Optional: custom endpoint for S3-compatible services +# endpoint = "https://s3.example.com" +``` + +S3 uses the AWS SDK default credential chain for authentication: +- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, optionally `AWS_SESSION_TOKEN`) +- Shared credentials file (`~/.aws/credentials`) +- IAM role (when running on AWS EC2, ECS, Lambda, etc.) + +The optional `endpoint` field allows using S3-compatible services such as MinIO, Backblaze B2, or +Cloudflare R2. diff --git a/compose.yml b/compose.yml new file mode 100644 index 00000000..0e6ef4b8 --- /dev/null +++ b/compose.yml @@ -0,0 +1,133 @@ +services: + # This requires changes to rfd-site which will be proposed via PR. + # rfd-site: + # build: + # context: https://github.com/oxidecomputer/rfd-site.git + # dockerfile: Containerfile + # image: localhost/rfd-site-test:latest + # networks: + # - rfd + # ports: + # - "3000:3000" + # environment: + # SESSION_SECRET: ${SESSION_SECRET} + # RFD_API_BACKEND_URL: http://rfd-api:8080 + # RFD_API_FRONTEND_URL: http://localhost:8080 + # RFD_API_CLIENT_ID: ${RFD_API_CLIENT_ID} + # RFD_API_CLIENT_SECRET: ${RFD_API_CLIENT_SECRET} + # RFD_API_GITHUB_CALLBACK_URL: http://localhost:3000/auth/github/callback + # AUTH_PROVIDERS: github + # STORAGE_PROVIDER: s3 + # S3_BUCKET: ${S3_BUCKET} + # AWS_REGION: ${AWS_REGION} + # AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + # AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + # AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN} + # TELEMETRY_DISABLE: true + # GITHUB_HOST: ${GITHUB_HOST} + # GITHUB_REPO_CLIENT_ID: ${GITHUB_REPO_CLIENT_ID} + # GITHUB_REPO_CLIENT_SECRET: ${GITHUB_REPO_CLIENT_SECRET} + + rfd-migration: + build: + dockerfile: Containerfile + image: localhost/rfd-api-test:latest + networks: + - rfd + depends_on: + postgres: + condition: service_healthy + entrypoint: [] + working_dir: /home/rfd/db + environment: + - DATABASE_URL=postgres://rfd:rfd@postgres/rfd + command: /usr/local/bin/rfd-installer && diesel migration run && echo "Migrations Complete!" + + rfd-api: + image: localhost/rfd-api-test:latest + # Uses default CMD: rfd-api + ports: + - "8080:8080" + environment: + # - RFD_API_CONFIG=/config/rfd-api.toml + - DATABASE_URL=postgres://rfd:rfd@postgres/rfd + command: + - /usr/local/bin/rfd-api + - /config/rfd-api.toml + volumes: + - ./config:/config:ro + networks: + - rfd + depends_on: + postgres: + condition: service_started + meilisearch: + condition: service_started + rfd-migration: + condition: service_completed_successfully + + rfd-processor: + image: localhost/rfd-api-test:latest + + # Override CMD to run rfd-processor instead + environment: + # - RFD_API_CONFIG=/config/rfd-api.toml + - DATABASE_URL=postgres://rfd:rfd@postgres/rfd + - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} + - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} + - AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} + command: + - /usr/local/bin/rfd-processor + - /config/rfd-processor.toml + volumes: + - ./config:/config:ro + networks: + - rfd + depends_on: + postgres: + condition: service_started + meilisearch: + condition: service_started + rfd-migration: + condition: service_completed_successfully + + postgres: + image: docker.io/postgres:18-trixie + environment: + POSTGRES_USER: rfd + POSTGRES_PASSWORD: rfd + POSTGRES_DB: rfd + volumes: + - postgres_data:/var/lib/postgresql + ports: + - "5432:5432" + networks: + - rfd + healthcheck: + test: ["CMD-SHELL", "pg_isready -U rfd"] + start_period: 5s + start_interval: 1s + retries: 10 + interval: 5s + + meilisearch: + image: docker.io/getmeili/meilisearch:v1.11 + environment: + MEILI_ENV: development + MEILI_MASTER_KEY: development-master-key + MEILI_NO_ANALYTICS: "true" + MEILI_DB_PATH: /meili_data/data.ms + volumes: + - meilisearch_data:/meili_data + ports: + - "7700:7700" + networks: + - rfd + +networks: + rfd: + driver: bridge + +volumes: + postgres_data: + meilisearch_data: diff --git a/rfd-api/Cargo.toml b/rfd-api/Cargo.toml index 4c75a32b..8f144c1b 100644 --- a/rfd-api/Cargo.toml +++ b/rfd-api/Cargo.toml @@ -43,6 +43,7 @@ ring = { workspace = true } rfd-data = { path = "../rfd-data" } rfd-github = { path = "../rfd-github" } rfd-model = { path = "../rfd-model" } +rfd-secret = { path = "../rfd-secret" } rsa = { workspace = true, features = ["sha2"] } schemars = { workspace = true, features = ["chrono"] } secrecy = { workspace = true, features = ["serde"] } diff --git a/rfd-api/config.example.toml b/rfd-api/config.example.toml index 909851c8..52c61276 100644 --- a/rfd-api/config.example.toml +++ b/rfd-api/config.example.toml @@ -16,8 +16,16 @@ public_url = "" # the server is running behind a proxy) server_port = 8080 -# Full url of the Postgres database to connect to -database_url = "postgres://:@/" +# Database connection configuration +# Password can be specified inline or read from a file: +# Inline: password = "your-password" +# From file: password = { path = "/run/secrets/db-password" } +[database] +host = "localhost" +port = 5432 +user = "rfd" +password = "" +database = "rfd" # Settings for JWT management [jwt] @@ -27,6 +35,10 @@ default_expiration = 3600 # Keys for signing JWTs and generating secrets. GCP Cloud KMS keys and local static keys # are supported. At least one signer and one verifier key must be configured. +# +# Secret values (private, public) can be specified inline or read from a file: +# Inline: private = "-----BEGIN RSA PRIVATE KEY-----\n..." +# From file: private = { path = "/run/secrets/jwt-private-key" } # Cloud KMS - Signer [[keys]] @@ -63,6 +75,10 @@ public = """""" # PEM encoded public key # OAuth Providers # Google and GitHub are supported. An OAuth provider needs to have both a web and device config. # At least one OAuth provider must be configured +# +# Secret values (client_id, client_secret) can be specified inline or read from a file: +# Inline: client_secret = "your-secret" +# From file: client_secret = { path = "/run/secrets/google-client-secret" } [authn.oauth.google.device] client_id = "" @@ -97,13 +113,13 @@ templates = [] # html = "Click here to login" # # [magic_link.email_service.resend] -# key = "re_xxxxxxxx" +# key = "re_xxxxxxxx" # or { path = "/run/secrets/resend-key" } # Search configuration [search] # Remote url of the search service host = "" -# Read-only search key +# Read-only search key (can be inline or { path = "/run/secrets/search-key" }) key = "" # Index to perform searches against index = "" @@ -144,6 +160,10 @@ default_branch = "" # 1. A GitHub App installation that is defined by an app_id, installation_id, and private_key # 2. A GitHub access token # Exactly one authentication must be specified +# +# Secret values (private_key, token) can be specified inline or read from a file: +# Inline: private_key = "-----BEGIN RSA PRIVATE KEY-----\n..." +# From file: private_key = { path = "/run/secrets/github-app-key" } # App Installation [services.github.auth] @@ -152,10 +172,11 @@ app_id = 1111111 # Numeric GitHub App installation id corresponding to the organization that the configured repo # belongs to installation_id = 2222222 -# PEM encoded private key for the GitHub App +# PEM encoded private key for the GitHub App (can be inline or { path = "..." }) private_key = """""" # Access Token [services.github.auth] # This may be any GitHub access token that has permission to the configured repo +# (can be inline or { path = "/run/secrets/github-token" }) token = "" diff --git a/rfd-api/src/config.rs b/rfd-api/src/config.rs index 064ec64f..3d290308 100644 --- a/rfd-api/src/config.rs +++ b/rfd-api/src/config.rs @@ -7,23 +7,150 @@ use std::{collections::HashMap, path::PathBuf}; use config::{Config, ConfigError, Environment, File}; use rfd_data::content::RfdTemplate; use serde::Deserialize; -use v_api::config::{AsymmetricKey, AuthnProviders, JwtConfig}; +use v_api::config::{AsymmetricKey, JwtConfig}; use v_model::schema_ext::MagicLinkMedium; +use rfd_secret::{SecretResolutionError, SecretString}; + use crate::server::SpecConfig; +// ============================================================================ +// Wrapper types for v_api configuration with SecretString support +// ============================================================================ + +/// Wrapper for v_api::config::AsymmetricKey that supports path-based secrets. +/// +/// Use `resolve()` to convert to the actual v_api AsymmetricKey type. +#[derive(Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum AsymmetricKeyConfig { + LocalSigner { + kid: String, + private: SecretString, + }, + LocalVerifier { + kid: String, + public: SecretString, + }, + CkmsSigner { + kid: String, + version: u16, + key: String, + keyring: String, + location: String, + project: String, + }, + CkmsVerifier { + kid: String, + version: u16, + key: String, + keyring: String, + location: String, + project: String, + }, +} + +impl AsymmetricKeyConfig { + /// Resolves any path-based secrets and converts to v_api AsymmetricKey. + pub fn resolve(self) -> Result { + match self { + AsymmetricKeyConfig::LocalSigner { kid, private } => Ok(AsymmetricKey::LocalSigner { + kid, + private: private.resolve()?, + }), + AsymmetricKeyConfig::LocalVerifier { kid, public } => { + Ok(AsymmetricKey::LocalVerifier { + kid, + public: public.resolve()?, + }) + } + AsymmetricKeyConfig::CkmsSigner { + kid, + version, + key, + keyring, + location, + project, + } => Ok(AsymmetricKey::CkmsSigner { + kid, + version, + key, + keyring, + location, + project, + }), + AsymmetricKeyConfig::CkmsVerifier { + kid, + version, + key, + keyring, + location, + project, + } => Ok(AsymmetricKey::CkmsVerifier { + kid, + version, + key, + keyring, + location, + project, + }), + } + } +} + +/// OAuth client configuration with SecretString support. +#[derive(Debug, Clone, Deserialize)] +pub struct OAuthClientConfig { + pub client_id: SecretString, + pub client_secret: SecretString, +} + +/// OAuth web client configuration with SecretString support. +#[derive(Debug, Clone, Deserialize)] +pub struct OAuthWebClientConfig { + pub client_id: SecretString, + pub client_secret: SecretString, + // pub redirect_uri: String, +} + +/// Per-provider OAuth configuration with device and web clients. +#[derive(Debug, Clone, Deserialize)] +pub struct OAuthProviderConfig { + pub device: OAuthClientConfig, + pub web: OAuthWebClientConfig, +} + +/// OAuth providers configuration wrapper. +#[derive(Debug, Default, Deserialize)] +pub struct OAuthProvidersConfig { + pub github: Option, + pub google: Option, +} + +/// Authentication providers configuration wrapper. +#[derive(Debug, Default, Deserialize)] +pub struct AuthnProvidersConfig { + #[serde(default)] + pub oauth: OAuthProvidersConfig, +} + +// ============================================================================ +// Main application configuration +// ============================================================================ + #[derive(Debug, Deserialize)] pub struct AppConfig { pub log_format: ServerLogFormat, + pub log_filter: Option, pub log_directory: Option, pub initial_mappers: Option, pub public_url: String, pub server_port: u16, - pub database_url: String, - pub keys: Vec, + pub database: DatabaseConfig, + pub keys: Vec, pub jwt: JwtConfig, pub spec: Option, - pub authn: AuthnProviders, + pub authn: AuthnProvidersConfig, pub magic_link: MagicLinkConfig, pub search: SearchConfig, pub content: ContentConfig, @@ -40,10 +167,29 @@ pub enum ServerLogFormat { #[derive(Debug, Default, Deserialize)] pub struct SearchConfig { pub host: String, - pub key: String, + pub key: SecretString, pub index: String, } +#[derive(Debug, Deserialize)] +pub struct DatabaseConfig { + pub host: String, + pub port: u16, + pub user: String, + pub password: SecretString, + pub database: String, +} + +impl DatabaseConfig { + pub fn to_url(&self) -> Result { + let password = self.password.resolve()?; + Ok(format!( + "postgres://{}:{}@{}:{}/{}", + self.user, password, self.host, self.port, self.database + )) + } +} + #[derive(Debug, Default, Deserialize)] pub struct ContentConfig { pub templates: HashMap, @@ -69,10 +215,10 @@ pub enum GitHubAuthConfig { Installation { app_id: i64, installation_id: i64, - private_key: String, + private_key: SecretString, }, User { - token: String, + token: SecretString, }, } @@ -96,7 +242,7 @@ pub struct MagicLinkTemplate { #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum EmailService { - Resend { key: String }, + Resend { key: SecretString }, } impl AppConfig { diff --git a/rfd-api/src/context.rs b/rfd-api/src/context.rs index 93c5a87f..bf681a5d 100644 --- a/rfd-api/src/context.rs +++ b/rfd-api/src/context.rs @@ -313,7 +313,7 @@ impl RfdContext { public_url, storage, search: SearchContext { - client: SearchClient::new(search.host, search.index, search.key), + client: SearchClient::new(search.host, search.index, search.key.resolve()?), }, content: ContentContext { placeholder_template: content @@ -333,24 +333,27 @@ impl RfdContext { app_id, installation_id, private_key, - } => GitHubClient::custom( - "rfd-api", - Credentials::InstallationToken(InstallationTokenGenerator::new( - installation_id, - JWTCredentials::new( - app_id, - RsaPrivateKey::from_pkcs1_pem(&private_key)? - .to_pkcs1_der()? - .to_bytes() - .to_vec(), - )?, - )), - client, - Box::new(NoCache), - ), + } => { + let resolved_key = private_key.resolve()?; + GitHubClient::custom( + "rfd-api", + Credentials::InstallationToken(InstallationTokenGenerator::new( + installation_id, + JWTCredentials::new( + app_id, + RsaPrivateKey::from_pkcs1_pem(&resolved_key)? + .to_pkcs1_der()? + .to_bytes() + .to_vec(), + )?, + )), + client, + Box::new(NoCache), + ) + } GitHubAuthConfig::User { token } => GitHubClient::custom( "rfd-api", - Credentials::Token(token.to_string()), + Credentials::Token(token.resolve()?), client, Box::new(NoCache), ), @@ -956,9 +959,9 @@ pub(crate) mod test_mocks { }; use v_model::storage::postgres::PostgresStore; - use crate::config::{ - ContentConfig, GitHubAuthConfig, GitHubConfig, SearchConfig, ServicesConfig, - }; + use rfd_secret::SecretString; + + use crate::config::{ContentConfig, GitHubAuthConfig, GitHubConfig, SearchConfig, ServicesConfig}; use super::RfdContext; @@ -1031,7 +1034,7 @@ pub(crate) mod test_mocks { path: String::new(), default_branch: String::new(), auth: GitHubAuthConfig::User { - token: String::default(), + token: SecretString::default(), }, }, }, diff --git a/rfd-api/src/endpoints/init.rs b/rfd-api/src/endpoints/init.rs new file mode 100644 index 00000000..2ac006c8 --- /dev/null +++ b/rfd-api/src/endpoints/init.rs @@ -0,0 +1,169 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::{collections::HashMap, sync::OnceLock}; + +use chrono::Utc; +use dropshot::{ + endpoint, ClientErrorStatusCode, HttpError, HttpResponseCreated, RequestContext, TypedBody, +}; +use newtype_uuid::{GenericUuid, TypedUuid}; +use rfd_model::{storage::InitializationStore, InitializationModel}; +use schemars::JsonSchema; +use secrecy::ExposeSecret; +use serde::{Deserialize, Serialize}; +use trace_request::trace_request; +use tracing::instrument; +use uuid::Uuid; +use v_api::{ + authn::key::RawKey, + permissions::VPermission, + response::{client_error, to_internal_error}, + ApiContext, +}; +use v_model::{permissions::Caller, OAuthClientId, UserId}; + +use crate::{context::RfdContext, permissions::RfdPermission}; + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct InitRequestBody { + pub redirect_uris: Vec, +} + +#[derive(Debug, Serialize, JsonSchema)] +pub struct InitResponse { + pub client_id: TypedUuid, + pub secret: String, + pub redirect_uris: Vec, +} + +/// Initialize the system with an initial OAuth client. +/// +/// This endpoint is unauthenticated and can only be called once. If the system +/// has already been initialized, this endpoint will return a 409 Conflict error. +/// +/// To re-initialize the system, an administrator must manually delete the +/// initialization record from the database. +#[trace_request] +#[endpoint { + method = POST, + path = "/init", + tags = ["hidden"], +}] +#[instrument(skip(rqctx), fields(request_id = rqctx.request_id), err(Debug))] +pub async fn init( + rqctx: RequestContext, + body: TypedBody, +) -> Result, HttpError> { + let ctx = rqctx.context(); + let body = body.into_inner(); + init_op(ctx, body).await +} + +/// Hardcoded caller ID for the init endpoint. This is used for logging purposes +/// to identify that the operation was performed by the init endpoint. +const INIT_CALLER_ID: Uuid = Uuid::from_u128(0x00000000_0000_0000_0000_000000000001); + +/// Static caller instance for the init endpoint, initialized once on first use. +static INIT_CALLER: OnceLock> = OnceLock::new(); + +/// Internal operation for system initialization, separated for testability. +#[instrument(skip(ctx), err(Debug))] +pub async fn init_op( + ctx: &RfdContext, + body: InitRequestBody, +) -> Result, HttpError> { + // Step 1: Check if initialization record already exists + let existing = InitializationStore::get(&*ctx.storage) + .await + .map_err(to_internal_error)?; + + if existing.is_some() { + return Err(client_error( + ClientErrorStatusCode::CONFLICT, + "System already initialized", + )); + } + + // Step 2: Get the singleton caller with full OAuth permissions for initialization. + // This is safe because we've already verified this is the first initialization via the + // InitializationStore check above. + let caller = INIT_CALLER.get_or_init(|| Caller { + id: TypedUuid::::from_untyped_uuid(INIT_CALLER_ID), + permissions: vec![ + RfdPermission::from(VPermission::CreateOAuthClient), + RfdPermission::from(VPermission::ManageOAuthClientsAll), + ] + .into(), + extensions: HashMap::new(), + }); + + // Step 3: Create the OAuth client + let client = ctx + .v_ctx() + .oauth + .create_oauth_client(&caller) + .await + .map_err(|e| { + tracing::error!(?e, "Failed to create OAuth client"); + to_internal_error(e) + })?; + + // Step 4: Create a secret for the client + let secret_id = TypedUuid::new_v4(); + let secret = RawKey::generate::<24>(secret_id.as_untyped_uuid()) + .sign(ctx.v_ctx().signer()) + .await + .map_err(|e| { + tracing::error!(?e, "Failed to sign OAuth client secret"); + to_internal_error(e) + })?; + + ctx.v_ctx() + .oauth + .add_oauth_secret(&caller, &secret_id, &client.id, &secret.signature().to_string()) + .await + .map_err(|e| { + tracing::error!(?e, "Failed to store OAuth client secret"); + to_internal_error(e) + })?; + + // Step 5: Add all redirect URIs + for redirect_uri in &body.redirect_uris { + ctx.v_ctx() + .oauth + .add_oauth_redirect_uri(&caller, &client.id, redirect_uri) + .await + .map_err(|e| { + tracing::error!(?e, ?redirect_uri, "Failed to add redirect URI"); + to_internal_error(e) + })?; + } + + // Step 6: Write the initialization record + let init_record = InitializationModel { + id: Uuid::new_v4(), + initialized_at: Utc::now(), + oauth_client_id: client.id.into_untyped_uuid(), + }; + + InitializationStore::insert(&*ctx.storage, init_record) + .await + .map_err(|e| { + tracing::error!(?e, "Failed to insert initialization record"); + to_internal_error(e) + })?; + + tracing::info!( + client_id = %client.id, + "System initialized successfully" + ); + + Ok(HttpResponseCreated(InitResponse { + client_id: client.id, + secret: secret.key().expose_secret().to_string(), + redirect_uris: body.redirect_uris, + })) +} + diff --git a/rfd-api/src/endpoints/mod.rs b/rfd-api/src/endpoints/mod.rs index 00f79f75..aebbd7ae 100644 --- a/rfd-api/src/endpoints/mod.rs +++ b/rfd-api/src/endpoints/mod.rs @@ -4,6 +4,7 @@ pub static UNLIMITED: i64 = 9999999; +pub mod init; pub mod job; pub mod rfd; pub mod webhook; diff --git a/rfd-api/src/error.rs b/rfd-api/src/error.rs index 0436a7a3..b4d3e78b 100644 --- a/rfd-api/src/error.rs +++ b/rfd-api/src/error.rs @@ -10,6 +10,8 @@ use thiserror::Error; use v_api::response::{conflict, forbidden, internal_error, not_found, ResourceError}; use v_model::storage::StoreError; +use rfd_secret::SecretResolutionError; + #[derive(Debug, Error)] pub enum AppError { #[error("Failed to construct HTTP client")] @@ -24,6 +26,8 @@ pub enum AppError { NoConfiguredJwtKeys, #[error("Failed to construct GitHub client")] Octorust(#[from] OctorustError), + #[error("Failed to resolve secret")] + SecretResolution(#[from] SecretResolutionError), } #[derive(Debug, Error)] diff --git a/rfd-api/src/main.rs b/rfd-api/src/main.rs index da4fd5f8..d99116c1 100644 --- a/rfd-api/src/main.rs +++ b/rfd-api/src/main.rs @@ -55,10 +55,15 @@ async fn main() -> anyhow::Result<()> { NonBlocking::new(std::io::stdout()) }; + let env_filter = match config.log_filter { + Some(ref filter) => EnvFilter::new(filter), + None => EnvFilter::from_default_env(), + }; + let subscriber = tracing_subscriber::fmt() .with_file(false) .with_line_number(false) - .with_env_filter(EnvFilter::from_default_env()) + .with_env_filter(env_filter) .with_writer(writer); match config.log_format { @@ -68,29 +73,50 @@ async fn main() -> anyhow::Result<()> { tracing::info!("Initialized logger"); + // Resolve path-based secrets for asymmetric keys + let resolved_keys: Vec<_> = config + .keys + .into_iter() + .map(|key| { + key.resolve().tap_err(|err| { + tracing::error!(?err, "Failed to resolve asymmetric key secret"); + }) + }) + .collect::>()?; + + // Resolve database URL from config + let database_url = config.database.to_url().tap_err(|err| { + tracing::error!(?err, "Failed to resolve database password secret"); + })?; + let mut v_ctx = VContext::new( config.public_url.clone(), Arc::new( - VApiPostgresStore::new(&config.database_url) + VApiPostgresStore::new(&database_url) .await .tap_err(|err| { tracing::error!(?err, "Failed to establish initial database connection"); })?, ), config.jwt, - config.keys, + resolved_keys, ) .await?; if let Some(github) = config.authn.oauth.github { + let device_client_id = github.device.client_id.resolve()?; + let device_client_secret = github.device.client_secret.resolve()?; + let web_client_id = github.web.client_id.resolve()?; + let web_client_secret = github.web.client_secret.resolve()?; + v_ctx.insert_oauth_provider( OAuthProviderName::GitHub, Box::new(move || { Box::new(GitHubOAuthProvider::new( - github.device.client_id.clone(), - github.device.client_secret.clone(), - github.web.client_id.clone(), - github.web.client_secret.clone(), + device_client_id.clone(), + device_client_secret.clone().into(), + web_client_id.clone(), + web_client_secret.clone().into(), None, )) }), @@ -100,14 +126,19 @@ async fn main() -> anyhow::Result<()> { } if let Some(google) = config.authn.oauth.google { + let device_client_id = google.device.client_id.resolve()?; + let device_client_secret = google.device.client_secret.resolve()?; + let web_client_id = google.web.client_id.resolve()?; + let web_client_secret = google.web.client_secret.resolve()?; + v_ctx.insert_oauth_provider( OAuthProviderName::Google, Box::new(move || { Box::new(GoogleOAuthProvider::new( - google.device.client_id.clone(), - google.device.client_secret.clone(), - google.web.client_id.clone(), - google.web.client_secret.clone(), + device_client_id.clone(), + device_client_secret.clone().into(), + web_client_id.clone(), + web_client_secret.clone().into(), None, )) }), @@ -142,10 +173,9 @@ async fn main() -> anyhow::Result<()> { if let Some(service) = &config.magic_link.email_service { match service { EmailService::Resend { key } => { - v_ctx.magic_link.set_messenger( - target, - ResendMagicLink::new(key.to_string(), template.from), - ); + v_ctx + .magic_link + .set_messenger(target, ResendMagicLink::new(key.resolve()?, template.from)); } } } @@ -157,7 +187,7 @@ async fn main() -> anyhow::Result<()> { let context = RfdContext::new( config.public_url, Arc::new( - VApiPostgresStore::new(&config.database_url) + VApiPostgresStore::new(&database_url) .await .tap_err(|err| { tracing::error!(?err, "Failed to establish initial database connection"); diff --git a/rfd-api/src/server.rs b/rfd-api/src/server.rs index 70f19e04..cb5f537c 100644 --- a/rfd-api/src/server.rs +++ b/rfd-api/src/server.rs @@ -15,6 +15,7 @@ use v_api::{inject_endpoints, v_system_endpoints}; use crate::{ context::RfdContext, endpoints::{ + init::init, job::list_jobs, rfd::{ discuss_rfd, list_rfd_revisions, list_rfds, publish_rfd, reserve_rfd, search_rfds, @@ -56,7 +57,7 @@ pub fn server( // Construct a shim to pipe dropshot logs into the global tracing logger let dropshot_logger = { - let level_drain = slog::LevelFilter(TracingSlogDrain, slog::Level::Debug).fuse(); + let level_drain = slog::LevelFilter(TracingSlogDrain, slog::Level::Trace).fuse(); let async_drain = slog_async::Async::new(level_drain).build().fuse(); slog::Logger::root(async_drain, slog::o!()) }; @@ -78,6 +79,9 @@ pub fn server( inject_endpoints!(api); + // Initialization + api.register(init).expect("Failed to register endpoint"); + // RFDs api.register(list_rfds) .expect("Failed to register endpoint"); diff --git a/rfd-installer/src/lib.rs b/rfd-installer/src/lib.rs index e968fa6e..18c2a899 100644 --- a/rfd-installer/src/lib.rs +++ b/rfd-installer/src/lib.rs @@ -11,6 +11,7 @@ use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; const MIGRATIONS: EmbeddedMigrations = embed_migrations!("../rfd-model/migrations"); pub fn run_migrations(url: &str, v_only: bool) { + // These are safe to run multiple times. v_api_installer::run_migrations(url); if !v_only { diff --git a/rfd-kube-init/Cargo.lock b/rfd-kube-init/Cargo.lock new file mode 100644 index 00000000..840a98c6 --- /dev/null +++ b/rfd-kube-init/Cargo.lock @@ -0,0 +1,3497 @@ +# This file is automatically @generated by Cargo. +# 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", + "getrandom 0.3.4", + "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 = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[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 = "clap" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +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 = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[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 = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[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-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[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 = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + +[[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", + "h2", + "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", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[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 = "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 = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[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 = "iso8601" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" +dependencies = [ + "nom", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonpath-rust" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633a7320c4bb672863a3782e89b9094ad70285e097ff6832cddd0ec615beadfa" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "jsonwebtoken" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +dependencies = [ + "base64", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", + "js-sys", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", +] + +[[package]] +name = "k8s-openapi" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05a6d6f3611ad1d21732adbd7a2e921f598af6c92d71ae6e2620da4b67ee1f0d" +dependencies = [ + "base64", + "jiff", + "serde", + "serde_json", +] + +[[package]] +name = "kube" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f96b537b4c4f61fc183594edbecbbefa3037e403feac0701bb24e6eff78e0034" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-runtime", +] + +[[package]] +name = "kube-client" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af97b8b696eb737e5694f087c498ca725b172c2a5bc3a6916328d160225537ee" +dependencies = [ + "base64", + "bytes", + "either", + "futures", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jiff", + "jsonpath-rust", + "k8s-openapi", + "kube-core", + "pem", + "rustls", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", +] + +[[package]] +name = "kube-core" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aeade7d2e9f165f96b3c1749ff01a8e2dc7ea954bd333bcfcecc37d5226bdd" +dependencies = [ + "derive_more", + "form_urlencoded", + "http", + "jiff", + "json-patch", + "k8s-openapi", + "serde", + "serde-value", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "kube-runtime" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc158473d6d86ec22692874bd5ddccf07474eab5c6bb41f226c522e945da5244" +dependencies = [ + "ahash", + "async-broadcast", + "async-stream", + "backon", + "educe", + "futures", + "hashbrown 0.16.1", + "hostname", + "json-patch", + "k8s-openapi", + "kube-client", + "parking_lot 0.12.5", + "pin-project", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[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 = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[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 = "meilisearch-index-setting-macro" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0795d177129fed792fc789cf07d4647bb9e3939409fbe01764805c060afcd0e" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "structmeta", + "syn", +] + +[[package]] +name = "meilisearch-sdk" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a20b5a4215a39c66854d14cbba97f615e84c49925d68629c05c42a5a85dac1" +dependencies = [ + "async-trait", + "bytes", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "iso8601", + "jsonwebtoken", + "log", + "meilisearch-index-setting-macro", + "pin-project-lite", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", + "tokio", + "uuid", + "wasm-bindgen-futures", + "web-sys", + "yaup", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[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 = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[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-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[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 = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[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 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[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 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[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 = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[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 2.0.18", + "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 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "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.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[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 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[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.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[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", + "futures-util", + "h2", + "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", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "reqwest-retry" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c73e4195a6bfbcb174b790d9b3407ab90646976c55de58a6515da25d851178" +dependencies = [ + "anyhow", + "async-trait", + "futures", + "getrandom 0.2.17", + "http", + "hyper", + "parking_lot 0.11.2", + "reqwest", + "reqwest-middleware", + "retry-policies", + "thiserror 1.0.69", + "tokio", + "tracing", + "wasm-timer", +] + +[[package]] +name = "retry-policies" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rfd-kube-init" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "clap", + "futures-io", + "k8s-openapi", + "kube", + "meilisearch-sdk", + "reqwest", + "reqwest-middleware", + "reqwest-retry", + "secrecy", + "serde", + "serde_json", + "time", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "yaup", +] + +[[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 = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[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 = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[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_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 = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[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 = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[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 = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +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 = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[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", + "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 = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "slab", + "tokio", +] + +[[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", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "base64", + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "mime", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[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 = [ + "log", + "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 = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[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-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[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", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[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.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +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 = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +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-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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 = "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 2.11.0", + "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 = "yaup" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0144f1a16a199846cb21024da74edd930b43443463292f536b7110b4855b5c6" +dependencies = [ + "form_urlencoded", + "serde", + "thiserror 1.0.69", +] + +[[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/rfd-kube-init/Cargo.toml b/rfd-kube-init/Cargo.toml new file mode 100644 index 00000000..5b92ed57 --- /dev/null +++ b/rfd-kube-init/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "rfd-kube-init" +version = "0.1.0" +edition = "2021" +description = "Kubernetes initialization tool for RFD services" +repository = "https://github.com/oxidecomputer/rfd-api" + +[[bin]] +name = "rfd-kube-init" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0" +async-trait = "0.1" +futures-io = "0.3" +clap = { version = "4", features = ["derive", "env"] } +k8s-openapi = { version = "0.27", features = ["v1_32"] } +kube = { version = "3.0.1", features = ["client", "runtime", "rustls-tls"] } +meilisearch-sdk = { version = "0.32.0", default-features = false, features = ["reqwest", "tls", "jwt_rust_crypto"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +reqwest-middleware = "0.4" +reqwest-retry = "0.7" +secrecy = "0.10" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +time = "0.3" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio-util = { version = "0.7", features = ["compat", "io"] } +yaup = "0.3" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[package.metadata.dist] +targets = [] diff --git a/rfd-kube-init/README.md b/rfd-kube-init/README.md new file mode 100644 index 00000000..eafdfaa4 --- /dev/null +++ b/rfd-kube-init/README.md @@ -0,0 +1,270 @@ +# rfd-kube-init + +A Kubernetes initialization tool that distributes secrets across namespaces. Supports Meilisearch tenant token generation and RFD API OAuth client initialization. + +## Overview + +This tool is designed to run as a Kubernetes Job or init container. It provides subcommands for different initialization tasks: + +- **meilisearch**: Reads the master key from Kubernetes, generates tenant tokens, and writes them to secrets in target namespaces +- **oauth-init**: Calls the RFD API `/init` endpoint to create an OAuth client and distributes credentials to target namespaces + +## Meilisearch Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `MEILI_MASTER_NAMESPACE` | Yes* | Namespace containing the Meilisearch master key secret | +| `MEILI_MASTER_SECRET_NAME` | Yes* | Name of the secret containing the master key | +| `MEILI_MASTER_SECRET_KEY` | Yes* | Key within the secret that holds the master key value | +| `MEILI_HOST` | Yes | Meilisearch host URL (e.g., `http://meilisearch:7700`) | +| `MEILI_RW_TOKEN_TARGET_NAMESPACES` | Yes** | Comma-delimited list of namespaces for read-write tokens (e.g., `rfd-api`) | +| `MEILI_RO_TOKEN_TARGET_NAMESPACES` | Yes** | Comma-delimited list of namespaces for read-only tokens (e.g., `rfd-web`) | +| `MEILI_SECRET_NAME` | No | Name of the secret to create (default: `meilisearch-token`) | +| `MEILI_API_EXPIRATION_SECONDS` | No | Token expiration in seconds from now (default: no expiration) | +| `MEILI_TOKEN_FILTER` | No | JSON search rules for the tenant token (default: `["*"]` - full access to all indexes) | + +*If any of `MEILI_MASTER_NAMESPACE`, `MEILI_MASTER_SECRET_NAME`, or `MEILI_MASTER_SECRET_KEY` are unset or empty, Meilisearch initialization is skipped entirely. + +**At least one of `MEILI_RW_TOKEN_TARGET_NAMESPACES` or `MEILI_RO_TOKEN_TARGET_NAMESPACES` must be set. + +## Token Types + +The tool generates two types of tenant tokens: + +- **RW (Read-Write)**: Generated from the "Default Admin API Key". Grants full access including document indexing, settings updates, and searches. Use for backend services that need to write to Meilisearch. + +- **RO (Read-Only)**: Generated from the "Default Search API Key". Grants search access only. Use for frontend applications or services that only need to query. + +## How It Works + +1. Reads the Meilisearch master key from the specified Kubernetes secret +2. Connects to Meilisearch and fetches the list of API keys +3. For RW namespaces: finds the "Default Admin API Key" and generates a tenant token +4. For RO namespaces: finds the "Default Search API Key" and generates a tenant token +5. Writes the appropriate token to secrets in each target namespace + +## Secret Format + +The tool creates an `Opaque` secret with the following data: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: meilisearch-token # or MEILI_SECRET_NAME +type: Opaque +stringData: + MEILISEARCH_API_KEY: +``` + +## Kubernetes RBAC + +The service account running this tool needs permissions to: +- **Read** secrets in the namespace containing the master key +- **Create/patch** secrets in target namespaces + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: rfd-kube-init + namespace: rfd-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: rfd-kube-init +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: rfd-kube-init +subjects: + - kind: ServiceAccount + name: rfd-kube-init + namespace: rfd-system +roleRef: + kind: ClusterRole + name: rfd-kube-init + apiGroup: rbac.authorization.k8s.io +``` + +For namespace-scoped permissions, use `Role` and `RoleBinding` instead. Note that you'll need read access in the source namespace and write access in target namespaces. + +## Example: Kubernetes Job + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: meilisearch-token-init + namespace: rfd-system +spec: + template: + spec: + serviceAccountName: rfd-kube-init + restartPolicy: OnFailure + containers: + - name: init + image: ghcr.io/oxidecomputer/rfd-kube-init:latest + env: + # Source of master key + - name: MEILI_MASTER_NAMESPACE + value: "meilisearch" + - name: MEILI_MASTER_SECRET_NAME + value: "meilisearch-master" + - name: MEILI_MASTER_SECRET_KEY + value: "MEILI_MASTER_KEY" + # Meilisearch connection + - name: MEILI_HOST + value: "http://meilisearch.meilisearch:7700" + # Token distribution + - name: MEILI_RW_TOKEN_TARGET_NAMESPACES + value: "rfd-api,rfd-processor" + - name: MEILI_RO_TOKEN_TARGET_NAMESPACES + value: "rfd-web" + - name: MEILI_API_EXPIRATION_SECONDS + value: "86400" # 24 hours +``` + +## Example: CronJob for Token Rotation + +```yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: meilisearch-token-refresh + namespace: rfd-system +spec: + schedule: "0 0 * * *" # Daily at midnight + jobTemplate: + spec: + template: + spec: + serviceAccountName: rfd-kube-init + restartPolicy: OnFailure + containers: + - name: init + image: ghcr.io/oxidecomputer/rfd-kube-init:latest + env: + - name: MEILI_MASTER_NAMESPACE + value: "meilisearch" + - name: MEILI_MASTER_SECRET_NAME + value: "meilisearch-master" + - name: MEILI_MASTER_SECRET_KEY + value: "MEILI_MASTER_KEY" + - name: MEILI_HOST + value: "http://meilisearch.meilisearch:7700" + - name: MEILI_RW_TOKEN_TARGET_NAMESPACES + value: "rfd-api,rfd-processor" + - name: MEILI_RO_TOKEN_TARGET_NAMESPACES + value: "rfd-web" + - name: MEILI_API_EXPIRATION_SECONDS + value: "90000" # 25 hours (overlap for safety) +``` + +## Token Filter Examples + +The `MEILI_TOKEN_FILTER` environment variable accepts a JSON value defining search rules. By default, the token grants access to all indexes with no filtering. + +### Full access (default) +```bash +MEILI_TOKEN_FILTER='["*"]' +``` + +### Restrict to specific indexes +```bash +MEILI_TOKEN_FILTER='{"rfd_index": null, "other_index": null}' +``` + +### Restrict with filters +```bash +MEILI_TOKEN_FILTER='{"rfd_index": {"filter": "public = true"}}' +``` + +See [Meilisearch Tenant Tokens documentation](https://www.meilisearch.com/docs/learn/security/tenant_tokens) for full search rules syntax. + +## Error Handling + +- If any namespace write fails, the tool logs to stderr and continues processing remaining namespaces +- The tool exits with code 1 if any operation failed, code 0 if all succeeded +- Check Job/Pod logs for detailed error messages + +## OAuth Init + +The `oauth-init` subcommand initializes an OAuth client by calling the RFD API `/init` endpoint and distributes the credentials to target namespaces. + +### OAuth Init Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `RFD_API_HOST` | Yes | RFD API host URL (e.g., `http://rfd-api:8080`) | +| `OAUTH_REDIRECT_URIS` | Yes | Comma-delimited list of redirect URIs for the OAuth client | +| `OAUTH_TARGET_NAMESPACES` | Yes | Comma-delimited list of namespaces to write credentials to | +| `OAUTH_SECRET_NAME` | No | Name of the secret to create (default: `rfd-oauth-client`) | + +### OAuth Init Response + +The `/init` endpoint returns the OAuth client credentials: + +```json +{ + "client_id": "01234567-89ab-cdef-0123-456789abcdef", + "secret": "rfd_abc123def456ghi789jkl012mno345pqr678stu901vwx234yz", + "redirect_uris": [ + "https://app.example.com/callback", + "http://localhost:3000/callback" + ] +} +``` + +### OAuth Init Secret Format + +The tool creates an `Opaque` secret with the following data: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: rfd-oauth-client # or OAUTH_SECRET_NAME +type: Opaque +stringData: + OAUTH_CLIENT_ID: + OAUTH_CLIENT_SECRET: +``` + +### OAuth Init Idempotency + +The `oauth-init` command is idempotent. If the system has already been initialized (409 Conflict), the command logs a warning and exits successfully. This allows the Kubernetes Job to be run multiple times without error. + +### Example: OAuth Init Kubernetes Job + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: rfd-oauth-init + namespace: rfd-system +spec: + template: + spec: + serviceAccountName: rfd-kube-init + restartPolicy: OnFailure + containers: + - name: init + image: ghcr.io/oxidecomputer/rfd-kube-init:latest + args: ["oauth-init"] + env: + - name: RFD_API_HOST + value: "http://rfd-api.rfd-system:8080" + - name: OAUTH_REDIRECT_URIS + value: "https://app.example.com/callback,http://localhost:3000/callback" + - name: OAUTH_TARGET_NAMESPACES + value: "rfd-web,rfd-api" + - name: OAUTH_SECRET_NAME + value: "rfd-oauth-client" +``` diff --git a/rfd-kube-init/src/kube.rs b/rfd-kube-init/src/kube.rs new file mode 100644 index 00000000..36defe01 --- /dev/null +++ b/rfd-kube-init/src/kube.rs @@ -0,0 +1,89 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{anyhow, Result}; +use k8s_openapi::api::core::v1::Secret; +use kube::{ + api::{Api, ObjectMeta, Patch, PatchParams}, + Client, +}; +use secrecy::SecretString; +use std::collections::BTreeMap; +use tracing::{debug, instrument}; + +/// Read a specific key from a Kubernetes secret. +/// Returns the value wrapped in a SecretString to protect it in memory. +#[instrument(skip(client), fields(namespace = %namespace, secret_name = %secret_name, key = %key))] +pub async fn read_secret_key( + client: &Client, + namespace: &str, + secret_name: &str, + key: &str, +) -> Result { + debug!("Reading secret key from Kubernetes"); + let secrets: Api = Api::namespaced(client.clone(), namespace); + let secret = secrets.get(secret_name).await?; + + let data = secret + .data + .ok_or_else(|| anyhow!("Secret '{}' in namespace '{}' has no data", secret_name, namespace))?; + + let value_bytes = data + .get(key) + .ok_or_else(|| { + anyhow!( + "Key '{}' not found in secret '{}' in namespace '{}'", + key, + secret_name, + namespace + ) + })?; + + let value_str = String::from_utf8(value_bytes.0.clone()) + .map_err(|_| anyhow!("Secret key '{}' is not valid UTF-8", key))?; + + debug!("Successfully read secret key"); + Ok(SecretString::from(value_str)) +} + +/// Write key-value pairs to a Kubernetes secret. +/// Creates the secret if it doesn't exist, patches if it does. +/// Uses server-side apply for idempotent create-or-update behavior. +#[instrument(skip(client, data), fields(namespace = %namespace, secret_name = %secret_name, key_count = data.len()))] +pub async fn write_secret( + client: &Client, + namespace: &str, + secret_name: &str, + data: &[(&str, &str)], +) -> Result<()> { + debug!("Writing secret to Kubernetes"); + let secrets: Api = Api::namespaced(client.clone(), namespace); + + let mut string_data = BTreeMap::new(); + for (key, value) in data { + string_data.insert(key.to_string(), value.to_string()); + } + + let secret = Secret { + metadata: ObjectMeta { + name: Some(secret_name.to_string()), + namespace: Some(namespace.to_string()), + ..Default::default() + }, + string_data: Some(string_data), + type_: Some("Opaque".to_string()), + ..Default::default() + }; + + secrets + .patch( + secret_name, + &PatchParams::apply("rfd-kube-init"), + &Patch::Apply(&secret), + ) + .await?; + + debug!("Successfully wrote secret"); + Ok(()) +} diff --git a/rfd-kube-init/src/main.rs b/rfd-kube-init/src/main.rs new file mode 100644 index 00000000..82238779 --- /dev/null +++ b/rfd-kube-init/src/main.rs @@ -0,0 +1,72 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +mod kube; +mod meilisearch; +mod meilisearch_client; +mod oauth_init; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use tracing_subscriber::{filter::LevelFilter, EnvFilter}; + +use crate::meilisearch::MeilisearchArgs; +use crate::oauth_init::OAuthInitArgs; + +#[derive(Parser)] +#[command(name = "rfd-kube-init")] +#[command(about = "Kubernetes initialization tool for RFD services")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Initialize Meilisearch secrets across target namespaces + Meilisearch(MeilisearchArgs), + /// Initialize OAuth client and distribute credentials to target namespaces + OauthInit(OAuthInitArgs), +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .with_writer(std::io::stdout) + .init(); + + tracing::info!("Starting rfd-kube-init"); + + let cli = Cli::parse(); + + match cli.command { + Commands::Meilisearch(args) => { + tracing::info!("Running meilisearch initialization"); + tracing::debug!("Initializing Kubernetes client"); + let kube_client = ::kube::Client::try_default().await?; + tracing::debug!("Kubernetes client initialized"); + let result = meilisearch::init(&kube_client, &args).await; + if result.is_ok() { + tracing::info!("Meilisearch initialization completed successfully"); + } + result + } + Commands::OauthInit(args) => { + tracing::info!("Running OAuth client initialization"); + tracing::debug!("Initializing Kubernetes client"); + let kube_client = ::kube::Client::try_default().await?; + tracing::debug!("Kubernetes client initialized"); + let result = oauth_init::init(&kube_client, &args).await; + if result.is_ok() { + tracing::info!("OAuth client initialization completed successfully"); + } + result + } + } +} diff --git a/rfd-kube-init/src/meilisearch.rs b/rfd-kube-init/src/meilisearch.rs new file mode 100644 index 00000000..c269a57e --- /dev/null +++ b/rfd-kube-init/src/meilisearch.rs @@ -0,0 +1,364 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{anyhow, Context, Result}; +use clap::Args; +use meilisearch_sdk::client::Client; +use meilisearch_sdk::key::Key; +use reqwest_middleware::ClientBuilder; +use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; +use secrecy::ExposeSecret; +use std::fmt; +use std::time::Duration; +use time::OffsetDateTime; + +use crate::kube; +use crate::meilisearch_client::RetryingMeilisearchClient; + +const DEFAULT_SEARCH_API_KEY_NAME: &str = "Default Search API Key"; +const DEFAULT_ADMIN_API_KEY_NAME: &str = "Default Admin API Key"; + +enum TokenType { + ReadOnly(Option), + ReadWrite(Option), +} + +impl TokenType { + fn key_name(&self) -> &'static str { + match self { + TokenType::ReadOnly(_) => DEFAULT_SEARCH_API_KEY_NAME, + TokenType::ReadWrite(_) => DEFAULT_ADMIN_API_KEY_NAME, + } + } + + fn key(self) -> Option { + match self { + TokenType::ReadOnly(key) => key, + TokenType::ReadWrite(key) => key, + } + } +} + +impl fmt::Display for TokenType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TokenType::ReadOnly(_) => write!(f, "RO"), + TokenType::ReadWrite(_) => write!(f, "RW"), + } + } +} + +#[derive(Args)] +pub struct MeilisearchArgs { + /// Namespace where the Meilisearch master key secret is located + #[arg(long, env = "MEILI_MASTER_NAMESPACE")] + master_namespace: String, + + /// Name of the Kubernetes secret containing the master key + #[arg(long, env = "MEILI_MASTER_SECRET_NAME")] + master_secret_name: String, + + /// Key within the secret that contains the master key value + #[arg(long, env = "MEILI_MASTER_SECRET_KEY")] + master_secret_key: String, + + /// Meilisearch host URL + #[arg(long, env = "MEILI_HOST")] + host: String, + + /// Name of the secret to create in target namespaces + #[arg(long, env = "MEILI_SECRET_NAME", default_value = "meilisearch-token")] + secret_name: String, + + /// Token expiration time in seconds + #[arg(long, env = "MEILI_API_EXPIRATION_SECONDS")] + expiration_seconds: Option, + + /// JSON search rules filter for the tenant token + #[arg(long, env = "MEILI_TOKEN_FILTER")] + token_filter: Option, + + #[command(flatten)] + namespaces: NamespaceArgs, + + /// Maximum number of retry attempts for Meilisearch connection + #[arg(long, env = "MEILI_MAX_RETRIES", default_value = "30")] + max_retries: u32, +} + +#[derive(Args)] +#[group(required = true, multiple = true)] +struct NamespaceArgs { + /// Target namespaces for read-write tokens (comma-separated) + #[arg(long, env = "MEILI_RW_TOKEN_TARGET_NAMESPACES", value_delimiter = ',')] + rw_namespaces: Vec, + + /// Target namespaces for read-only tokens (comma-separated) + #[arg(long, env = "MEILI_RO_TOKEN_TARGET_NAMESPACES", value_delimiter = ',')] + ro_namespaces: Vec, +} + +struct FailedNamespace { + namespace: String, + token_type: String, +} + +impl fmt::Display for FailedNamespace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}({})", self.namespace, self.token_type) + } +} + +/// Initialize meilisearch secrets across target namespaces. +pub async fn init(kube_client: &::kube::Client, args: &MeilisearchArgs) -> Result<()> { + // Read master key from kubernetes secret + let master_key = kube::read_secret_key( + kube_client, + &args.master_namespace, + &args.master_secret_name, + &args.master_secret_key, + ) + .await + .context("Failed to read Meilisearch master key from Kubernetes secret")?; + + // Calculate expiration time if provided + let expires_at = args + .expiration_seconds + .map(|secs| OffsetDateTime::now_utc() + time::Duration::seconds(secs)); + + // Parse search rules filter (empty string or missing means wildcard access to all indexes) + let search_rules: serde_json::Value = match &args.token_filter { + Some(filter) if !filter.is_empty() => { + serde_json::from_str(filter).context("token-filter must be valid JSON")? + } + _ => serde_json::json!(["*"]), + }; + + // Create meilisearch client with retry middleware + let retry_policy = ExponentialBackoff::builder() + .retry_bounds(Duration::from_secs(1), Duration::from_secs(30)) + .build_with_max_retries(args.max_retries); + + let http_client = RetryingMeilisearchClient::new( + ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(), + master_key.expose_secret().to_string(), + ); + + let client = Client::new_with_client(&args.host, Some(master_key.expose_secret()), http_client); + let (search_key, admin_key) = get_api_keys(&client).await?; + + let mut failures = Vec::new(); + + // Generate and distribute RW token + failures.extend( + generate_and_distribute_token( + &client, + kube_client, + &args.namespaces.rw_namespaces, + &args.secret_name, + search_rules.clone(), + expires_at, + TokenType::ReadWrite(admin_key), + ) + .await?, + ); + + // Generate and distribute RO token + failures.extend( + generate_and_distribute_token( + &client, + kube_client, + &args.namespaces.ro_namespaces, + &args.secret_name, + search_rules, + expires_at, + TokenType::ReadOnly(search_key), + ) + .await?, + ); + + if !failures.is_empty() { + let failed_list: Vec = failures.iter().map(|f| f.to_string()).collect(); + tracing::error!( + failed_count = failures.len(), + failed_namespaces = %failed_list.join(", "), + "Failed to write secrets to some namespaces" + ); + return Err(anyhow!( + "Failed to write secrets to namespaces: {}", + failed_list.join(", ") + )); + } + + Ok(()) +} + +/// Fetch API keys from Meilisearch and return the search and admin keys. +async fn get_api_keys( + client: &Client, +) -> Result<(Option, Option)> { + tracing::debug!("Fetching API keys from Meilisearch"); + let keys_result = client + .get_keys() + .await + .context("Failed to fetch API keys from Meilisearch")?; + + tracing::debug!( + key_count = keys_result.results.len(), + "Retrieved API keys from Meilisearch" + ); + + let mut search_key = None; + let mut admin_key = None; + + for key in keys_result.results { + match key.name.as_deref() { + Some(DEFAULT_SEARCH_API_KEY_NAME) => { + tracing::debug!("Found default search API key"); + search_key = Some(key); + } + Some(DEFAULT_ADMIN_API_KEY_NAME) => { + tracing::debug!("Found default admin API key"); + admin_key = Some(key); + } + _ => {} + } + } + + if search_key.is_none() { + tracing::warn!("Default search API key not found"); + } + if admin_key.is_none() { + tracing::warn!("Default admin API key not found"); + } + + Ok((search_key, admin_key)) +} + +/// Generate a tenant token and distribute it to the specified namespaces. +/// Returns a list of namespaces that failed to write. +async fn generate_and_distribute_token( + client: &Client, + kube_client: &::kube::Client, + namespaces: &[String], + secret_name: &str, + search_rules: serde_json::Value, + expires_at: Option, + token_type: TokenType, +) -> Result> { + if namespaces.is_empty() { + tracing::debug!(token_type = %token_type, "No namespaces configured, skipping"); + return Ok(Vec::new()); + } + + tracing::info!( + token_type = %token_type, + namespace_count = namespaces.len(), + "Distributing tokens to namespaces" + ); + + let token_type_str = token_type.to_string(); + let key_name = token_type.key_name(); + + let key = match token_type.key() { + Some(key) => key, + None => { + tracing::error!( + key_name = key_name, + token_type = token_type_str.as_str(), + "API key not found in Meilisearch but namespaces are configured" + ); + return Err(anyhow!("Could not find '{}' in Meilisearch keys", key_name)); + } + }; + + tracing::info!( + key_name = key.name, + token_type = token_type_str.as_str(), + "Found API key for token", + ); + + let token = client.generate_tenant_token(key.uid, search_rules, Some(&key.key), expires_at)?; + + tracing::info!( + token_type = token_type_str.as_str(), + "Generated Meilisearch tenant token" + ); + + let mut failures = Vec::new(); + + for ns in namespaces { + match kube::write_secret( + kube_client, + ns, + secret_name, + &[("MEILISEARCH_API_KEY", &token)], + ) + .await + { + Ok(()) => { + tracing::info!( + namespace = ns.as_str(), + secret = secret_name, + token_type = token_type_str.as_str(), + "Wrote meilisearch secret" + ); + } + Err(err) => { + failures.push(FailedNamespace { + namespace: ns.clone(), + token_type: token_type_str.clone(), + }); + tracing::error!( + namespace = ns.as_str(), + secret = secret_name, + token_type = token_type_str.as_str(), + error = %err, + "Failed to write meilisearch secret" + ); + } + } + } + + Ok(failures) +} + +#[cfg(test)] +mod tests { + use meilisearch_sdk::client::Client; + + /// Test that crypto providers are properly configured. + /// This exercises both jsonwebtoken (JWT signing) and rustls (TLS) crypto backends. + #[test] + fn test_crypto_providers_configured() { + // Create a meilisearch client - this validates rustls crypto provider is available + let client = Client::new("http://localhost:7700", Some("test_master_key")) + .expect("Failed to create meilisearch client"); + + // Generate a tenant token - this exercises jsonwebtoken crypto + // We use a fake UID but the signing operation still runs + let search_rules = serde_json::json!(["*"]); + let fake_uid = "550e8400-e29b-41d4-a716-446655440000".to_string(); + + let result = client.generate_tenant_token(fake_uid, search_rules, None, None); + + // The token generation should succeed (crypto works) even though + // the key UID is fake - we're testing the signing, not validation + assert!( + result.is_ok(), + "JWT token generation failed: {:?}", + result.err() + ); + + let token = result.unwrap(); + // JWT tokens have 3 parts separated by dots + assert_eq!( + token.split('.').count(), + 3, + "Generated token should be a valid JWT format" + ); + } +} diff --git a/rfd-kube-init/src/meilisearch_client.rs b/rfd-kube-init/src/meilisearch_client.rs new file mode 100644 index 00000000..43f49433 --- /dev/null +++ b/rfd-kube-init/src/meilisearch_client.rs @@ -0,0 +1,66 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use futures_io::AsyncRead; +use meilisearch_sdk::errors::Error; +use meilisearch_sdk::request::{parse_response, HttpClient, Method}; +use reqwest_middleware::ClientWithMiddleware; +use serde::{de::DeserializeOwned, Serialize}; +use tokio_util::compat::FuturesAsyncReadCompatExt; +use tokio_util::io::ReaderStream; + +#[derive(Clone)] +pub struct RetryingMeilisearchClient { + client: ClientWithMiddleware, + api_key: String, +} + +impl RetryingMeilisearchClient { + pub fn new(client: ClientWithMiddleware, api_key: String) -> Self { + Self { client, api_key } + } +} + +#[async_trait::async_trait] +impl HttpClient for RetryingMeilisearchClient { + async fn stream_request< + Query: Serialize + Send + Sync, + Body: AsyncRead + Send + Sync + 'static, + Output: DeserializeOwned + 'static, + >( + &self, + url: &str, + method: Method, + content_type: &str, + expected_status_code: u16, + ) -> Result { + let url = format!("{url}?{}", yaup::to_string(method.query())?); + + let request = match method { + Method::Get { .. } => self.client.get(&url), + Method::Delete { .. } => self.client.delete(&url), + Method::Post { body, .. } => self.client.post(&url).body(to_body(body)), + Method::Put { body, .. } => self.client.put(&url).body(to_body(body)), + Method::Patch { body, .. } => self.client.patch(&url).body(to_body(body)), + } + .header(reqwest::header::CONTENT_TYPE, content_type); + + let request = request.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", self.api_key)); + + let response = request.send().await.map_err(|e| Error::Other(e.into()))?; + let status = response.status().as_u16(); + let body = response.text().await.map_err(|e| Error::Other(e.into()))?; + let body = if body.is_empty() { "null" } else { &body }; + + parse_response(status, expected_status_code, body, url.to_string()) + } + + fn is_tokio(&self) -> bool { + true + } +} + +fn to_body(reader: impl AsyncRead + Send + Sync + 'static) -> reqwest::Body { + reqwest::Body::wrap_stream(ReaderStream::new(reader.compat())) +} diff --git a/rfd-kube-init/src/oauth_init.rs b/rfd-kube-init/src/oauth_init.rs new file mode 100644 index 00000000..3f13b860 --- /dev/null +++ b/rfd-kube-init/src/oauth_init.rs @@ -0,0 +1,185 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{anyhow, Context, Result}; +use clap::Args; +use reqwest::Response; +use reqwest_middleware::ClientBuilder; +use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +use crate::kube; + +/// Request body for the /init endpoint +#[derive(Debug, Serialize)] +struct InitRequest { + redirect_uris: Vec, +} + +/// Response from the /init endpoint +#[derive(Debug, Deserialize)] +struct InitResponse { + client_id: String, + secret: String, + redirect_uris: Vec, +} + +/// Result of calling the /init endpoint +enum InitResult { + /// Successfully initialized, contains the response + Success(InitResponse), + /// System was already initialized (409 Conflict) + AlreadyInitialized, +} + +#[derive(Args)] +pub struct OAuthInitArgs { + /// RFD API host URL (e.g., http://rfd-api:8080) + #[arg(long, env = "RFD_API_HOST")] + host: String, + + /// Redirect URIs for the OAuth client (comma-separated) + #[arg(long, env = "OAUTH_REDIRECT_URIS", value_delimiter = ',')] + redirect_uris: Vec, + + /// Target namespaces to write the OAuth client credentials (comma-separated) + #[arg(long, env = "OAUTH_TARGET_NAMESPACES", value_delimiter = ',')] + target_namespaces: Vec, + + /// Name of the secret to create in target namespaces + #[arg(long, env = "OAUTH_SECRET_NAME", default_value = "rfd-oauth-client")] + secret_name: String, + + /// Maximum number of retry attempts for the /init endpoint + #[arg(long, env = "OAUTH_MAX_RETRIES", default_value = "30")] + max_retries: u32, +} + +/// Initialize OAuth client and distribute credentials to target namespaces. +pub async fn init(kube_client: &::kube::Client, args: &OAuthInitArgs) -> Result<()> { + if args.redirect_uris.is_empty() { + return Err(anyhow!("At least one redirect URI must be provided")); + } + + if args.target_namespaces.is_empty() { + return Err(anyhow!("At least one target namespace must be provided")); + } + + tracing::info!( + host = %args.host, + redirect_uri_count = args.redirect_uris.len(), + target_namespace_count = args.target_namespaces.len(), + "Initializing OAuth client" + ); + + // Build retry-enabled HTTP client + let retry_policy = ExponentialBackoff::builder() + .retry_bounds(Duration::from_secs(1), Duration::from_secs(30)) + .build_with_max_retries(args.max_retries); + + let client = ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + + let init_url = format!("{}/init", args.host.trim_end_matches('/')); + + let request_body = InitRequest { + redirect_uris: args.redirect_uris.clone(), + }; + + tracing::debug!(url = %init_url, "Calling /init endpoint"); + + let response: Response = client + .post(&init_url) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&request_body)?) + .send() + .await + .context("Failed to send request to /init endpoint")?; + + let init_response = match handle_init_response(response).await? { + InitResult::Success(response) => response, + InitResult::AlreadyInitialized => return Ok(()), + }; + + tracing::info!( + client_id = %init_response.client_id, + redirect_uri_count = init_response.redirect_uris.len(), + "OAuth client created successfully" + ); + + // Distribute credentials to target namespaces + let mut failures = Vec::new(); + + for ns in &args.target_namespaces { + match kube::write_secret( + kube_client, + ns, + &args.secret_name, + &[ + ("OAUTH_CLIENT_ID", &init_response.client_id), + ("OAUTH_CLIENT_SECRET", &init_response.secret), + ], + ) + .await + { + Ok(()) => { + tracing::info!( + namespace = ns.as_str(), + secret = args.secret_name.as_str(), + "Wrote OAuth client credentials" + ); + } + Err(err) => { + failures.push(ns.clone()); + tracing::error!( + namespace = ns.as_str(), + secret = args.secret_name.as_str(), + error = %err, + "Failed to write OAuth client credentials" + ); + } + } + } + + if !failures.is_empty() { + return Err(anyhow!( + "Failed to write secrets to namespaces: {}", + failures.join(", ") + )); + } + + Ok(()) +} + +/// Process the HTTP response from the /init endpoint. +async fn handle_init_response(response: reqwest::Response) -> Result { + let status = response.status(); + + match status { + reqwest::StatusCode::CONFLICT => { + tracing::warn!("System already initialized (409 Conflict), skipping"); + Ok(InitResult::AlreadyInitialized) + } + reqwest::StatusCode::OK | reqwest::StatusCode::CREATED => { + let parsed = response + .json() + .await + .context("Failed to parse /init response")?; + Ok(InitResult::Success(parsed)) + } + _ => { + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); + Err(anyhow!( + "Failed to initialize OAuth client: {} - {}", + status, + error_text + )) + } + } +} diff --git a/rfd-model/diesel.toml b/rfd-model/diesel.toml index 2f2493e1..863549e3 100644 --- a/rfd-model/diesel.toml +++ b/rfd-model/diesel.toml @@ -1,9 +1,9 @@ # For documentation on how to configure this file, # see https://diesel.rs/guides/configuring-diesel-cli -[print_schema] -file = "src/schema.rs" -patch_file = "diesel-schema.patch" +# [print_schema] +# file = "src/schema.rs" +# patch_file = "diesel-schema.patch" [print_schema.filter] except_tables = [ diff --git a/rfd-model/migrations/2026-02-18_initialization/down.sql b/rfd-model/migrations/2026-02-18_initialization/down.sql new file mode 100644 index 00000000..dfeb86d5 --- /dev/null +++ b/rfd-model/migrations/2026-02-18_initialization/down.sql @@ -0,0 +1,2 @@ +-- Rollback: drop the initialization table +DROP TABLE IF EXISTS initialization; diff --git a/rfd-model/migrations/2026-02-18_initialization/up.sql b/rfd-model/migrations/2026-02-18_initialization/up.sql new file mode 100644 index 00000000..a790175a --- /dev/null +++ b/rfd-model/migrations/2026-02-18_initialization/up.sql @@ -0,0 +1,8 @@ +-- Migration to create the initialization table +-- This table tracks whether the system has been initialized with an initial OAuth client + +CREATE TABLE initialization ( + id UUID PRIMARY KEY, + initialized_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + oauth_client_id UUID NOT NULL +); diff --git a/rfd-model/src/db.rs b/rfd-model/src/db.rs index a1d44814..486767bf 100644 --- a/rfd-model/src/db.rs +++ b/rfd-model/src/db.rs @@ -9,10 +9,18 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - schema::{job, rfd, rfd_pdf, rfd_revision}, + schema::{initialization, job, rfd, rfd_pdf, rfd_revision}, schema_ext::{rfd_meta_join, rfd_pdf_join, ContentFormat, PdfSource, Visibility}, }; +#[derive(Debug, Deserialize, Serialize, Queryable, Insertable, Selectable)] +#[diesel(table_name = initialization)] +pub struct InitializationModel { + pub id: Uuid, + pub initialized_at: DateTime, + pub oauth_client_id: Uuid, +} + #[derive(Debug, Deserialize, Serialize, Queryable, Insertable, Selectable)] #[diesel(table_name = rfd)] pub struct RfdModel { diff --git a/rfd-model/src/lib.rs b/rfd-model/src/lib.rs index 73f133f3..a0b42b95 100644 --- a/rfd-model/src/lib.rs +++ b/rfd-model/src/lib.rs @@ -7,6 +7,7 @@ use db::{ JobModel, RfdLatestMajorChange, RfdModel, RfdPdfModel, RfdRevisionMetaModel, RfdRevisionModel, RfdRevisionPdfModel, }; +pub use db::InitializationModel; use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag}; use partial_struct::partial; use schema_ext::{ContentFormat, PdfSource, Visibility}; diff --git a/rfd-model/src/schema.rs b/rfd-model/src/schema.rs index 8f2b23be..5a753c5c 100644 --- a/rfd-model/src/schema.rs +++ b/rfd-model/src/schema.rs @@ -16,6 +16,14 @@ pub mod sql_types { pub struct RfdVisibility; } +diesel::table! { + initialization (id) { + id -> Uuid, + initialized_at -> Timestamptz, + oauth_client_id -> Uuid, + } +} + diesel::table! { job (id) { id -> Int4, @@ -93,6 +101,7 @@ diesel::joinable!(rfd_pdf -> rfd_revision (rfd_revision_id)); diesel::joinable!(rfd_revision -> rfd (rfd_id)); diesel::allow_tables_to_appear_in_same_query!( + initialization, job, rfd, rfd_pdf, diff --git a/rfd-model/src/storage/mock.rs b/rfd-model/src/storage/mock.rs index 6477ca3c..3974ec8c 100644 --- a/rfd-model/src/storage/mock.rs +++ b/rfd-model/src/storage/mock.rs @@ -8,15 +8,16 @@ use std::sync::Arc; use v_model::storage::StoreError; use crate::{ - Job, NewJob, NewRfd, NewRfdPdf, NewRfdRevision, Rfd, RfdId, RfdMeta, RfdPdf, RfdPdfId, RfdPdfs, - RfdRevision, RfdRevisionId, RfdRevisionMeta, + db::InitializationModel, Job, NewJob, NewRfd, NewRfdPdf, NewRfdRevision, Rfd, RfdId, RfdMeta, + RfdPdf, RfdPdfId, RfdPdfs, RfdRevision, RfdRevisionId, RfdRevisionMeta, }; use super::{ - JobFilter, JobStore, ListPagination, MockJobStore, MockRfdMetaStore, MockRfdPdfStore, - MockRfdPdfsStore, MockRfdRevisionMetaStore, MockRfdRevisionPdfStore, MockRfdRevisionStore, - MockRfdStore, RfdFilter, RfdMetaStore, RfdPdfFilter, RfdPdfStore, RfdPdfsStore, - RfdRevisionFilter, RfdRevisionMetaStore, RfdRevisionStore, RfdStore, + InitializationStore, JobFilter, JobStore, ListPagination, MockInitializationStore, + MockJobStore, MockRfdMetaStore, MockRfdPdfStore, MockRfdPdfsStore, MockRfdRevisionMetaStore, + MockRfdRevisionPdfStore, MockRfdRevisionStore, MockRfdStore, RfdFilter, RfdMetaStore, + RfdPdfFilter, RfdPdfStore, RfdPdfsStore, RfdRevisionFilter, RfdRevisionMetaStore, + RfdRevisionStore, RfdStore, }; pub struct MockStorage { @@ -28,6 +29,7 @@ pub struct MockStorage { pub rfd_revision_pdf_store: Option>, pub rfd_pdf_store: Option>, pub job_store: Option>, + pub initialization_store: Option>, } impl MockStorage { @@ -41,6 +43,7 @@ impl MockStorage { rfd_revision_pdf_store: None, rfd_pdf_store: None, job_store: None, + initialization_store: None, } } } @@ -267,3 +270,21 @@ impl JobStore for MockStorage { self.job_store.as_ref().unwrap().complete(id).await } } + +#[async_trait] +impl InitializationStore for MockStorage { + async fn get(&self) -> Result, StoreError> { + self.initialization_store.as_ref().unwrap().get().await + } + + async fn insert( + &self, + record: InitializationModel, + ) -> Result { + self.initialization_store + .as_ref() + .unwrap() + .insert(record) + .await + } +} diff --git a/rfd-model/src/storage/mod.rs b/rfd-model/src/storage/mod.rs index d7a99d4b..68eea5f3 100644 --- a/rfd-model/src/storage/mod.rs +++ b/rfd-model/src/storage/mod.rs @@ -12,9 +12,9 @@ use std::fmt::Debug; use v_model::storage::{ListPagination, StoreError}; use crate::{ - schema_ext::PdfSource, CommitSha, Job, NewJob, NewRfd, NewRfdPdf, NewRfdRevision, Rfd, RfdId, - RfdMeta, RfdPdf, RfdPdfId, RfdPdfs, RfdRevision, RfdRevisionId, RfdRevisionMeta, - RfdRevisionPdf, + db::InitializationModel, schema_ext::PdfSource, CommitSha, Job, NewJob, NewRfd, NewRfdPdf, + NewRfdRevision, Rfd, RfdId, RfdMeta, RfdPdf, RfdPdfId, RfdPdfs, RfdRevision, RfdRevisionId, + RfdRevisionMeta, RfdRevisionPdf, }; #[cfg(feature = "mock")] @@ -29,6 +29,7 @@ pub trait RfdStorage: + RfdPdfStore + RfdPdfsStore + JobStore + + InitializationStore + Send + Sync + 'static @@ -42,6 +43,7 @@ impl RfdStorage for T where + RfdPdfStore + RfdPdfsStore + JobStore + + InitializationStore + Send + Sync + 'static @@ -338,3 +340,10 @@ pub trait JobStore { async fn start(&self, id: i32) -> Result, StoreError>; async fn complete(&self, id: i32) -> Result, StoreError>; } + +#[cfg_attr(feature = "mock", automock)] +#[async_trait] +pub trait InitializationStore { + async fn get(&self) -> Result, StoreError>; + async fn insert(&self, record: InitializationModel) -> Result; +} diff --git a/rfd-model/src/storage/postgres.rs b/rfd-model/src/storage/postgres.rs index 2d3d2d85..8ae6164a 100644 --- a/rfd-model/src/storage/postgres.rs +++ b/rfd-model/src/storage/postgres.rs @@ -26,10 +26,10 @@ use v_model::storage::postgres::PostgresStore; use crate::{ db::{ - JobModel, RfdLatestMajorChange, RfdMetaJoinRow, RfdModel, RfdPdfJoinRow, RfdPdfModel, - RfdRevisionMetaModel, RfdRevisionModel, RfdRevisionPdfModel, + InitializationModel, JobModel, RfdLatestMajorChange, RfdMetaJoinRow, RfdModel, + RfdPdfJoinRow, RfdPdfModel, RfdRevisionMetaModel, RfdRevisionModel, RfdRevisionPdfModel, }, - schema::{job, rfd, rfd_pdf, rfd_revision}, + schema::{initialization, job, rfd, rfd_pdf, rfd_revision}, schema_ext::Visibility, storage::StoreError, Job, NewJob, NewRfd, NewRfdPdf, NewRfdRevision, Rfd, RfdId, RfdMeta, RfdPdf, RfdPdfId, RfdPdfs, @@ -37,9 +37,9 @@ use crate::{ }; use super::{ - JobFilter, JobStore, ListPagination, RfdFilter, RfdMetaStore, RfdPdfFilter, RfdPdfStore, - RfdPdfsStore, RfdRevisionFilter, RfdRevisionMetaStore, RfdRevisionPdfStore, RfdRevisionStore, - RfdStore, + InitializationStore, JobFilter, JobStore, ListPagination, RfdFilter, RfdMetaStore, + RfdPdfFilter, RfdPdfStore, RfdPdfsStore, RfdRevisionFilter, RfdRevisionMetaStore, + RfdRevisionPdfStore, RfdRevisionStore, RfdStore, }; #[async_trait] @@ -1396,6 +1396,43 @@ impl JobStore for PostgresStore { } } +#[async_trait] +impl InitializationStore for PostgresStore { + async fn get(&self) -> Result, StoreError> { + let result = initialization::table + .first_async::( + &*self.pool.get().await.tap_err(|err| { + tracing::error!(?err, "Failed to acquire database connection") + })?, + ) + .await; + + match result { + Ok(record) => Ok(Some(record)), + Err(diesel::result::Error::NotFound) => Ok(None), + Err(e) => Err(e.into()), + } + } + + async fn insert( + &self, + record: InitializationModel, + ) -> Result { + let result = insert_into(initialization::table) + .values(( + initialization::id.eq(record.id), + initialization::initialized_at.eq(record.initialized_at), + initialization::oauth_client_id.eq(record.oauth_client_id), + )) + .get_result_async(&*self.pool.get().await.tap_err(|err| { + tracing::error!(?err, "Failed to acquire database connection") + })?) + .await?; + + Ok(result) + } +} + #[allow(clippy::type_complexity)] fn flatten_predicates( predicates: Vec>>>, diff --git a/rfd-processor/Cargo.toml b/rfd-processor/Cargo.toml index c853f700..4b223496 100644 --- a/rfd-processor/Cargo.toml +++ b/rfd-processor/Cargo.toml @@ -5,8 +5,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dev-dependencies] +mockall = { workspace = true } + [dependencies] async-trait = { workspace = true } +aws-config = { workspace = true } +aws-sdk-s3 = { workspace = true } base64 = { workspace = true } chrono = { workspace = true } config = { workspace = true } @@ -31,6 +36,7 @@ reqwest-tracing = { workspace = true } rfd-data = { path = "../rfd-data" } rfd-github = { path = "../rfd-github" } rfd-model = { path = "../rfd-model" } +rfd-secret = { path = "../rfd-secret" } rsa = { workspace = true } serde = { workspace = true } tap = { workspace = true } diff --git a/rfd-processor/config.example.toml b/rfd-processor/config.example.toml index f33fbfed..f5ff7ab7 100644 --- a/rfd-processor/config.example.toml +++ b/rfd-processor/config.example.toml @@ -23,8 +23,16 @@ scanner_enabled = true # How often the processor scanner should check the remote GitHub repo for RFDs scanner_interval = 900 -# The internal database url to store RFD information -database_url = "postgres://:@/" +# Database connection configuration +# Password can be specified inline or read from a file: +# Inline: password = "your-password" +# From file: password = { path = "/run/secrets/db-password" } +[database] +host = "localhost" +port = 5432 +user = "rfd" +password = "" +database = "rfd" # The list of actions that should be run for each processing job actions = [ @@ -42,6 +50,10 @@ actions = [ # 1. A GitHub App installation that is defined by an app_id, installation_id, and private_key # 2. A GitHub access token # Exactly one authentication must be specified +# +# Secret values (private_key, token) can be specified inline or read from a file: +# Inline: private_key = "-----BEGIN RSA PRIVATE KEY-----\n..." +# From file: private_key = { path = "/run/secrets/github-app-key" } # App Installation [auth.github] @@ -50,12 +62,13 @@ app_id = 1111111 # Numeric GitHub App installation id corresponding to the organization that the configured repo # belongs to installation_id = 2222222 -# PEM encoded private key for the GitHub App +# PEM encoded private key for the GitHub App (can be inline or { path = "..." }) private_key = """""" # Access Token [auth.github] # This may be any GitHub access token that has permission to the configured repo +# (can be inline or { path = "/run/secrets/github-token" }) token = "" # The GitHub repository to use to read and write RFDs @@ -69,10 +82,24 @@ path = "" # Branch to use as the default branch of the repository default_branch = "" -# Bucket to push static assets pulled from RFDs to (currently only GCP Storage buckets are supported) -[[static_storage]] -# Name of the bucket +# Static asset storage for images extracted from RFDs. Both GCS and S3 are supported. +# Multiple storage backends can be configured, and assets will be pushed to all of them. + +# Google Cloud Storage (GCS) +# Requires GCP Application Default Credentials to be configured +[[gcs_storage]] +# Name of the GCS bucket +bucket = "" + +# Amazon S3 (or S3-compatible storage) +# Uses AWS SDK default credential chain +[[s3_storage]] +# Name of the S3 bucket bucket = "" +# AWS region (e.g., "us-west-2") +region = "" +# Optional: Custom endpoint URL for S3-compatible services (e.g., MinIO, Backblaze B2) +# endpoint = "https://s3.example.com" # Location to store generated PDFs (currently on Google Drive Shared Drives are supported) [pdf_storage] @@ -85,7 +112,7 @@ folder = "" [[search_storage]] # Https endpoint of the search instance host = "" -# API Key for reading and writing documents +# API Key for reading and writing documents (can be inline or { path = "/run/secrets/search-key" }) key = "" # Search index to store documents in index = "" diff --git a/rfd-processor/src/context.rs b/rfd-processor/src/context.rs index 7c5324b1..b6c2ff7d 100644 --- a/rfd-processor/src/context.rs +++ b/rfd-processor/src/context.rs @@ -29,14 +29,29 @@ use thiserror::Error; use tracing::instrument; use v_model::storage::postgres::PostgresStore; +use rfd_secret::SecretResolutionError; + use crate::{ pdf::{PdfFileLocation, PdfStorage, RfdPdf, RfdPdfError}, search::{RfdSearchIndex, SearchError}, updater::{BoxedAction, RfdUpdateMode, RfdUpdaterError}, util::{gdrive_client, GDriveError}, - AppConfig, GitHubAuthConfig, PdfStorageConfig, SearchConfig, StaticStorageConfig, + AppConfig, GcsStorageConfig, GitHubAuthConfig, PdfStorageConfig, S3StorageConfig, SearchConfig, }; +pub type StaticStorageError = Box; + +#[async_trait] +pub trait StaticStorage: Send + Sync { + async fn put( + &self, + key: &str, + data: Vec, + content_type: &str, + ) -> Result<(), StaticStorageError>; + fn name(&self) -> &str; +} + pub struct Database { pub storage: PostgresStore, } @@ -72,6 +87,8 @@ pub enum ContextError { InvalidGitHubPrivateKey(#[from] rsa::pkcs1::Error), #[error(transparent)] Search(#[from] SearchError), + #[error(transparent)] + SecretResolution(#[from] SecretResolutionError), } pub struct Context { @@ -80,8 +97,8 @@ pub struct Context { pub db: Database, pub github: GitHubCtx, pub actions: Vec, - pub assets: StaticAssetStorageCtx, - pub pdf: PdfStorageCtx, + pub static_storage: Vec>, + pub pdf: Option, pub search: SearchCtx, } @@ -107,24 +124,27 @@ impl Context { app_id, installation_id, private_key, - } => GitHubClient::custom( - "rfd-processor", - Credentials::InstallationToken(InstallationTokenGenerator::new( - *installation_id, - JWTCredentials::new( - *app_id, - RsaPrivateKey::from_pkcs1_pem(private_key)? - .to_pkcs1_der()? - .to_bytes() - .to_vec(), - )?, - )), - client, - http_cache, - ), + } => { + let resolved_key = private_key.resolve()?; + GitHubClient::custom( + "rfd-processor", + Credentials::InstallationToken(InstallationTokenGenerator::new( + *installation_id, + JWTCredentials::new( + *app_id, + RsaPrivateKey::from_pkcs1_pem(&resolved_key)? + .to_pkcs1_der()? + .to_bytes() + .to_vec(), + )?, + )), + client, + http_cache, + ) + } GitHubAuthConfig::User { token } => GitHubClient::custom( "rfd-processor", - Credentials::Token(token.to_string()), + Credentials::Token(token.resolve()?), client, http_cache, ), @@ -161,7 +181,7 @@ impl Context { .iter() .map(|action| action.as_str().try_into()) .collect::, RfdUpdaterError>>()?, - assets: StaticAssetStorageCtx::new(&config.static_storage).await?, + static_storage: build_static_storage(&config.gcs_storage, &config.s3_storage).await?, pdf: PdfStorageCtx::new(&config.pdf_storage).await?, search: SearchCtx::new(&config.search_storage)?, }) @@ -186,69 +206,150 @@ pub struct GitHubCtx { pub repository: GitHubRfdRepo, } -pub struct StaticAssetStorageCtx { - pub client: Storage>, - pub locations: Vec, +async fn build_static_storage( + gcs_entries: &[GcsStorageConfig], + s3_entries: &[S3StorageConfig], +) -> Result>, ContextError> { + let mut storage: Vec> = Vec::new(); + + // Build GCS storage instances + for entry in gcs_entries { + let client = build_gcs_client().await?; + storage.push(Box::new(GcsStorage { + client, + bucket: entry.bucket.clone(), + })); + } + + // Build S3 storage instances + for entry in s3_entries { + let mut config_loader = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(aws_config::Region::new(entry.region.clone())); + + if let Some(endpoint) = &entry.endpoint { + config_loader = config_loader.endpoint_url(endpoint); + } + + let sdk_config = config_loader.load().await; + let client = aws_sdk_s3::Client::new(&sdk_config); + + storage.push(Box::new(S3Storage { + client, + bucket: entry.bucket.clone(), + })); + } + + Ok(storage) } -impl StaticAssetStorageCtx { - pub async fn new(entries: &[StaticStorageConfig]) -> Result { - let opts = yup_oauth2::ApplicationDefaultCredentialsFlowOpts::default(); - let gcp_auth = match yup_oauth2::ApplicationDefaultCredentialsAuthenticator::builder(opts) - .await - { - yup_oauth2::authenticator::ApplicationDefaultCredentialsTypes::ServiceAccount(auth) => { - tracing::debug!("Service account based credentials"); - - auth.build().await.map_err(|err| { - tracing::error!( - ?err, - "Failed to construct Cloud Storage credentials from service account" - ); - ContextError::FailedToFindGcpCredentials(err) - })? - } - yup_oauth2::authenticator::ApplicationDefaultCredentialsTypes::InstanceMetadata( - auth, - ) => { - tracing::debug!("Create instance based credentials"); - - auth.build().await.map_err(|err| { - tracing::error!( - ?err, - "Failed to construct Cloud Storage credentials from instance metadata" - ); - ContextError::FailedToFindGcpCredentials(err) - })? - } - }; +async fn build_gcs_client() -> Result>, ContextError> { + let opts = yup_oauth2::ApplicationDefaultCredentialsFlowOpts::default(); + let gcp_auth = match yup_oauth2::ApplicationDefaultCredentialsAuthenticator::builder(opts).await + { + yup_oauth2::authenticator::ApplicationDefaultCredentialsTypes::ServiceAccount(auth) => { + tracing::debug!("Service account based credentials"); + + auth.build().await.map_err(|err| { + tracing::error!( + ?err, + "Failed to construct Cloud Storage credentials from service account" + ); + ContextError::FailedToFindGcpCredentials(err) + })? + } + yup_oauth2::authenticator::ApplicationDefaultCredentialsTypes::InstanceMetadata(auth) => { + tracing::debug!("Create instance based credentials"); + + auth.build().await.map_err(|err| { + tracing::error!( + ?err, + "Failed to construct Cloud Storage credentials from instance metadata" + ); + ContextError::FailedToFindGcpCredentials(err) + })? + } + }; + + Ok(Storage::new( + Client::builder(TokioExecutor::new()).build( + HttpsConnectorBuilder::new() + .with_native_roots() + .unwrap() + .https_only() + .enable_http2() + .build(), + ), + gcp_auth, + )) +} - let storage = Storage::new( - Client::builder(TokioExecutor::new()).build( - HttpsConnectorBuilder::new() - .with_native_roots() - .unwrap() - .https_only() - .enable_http2() - .build(), - ), - gcp_auth, - ); +pub struct GcsStorage { + client: Storage>, + bucket: String, +} - Ok(Self { - client: storage, - locations: entries - .iter() - .map(|e| StaticAssetLocation { - bucket: e.bucket.to_string(), - }) - .collect(), - }) +#[async_trait] +impl StaticStorage for GcsStorage { + async fn put( + &self, + key: &str, + data: Vec, + content_type: &str, + ) -> Result<(), StaticStorageError> { + use google_storage1::api::Object; + + let mime_type: mime_guess::Mime = content_type + .parse() + .unwrap_or(mime_guess::mime::APPLICATION_OCTET_STREAM); + let cursor = std::io::Cursor::new(data); + + self.client + .objects() + .insert(Object::default(), &self.bucket) + .name(key) + .upload(cursor, mime_type) + .await?; + + Ok(()) + } + + fn name(&self) -> &str { + &self.bucket } } -pub struct StaticAssetLocation { - pub bucket: String, +pub struct S3Storage { + client: aws_sdk_s3::Client, + bucket: String, +} + +#[async_trait] +impl StaticStorage for S3Storage { + async fn put( + &self, + key: &str, + data: Vec, + content_type: &str, + ) -> Result<(), StaticStorageError> { + use aws_sdk_s3::primitives::ByteStream; + + let body = ByteStream::from(data); + + self.client + .put_object() + .bucket(&self.bucket) + .key(key) + .content_type(content_type) + .body(body) + .send() + .await?; + + Ok(()) + } + + fn name(&self) -> &str { + &self.bucket + } } pub type GDriveClient = DriveHub>; @@ -259,19 +360,16 @@ pub struct PdfStorageCtx { } impl PdfStorageCtx { - pub async fn new(config: &Option) -> Result { - Ok(Self { - // A client is only needed if files are going to be written - client: gdrive_client().await?, - locations: config - .as_ref() - .map(|config| { - vec![PdfStorageLocation { - folder_id: config.folder.clone(), - }] - }) - .unwrap_or_default(), - }) + pub async fn new(config: &Option) -> Result, GDriveError> { + match config { + Some(config) => Ok(Some(Self { + client: gdrive_client().await?, + locations: vec![PdfStorageLocation { + folder_id: config.folder.clone(), + }], + })), + None => Ok(None), + } } } @@ -324,7 +422,7 @@ impl PdfStorage for PdfStorageCtx { } } .tap_ok(|_| { - tracing::info!("Sucessfully uploaded PDF"); + tracing::info!("Successfully uploaded PDF"); }) .tap_err(|err| { tracing::error!(?err, "Failed to upload PDF"); @@ -355,12 +453,156 @@ pub struct SearchCtx { } impl SearchCtx { - pub fn new(entries: &[SearchConfig]) -> Result { + pub fn new(entries: &[SearchConfig]) -> Result { Ok(Self { indexes: entries .iter() - .map(|c| RfdSearchIndex::new(&c.host, &c.key, &c.index)) + .map(|c| { + let key = c.key.resolve()?; + RfdSearchIndex::new(&c.host, key, &c.index).map_err(ContextError::from) + }) .collect::, _>>()?, }) } } + +#[cfg(test)] +pub mod test { + use super::*; + use mockall::mock; + use std::collections::HashMap; + use std::sync::{Arc, Mutex}; + + mock! { + pub StaticStorage {} + + #[async_trait] + impl StaticStorage for StaticStorage { + async fn put( + &self, + key: &str, + data: Vec, + content_type: &str, + ) -> Result<(), StaticStorageError>; + fn name(&self) -> &str; + } + } + + /// In-memory storage implementation for testing + pub struct InMemoryStorage { + name: String, + objects: Arc>>, + } + + #[derive(Clone)] + pub struct StoredObject { + pub data: Vec, + pub content_type: String, + } + + impl InMemoryStorage { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + objects: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub fn get(&self, key: &str) -> Option { + self.objects.lock().unwrap().get(key).cloned() + } + + pub fn len(&self) -> usize { + self.objects.lock().unwrap().len() + } + } + + #[async_trait] + impl StaticStorage for InMemoryStorage { + async fn put( + &self, + key: &str, + data: Vec, + content_type: &str, + ) -> Result<(), StaticStorageError> { + self.objects.lock().unwrap().insert( + key.to_string(), + StoredObject { + data, + content_type: content_type.to_string(), + }, + ); + Ok(()) + } + + fn name(&self) -> &str { + &self.name + } + } + + #[tokio::test] + async fn in_memory_storage_stores_and_retrieves_objects() { + let storage = InMemoryStorage::new("test-bucket"); + + let data = b"hello world".to_vec(); + storage + .put("test/key.txt", data.clone(), "text/plain") + .await + .unwrap(); + + let obj = storage.get("test/key.txt").unwrap(); + assert_eq!(obj.data, data); + assert_eq!(obj.content_type, "text/plain"); + } + + #[tokio::test] + async fn in_memory_storage_returns_correct_name() { + let storage = InMemoryStorage::new("my-bucket"); + assert_eq!(storage.name(), "my-bucket"); + } + + #[tokio::test] + async fn in_memory_storage_overwrites_existing_objects() { + let storage = InMemoryStorage::new("test-bucket"); + + storage + .put("key", b"first".to_vec(), "text/plain") + .await + .unwrap(); + storage + .put("key", b"second".to_vec(), "text/plain") + .await + .unwrap(); + + let obj = storage.get("key").unwrap(); + assert_eq!(obj.data, b"second".to_vec()); + assert_eq!(storage.len(), 1); + } + + #[tokio::test] + async fn mock_storage_can_simulate_failure() { + let mut mock = MockStaticStorage::new(); + mock.expect_put() + .returning(|_, _, _| Err("simulated failure".into())); + mock.expect_name().return_const("mock-bucket".to_string()); + + let result = mock.put("key", vec![], "text/plain").await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn mock_storage_can_verify_calls() { + let mut mock = MockStaticStorage::new(); + mock.expect_put() + .withf(|key, data, content_type| { + key == "expected/key.png" && data == b"image data" && content_type == "image/png" + }) + .times(1) + .returning(|_, _, _| Ok(())); + mock.expect_name().return_const("mock-bucket".to_string()); + + mock.put("expected/key.png", b"image data".to_vec(), "image/png") + .await + .unwrap(); + } +} diff --git a/rfd-processor/src/main.rs b/rfd-processor/src/main.rs index 0641d2b5..ef4c34fb 100644 --- a/rfd-processor/src/main.rs +++ b/rfd-processor/src/main.rs @@ -4,6 +4,7 @@ use config::{Config, ConfigError, Environment, File}; use processor::{processor, JobError}; +use rfd_secret::SecretString; use serde::{Deserialize, Serialize}; use std::sync::Arc; use thiserror::Error; @@ -31,6 +32,7 @@ mod util; #[derive(Debug, Deserialize)] pub struct AppConfig { pub log_directory: Option, + pub log_filter: Option, #[serde(default)] pub log_format: LogFormat, pub processor_enabled: bool, @@ -40,12 +42,14 @@ pub struct AppConfig { pub processor_update_mode: RfdUpdateMode, pub scanner_enabled: bool, pub scanner_interval: u64, - pub database_url: String, + pub database: DatabaseConfig, pub actions: Vec, pub auth: AuthConfig, pub source: GitHubSourceRepo, #[serde(default)] - pub static_storage: Vec, + pub gcs_storage: Vec, + #[serde(default)] + pub s3_storage: Vec, #[serde(default)] pub pdf_storage: Option, #[serde(default)] @@ -80,10 +84,10 @@ pub enum GitHubAuthConfig { Installation { app_id: i64, installation_id: i64, - private_key: String, + private_key: SecretString, }, User { - token: String, + token: SecretString, }, } @@ -96,10 +100,17 @@ pub struct GitHubSourceRepo { } #[derive(Debug, Deserialize, Serialize)] -pub struct StaticStorageConfig { +pub struct GcsStorageConfig { pub bucket: String, } +#[derive(Debug, Deserialize, Serialize)] +pub struct S3StorageConfig { + pub bucket: String, + pub region: String, + pub endpoint: Option, +} + #[derive(Debug, Deserialize, Serialize)] pub struct PdfStorageConfig { pub drive: Option, @@ -109,10 +120,29 @@ pub struct PdfStorageConfig { #[derive(Debug, Deserialize, Serialize)] pub struct SearchConfig { pub host: String, - pub key: String, + pub key: SecretString, pub index: String, } +#[derive(Debug, Deserialize)] +pub struct DatabaseConfig { + pub host: String, + pub port: u16, + pub user: String, + pub password: SecretString, + pub database: String, +} + +impl DatabaseConfig { + pub fn to_url(&self) -> Result { + let password = self.password.resolve()?; + Ok(format!( + "postgres://{}:{}@{}:{}/{}", + self.user, password, self.host, self.port, self.database + )) + } +} + impl AppConfig { pub fn new(config_sources: Option>) -> Result { let mut config = Config::builder() @@ -145,10 +175,15 @@ async fn main() -> Result<(), Box> { NonBlocking::new(std::io::stdout()) }; + let env_filter = match config.log_filter { + Some(ref filter) => EnvFilter::new(filter), + None => EnvFilter::from_default_env(), + }; + let subscriber = tracing_subscriber::fmt() .with_file(false) .with_line_number(false) - .with_env_filter(EnvFilter::from_default_env()) + .with_env_filter(env_filter) .with_writer(writer); match config.log_format { @@ -156,7 +191,8 @@ async fn main() -> Result<(), Box> { LogFormat::Json => subscriber.json().init(), } - let ctx = Arc::new(Context::new(Database::new(&config.database_url).await, &config).await?); + let database_url = config.database.to_url()?; + let ctx = Arc::new(Context::new(Database::new(&database_url).await, &config).await?); let scanner_ctx = ctx.clone(); let scanner_handle = tokio::spawn(async move { diff --git a/rfd-processor/src/updater/copy_images_to_storage.rs b/rfd-processor/src/updater/copy_images_to_storage.rs index ee72a669..73a7768f 100644 --- a/rfd-processor/src/updater/copy_images_to_storage.rs +++ b/rfd-processor/src/updater/copy_images_to_storage.rs @@ -3,7 +3,6 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use async_trait::async_trait; -use google_storage1::api::Object; use tracing::instrument; use crate::{rfd::PersistedRfd, util::decode_base64}; @@ -49,24 +48,25 @@ impl RfdUpdateAction for CopyImagesToStorage { "Writing file to storage buckets" ); - let cursor = std::io::Cursor::new(data); - - for location in &ctx.assets.locations { - tracing::info!(bucket = ?location.bucket, ?object_name, "Writing to location"); + for storage in &ctx.static_storage { + tracing::info!(name = storage.name(), ?object_name, "Writing to storage"); if mode == RfdUpdateMode::Write { - // TODO: Move implementation to a trait and abstract over different storage systems - if let Err(err) = ctx - .assets - .client - .objects() - .insert(Object::default(), &location.bucket) - .name(&object_name) - .upload(cursor.clone(), mime_type.clone()) + if let Err(err) = storage + .put(&object_name, data.clone(), mime_type.as_ref()) .await { - tracing::error!(?err, "Failed to upload static file to GCP"); + tracing::error!( + name = storage.name(), + ?err, + "Failed to upload static file" + ); } + } else { + tracing::warn!( + "CopyImagesToStorage is enabled however RfdUpdateMode is not write: {:?}", + mode + ); } } } diff --git a/rfd-processor/src/updater/update_pdfs.rs b/rfd-processor/src/updater/update_pdfs.rs index 365dcac0..1c20d992 100644 --- a/rfd-processor/src/updater/update_pdfs.rs +++ b/rfd-processor/src/updater/update_pdfs.rs @@ -68,15 +68,21 @@ impl UpdatePdfs { let store_results = match mode { RfdUpdateMode::Read => Vec::new(), - RfdUpdateMode::Write => { - ctx.pdf - .store_rfd_pdf( - new.pdf_external_id.as_deref(), - &new.get_pdf_filename(), - &pdf, - ) - .await - } + RfdUpdateMode::Write => match &ctx.pdf { + Some(pdf_storage) => { + pdf_storage + .store_rfd_pdf( + new.pdf_external_id.as_deref(), + &new.get_pdf_filename(), + &pdf, + ) + .await + } + None => { + tracing::debug!("PDF storage is disabled, skipping upload"); + Vec::new() + } + }, }; Ok(store_results diff --git a/rfd-secret/Cargo.toml b/rfd-secret/Cargo.toml new file mode 100644 index 00000000..5d7f9bca --- /dev/null +++ b/rfd-secret/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rfd-secret" +version = "0.1.0" +edition = "2021" +description = "Secret string utilities for RFD API configuration" +repository = "https://github.com/oxidecomputer/rfd-api" + +[dependencies] +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } +toml = { workspace = true } diff --git a/rfd-secret/src/lib.rs b/rfd-secret/src/lib.rs new file mode 100644 index 00000000..e70567ae --- /dev/null +++ b/rfd-secret/src/lib.rs @@ -0,0 +1,151 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Secret string utilities for RFD API configuration. +//! +//! This crate provides a [`SecretString`] type that can be deserialized from either +//! an inline value or a file path, allowing secrets to be stored outside of +//! configuration files. +//! +//! # TOML Usage +//! +//! Inline value: +//! ```toml +//! key = "my-secret-value" +//! ``` +//! +//! Path-based value (reads secret from file at runtime): +//! ```toml +//! key = { path = "/run/secrets/my-key" } +//! ``` + +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SecretResolutionError { + #[error("Failed to read secret from path '{path}'")] + FileRead { + path: String, + #[source] + source: std::io::Error, + }, +} + +/// A secret string that can be specified either inline or as a path to a file. +/// +/// When deserialized from TOML/JSON, accepts either: +/// - A plain string: `"my-secret"` +/// - An object with path: `{ path = "/path/to/secret" }` +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum SecretString { + /// Secret value specified directly inline + Inline(String), + /// Path to a file containing the secret + FromPath { path: PathBuf }, +} + +impl SecretString { + /// Resolves the secret value, reading from file if necessary. + /// + /// For inline values, returns the value directly. + /// For path-based values, reads the file contents and trims trailing whitespace. + pub fn resolve(&self) -> Result { + match self { + SecretString::Inline(value) => Ok(value.clone()), + SecretString::FromPath { path } => { + let content = + std::fs::read_to_string(path).map_err(|source| SecretResolutionError::FileRead { + path: path.display().to_string(), + source, + })?; + // Trim trailing whitespace/newlines that are common in secret files + Ok(content.trim_end().to_string()) + } + } + } +} + +impl Default for SecretString { + fn default() -> Self { + SecretString::Inline(String::new()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_inline_value() { + let secret = SecretString::Inline("my-secret".to_string()); + assert_eq!(secret.resolve().unwrap(), "my-secret"); + } + + #[test] + fn test_from_path() { + let mut file = NamedTempFile::new().unwrap(); + write!(file, "file-secret").unwrap(); + + let secret = SecretString::FromPath { + path: file.path().to_path_buf(), + }; + assert_eq!(secret.resolve().unwrap(), "file-secret"); + } + + #[test] + fn test_from_path_trims_trailing_whitespace() { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "file-secret").unwrap(); + writeln!(file).unwrap(); + + let secret = SecretString::FromPath { + path: file.path().to_path_buf(), + }; + assert_eq!(secret.resolve().unwrap(), "file-secret"); + } + + #[test] + fn test_from_path_file_not_found() { + let secret = SecretString::FromPath { + path: PathBuf::from("/nonexistent/path"), + }; + let result = secret.resolve(); + assert!(matches!(result, Err(SecretResolutionError::FileRead { .. }))); + } + + #[test] + fn test_deserialize_inline() { + let toml = r#"key = "inline-value""#; + + #[derive(Deserialize)] + struct Config { + key: SecretString, + } + + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.key.resolve().unwrap(), "inline-value"); + } + + #[test] + fn test_deserialize_from_path() { + let mut file = NamedTempFile::new().unwrap(); + write!(file, "path-value").unwrap(); + + let toml = format!(r#"key = {{ path = "{}" }}"#, file.path().display()); + + #[derive(Deserialize)] + struct Config { + key: SecretString, + } + + let config: Config = toml::from_str(&toml).unwrap(); + assert_eq!(config.key.resolve().unwrap(), "path-value"); + } +}