diff --git a/README.md b/README.md index 02519b33..8846d05c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ asset for the host from the workers registry API. | Worker | Kind | Summary | |---|---|---| +| [`auth`](auth/) | Rust | OAuth authority under `auth::*`: RBAC validation, discovery, DCR, JWKS, token issuance. | | [`acp`](acp/) | Rust | Agent Client Protocol surface — stdio JSON-RPC, exposes iii agents as ACP sessions. | | [`harness-node`](harness-node/) | Node | TS port of the iii harness stack — bundles `harness`, `turn-orchestrator`, `approval-gate`, `session`, `hook-fanout`, `auth-credentials`, `models-catalog`, `provider-anthropic`, `provider-openai`, `llm-budget`, and `context-compaction` as one pnpm monorepo. See [`harness-node/README.md`](harness-node/README.md). | | [`iii-database`](iii-database/) | Rust | PostgreSQL, MySQL, and SQLite client — query, execute, transactions, prepared statements, and change feeds. | diff --git a/auth/Cargo.lock b/auth/Cargo.lock new file mode 100644 index 00000000..5dd8fad3 --- /dev/null +++ b/auth/Cargo.lock @@ -0,0 +1,3032 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auth" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "chrono", + "clap", + "iii-sdk", + "jsonwebtoken", + "rand 0.8.6", + "rsa", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[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.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-hex" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" +dependencies = [ + "cfg-if", + "cpufeatures", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[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 = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[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", +] + +[[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 = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[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 = "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 = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-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-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-core", + "futures-macro", + "futures-sink", + "futures-task", + "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", +] + +[[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 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +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", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "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 = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "iii-sdk" +version = "0.11.7-next.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dfc486e53858bbde04b66ab9c8d23bef9587fb5312ce2725dd82fa69b8b970" +dependencies = [ + "async-trait", + "futures-util", + "hostname", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "schemars", + "serde", + "serde_json", + "sysinfo", + "thiserror", + "tokio", + "tokio-tungstenite", + "tracing", + "uuid", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[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.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[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" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[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.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[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 = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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 = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +dependencies = [ + "base64", + "const-hex", + "opentelemetry", + "opentelemetry_sdk", + "prost", + "serde", + "serde_json", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.4", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "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 = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[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 = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +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 = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +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 = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.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 = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sysinfo" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +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-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[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-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +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 = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.4", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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 = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[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.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "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.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[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 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +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" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[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" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +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/auth/Cargo.toml b/auth/Cargo.toml new file mode 100644 index 00000000..bae92cf3 --- /dev/null +++ b/auth/Cargo.toml @@ -0,0 +1,112 @@ +[workspace] + +[package] +name = "auth" +version = "0.1.0" +description = "OAuth authority worker for iii RBAC, discovery, DCR, JWKS, and token validation." +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/iii-hq/workers" +authors = ["iii contributors"] +publish = false +build = "build.rs" + +[lib] +name = "iii_auth" +path = "src/lib.rs" + +[[bin]] +name = "iii-auth" +path = "src/main.rs" + +[dependencies] +iii-sdk = "0.11.7-next.1" +anyhow = "1" +async-trait = "0.1" +base64 = "0.22" +chrono = { version = "0.4", features = ["serde"] } +clap = { version = "4", features = ["derive", "env"] } +jsonwebtoken = "9" +rand = "0.8" +rsa = { version = "0.9", features = ["pem"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +sha2 = "0.10" +tokio = { version = "1", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } +uuid = { version = "1", features = ["v4", "serde"] } + +[dev-dependencies] +serde_json = "1" + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +module_name_repetitions = "allow" +must_use_candidate = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +struct_excessive_bools = "allow" +doc_markdown = "allow" +match_same_arms = "allow" +missing_const_for_fn = "allow" +derive_partial_eq_without_eq = "allow" +unreadable_literal = "allow" +needless_pass_by_value = "allow" +single_match_else = "allow" +option_if_let_else = "allow" +redundant_closure_for_method_calls = "allow" +significant_drop_tightening = "allow" +manual_let_else = "allow" +collapsible_if = "allow" +drain_collect = "allow" +manual_map = "allow" +useless_let_if_seq = "allow" +needless_continue = "allow" +items_after_statements = "allow" +too_many_lines = "allow" +unused_self = "allow" +single_call_fn = "allow" +inefficient_to_string = "allow" +similar_names = "allow" +or_fun_call = "allow" +unnecessary_wraps = "allow" +unused_async = "allow" +needless_collect = "allow" +case_sensitive_file_extension_comparisons = "allow" +significant_drop_in_scrutinee = "allow" +redundant_pub_crate = "allow" +ignored_unit_patterns = "allow" +manual_string_new = "allow" +redundant_else = "allow" +cognitive_complexity = "allow" +branches_sharing_code = "allow" +unnecessary_lazy_evaluations = "allow" +non_std_lazy_statics = "allow" +ignore_without_reason = "allow" +struct_field_names = "allow" +naive_bytecount = "allow" +suspicious_arithmetic_impl = "allow" +return_self_not_must_use = "allow" +needless_pass_by_ref_mut = "allow" +ref_option = "allow" +mut_mut = "allow" +should_implement_trait = "allow" +default_trait_access = "allow" +cloned_instead_of_copied = "allow" +implicit_clone = "allow" +use_self = "allow" +derivable_impls = "allow" +redundant_clone = "allow" +unnecessary_map_or = "allow" +double_ended_iterator_last = "allow" +format_push_string = "allow" +uninlined_format_args = "allow" +too_long_first_doc_paragraph = "allow" +map_unwrap_or = "allow" diff --git a/auth/README.md b/auth/README.md new file mode 100644 index 00000000..c2725eb4 --- /dev/null +++ b/auth/README.md @@ -0,0 +1,146 @@ +# auth + +OAuth authority worker for iii. It gives MCP, A2A, virtual workers, and normal iii workers one shared auth surface for token issue, validation, discovery, revocation, and worker-manager RBAC. + +## Functions + +- `auth::validate` validates a Bearer token for `iii-worker-manager` RBAC and returns the session decision shape the engine expects. +- `auth::server_metadata` returns the RFC 8414 authorization server discovery document. +- `auth::resource_metadata` returns the RFC 9728 protected resource discovery document. +- `auth::register` performs RFC 7591-style dynamic client registration. +- `auth::jwks` returns active public signing keys. +- `auth::jwks_rotate` rotates the local signing key and keeps old keys through the overlap window. +- `auth::token` issues client-credentials tokens and rotates refresh tokens. +- `auth::introspect` returns token activity for authenticated clients. +- `auth::revoke` revokes access tokens or refresh tokens. + +## Install + +```bash +iii worker add auth +``` + +Then point `iii-worker-manager` RBAC at `auth::validate`: + +```yaml +workers: + - name: iii-worker-manager + config: + rbac: + auth_function_id: auth::validate + expose_functions: + - metadata: + public: true + - name: auth + config: + issuer: https://api.example.com + idp_mode: local +``` + +## Quickstart + +Register a client: + +```json +{ + "client_name": "local-mcp-client", + "grant_types": ["client_credentials", "refresh_token"], + "scope": "mcp:tools" +} +``` + +Call `auth::register` with that payload. The response includes `client_id` and, for confidential clients, a one-time `client_secret`. + +Privileged scopes are intentionally blocked for public registration. Set `III_AUTH_REGISTRATION_TOKEN` and pass it as `Authorization: Bearer ` only for internal bootstrap clients that need `function:*`, `trigger:*`, or `iii:*` scopes. + +Issue a token: + +```json +{ + "grant_type": "client_credentials", + "client_id": "", + "client_secret": "", + "scope": "mcp:tools" +} +``` + +Call `auth::token`. Use the returned Bearer token when connecting to the worker manager, MCP, or A2A bridge. + +Refresh a token: + +```json +{ + "grant_type": "refresh_token", + "client_id": "", + "client_secret": "", + "refresh_token": "" +} +``` + +The old refresh token is revoked and the response includes a new one. + +Revoke a token: + +```json +{ + "client_id": "", + "client_secret": "", + "token": "", + "token_type_hint": "access_token" +} +``` + +## Configuration + +```yaml +environment: "local" +engine_url: "ws://127.0.0.1:49134" +issuer: "https://api.example.com" +audience: "iii" +idp_mode: "local" +store: "iii_state" +access_token_ttl_seconds: 900 +refresh_token_ttl_seconds: 2592000 +rotation_overlap_seconds: 86400 +default_scopes: ["mcp:tools"] +supported_scopes: + - "mcp:tools" + - "a2a:message" +token_endpoint_auth_methods_supported: + - "client_secret_post" + - "client_secret_basic" +registration_admin_token_env: "III_AUTH_REGISTRATION_TOKEN" +state_timeout_ms: 5000 +connection_ready_attempts: 150 +connection_ready_interval_ms: 200 +``` + +Privileged scopes are opt-in. Add them only for deployments that need worker-manager bootstrap authority, and protect registration with `III_AUTH_REGISTRATION_TOKEN`: + +```yaml +supported_scopes: + - "mcp:tools" + - "a2a:message" + - "function:*" + - "iii:function_registration" + - "iii:trigger_type_registration" + - "iii:trusted_internal" +``` + +`idp_mode: local` issues and validates local RS256 JWTs. The worker fails closed if its config file cannot be loaded. The iii state store uses bounded timeouts so auth paths do not wait forever on state. + +Set `environment: "production"` or `III_AUTH_ENV=production` to reject insecure `ws://` and `http://` endpoints at startup. + +The registry default uses an HTTPS issuer placeholder. Replace it with the real HTTPS authority and certificate for any shared, remote, or production deployment. + +## IdP Matrix + +| IdP | DCR | Metadata | PKCE | Notes | +|---|---|---|---|---| +| Keycloak | yes | yes | required | Best reference bridge target. | +| Okta | yes | yes | required | Good DCR support. | +| Auth0 | yes | yes | required | Good DCR support. | +| Entra ID | no | yes | required | Pre-register clients. | +| Google | no | yes | required | Pre-register clients. | +| Ping | yes | yes | required | Good DCR support. | +| ForgeRock | yes | yes | required | Good DCR support. | diff --git a/auth/build.rs b/auth/build.rs new file mode 100644 index 00000000..6867bd0f --- /dev/null +++ b/auth/build.rs @@ -0,0 +1,4 @@ +fn main() { + let target = std::env::var("TARGET").expect("TARGET must be set by Cargo for auth/build.rs"); + println!("cargo:rustc-env=TARGET_TRIPLE={}", target); +} diff --git a/auth/config.yaml b/auth/config.yaml new file mode 100644 index 00000000..727440c3 --- /dev/null +++ b/auth/config.yaml @@ -0,0 +1,24 @@ +# Local development defaults. Use wss:// engine_url and https:// issuer when +# environment is production. +environment: "local" +engine_url: "ws://127.0.0.1:49134" +issuer: "http://127.0.0.1:3111" +audience: "iii" +idp_mode: "local" +store: "iii_state" +access_token_ttl_seconds: 900 +refresh_token_ttl_seconds: 2592000 +rotation_overlap_seconds: 86400 +rotation_cron: "0 0 3 * * *" +default_scopes: + - "mcp:tools" +supported_scopes: + - "mcp:tools" + - "a2a:message" +token_endpoint_auth_methods_supported: + - "client_secret_post" + - "client_secret_basic" +registration_admin_token_env: "III_AUTH_REGISTRATION_TOKEN" +state_timeout_ms: 5000 +connection_ready_attempts: 150 +connection_ready_interval_ms: 200 diff --git a/auth/iii.worker.yaml b/auth/iii.worker.yaml new file mode 100644 index 00000000..e525e66e --- /dev/null +++ b/auth/iii.worker.yaml @@ -0,0 +1,7 @@ +iii: v1 +name: auth +language: rust +deploy: binary +manifest: Cargo.toml +bin: iii-auth +description: OAuth authority worker for iii RBAC, discovery, DCR, JWKS, and token validation. diff --git a/auth/skills/index.md b/auth/skills/index.md new file mode 100644 index 00000000..38e6cb36 --- /dev/null +++ b/auth/skills/index.md @@ -0,0 +1,87 @@ +# auth + +Use this worker when an iii project needs one OAuth authority for worker-manager RBAC, MCP bridges, A2A bridges, or generated workers that need bearer-token access. + +It is useful when you need to: + +- let clients discover auth endpoints without hardcoding URLs +- dynamically register clients at runtime +- issue short-lived RS256 access tokens plus refresh tokens +- validate incoming worker-manager sessions into concrete RBAC decisions +- expose JWKS for local token verification +- introspect or revoke tokens from trusted resource workers + +Prefer the smallest function that answers the job: + +- `auth::server_metadata` for authorization server discovery +- `auth::resource_metadata` for protected resource discovery +- `auth::register` before a new client can request tokens +- `auth::token` to issue or refresh tokens +- `auth::validate` in worker-manager RBAC middleware +- `auth::jwks` when a verifier needs public signing keys +- `auth::jwks_rotate` for scheduled or manual signing-key rotation +- `auth::introspect` when a trusted resource needs token status +- `auth::revoke` when a client signs out or a token must stop working + +Typical flow: + +```text +auth::server_metadata -> auth::register -> auth::token -> auth::validate +``` + +For HTTP clients, the same worker exposes: + +```text +GET /.well-known/oauth-authorization-server +GET /.well-known/oauth-protected-resource +POST /register +POST /token +GET /.well-known/jwks.json +POST /introspect +POST /revoke +``` + +Example client registration: + +```json +{ + "client_name": "artifact-worker", + "scope": "mcp:tools a2a:message" +} +``` + +Example token request: + +```json +{ + "grant_type": "client_credentials", + "client_id": "client_123", + "client_secret": "secret_456" +} +``` + +Example validation request: + +```json +{ + "headers": { + "authorization": "Bearer eyJhbGciOiJSUzI1NiIs..." + }, + "ip_address": "127.0.0.1" +} +``` + +Example validation output: + +```json +{ + "allowed_functions": ["tools::search"], + "forbidden_functions": [], + "allow_function_registration": false, + "trusted_internal": false, + "context": { + "client_id": "client_123", + "subject": "client_123" + } +} +``` diff --git a/auth/skills/introspect.md b/auth/skills/introspect.md new file mode 100644 index 00000000..63bd6bbf --- /dev/null +++ b/auth/skills/introspect.md @@ -0,0 +1,47 @@ +# auth::introspect + +Use this when a trusted resource worker needs to know whether a token is active and which client/scopes it represents. + +HTTP route: `POST /introspect` + +Authenticate with one of: + +- `client_secret_post`: include `client_id` and `client_secret` in the JSON body. +- `client_secret_basic`: send `Authorization: Basic base64(client_id:client_secret)`. + +Input: + +```json +{ + "client_id": "QGxGq7m6bcqXkFY7Q0c1p2Jf", + "client_secret": "iN1PqXZJDEU6M5HsR3uHz12vQk1eQJ3TR1T1lPYU6Oc", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...", + "token_type_hint": "access_token" +} +``` + +Sample active output: + +```json +{ + "active": true, + "client_id": "QGxGq7m6bcqXkFY7Q0c1p2Jf", + "sub": "QGxGq7m6bcqXkFY7Q0c1p2Jf", + "aud": "iii", + "iss": "http://127.0.0.1:3111", + "exp": 1770000000, + "iat": 1769999100, + "scope": "mcp:tools", + "jti": "token-id" +} +``` + +Inactive response: + +```json +{ "active": false } +``` + +Use `token_type_hint: refresh_token` when checking a refresh token, otherwise omit the hint or use `access_token`. + +Do not expose introspection to untrusted callers. A caller with valid client credentials can learn token activity and subject data. diff --git a/auth/skills/jwks.md b/auth/skills/jwks.md new file mode 100644 index 00000000..54b1c3a8 --- /dev/null +++ b/auth/skills/jwks.md @@ -0,0 +1,30 @@ +# auth::jwks + +Use this when a verifier needs public keys for RS256 access tokens issued by this auth worker. + +HTTP route: `GET /.well-known/jwks.json` + +Input: + +```json +{} +``` + +Sample output: + +```json +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "current-key-id", + "alg": "RS256", + "n": "base64url-modulus", + "e": "AQAB" + } + ] +} +``` + +Cache by `kid`, but refresh this document when token validation sees an unknown `kid`. After `auth::jwks_rotate`, the previous key remains available until the configured overlap window ends. diff --git a/auth/skills/jwks_rotate.md b/auth/skills/jwks_rotate.md new file mode 100644 index 00000000..d450099a --- /dev/null +++ b/auth/skills/jwks_rotate.md @@ -0,0 +1,23 @@ +# auth::jwks_rotate + +Use this for scheduled or manual signing-key rotation. + +Trigger: cron from `rotation_cron`, or direct function call when an operator needs rotation immediately. + +Input: + +```json +{} +``` + +Sample output: + +```json +{ + "ok": true, + "current_kid": "new-key-id", + "retained_keys": 2 +} +``` + +The worker keeps the previous key through `rotation_overlap_seconds` so existing access tokens can still verify. Run `auth::jwks` after rotation if a verifier needs the latest public key set. diff --git a/auth/skills/register.md b/auth/skills/register.md new file mode 100644 index 00000000..d3b40967 --- /dev/null +++ b/auth/skills/register.md @@ -0,0 +1,56 @@ +# auth::register + +Use this before a new MCP bridge, A2A bridge, generated worker, or internal client asks `auth::token` for credentials. + +HTTP route: `POST /register` + +Use public registration for normal scopes such as `mcp:tools` and `a2a:message`. Use an admin bearer token only when registering privileged scopes such as `function:*`, `trigger:*`, or `iii:*`. + +Minimal input: + +```json +{ + "client_name": "artifact-worker" +} +``` + +Input with explicit scopes and grants: + +```json +{ + "client_name": "artifact-worker", + "redirect_uris": ["http://127.0.0.1:3000/callback"], + "grant_types": ["client_credentials", "refresh_token"], + "scope": "mcp:tools a2a:message", + "token_endpoint_auth_method": "client_secret_post" +} +``` + +Privileged registration input: + +```json +{ + "headers": { + "authorization": "Bearer " + }, + "client_name": "", + "scope": "function:* iii:function_registration iii:trusted_internal" +} +``` + +Sample output: + +```json +{ + "client_id": "", + "client_secret": "", + "client_name": "", + "client_id_issued_at": 1770000000, + "grant_types": ["client_credentials", "refresh_token"], + "redirect_uris": [], + "scope": "mcp:tools a2a:message", + "token_endpoint_auth_method": "client_secret_post" +} +``` + +Do not request privileged scopes for public clients. The worker rejects those unless the configured admin token is present. diff --git a/auth/skills/resource_metadata.md b/auth/skills/resource_metadata.md new file mode 100644 index 00000000..271cce9b --- /dev/null +++ b/auth/skills/resource_metadata.md @@ -0,0 +1,23 @@ +# auth::resource_metadata + +Use this when a protected MCP or A2A resource needs to advertise which authorization server and scopes clients should use. + +HTTP route: `GET /.well-known/oauth-protected-resource` + +Input: + +```json +{} +``` + +Sample output: + +```json +{ + "resource": "iii", + "authorization_servers": ["http://127.0.0.1:3111"], + "scopes_supported": ["mcp:tools", "a2a:message"] +} +``` + +Use `auth::server_metadata` next when the client needs token, registration, JWKS, introspection, or revocation endpoint URLs. diff --git a/auth/skills/revoke.md b/auth/skills/revoke.md new file mode 100644 index 00000000..267e07d6 --- /dev/null +++ b/auth/skills/revoke.md @@ -0,0 +1,28 @@ +# auth::revoke + +Use this when a client signs out, a refresh token is rotated out of use, or an operator needs an access token or refresh token to stop working. + +HTTP route: `POST /revoke` + +Authenticate with `client_secret_post` or `client_secret_basic`. + +Input: + +```json +{ + "client_id": "QGxGq7m6bcqXkFY7Q0c1p2Jf", + "client_secret": "iN1PqXZJDEU6M5HsR3uHz12vQk1eQJ3TR1T1lPYU6Oc", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...", + "token_type_hint": "access_token" +} +``` + +Sample output: + +```json +{ + "ok": true +} +``` + +Use `token_type_hint: refresh_token` for refresh tokens. Unknown tokens also return success so callers cannot use revocation as a token oracle. diff --git a/auth/skills/server_metadata.md b/auth/skills/server_metadata.md new file mode 100644 index 00000000..3962f449 --- /dev/null +++ b/auth/skills/server_metadata.md @@ -0,0 +1,30 @@ +# auth::server_metadata + +Use this when a client, bridge, or generated worker needs OAuth discovery for the auth worker. + +HTTP route: `GET /.well-known/oauth-authorization-server` + +Input: + +```json +{} +``` + +Sample output: + +```json +{ + "issuer": "http://127.0.0.1:3111", + "token_endpoint": "http://127.0.0.1:3111/token", + "registration_endpoint": "http://127.0.0.1:3111/register", + "jwks_uri": "http://127.0.0.1:3111/.well-known/jwks.json", + "introspection_endpoint": "http://127.0.0.1:3111/introspect", + "revocation_endpoint": "http://127.0.0.1:3111/revoke", + "grant_types_supported": ["client_credentials", "refresh_token"], + "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"], + "scopes_supported": ["mcp:tools", "a2a:message"], + "idp_mode": "local" +} +``` + +Use this instead of hardcoding URLs. The worker builds endpoint URLs from the configured issuer. diff --git a/auth/skills/token.md b/auth/skills/token.md new file mode 100644 index 00000000..6fd5d04a --- /dev/null +++ b/auth/skills/token.md @@ -0,0 +1,58 @@ +# auth::token + +Use this after `auth::register` to issue an access token or rotate a refresh token. + +HTTP route: `POST /token` + +Supported grants: + +- `client_credentials`: issue a new access token and refresh token +- `refresh_token`: revoke the used refresh token, issue a new access token, and return a new refresh token + +Client-secret-post input: + +```json +{ + "grant_type": "client_credentials", + "client_id": "QGxGq7m6bcqXkFY7Q0c1p2Jf", + "client_secret": "iN1PqXZJDEU6M5HsR3uHz12vQk1eQJ3TR1T1lPYU6Oc", + "scope": "mcp:tools" +} +``` + +Client-secret-basic input: + +```json +{ + "headers": { + "authorization": "Basic UUd4R3E3bTZiY3FYa0ZZN1EwYzFwMkpmOmlOMVBxWFpKREVVNk01SHNSM3VIejEydlFrMWVRSjNUUjFUMWxQWVU2T2M=" + }, + "grant_type": "client_credentials", + "scope": "mcp:tools" +} +``` + +Refresh input: + +```json +{ + "grant_type": "refresh_token", + "refresh_token": "old-refresh-token", + "client_id": "QGxGq7m6bcqXkFY7Q0c1p2Jf", + "client_secret": "iN1PqXZJDEU6M5HsR3uHz12vQk1eQJ3TR1T1lPYU6Oc" +} +``` + +Sample output: + +```json +{ + "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...", + "token_type": "Bearer", + "expires_in": 900, + "refresh_token": "QGKb6A0lHDxcnwhYE4V3pKL40ZVvL2r8G9E4jWoSvdA", + "scope": "mcp:tools" +} +``` + +Never ask for wildcard scopes at token time. Wildcards can be registered for policy, but concrete tokens must carry concrete scopes. diff --git a/auth/skills/validate.md b/auth/skills/validate.md new file mode 100644 index 00000000..2c3783a5 --- /dev/null +++ b/auth/skills/validate.md @@ -0,0 +1,55 @@ +# auth::validate + +Use this inside worker-manager RBAC to turn an incoming bearer token into an iii authorization decision. + +Input can provide the token through `headers.authorization` or compatible request metadata. Include `ip_address` when available so loopback/internal policies can be evaluated consistently. + +Input: + +```json +{ + "headers": { + "authorization": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6..." + }, + "query_params": {}, + "ip_address": "127.0.0.1" +} +``` + +Sample output for a normal client: + +```json +{ + "allowed_functions": ["tools::search"], + "forbidden_functions": [], + "allowed_trigger_types": null, + "allow_trigger_type_registration": false, + "allow_function_registration": false, + "trusted_internal": false, + "context": { + "client_id": "QGxGq7m6bcqXkFY7Q0c1p2Jf", + "subject": "QGxGq7m6bcqXkFY7Q0c1p2Jf", + "scope": "function:tools::search" + } +} +``` + +Sample output for a privileged internal client: + +```json +{ + "allowed_functions": ["*"], + "forbidden_functions": [], + "allowed_trigger_types": ["http"], + "allow_trigger_type_registration": true, + "allow_function_registration": true, + "trusted_internal": true, + "function_registration_prefix": null, + "context": { + "client_id": "worker-manager", + "subject": "worker-manager" + } +} +``` + +Reject the session if this function errors, returns an inactive decision, or lacks the function/trigger permission required by the requested operation. diff --git a/auth/src/config.rs b/auth/src/config.rs new file mode 100644 index 00000000..2353f720 --- /dev/null +++ b/auth/src/config.rs @@ -0,0 +1,337 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum StoreBackend { + #[default] + IiiState, + Memory, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct AuthConfig { + #[serde(default = "default_environment")] + pub environment: String, + #[serde(default = "default_engine_url")] + pub engine_url: String, + #[serde(default = "default_issuer")] + pub issuer: String, + #[serde(default = "default_audience")] + pub audience: String, + #[serde(default = "default_idp_mode")] + pub idp_mode: String, + #[serde(default)] + pub store: StoreBackend, + #[serde(default = "default_access_token_ttl_seconds")] + pub access_token_ttl_seconds: i64, + #[serde(default = "default_refresh_token_ttl_seconds")] + pub refresh_token_ttl_seconds: i64, + #[serde(default = "default_rotation_overlap_seconds")] + pub rotation_overlap_seconds: i64, + #[serde(default = "default_rotation_cron")] + pub rotation_cron: String, + #[serde(default = "default_default_scopes")] + pub default_scopes: Vec, + #[serde(default = "default_supported_scopes")] + pub supported_scopes: Vec, + #[serde(default = "default_token_endpoint_auth_methods_supported")] + pub token_endpoint_auth_methods_supported: Vec, + #[serde(default = "default_registration_admin_token_env")] + pub registration_admin_token_env: String, + #[serde(default = "default_state_timeout_ms")] + pub state_timeout_ms: u64, + #[serde(default = "default_connection_ready_attempts")] + pub connection_ready_attempts: usize, + #[serde(default = "default_connection_ready_interval_ms")] + pub connection_ready_interval_ms: u64, + #[serde(default = "default_skills_timeout_ms")] + pub skills_register_timeout_ms: u64, + #[serde(default = "default_skills_timeout_ms")] + pub skills_unregister_timeout_ms: u64, +} + +fn default_engine_url() -> String { + "ws://127.0.0.1:49134".to_string() +} + +fn default_environment() -> String { + "local".to_string() +} + +fn default_issuer() -> String { + "http://127.0.0.1:3111".to_string() +} + +fn default_audience() -> String { + "iii".to_string() +} + +fn default_idp_mode() -> String { + "local".to_string() +} + +fn default_access_token_ttl_seconds() -> i64 { + 900 +} + +fn default_refresh_token_ttl_seconds() -> i64 { + 2_592_000 +} + +fn default_rotation_overlap_seconds() -> i64 { + 86_400 +} + +fn default_rotation_cron() -> String { + "0 0 3 * * *".to_string() +} + +fn default_skills_timeout_ms() -> u64 { + 5_000 +} + +fn default_default_scopes() -> Vec { + vec!["mcp:tools".to_string()] +} + +fn default_supported_scopes() -> Vec { + vec!["mcp:tools".to_string(), "a2a:message".to_string()] +} + +fn default_token_endpoint_auth_methods_supported() -> Vec { + vec![ + "client_secret_post".to_string(), + "client_secret_basic".to_string(), + ] +} + +fn default_registration_admin_token_env() -> String { + "III_AUTH_REGISTRATION_TOKEN".to_string() +} + +fn default_state_timeout_ms() -> u64 { + 5_000 +} + +fn default_connection_ready_attempts() -> usize { + 150 +} + +fn default_connection_ready_interval_ms() -> u64 { + 200 +} + +impl Default for AuthConfig { + fn default() -> Self { + Self { + environment: default_environment(), + engine_url: default_engine_url(), + issuer: default_issuer(), + audience: default_audience(), + idp_mode: default_idp_mode(), + store: StoreBackend::default(), + access_token_ttl_seconds: default_access_token_ttl_seconds(), + refresh_token_ttl_seconds: default_refresh_token_ttl_seconds(), + rotation_overlap_seconds: default_rotation_overlap_seconds(), + rotation_cron: default_rotation_cron(), + default_scopes: default_default_scopes(), + supported_scopes: default_supported_scopes(), + token_endpoint_auth_methods_supported: default_token_endpoint_auth_methods_supported(), + registration_admin_token_env: default_registration_admin_token_env(), + state_timeout_ms: default_state_timeout_ms(), + connection_ready_attempts: default_connection_ready_attempts(), + connection_ready_interval_ms: default_connection_ready_interval_ms(), + skills_register_timeout_ms: default_skills_timeout_ms(), + skills_unregister_timeout_ms: default_skills_timeout_ms(), + } + } +} + +pub fn load_config(path: &str) -> Result { + let contents = std::fs::read_to_string(path)?; + let mut cfg: AuthConfig = serde_yaml::from_str(&contents)?; + if let Ok(environment) = std::env::var("III_AUTH_ENV") { + if !environment.is_empty() { + cfg.environment = environment; + } + } + validate_config(&cfg)?; + Ok(cfg) +} + +pub fn validate_config(cfg: &AuthConfig) -> Result<()> { + if cfg.access_token_ttl_seconds <= 0 { + anyhow::bail!("access_token_ttl_seconds must be positive"); + } + if cfg.refresh_token_ttl_seconds <= 0 { + anyhow::bail!("refresh_token_ttl_seconds must be positive"); + } + if cfg.rotation_overlap_seconds <= 0 { + anyhow::bail!("rotation_overlap_seconds must be positive"); + } + if cfg.rotation_overlap_seconds >= cfg.refresh_token_ttl_seconds { + anyhow::bail!("rotation_overlap_seconds must be less than refresh_token_ttl_seconds"); + } + if cfg.state_timeout_ms == 0 { + anyhow::bail!("state_timeout_ms must be positive"); + } + if cfg.connection_ready_attempts == 0 { + anyhow::bail!("connection_ready_attempts must be positive"); + } + if cfg.connection_ready_interval_ms == 0 { + anyhow::bail!("connection_ready_interval_ms must be positive"); + } + if cfg.skills_register_timeout_ms == 0 { + anyhow::bail!("skills_register_timeout_ms must be positive"); + } + if cfg.skills_unregister_timeout_ms == 0 { + anyhow::bail!("skills_unregister_timeout_ms must be positive"); + } + if cfg.supported_scopes.is_empty() { + anyhow::bail!("supported_scopes must not be empty"); + } + for scope in &cfg.default_scopes { + if !scope_supported_by(scope, &cfg.supported_scopes) { + anyhow::bail!("default scope {scope} must be listed in supported_scopes"); + } + } + if cfg.environment.eq_ignore_ascii_case("production") { + if !cfg.engine_url.starts_with("wss://") { + anyhow::bail!("production auth config requires wss:// engine_url"); + } + if !cfg.issuer.starts_with("https://") { + anyhow::bail!("production auth config requires https:// issuer"); + } + } + let cron_fields = cfg.rotation_cron.split_whitespace().count(); + if cron_fields != 6 { + anyhow::bail!("rotation_cron must use iii's 6-field cron format"); + } + Ok(()) +} + +fn scope_supported_by(scope: &str, supported_scopes: &[String]) -> bool { + supported_scopes.iter().any(|supported| { + supported == scope + || (supported == "function:*" && scope.starts_with("function:")) + || (supported == "trigger:*" && scope.starts_with("trigger:")) + }) +} + +pub fn resolve_store_backend(cfg: &AuthConfig) -> StoreBackend { + match std::env::var("III_AUTH_STORE").as_deref() { + Ok("memory") => StoreBackend::Memory, + Ok("iii_state") => StoreBackend::IiiState, + Ok(other) if !other.is_empty() => { + tracing::warn!(%other, "unknown III_AUTH_STORE, using configured store"); + cfg.store + } + _ => cfg.store, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_from_empty_yaml() { + let cfg: AuthConfig = serde_yaml::from_str("{}").unwrap(); + assert_eq!(cfg.engine_url, "ws://127.0.0.1:49134"); + assert_eq!(cfg.issuer, "http://127.0.0.1:3111"); + assert_eq!(cfg.environment, "local"); + assert_eq!(cfg.store, StoreBackend::IiiState); + assert!(cfg.supported_scopes.contains(&"mcp:tools".to_string())); + assert!(!cfg.supported_scopes.contains(&"function:*".to_string())); + assert_eq!( + cfg.registration_admin_token_env, + "III_AUTH_REGISTRATION_TOKEN" + ); + assert_eq!(cfg.state_timeout_ms, 5_000); + assert_eq!(cfg.connection_ready_attempts, 150); + assert_eq!(cfg.connection_ready_interval_ms, 200); + } + + #[test] + fn custom_yaml_overrides() { + let cfg: AuthConfig = serde_yaml::from_str( + r#"engine_url: "ws://example:49134" +issuer: "https://auth.example" +audience: "workers" +store: memory +default_scopes: ["function:demo::read"] +"#, + ) + .unwrap(); + assert_eq!(cfg.engine_url, "ws://example:49134"); + assert_eq!(cfg.issuer, "https://auth.example"); + assert_eq!(cfg.audience, "workers"); + assert_eq!(cfg.store, StoreBackend::Memory); + assert_eq!(cfg.default_scopes, vec!["function:demo::read"]); + } + + #[test] + fn production_rejects_insecure_urls() { + let cfg = AuthConfig { + environment: "production".to_string(), + ..AuthConfig::default() + }; + let err = validate_config(&cfg).unwrap_err(); + assert!(err.to_string().contains("wss:// engine_url")); + } + + #[test] + fn cron_must_be_six_fields() { + let cfg = AuthConfig { + rotation_cron: "0 0 3 * * * *".to_string(), + ..AuthConfig::default() + }; + let err = validate_config(&cfg).unwrap_err(); + assert!(err.to_string().contains("6-field cron")); + } + + #[test] + fn ttls_must_be_positive() { + let cfg = AuthConfig { + access_token_ttl_seconds: 0, + ..AuthConfig::default() + }; + let err = validate_config(&cfg).unwrap_err(); + assert!(err.to_string().contains("access_token_ttl_seconds")); + } + + #[test] + fn rotation_overlap_must_be_less_than_refresh_ttl() { + let cfg = AuthConfig { + refresh_token_ttl_seconds: 60, + rotation_overlap_seconds: 60, + ..AuthConfig::default() + }; + let err = validate_config(&cfg).unwrap_err(); + assert!(err + .to_string() + .contains("rotation_overlap_seconds must be less")); + } + + #[test] + fn default_scopes_must_be_supported() { + let cfg = AuthConfig { + default_scopes: vec!["function:demo::read".to_string()], + supported_scopes: vec!["mcp:tools".to_string()], + ..AuthConfig::default() + }; + let err = validate_config(&cfg).unwrap_err(); + assert!(err.to_string().contains("default scope")); + } + + #[test] + fn wildcard_supported_scopes_cover_default_scopes() { + let cfg = AuthConfig { + default_scopes: vec!["function:demo::read".to_string()], + supported_scopes: vec!["function:*".to_string()], + ..AuthConfig::default() + }; + validate_config(&cfg).unwrap(); + } +} diff --git a/auth/src/io.rs b/auth/src/io.rs new file mode 100644 index 00000000..5b8e7104 --- /dev/null +++ b/auth/src/io.rs @@ -0,0 +1,14 @@ +use iii_sdk::{IIIError, TriggerRequest}; +use serde_json::Value; + +#[async_trait::async_trait] +pub trait IIITrigger: Send + Sync + 'static { + async fn trigger(&self, request: TriggerRequest) -> Result; +} + +#[async_trait::async_trait] +impl IIITrigger for iii_sdk::III { + async fn trigger(&self, request: TriggerRequest) -> Result { + iii_sdk::III::trigger(self, request).await + } +} diff --git a/auth/src/lib.rs b/auth/src/lib.rs new file mode 100644 index 00000000..86e78a33 --- /dev/null +++ b/auth/src/lib.rs @@ -0,0 +1,1498 @@ +pub mod config; +pub mod io; +pub mod store; + +use std::collections::{BTreeSet, HashMap}; +use std::sync::Arc; + +use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; +use base64::Engine; +use chrono::Utc; +use jsonwebtoken::{ + decode, decode_header, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation, +}; +use rand::rngs::OsRng; +use rand::RngCore; +use rsa::pkcs8::{EncodePrivateKey, LineEnding}; +use rsa::traits::PublicKeyParts; +use rsa::{RsaPrivateKey, RsaPublicKey}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Value}; +use sha2::{Digest, Sha256}; +use uuid::Uuid; + +use crate::config::AuthConfig; +use crate::store::AuthStore; + +pub const SKILL_ID: &str = "auth"; +pub const SKILL_MD: &str = include_str!("../skills/index.md"); + +pub const SUB_SKILLS: &[(&str, &str)] = &[ + ("auth/validate", include_str!("../skills/validate.md")), + ( + "auth/server_metadata", + include_str!("../skills/server_metadata.md"), + ), + ( + "auth/resource_metadata", + include_str!("../skills/resource_metadata.md"), + ), + ("auth/register", include_str!("../skills/register.md")), + ("auth/jwks", include_str!("../skills/jwks.md")), + ("auth/jwks_rotate", include_str!("../skills/jwks_rotate.md")), + ("auth/token", include_str!("../skills/token.md")), + ("auth/introspect", include_str!("../skills/introspect.md")), + ("auth/revoke", include_str!("../skills/revoke.md")), +]; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ClientRecord { + pub client_id: String, + pub client_name: String, + pub client_secret_sha256: Option, + pub redirect_uris: Vec, + pub grant_types: Vec, + pub scopes: Vec, + pub token_endpoint_auth_method: String, + pub created_at: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PublicJwk { + pub kty: String, + #[serde(rename = "use")] + pub use_: String, + pub kid: String, + pub alg: String, + pub n: String, + pub e: String, +} + +impl PublicJwk { + fn to_json(&self) -> Value { + json!({ + "kty": self.kty, + "use": self.use_, + "kid": self.kid, + "alg": self.alg, + "n": self.n, + "e": self.e, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct KeyRecord { + pub kid: String, + pub private_pem: String, + pub public_jwk: PublicJwk, + pub created_at: i64, + pub retire_after: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct KeySet { + pub current_kid: String, + pub keys: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RefreshTokenRecord { + pub token_id: String, + pub client_id: String, + pub subject: String, + pub scopes: Vec, + pub expires_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Claims { + iss: String, + sub: String, + aud: String, + exp: u64, + iat: u64, + nbf: u64, + scope: String, + client_id: String, + jti: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthDecision { + #[serde(default)] + pub allowed_functions: Vec, + #[serde(default)] + pub forbidden_functions: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub allowed_trigger_types: Option>, + #[serde(default)] + pub allow_trigger_type_registration: bool, + #[serde(default)] + pub allow_function_registration: bool, + #[serde(default)] + pub trusted_internal: bool, + #[serde(default)] + pub context: Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub function_registration_prefix: Option, +} + +fn now() -> i64 { + Utc::now().timestamp() +} + +fn random_url_token(bytes: usize) -> String { + let mut buf = vec![0_u8; bytes]; + OsRng.fill_bytes(&mut buf); + URL_SAFE_NO_PAD.encode(buf) +} + +fn sha256_url(value: &str) -> String { + URL_SAFE_NO_PAD.encode(Sha256::digest(value.as_bytes())) +} + +fn timestamp_claim(value: i64) -> anyhow::Result { + u64::try_from(value).map_err(|_| anyhow::anyhow!("timestamp out of JWT claim range: {value}")) +} + +fn constant_time_eq(left: &str, right: &str) -> bool { + let left = left.as_bytes(); + let right = right.as_bytes(); + let max_len = left.len().max(right.len()); + let mut diff = left.len() ^ right.len(); + for i in 0..max_len { + let l = left.get(i).copied().unwrap_or(0); + let r = right.get(i).copied().unwrap_or(0); + diff |= usize::from(l ^ r); + } + diff == 0 +} + +fn split_scope(value: &str) -> Vec { + value + .split_whitespace() + .filter(|scope| !scope.trim().is_empty()) + .map(str::to_string) + .collect() +} + +fn normalize_body(payload: &Value) -> Value { + payload + .get("body") + .filter(|body| body.is_object()) + .cloned() + .unwrap_or_else(|| payload.clone()) +} + +fn string_field<'a>(value: &'a Value, key: &str) -> Option<&'a str> { + value + .get(key) + .and_then(Value::as_str) + .filter(|v| !v.is_empty()) +} + +fn string_array_field(value: &Value, key: &str) -> Vec { + value + .get(key) + .and_then(Value::as_array) + .map(|values| { + values + .iter() + .filter_map(Value::as_str) + .filter(|v| !v.is_empty()) + .map(str::to_string) + .collect() + }) + .unwrap_or_default() +} + +fn scope_field(value: &Value, key: &str) -> Vec { + if let Some(raw) = string_field(value, key) { + split_scope(raw) + } else { + string_array_field(value, key) + } +} + +fn endpoint(issuer: &str, path: &str) -> String { + format!("{}{}", issuer.trim_end_matches('/'), path) +} + +pub fn idp_capability_matrix() -> Vec { + vec![ + json!({"idp": "keycloak", "dynamic_client_registration": true, "authorization_server_metadata": true, "pkce": "required", "notes": "reference bridge target"}), + json!({"idp": "okta", "dynamic_client_registration": true, "authorization_server_metadata": true, "pkce": "required", "notes": "bridge config straightforward"}), + json!({"idp": "auth0", "dynamic_client_registration": true, "authorization_server_metadata": true, "pkce": "required", "notes": "bridge config straightforward"}), + json!({"idp": "entra", "dynamic_client_registration": false, "authorization_server_metadata": true, "pkce": "required", "notes": "pre-register clients"}), + json!({"idp": "google", "dynamic_client_registration": false, "authorization_server_metadata": true, "pkce": "required", "notes": "pre-register clients"}), + json!({"idp": "ping", "dynamic_client_registration": true, "authorization_server_metadata": true, "pkce": "required", "notes": "bridge config straightforward"}), + json!({"idp": "forgerock", "dynamic_client_registration": true, "authorization_server_metadata": true, "pkce": "required", "notes": "bridge config straightforward"}), + ] +} + +pub fn server_metadata_document(cfg: &AuthConfig) -> Value { + json!({ + "issuer": cfg.issuer, + "token_endpoint": endpoint(&cfg.issuer, "/token"), + "registration_endpoint": endpoint(&cfg.issuer, "/register"), + "introspection_endpoint": endpoint(&cfg.issuer, "/introspect"), + "revocation_endpoint": endpoint(&cfg.issuer, "/revoke"), + "jwks_uri": endpoint(&cfg.issuer, "/.well-known/jwks.json"), + "grant_types_supported": ["client_credentials", "refresh_token"], + "token_endpoint_auth_methods_supported": cfg.token_endpoint_auth_methods_supported, + "scopes_supported": cfg.supported_scopes, + "idp_mode": cfg.idp_mode, + "idp_capabilities": idp_capability_matrix(), + }) +} + +pub fn resource_metadata_document(cfg: &AuthConfig) -> Value { + json!({ + "resource": cfg.audience, + "authorization_servers": [cfg.issuer], + "jwks_uri": endpoint(&cfg.issuer, "/.well-known/jwks.json"), + "scopes_supported": cfg.supported_scopes, + "bearer_methods_supported": ["header"], + }) +} + +fn http_json(body: Value) -> Value { + json!({ + "status_code": 200, + "headers": { "content-type": "application/json" }, + "body": body, + }) +} + +pub fn server_metadata_response(cfg: &AuthConfig) -> Value { + http_json(server_metadata_document(cfg)) +} + +pub fn resource_metadata_response(cfg: &AuthConfig) -> Value { + http_json(resource_metadata_document(cfg)) +} + +fn is_privileged_scope(scope: &str) -> bool { + scope.starts_with("function:") || scope.starts_with("trigger:") || scope.starts_with("iii:") +} + +fn scope_supported(scope: &str, cfg: &AuthConfig) -> bool { + cfg.supported_scopes.iter().any(|supported| { + supported == scope + || (supported == "function:*" && scope.starts_with("function:")) + || (supported == "trigger:*" && scope.starts_with("trigger:")) + }) +} + +fn scope_allowed_by_client(scope: &str, client: &ClientRecord) -> bool { + client.scopes.iter().any(|allowed| { + allowed == scope + || (allowed == "function:*" && scope.starts_with("function:") && scope != "function:*") + || (allowed == "trigger:*" && scope.starts_with("trigger:") && scope != "trigger:*") + }) +} + +fn requested_or_default(requested: &[String], cfg: &AuthConfig) -> Vec { + if requested.is_empty() { + cfg.default_scopes.clone() + } else { + requested.to_vec() + } +} + +fn validate_registration_scopes( + requested: &[String], + cfg: &AuthConfig, + admin_authorized: bool, +) -> anyhow::Result> { + let scopes = requested_or_default(requested, cfg); + let mut out = Vec::new(); + let mut seen = BTreeSet::new(); + for scope in scopes { + if !scope_supported(&scope, cfg) { + anyhow::bail!("unsupported scope: {scope}"); + } + if is_privileged_scope(&scope) && !admin_authorized { + anyhow::bail!("admin authorization required for privileged scope: {scope}"); + } + if seen.insert(scope.clone()) { + out.push(scope); + } + } + Ok(out) +} + +fn validate_token_scopes( + requested: &[String], + client: &ClientRecord, + cfg: &AuthConfig, +) -> anyhow::Result> { + let scopes = requested_or_default(requested, cfg); + let mut out = Vec::new(); + let mut seen = BTreeSet::new(); + for scope in scopes { + if !scope_supported(&scope, cfg) { + anyhow::bail!("unsupported scope: {scope}"); + } + if scope == "function:*" || scope == "trigger:*" { + anyhow::bail!("wildcard scopes can be registered but cannot be issued: {scope}"); + } + if !scope_allowed_by_client(&scope, client) { + anyhow::bail!("scope not allowed for client: {scope}"); + } + if seen.insert(scope.clone()) { + out.push(scope); + } + } + Ok(out) +} + +fn supported_grant_type(value: &str) -> bool { + matches!(value, "client_credentials" | "refresh_token") +} + +fn requested_grant_types(body: &Value) -> anyhow::Result> { + let values = string_array_field(body, "grant_types"); + let grants = if values.is_empty() { + vec![ + "client_credentials".to_string(), + "refresh_token".to_string(), + ] + } else { + values + }; + for grant in &grants { + if !supported_grant_type(grant) { + anyhow::bail!("unsupported grant_type for local auth worker: {grant}"); + } + } + Ok(grants) +} + +fn requested_auth_method(body: &Value, cfg: &AuthConfig) -> anyhow::Result { + let method = string_field(body, "token_endpoint_auth_method") + .unwrap_or("client_secret_post") + .to_string(); + if !cfg + .token_endpoint_auth_methods_supported + .iter() + .any(|supported| supported == &method) + { + anyhow::bail!("unsupported token_endpoint_auth_method: {method}"); + } + Ok(method) +} + +fn admin_registration_token(cfg: &AuthConfig) -> Option { + if cfg.registration_admin_token_env.is_empty() { + return None; + } + std::env::var(&cfg.registration_admin_token_env) + .ok() + .filter(|token| !token.is_empty()) +} + +fn registration_admin_authorized(payload: &Value, body: &Value, cfg: &AuthConfig) -> bool { + let Some(expected) = admin_registration_token(cfg) else { + return false; + }; + let bearer = auth_header_from_payload(payload, body) + .and_then(|raw| strip_auth_scheme(&raw, "Bearer").map(str::to_string)); + let body_token = string_field(body, "admin_token").map(str::to_string); + [bearer, body_token] + .into_iter() + .flatten() + .any(|token| constant_time_eq(&token, &expected)) +} + +fn generate_key_record() -> anyhow::Result { + let mut rng = OsRng; + let private = RsaPrivateKey::new(&mut rng, 2048)?; + let public = RsaPublicKey::from(&private); + let kid = Uuid::new_v4().to_string(); + let private_pem = private.to_pkcs8_pem(LineEnding::LF)?.to_string(); + Ok(KeyRecord { + kid: kid.clone(), + private_pem, + public_jwk: PublicJwk { + kty: "RSA".to_string(), + use_: "sig".to_string(), + kid, + alg: "RS256".to_string(), + n: URL_SAFE_NO_PAD.encode(public.n().to_bytes_be()), + e: URL_SAFE_NO_PAD.encode(public.e().to_bytes_be()), + }, + created_at: now(), + retire_after: None, + }) +} + +async fn ensure_keyset(store: &dyn AuthStore) -> anyhow::Result { + if let Some(keyset) = store.get_keyset().await? { + return Ok(keyset); + } + let key = generate_key_record()?; + let keyset = KeySet { + current_kid: key.kid.clone(), + keys: vec![key], + }; + store.create_keyset_if_absent(keyset).await +} + +pub async fn rotate_jwks(store: &dyn AuthStore, cfg: &AuthConfig) -> anyhow::Result { + let current_time = now(); + let new_key = generate_key_record()?; + let keyset = store + .rotate_keyset(new_key, current_time, cfg.rotation_overlap_seconds) + .await?; + Ok(json!({ + "ok": true, + "current_kid": keyset.current_kid, + "active_keys": keyset.keys.len(), + })) +} + +pub async fn jwks_document(store: &dyn AuthStore) -> anyhow::Result { + let keyset = ensure_keyset(store).await?; + let current_time = now(); + let keys: Vec = keyset + .keys + .iter() + .filter(|key| key.retire_after.is_none_or(|retire| retire > current_time)) + .map(|key| key.public_jwk.to_json()) + .collect(); + Ok(json!({ "keys": keys })) +} + +pub async fn register_client( + store: &dyn AuthStore, + cfg: &AuthConfig, + payload: Value, +) -> anyhow::Result { + let body = normalize_body(&payload); + let client_id = random_url_token(18); + let client_secret = random_url_token(32); + let method = requested_auth_method(&body, cfg)?; + let requested_scopes = scope_field(&body, "scope"); + let admin_authorized = registration_admin_authorized(&payload, &body, cfg); + let scopes = validate_registration_scopes(&requested_scopes, cfg, admin_authorized)?; + let grant_types = requested_grant_types(&body)?; + if method == "none" + && grant_types + .iter() + .any(|grant| grant == "client_credentials" || grant == "refresh_token") + { + anyhow::bail!("token_endpoint_auth_method none cannot use local client_credentials or refresh_token grants"); + } + let redirect_uris = string_array_field(&body, "redirect_uris"); + let client_name = string_field(&body, "client_name") + .unwrap_or("iii client") + .to_string(); + let response_client_name = client_name.clone(); + let stored_secret = if method == "none" { + None + } else { + Some(sha256_url(&client_secret)) + }; + let record = ClientRecord { + client_id: client_id.clone(), + client_name, + client_secret_sha256: stored_secret, + redirect_uris: redirect_uris.clone(), + grant_types: grant_types.clone(), + scopes: scopes.clone(), + token_endpoint_auth_method: method.clone(), + created_at: now(), + }; + store.set_client(record).await?; + let mut out = json!({ + "client_id": client_id, + "client_id_issued_at": now(), + "client_name": response_client_name, + "redirect_uris": redirect_uris, + "grant_types": grant_types, + "scope": scopes.join(" "), + "token_endpoint_auth_method": method, + }); + if method != "none" { + out.as_object_mut() + .expect("registration response is object") + .insert("client_secret".to_string(), Value::String(client_secret)); + } + Ok(out) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CredentialSource { + Basic, + Post, + None, +} + +fn client_secret_matches( + client: &ClientRecord, + secret: Option<&str>, + source: CredentialSource, +) -> bool { + match (&client.client_secret_sha256, secret) { + (None, None) => client.token_endpoint_auth_method == "none", + (None, Some(_)) => false, + (Some(expected), Some(actual)) => { + let source_allowed = match client.token_endpoint_auth_method.as_str() { + "client_secret_basic" => source == CredentialSource::Basic, + "client_secret_post" => source == CredentialSource::Post, + _ => false, + }; + source_allowed && constant_time_eq(&sha256_url(actual), expected) + } + _ => false, + } +} + +fn issue_access_token( + cfg: &AuthConfig, + keyset: &KeySet, + client: &ClientRecord, + subject: &str, + scopes: &[String], +) -> anyhow::Result<(String, Claims)> { + let current = keyset + .keys + .iter() + .find(|key| key.kid == keyset.current_kid) + .ok_or_else(|| anyhow::anyhow!("current signing key missing"))?; + let issued_at = now(); + let expires_at = issued_at + cfg.access_token_ttl_seconds; + let claims = Claims { + iss: cfg.issuer.clone(), + sub: subject.to_string(), + aud: cfg.audience.clone(), + exp: timestamp_claim(expires_at)?, + iat: timestamp_claim(issued_at)?, + nbf: timestamp_claim(issued_at)?, + scope: scopes.join(" "), + client_id: client.client_id.clone(), + jti: Uuid::new_v4().to_string(), + }; + let mut header = Header::new(Algorithm::RS256); + header.kid = Some(current.kid.clone()); + let token = encode( + &header, + &claims, + &EncodingKey::from_rsa_pem(current.private_pem.as_bytes())?, + )?; + Ok((token, claims)) +} + +fn decode_token(cfg: &AuthConfig, keyset: &KeySet, token: &str) -> anyhow::Result { + let header = decode_header(token)?; + let kid = header + .kid + .ok_or_else(|| anyhow::anyhow!("token missing kid"))?; + let key = keyset + .keys + .iter() + .find(|key| key.kid == kid) + .ok_or_else(|| anyhow::anyhow!("unknown kid"))?; + let mut validation = Validation::new(Algorithm::RS256); + validation.set_audience(std::slice::from_ref(&cfg.audience)); + validation.set_issuer(std::slice::from_ref(&cfg.issuer)); + let decoded = decode::( + token, + &DecodingKey::from_rsa_components(&key.public_jwk.n, &key.public_jwk.e)?, + &validation, + )?; + Ok(decoded.claims) +} + +fn auth_header(headers: &Map) -> Option { + headers + .iter() + .find(|(key, _)| key.eq_ignore_ascii_case("authorization")) + .and_then(|(_, value)| value.as_str()) + .map(str::to_string) +} + +fn auth_header_from_payload(payload: &Value, body: &Value) -> Option { + payload + .get("headers") + .and_then(Value::as_object) + .and_then(auth_header) + .or_else(|| { + body.get("headers") + .and_then(Value::as_object) + .and_then(auth_header) + }) +} + +fn strip_auth_scheme<'a>(value: &'a str, scheme: &str) -> Option<&'a str> { + let (actual, rest) = value.split_once(' ')?; + actual.eq_ignore_ascii_case(scheme).then_some(rest) +} + +fn basic_credentials(value: &str) -> Option<(String, String)> { + let encoded = strip_auth_scheme(value, "Basic")?; + let decoded = STANDARD.decode(encoded).ok()?; + let decoded = String::from_utf8(decoded).ok()?; + let (client_id, secret) = decoded.split_once(':')?; + Some((client_id.to_string(), secret.to_string())) +} + +fn bearer_token_from_payload(payload: &Value) -> Option { + if let Some(token) = string_field(payload, "token") { + return Some(token.to_string()); + } + let headers = payload.get("headers").and_then(Value::as_object)?; + let raw = auth_header(headers)?; + strip_auth_scheme(&raw, "Bearer").map(str::to_string) +} + +fn client_credentials( + payload: &Value, + body: &Value, +) -> (Option, Option, CredentialSource) { + if let Some(raw) = auth_header_from_payload(payload, body) { + if let Some((client_id, secret)) = basic_credentials(&raw) { + return (Some(client_id), Some(secret), CredentialSource::Basic); + } + } + ( + string_field(body, "client_id").map(str::to_string), + string_field(body, "client_secret").map(str::to_string), + if string_field(body, "client_secret").is_some() { + CredentialSource::Post + } else { + CredentialSource::None + }, + ) +} + +async fn issue_for_client_credentials( + store: &dyn AuthStore, + cfg: &AuthConfig, + payload: &Value, + body: &Value, +) -> anyhow::Result { + store + .cleanup_expired_tokens(now(), cfg.refresh_token_ttl_seconds) + .await?; + let (client_id, secret, source) = client_credentials(payload, body); + let client_id = client_id.ok_or_else(|| anyhow::anyhow!("missing client_id"))?; + let client = store + .get_client(&client_id) + .await? + .ok_or_else(|| anyhow::anyhow!("unknown client_id"))?; + if !client + .grant_types + .iter() + .any(|grant| grant == "client_credentials") + { + anyhow::bail!("client_credentials grant not allowed for client"); + } + if !client_secret_matches(&client, secret.as_deref(), source) { + anyhow::bail!("invalid client_secret"); + } + let requested = scope_field(body, "scope"); + let scopes = validate_token_scopes(&requested, &client, cfg)?; + let keyset = ensure_keyset(store).await?; + let (access_token, claims) = + issue_access_token(cfg, &keyset, &client, &client.client_id, &scopes)?; + let refresh_token = random_url_token(32); + let refresh = RefreshTokenRecord { + token_id: refresh_token.clone(), + client_id: client.client_id.clone(), + subject: claims.sub, + scopes: scopes.clone(), + expires_at: now() + cfg.refresh_token_ttl_seconds, + }; + store.set_refresh_token(refresh).await?; + Ok(json!({ + "access_token": access_token, + "token_type": "Bearer", + "expires_in": cfg.access_token_ttl_seconds, + "refresh_token": refresh_token, + "scope": scopes.join(" "), + })) +} + +async fn issue_for_refresh_token( + store: &dyn AuthStore, + cfg: &AuthConfig, + payload: &Value, + body: &Value, +) -> anyhow::Result { + store + .cleanup_expired_tokens(now(), cfg.refresh_token_ttl_seconds) + .await?; + let refresh_token = string_field(body, "refresh_token") + .ok_or_else(|| anyhow::anyhow!("missing refresh_token"))?; + let (client_id, secret, source) = client_credentials(payload, body); + let client_id = client_id.ok_or_else(|| anyhow::anyhow!("missing client_id"))?; + let refresh = store + .get_refresh_token(refresh_token) + .await? + .ok_or_else(|| anyhow::anyhow!("unknown refresh_token"))?; + if refresh.expires_at <= now() || store.is_revoked(refresh_token).await? { + anyhow::bail!("refresh_token expired or revoked"); + } + if refresh.client_id != client_id { + anyhow::bail!("refresh_token client mismatch"); + } + let client = store + .get_client(&refresh.client_id) + .await? + .ok_or_else(|| anyhow::anyhow!("refresh token client missing"))?; + if !client_secret_matches(&client, secret.as_deref(), source) { + anyhow::bail!("invalid client_secret"); + } + let keyset = ensure_keyset(store).await?; + let (access_token, _) = + issue_access_token(cfg, &keyset, &client, &refresh.subject, &refresh.scopes)?; + let new_refresh_token = random_url_token(32); + let new_refresh_record = RefreshTokenRecord { + token_id: new_refresh_token.clone(), + client_id: client.client_id.clone(), + subject: refresh.subject, + scopes: refresh.scopes.clone(), + expires_at: now() + cfg.refresh_token_ttl_seconds, + }; + store + .rotate_refresh_token(refresh_token, new_refresh_record) + .await?; + Ok(json!({ + "access_token": access_token, + "token_type": "Bearer", + "expires_in": cfg.access_token_ttl_seconds, + "refresh_token": new_refresh_token, + "scope": refresh.scopes.join(" "), + })) +} + +pub async fn introspect_token( + store: &dyn AuthStore, + cfg: &AuthConfig, + token: &str, +) -> anyhow::Result { + let keyset = ensure_keyset(store).await?; + let claims = match decode_token(cfg, &keyset, token) { + Ok(claims) => claims, + Err(_) => return Ok(json!({ "active": false })), + }; + if store.is_revoked(&claims.jti).await? { + return Ok(json!({ "active": false })); + } + Ok(json!({ + "active": true, + "client_id": claims.client_id, + "sub": claims.sub, + "aud": claims.aud, + "iss": claims.iss, + "exp": claims.exp, + "iat": claims.iat, + "scope": claims.scope, + "jti": claims.jti, + })) +} + +pub async fn token_endpoint( + store: &dyn AuthStore, + cfg: &AuthConfig, + payload: Value, +) -> anyhow::Result { + let body = normalize_body(&payload); + let action = string_field(&body, "action"); + let grant_type = string_field(&body, "grant_type"); + if action == Some("introspect") || grant_type == Some("introspection") { + return introspect_endpoint(store, cfg, payload).await; + } + match grant_type.unwrap_or("client_credentials") { + "client_credentials" => issue_for_client_credentials(store, cfg, &payload, &body).await, + "refresh_token" => issue_for_refresh_token(store, cfg, &payload, &body).await, + other => anyhow::bail!("unsupported grant_type: {other}"), + } +} + +async fn authenticated_client( + store: &dyn AuthStore, + payload: &Value, + body: &Value, +) -> anyhow::Result { + let (client_id, secret, source) = client_credentials(payload, body); + let client_id = client_id.ok_or_else(|| anyhow::anyhow!("missing client_id"))?; + let client = store + .get_client(&client_id) + .await? + .ok_or_else(|| anyhow::anyhow!("unknown client_id"))?; + if !client_secret_matches(&client, secret.as_deref(), source) { + anyhow::bail!("invalid client_secret"); + } + Ok(client) +} + +pub async fn introspect_endpoint( + store: &dyn AuthStore, + cfg: &AuthConfig, + payload: Value, +) -> anyhow::Result { + let body = normalize_body(&payload); + let _client = authenticated_client(store, &payload, &body).await?; + let token = string_field(&body, "token").ok_or_else(|| anyhow::anyhow!("missing token"))?; + introspect_token(store, cfg, token).await +} + +pub async fn revoke_endpoint( + store: &dyn AuthStore, + cfg: &AuthConfig, + payload: Value, +) -> anyhow::Result { + let body = normalize_body(&payload); + let client = authenticated_client(store, &payload, &body).await?; + let token = string_field(&body, "token").ok_or_else(|| anyhow::anyhow!("missing token"))?; + let hint = string_field(&body, "token_type_hint"); + let refresh = store.get_refresh_token(token).await?; + + if hint == Some("refresh_token") || refresh.is_some() { + if let Some(refresh) = refresh { + if refresh.client_id == client.client_id { + store.revoke(token).await?; + } + } + return Ok(json!({ "ok": true })); + } + + let keyset = ensure_keyset(store).await?; + if let Ok(claims) = decode_token(cfg, &keyset, token) { + if claims.client_id == client.client_id { + store.revoke(&claims.jti).await?; + } + } + Ok(json!({ "ok": true })) +} + +fn scopes_to_decision(claims: &Claims) -> AuthDecision { + let scopes = split_scope(&claims.scope); + let allowed_functions: Vec = scopes + .iter() + .filter_map(|scope| scope.strip_prefix("function:").map(str::to_string)) + .filter(|function_id| function_id != "*") + .collect(); + let trigger_types: Vec = scopes + .iter() + .filter_map(|scope| scope.strip_prefix("trigger:").map(str::to_string)) + .collect(); + AuthDecision { + allowed_functions, + forbidden_functions: vec![], + allowed_trigger_types: if trigger_types.is_empty() { + None + } else { + Some(trigger_types) + }, + allow_trigger_type_registration: scopes + .iter() + .any(|scope| scope == "iii:trigger_type_registration"), + allow_function_registration: scopes + .iter() + .any(|scope| scope == "iii:function_registration"), + trusted_internal: scopes.iter().any(|scope| scope == "iii:trusted_internal"), + context: json!({ + "client_id": claims.client_id, + "subject": claims.sub, + "scopes": scopes, + "token_id": claims.jti, + }), + function_registration_prefix: None, + } +} + +pub async fn validate_session( + store: &dyn AuthStore, + cfg: &AuthConfig, + payload: Value, +) -> anyhow::Result { + let token = bearer_token_from_payload(&payload) + .ok_or_else(|| anyhow::anyhow!("missing bearer token"))?; + let keyset = ensure_keyset(store).await?; + let claims = decode_token(cfg, &keyset, &token)?; + if store.is_revoked(&claims.jti).await? { + anyhow::bail!("token revoked"); + } + Ok(scopes_to_decision(&claims)) +} + +pub async fn register_with_iii( + iii: &iii_sdk::III, + store: Arc, + cfg: Arc, +) -> anyhow::Result { + use iii_sdk::{IIIError, RegisterFunctionMessage}; + + let validate_store = store.clone(); + let validate_cfg = cfg.clone(); + let validate = iii.register_function(( + RegisterFunctionMessage::with_id("auth::validate".to_string()).with_description( + "Validate a Bearer token and return an iii RBAC session decision.".into(), + ), + move |payload: Value| { + let store = validate_store.clone(); + let cfg = validate_cfg.clone(); + async move { + validate_session(&*store, &cfg, payload) + .await + .and_then(|decision| serde_json::to_value(decision).map_err(Into::into)) + .map_err(|e: anyhow::Error| IIIError::Handler(e.to_string())) + } + }, + )); + + let server_cfg = cfg.clone(); + let server_metadata = iii.register_function(( + RegisterFunctionMessage::with_id("auth::server_metadata".to_string()) + .with_description("Return RFC 8414 authorization server metadata.".into()), + move |_payload: Value| { + let cfg = server_cfg.clone(); + async move { Ok(server_metadata_response(&cfg)) } + }, + )); + + let resource_cfg = cfg.clone(); + let resource_metadata = iii.register_function(( + RegisterFunctionMessage::with_id("auth::resource_metadata".to_string()) + .with_description("Return RFC 9728 protected resource metadata.".into()), + move |_payload: Value| { + let cfg = resource_cfg.clone(); + async move { Ok(resource_metadata_response(&cfg)) } + }, + )); + + let register_store = store.clone(); + let register_cfg = cfg.clone(); + let register = iii.register_function(( + RegisterFunctionMessage::with_id("auth::register".to_string()) + .with_description("Register an OAuth client at runtime.".into()), + move |payload: Value| { + let store = register_store.clone(); + let cfg = register_cfg.clone(); + async move { + register_client(&*store, &cfg, payload) + .await + .map(http_json) + .map_err(|e| IIIError::Handler(e.to_string())) + } + }, + )); + + let jwks_store = store.clone(); + let jwks = iii.register_function(( + RegisterFunctionMessage::with_id("auth::jwks".to_string()) + .with_description("Return the active public JSON Web Key Set.".into()), + move |_payload: Value| { + let store = jwks_store.clone(); + async move { + jwks_document(&*store) + .await + .map(http_json) + .map_err(|e| IIIError::Handler(e.to_string())) + } + }, + )); + + let rotate_store = store.clone(); + let rotate_cfg = cfg.clone(); + let jwks_rotate = iii.register_function(( + RegisterFunctionMessage::with_id("auth::jwks_rotate".to_string()) + .with_description("Rotate the active local signing key.".into()), + move |_payload: Value| { + let store = rotate_store.clone(); + let cfg = rotate_cfg.clone(); + async move { + rotate_jwks(&*store, &cfg) + .await + .map_err(|e| IIIError::Handler(e.to_string())) + } + }, + )); + + let token_store = store.clone(); + let token_cfg = cfg.clone(); + let token = iii.register_function(( + RegisterFunctionMessage::with_id("auth::token".to_string()) + .with_description("Issue or refresh OAuth tokens.".into()), + move |payload: Value| { + let store = token_store.clone(); + let cfg = token_cfg.clone(); + async move { + token_endpoint(&*store, &cfg, payload) + .await + .map(http_json) + .map_err(|e| IIIError::Handler(e.to_string())) + } + }, + )); + + let introspect_store = store.clone(); + let introspect_cfg = cfg.clone(); + let introspect = iii.register_function(( + RegisterFunctionMessage::with_id("auth::introspect".to_string()) + .with_description("Introspect an OAuth token for an authenticated client.".into()), + move |payload: Value| { + let store = introspect_store.clone(); + let cfg = introspect_cfg.clone(); + async move { + introspect_endpoint(&*store, &cfg, payload) + .await + .map(http_json) + .map_err(|e| IIIError::Handler(e.to_string())) + } + }, + )); + + let revoke_store = store; + let revoke_cfg = cfg; + let revoke = iii.register_function(( + RegisterFunctionMessage::with_id("auth::revoke".to_string()) + .with_description("Revoke an access token or refresh token.".into()), + move |payload: Value| { + let store = revoke_store.clone(); + let cfg = revoke_cfg.clone(); + async move { + revoke_endpoint(&*store, &cfg, payload) + .await + .map(http_json) + .map_err(|e| IIIError::Handler(e.to_string())) + } + }, + )); + + Ok(AuthFunctionRefs { + validate, + server_metadata, + resource_metadata, + register, + jwks, + jwks_rotate, + token, + introspect, + revoke, + }) +} + +pub struct AuthFunctionRefs { + pub validate: iii_sdk::FunctionRef, + pub server_metadata: iii_sdk::FunctionRef, + pub resource_metadata: iii_sdk::FunctionRef, + pub register: iii_sdk::FunctionRef, + pub jwks: iii_sdk::FunctionRef, + pub jwks_rotate: iii_sdk::FunctionRef, + pub token: iii_sdk::FunctionRef, + pub introspect: iii_sdk::FunctionRef, + pub revoke: iii_sdk::FunctionRef, +} + +impl AuthFunctionRefs { + pub fn unregister_all(self) { + for reference in [ + self.validate, + self.server_metadata, + self.resource_metadata, + self.register, + self.jwks, + self.jwks_rotate, + self.token, + self.introspect, + self.revoke, + ] { + reference.unregister(); + } + } +} + +pub fn extract_response_body(value: &Value) -> Option<&Value> { + value.get("body").or(Some(value)) +} + +pub fn public_token_payload(value: &Value) -> HashMap { + value + .as_object() + .map(|object| object.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::store::InMemoryAuthStore; + + fn cfg() -> AuthConfig { + AuthConfig { + issuer: "https://auth.test".to_string(), + audience: "iii-test".to_string(), + store: crate::config::StoreBackend::Memory, + supported_scopes: vec![ + "mcp:tools".to_string(), + "a2a:message".to_string(), + "function:demo::read".to_string(), + "function:*".to_string(), + "trigger:http".to_string(), + "iii:function_registration".to_string(), + "iii:trusted_internal".to_string(), + ], + default_scopes: vec!["mcp:tools".to_string()], + registration_admin_token_env: "III_AUTH_TEST_ADMIN_TOKEN".to_string(), + ..AuthConfig::default() + } + } + + fn cfg_with_admin_env() -> AuthConfig { + let env_name = format!("III_AUTH_TEST_ADMIN_TOKEN_{}", Uuid::new_v4().simple()); + std::env::set_var(&env_name, "admin-secret"); + AuthConfig { + registration_admin_token_env: env_name, + ..cfg() + } + } + + #[tokio::test] + async fn client_credentials_roundtrip_validates_session() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg_with_admin_env(); + let registration = register_client( + &store, + &cfg, + json!({ + "headers": { "authorization": "bearer admin-secret" }, + "client_name": "test", + "scope": "function:demo::read iii:function_registration iii:trusted_internal" + }), + ) + .await?; + let client_id = registration["client_id"].as_str().unwrap(); + let client_secret = registration["client_secret"].as_str().unwrap(); + let token = token_endpoint( + &store, + &cfg, + json!({ + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + "scope": "function:demo::read iii:function_registration iii:trusted_internal" + }), + ) + .await?; + let access_token = token["access_token"].as_str().unwrap(); + let decision = validate_session( + &store, + &cfg, + json!({ "headers": { "authorization": format!("beAREr {access_token}") } }), + ) + .await?; + assert_eq!(decision.allowed_functions, vec!["demo::read"]); + assert!(decision.allow_function_registration); + assert!(decision.trusted_internal); + assert_eq!(decision.context["client_id"], client_id); + Ok(()) + } + + #[tokio::test] + async fn public_registration_rejects_privileged_scopes() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg_with_admin_env(); + let err = register_client( + &store, + &cfg, + json!({ + "client_name": "test", + "scope": "function:demo::read iii:trusted_internal" + }), + ) + .await + .unwrap_err(); + assert!(err + .to_string() + .contains("admin authorization required for privileged scope")); + Ok(()) + } + + #[tokio::test] + async fn public_registration_allows_public_scopes() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg(); + let registration = register_client( + &store, + &cfg, + json!({ + "client_name": "test", + "scope": "mcp:tools a2a:message" + }), + ) + .await?; + assert_eq!(registration["scope"], "mcp:tools a2a:message"); + Ok(()) + } + + #[tokio::test] + async fn client_cannot_escalate_scopes_at_token_time() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg_with_admin_env(); + let registration = register_client( + &store, + &cfg, + json!({ + "headers": { "authorization": "Bearer admin-secret" }, + "client_name": "test", + "scope": "function:demo::read" + }), + ) + .await?; + let err = token_endpoint( + &store, + &cfg, + json!({ + "grant_type": "client_credentials", + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + "scope": "iii:trusted_internal" + }), + ) + .await + .unwrap_err(); + assert!(err.to_string().contains("scope not allowed for client")); + Ok(()) + } + + #[tokio::test] + async fn client_secret_basic_requires_basic_auth_header() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg(); + let registration = register_client( + &store, + &cfg, + json!({ + "client_name": "basic-client", + "token_endpoint_auth_method": "client_secret_basic" + }), + ) + .await?; + let client_id = registration["client_id"].as_str().unwrap(); + let client_secret = registration["client_secret"].as_str().unwrap(); + let post_err = token_endpoint( + &store, + &cfg, + json!({ + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret + }), + ) + .await + .unwrap_err(); + assert!(post_err.to_string().contains("invalid client_secret")); + let encoded = STANDARD.encode(format!("{client_id}:{client_secret}")); + let token = token_endpoint( + &store, + &cfg, + json!({ + "headers": { "authorization": format!("bAsIc {encoded}") }, + "grant_type": "client_credentials" + }), + ) + .await?; + assert_eq!(token["token_type"], "Bearer"); + Ok(()) + } + + #[tokio::test] + async fn wildcard_scope_is_not_issued_as_token_scope() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg_with_admin_env(); + let registration = register_client( + &store, + &cfg, + json!({ + "headers": { "authorization": "Bearer admin-secret" }, + "client_name": "wildcard-client", + "scope": "function:*" + }), + ) + .await?; + let err = token_endpoint( + &store, + &cfg, + json!({ + "grant_type": "client_credentials", + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + "scope": "function:*" + }), + ) + .await + .unwrap_err(); + assert!(err + .to_string() + .contains("wildcard scopes can be registered")); + Ok(()) + } + + #[tokio::test] + async fn refresh_token_issues_new_access_token() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg(); + let registration = register_client(&store, &cfg, json!({ "client_name": "test" })).await?; + let token = token_endpoint( + &store, + &cfg, + json!({ + "grant_type": "client_credentials", + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + }), + ) + .await?; + let refreshed = token_endpoint( + &store, + &cfg, + json!({ + "grant_type": "refresh_token", + "refresh_token": token["refresh_token"], + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + }), + ) + .await?; + assert_eq!(refreshed["token_type"], "Bearer"); + assert!(refreshed["access_token"].as_str().unwrap().len() > 100); + assert!(refreshed["refresh_token"].as_str().unwrap().len() > 20); + let old_refresh = token_endpoint( + &store, + &cfg, + json!({ + "grant_type": "refresh_token", + "refresh_token": token["refresh_token"], + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + }), + ) + .await + .unwrap_err(); + assert!(old_refresh.to_string().contains("expired or revoked")); + Ok(()) + } + + #[tokio::test] + async fn revoke_invalidates_access_token() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg(); + let registration = register_client(&store, &cfg, json!({ "client_name": "test" })).await?; + let token = token_endpoint( + &store, + &cfg, + json!({ + "grant_type": "client_credentials", + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + }), + ) + .await?; + let access_token = token["access_token"].as_str().unwrap(); + let active = introspect_endpoint( + &store, + &cfg, + json!({ + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + "token": access_token + }), + ) + .await?; + assert_eq!(active["active"], true); + revoke_endpoint( + &store, + &cfg, + json!({ + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + "token": access_token, + "token_type_hint": "access_token" + }), + ) + .await?; + let rejected = validate_session( + &store, + &cfg, + json!({ "headers": { "authorization": format!("Bearer {access_token}") } }), + ) + .await + .unwrap_err(); + assert!(rejected.to_string().contains("token revoked")); + let inactive = introspect_endpoint( + &store, + &cfg, + json!({ + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + "token": access_token + }), + ) + .await?; + assert_eq!(inactive["active"], false); + Ok(()) + } + + #[tokio::test] + async fn public_client_method_none_cannot_use_local_grants() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let mut cfg = cfg(); + cfg.token_endpoint_auth_methods_supported + .push("none".to_string()); + let err = register_client( + &store, + &cfg, + json!({ + "client_name": "public", + "token_endpoint_auth_method": "none", + "grant_types": ["client_credentials"] + }), + ) + .await + .unwrap_err(); + assert!(err + .to_string() + .contains("cannot use local client_credentials")); + Ok(()) + } + + #[tokio::test] + async fn jwks_rotate_keeps_previous_key() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg(); + let first = jwks_document(&store).await?; + assert_eq!(first["keys"].as_array().unwrap().len(), 1); + rotate_jwks(&store, &cfg).await?; + let second = jwks_document(&store).await?; + assert_eq!(second["keys"].as_array().unwrap().len(), 2); + Ok(()) + } + + #[test] + fn metadata_contains_required_endpoints() { + let cfg = cfg(); + let metadata = server_metadata_document(&cfg); + assert_eq!(metadata["issuer"], "https://auth.test"); + assert_eq!( + metadata["registration_endpoint"], + "https://auth.test/register" + ); + assert_eq!( + metadata["jwks_uri"], + "https://auth.test/.well-known/jwks.json" + ); + assert_eq!(metadata["revocation_endpoint"], "https://auth.test/revoke"); + assert_eq!( + metadata["introspection_endpoint"], + "https://auth.test/introspect" + ); + assert!(metadata.get("authorization_endpoint").is_none()); + assert!(metadata.get("code_challenge_methods_supported").is_none()); + } +} diff --git a/auth/src/main.rs b/auth/src/main.rs new file mode 100644 index 00000000..4c822082 --- /dev/null +++ b/auth/src/main.rs @@ -0,0 +1,256 @@ +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result}; +use clap::Parser; +use iii_sdk::{ + register_worker, InitOptions, OtelConfig, RegisterTriggerInput, TriggerRequest, WorkerMetadata, +}; +use serde_json::json; +use tokio::task::JoinHandle; +use tracing_subscriber::EnvFilter; + +mod manifest; + +use iii_auth::config::{resolve_store_backend, validate_config, AuthConfig, StoreBackend}; +use iii_auth::store::{IiiStateAuthStore, InMemoryAuthStore}; + +#[derive(Parser, Debug)] +#[command( + name = "iii-auth", + about = "OAuth authority worker for iii RBAC, discovery, DCR, JWKS, and token validation." +)] +struct Cli { + #[arg(long, env = "III_AUTH_CONFIG", default_value = "./config.yaml")] + config: String, + + #[arg(long, env = "III_URL")] + url: Option, + + #[arg(long)] + issuer: Option, + + #[arg(long)] + idp_mode: Option, + + #[arg(long)] + rotation_cron: Option, + + #[arg(long)] + manifest: bool, +} + +const CONNECTION_READY_SETTLE: Duration = Duration::from_millis(50); +const SKILL_REGISTER_TIMEOUT: Duration = Duration::new(3 * 60, 0); +const SKILL_REGISTER_MAX_BACKOFF: Duration = Duration::new(60, 0); +const SKILL_REGISTER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2); + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + let cli = Cli::parse(); + + if cli.manifest { + let m = manifest::build_manifest(); + println!("{}", serde_json::to_string_pretty(&m)?); + return Ok(()); + } + + let mut cfg = iii_auth::config::load_config(&cli.config) + .with_context(|| format!("failed to load auth config from {}", cli.config))?; + if let Some(issuer) = cli.issuer { + cfg.issuer = issuer; + } + if let Some(idp_mode) = cli.idp_mode { + cfg.idp_mode = idp_mode; + } + if let Some(rotation_cron) = cli.rotation_cron { + cfg.rotation_cron = rotation_cron; + } + validate_config(&cfg).context("invalid auth config")?; + let engine_url = cli.url.unwrap_or_else(|| cfg.engine_url.clone()); + let cfg = Arc::new(cfg); + + let iii = Arc::new(register_worker( + &engine_url, + InitOptions { + otel: Some(OtelConfig::default()), + metadata: Some(WorkerMetadata { + runtime: "rust".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + name: "auth".to_string(), + os: std::env::consts::OS.to_string(), + pid: Some(std::process::id()), + telemetry: None, + ..WorkerMetadata::default() + }), + ..InitOptions::default() + }, + )); + wait_for_connection_ready(&iii, &cfg).await?; + + let store: Arc = match resolve_store_backend(&cfg) { + StoreBackend::Memory => Arc::new(InMemoryAuthStore::new()), + StoreBackend::IiiState => { + let iii_for_store: Arc = iii.clone(); + Arc::new(IiiStateAuthStore::new(iii_for_store, cfg.state_timeout_ms)) + } + }; + + let _refs = iii_auth::register_with_iii(&iii, store, cfg.clone()) + .await + .context("auth register failed")?; + + register_triggers(&iii, &cfg).context("auth trigger registration failed")?; + let skill_register_handle = spawn_skill_register(iii.clone(), cfg.clone()); + + tracing::info!("auth ready"); + wait_for_shutdown().await?; + skill_register_handle.abort(); + let _ = tokio::time::timeout(SKILL_REGISTER_SHUTDOWN_TIMEOUT, skill_register_handle).await; + unregister_skill(&iii, &cfg).await; + tracing::info!("auth shutting down"); + iii.shutdown_async().await; + Ok(()) +} + +fn register_triggers(iii: &iii_sdk::III, cfg: &AuthConfig) -> Result<()> { + let http_routes = [ + ( + "auth::server_metadata", + "GET", + ".well-known/oauth-authorization-server", + ), + ( + "auth::resource_metadata", + "GET", + ".well-known/oauth-protected-resource", + ), + ("auth::register", "POST", "register"), + ("auth::jwks", "GET", ".well-known/jwks.json"), + ("auth::token", "POST", "token"), + ("auth::introspect", "POST", "introspect"), + ("auth::revoke", "POST", "revoke"), + ]; + for (function_id, method, api_path) in http_routes { + iii.register_trigger(RegisterTriggerInput { + trigger_type: "http".to_string(), + function_id: function_id.to_string(), + config: json!({ "api_path": api_path, "http_method": method }), + metadata: None, + }) + .with_context(|| format!("failed to register {method} {api_path} for {function_id}"))?; + } + iii.register_trigger(RegisterTriggerInput { + trigger_type: "cron".to_string(), + function_id: "auth::jwks_rotate".to_string(), + config: json!({ "expression": cfg.rotation_cron }), + metadata: None, + }) + .context("failed to register JWKS rotation trigger")?; + Ok(()) +} + +async fn register_skill_with_retry(iii: &iii_sdk::III, id: &str, body: &str, timeout_ms: u64) { + let mut backoff = Duration::from_secs(5); + let started = Instant::now(); + loop { + let res = iii + .trigger(TriggerRequest { + function_id: "skills::register".into(), + payload: json!({ "id": id, "skill": body }), + action: None, + timeout_ms: Some(timeout_ms), + }) + .await; + match res { + Ok(_) => return, + Err(e) => { + if started.elapsed() > SKILL_REGISTER_TIMEOUT { + tracing::warn!(%id, error = %e, "skills handshake gave up"); + return; + } + } + } + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(SKILL_REGISTER_MAX_BACKOFF); + } +} + +fn spawn_skill_register(iii: Arc, cfg: Arc) -> JoinHandle<()> { + tokio::spawn(async move { + register_skill_with_retry( + &iii, + iii_auth::SKILL_ID, + iii_auth::SKILL_MD, + cfg.skills_register_timeout_ms, + ) + .await; + for (id, body) in iii_auth::SUB_SKILLS { + register_skill_with_retry(&iii, id, body, cfg.skills_register_timeout_ms).await; + } + }) +} + +async fn unregister_skill(iii: &Arc, cfg: &Arc) { + for (id, _) in iii_auth::SUB_SKILLS { + let _ = iii + .trigger(TriggerRequest { + function_id: "skills::unregister".into(), + payload: json!({ "id": id }), + action: None, + timeout_ms: Some(cfg.skills_unregister_timeout_ms), + }) + .await; + } + let _ = iii + .trigger(TriggerRequest { + function_id: "skills::unregister".into(), + payload: json!({ "id": iii_auth::SKILL_ID }), + action: None, + timeout_ms: Some(cfg.skills_unregister_timeout_ms), + }) + .await; +} + +async fn wait_for_connection_ready(iii: &iii_sdk::III, cfg: &AuthConfig) -> Result<()> { + let interval = Duration::from_millis(cfg.connection_ready_interval_ms); + for attempt in 1..=cfg.connection_ready_attempts { + let state = iii.get_connection_state(); + if state == iii_sdk::IIIConnectionState::Connected { + tokio::time::sleep(CONNECTION_READY_SETTLE).await; + return Ok(()); + } + tracing::debug!(attempt, state = ?state, "iii engine connection not ready"); + tokio::time::sleep(interval).await; + } + anyhow::bail!( + "timed out waiting for iii engine connection after {} attempts", + cfg.connection_ready_attempts + ) +} + +async fn wait_for_shutdown() -> Result<()> { + #[cfg(unix)] + { + use tokio::signal::unix::{signal, SignalKind}; + let mut sigterm = + signal(SignalKind::terminate()).context("failed to install SIGTERM handler")?; + tokio::select! { + r = tokio::signal::ctrl_c() => r.context("failed to await SIGINT")?, + _ = sigterm.recv() => {} + } + Ok(()) + } + #[cfg(not(unix))] + { + tokio::signal::ctrl_c() + .await + .context("failed to await SIGINT") + } +} diff --git a/auth/src/manifest.rs b/auth/src/manifest.rs new file mode 100644 index 00000000..128e4c04 --- /dev/null +++ b/auth/src/manifest.rs @@ -0,0 +1,38 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct WorkerManifest { + pub name: String, + pub version: String, + pub description: String, + pub default_config: serde_json::Value, + pub supported_targets: Vec, +} + +pub fn build_manifest() -> WorkerManifest { + WorkerManifest { + name: env!("CARGO_PKG_NAME").to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + description: env!("CARGO_PKG_DESCRIPTION").to_string(), + default_config: serde_json::to_value(iii_auth::config::AuthConfig::default()) + .expect("config serializes to JSON"), + supported_targets: vec![env!("TARGET_TRIPLE").to_string()], + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn json_roundtrip_has_required_fields() { + let m = build_manifest(); + let json = serde_json::to_string_pretty(&m).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed["name"], env!("CARGO_PKG_NAME")); + assert_eq!(parsed["version"], env!("CARGO_PKG_VERSION")); + assert!(parsed["description"].is_string()); + assert!(parsed["default_config"].is_object()); + assert!(!parsed["supported_targets"].as_array().unwrap().is_empty()); + } +} diff --git a/auth/src/store.rs b/auth/src/store.rs new file mode 100644 index 00000000..a8232036 --- /dev/null +++ b/auth/src/store.rs @@ -0,0 +1,428 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use chrono::Utc; +use iii_sdk::TriggerRequest; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use tokio::sync::{Mutex, RwLock}; + +use crate::io::IIITrigger; +use crate::{ClientRecord, KeyRecord, KeySet, RefreshTokenRecord}; + +pub const CLIENTS_SCOPE: &str = "auth:clients"; +pub const JWKS_SCOPE: &str = "auth:jwks"; +pub const TOKENS_SCOPE: &str = "auth:tokens"; +const KEYSET_KEY: &str = "keyset"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct RevokedTokenRecord { + token_id: String, + created_at: i64, +} + +#[async_trait::async_trait] +pub trait AuthStore: Send + Sync { + async fn get_client(&self, client_id: &str) -> anyhow::Result>; + async fn set_client(&self, client: ClientRecord) -> anyhow::Result<()>; + async fn get_keyset(&self) -> anyhow::Result>; + async fn set_keyset(&self, keyset: KeySet) -> anyhow::Result<()>; + async fn create_keyset_if_absent(&self, keyset: KeySet) -> anyhow::Result; + async fn rotate_keyset( + &self, + new_key: KeyRecord, + current_time: i64, + rotation_overlap_seconds: i64, + ) -> anyhow::Result; + async fn get_refresh_token(&self, token_id: &str) + -> anyhow::Result>; + async fn set_refresh_token(&self, record: RefreshTokenRecord) -> anyhow::Result<()>; + async fn rotate_refresh_token( + &self, + old_token_id: &str, + new_record: RefreshTokenRecord, + ) -> anyhow::Result<()>; + async fn is_revoked(&self, token_id: &str) -> anyhow::Result; + async fn revoke(&self, token_id: &str) -> anyhow::Result<()>; + async fn cleanup_expired_tokens( + &self, + current_time: i64, + revoked_retention_seconds: i64, + ) -> anyhow::Result<()>; +} + +#[derive(Debug, Clone, Default)] +pub struct InMemoryAuthStore { + clients: Arc>>, + keyset: Arc>>, + refresh_tokens: Arc>>, + revoked: Arc>>, +} + +impl InMemoryAuthStore { + pub fn new() -> Self { + Self::default() + } +} + +#[async_trait::async_trait] +impl AuthStore for InMemoryAuthStore { + async fn get_client(&self, client_id: &str) -> anyhow::Result> { + Ok(self.clients.read().await.get(client_id).cloned()) + } + + async fn set_client(&self, client: ClientRecord) -> anyhow::Result<()> { + self.clients + .write() + .await + .insert(client.client_id.clone(), client); + Ok(()) + } + + async fn get_keyset(&self) -> anyhow::Result> { + Ok(self.keyset.read().await.clone()) + } + + async fn set_keyset(&self, keyset: KeySet) -> anyhow::Result<()> { + *self.keyset.write().await = Some(keyset); + Ok(()) + } + + async fn create_keyset_if_absent(&self, keyset: KeySet) -> anyhow::Result { + let mut current = self.keyset.write().await; + if let Some(existing) = current.clone() { + return Ok(existing); + } + *current = Some(keyset.clone()); + Ok(keyset) + } + + async fn rotate_keyset( + &self, + new_key: KeyRecord, + current_time: i64, + rotation_overlap_seconds: i64, + ) -> anyhow::Result { + let mut current = self.keyset.write().await; + let mut keyset = current.clone().unwrap_or_else(|| KeySet { + current_kid: new_key.kid.clone(), + keys: Vec::new(), + }); + if !keyset.keys.is_empty() { + for key in &mut keyset.keys { + if key.kid == keyset.current_kid && key.retire_after.is_none() { + key.retire_after = Some(current_time + rotation_overlap_seconds); + } + } + keyset + .keys + .retain(|key| key.retire_after.is_none_or(|retire| retire > current_time)); + } + keyset.current_kid.clone_from(&new_key.kid); + keyset.keys.push(new_key); + *current = Some(keyset.clone()); + Ok(keyset) + } + + async fn get_refresh_token( + &self, + token_id: &str, + ) -> anyhow::Result> { + Ok(self.refresh_tokens.read().await.get(token_id).cloned()) + } + + async fn set_refresh_token(&self, record: RefreshTokenRecord) -> anyhow::Result<()> { + self.refresh_tokens + .write() + .await + .insert(record.token_id.clone(), record); + Ok(()) + } + + async fn rotate_refresh_token( + &self, + old_token_id: &str, + new_record: RefreshTokenRecord, + ) -> anyhow::Result<()> { + let mut refresh_tokens = self.refresh_tokens.write().await; + let mut revoked = self.revoked.write().await; + refresh_tokens.insert(new_record.token_id.clone(), new_record); + revoked.insert(old_token_id.to_string(), Utc::now().timestamp()); + Ok(()) + } + + async fn is_revoked(&self, token_id: &str) -> anyhow::Result { + Ok(self.revoked.read().await.contains_key(token_id)) + } + + async fn revoke(&self, token_id: &str) -> anyhow::Result<()> { + self.revoked + .write() + .await + .insert(token_id.to_string(), Utc::now().timestamp()); + Ok(()) + } + + async fn cleanup_expired_tokens( + &self, + current_time: i64, + revoked_retention_seconds: i64, + ) -> anyhow::Result<()> { + self.refresh_tokens + .write() + .await + .retain(|_, record| record.expires_at > current_time); + let revoked_cutoff = current_time.saturating_sub(revoked_retention_seconds); + self.revoked + .write() + .await + .retain(|_, created_at| *created_at >= revoked_cutoff); + Ok(()) + } +} + +pub struct IiiStateAuthStore { + iii: Arc, + timeout_ms: u64, + lock: Arc>, +} + +impl IiiStateAuthStore { + pub fn new(iii: Arc, timeout_ms: u64) -> Self { + Self { + iii, + timeout_ms, + lock: Arc::new(Mutex::new(())), + } + } + + async fn get_value(&self, scope: &str, key: &str) -> anyhow::Result> { + let resp = self + .iii + .trigger(TriggerRequest { + function_id: "state::get".into(), + payload: json!({ "scope": scope, "key": key }), + action: None, + timeout_ms: Some(self.timeout_ms), + }) + .await + .map_err(|e| anyhow::anyhow!("state::get failed: {e}"))?; + if resp.is_null() { + Ok(None) + } else { + Ok(Some(resp)) + } + } + + async fn set_value(&self, scope: &str, key: &str, value: Value) -> anyhow::Result<()> { + self.iii + .trigger(TriggerRequest { + function_id: "state::set".into(), + payload: json!({ "scope": scope, "key": key, "value": value }), + action: None, + timeout_ms: Some(self.timeout_ms), + }) + .await + .map_err(|e| anyhow::anyhow!("state::set failed: {e}"))?; + Ok(()) + } + + async fn delete_value(&self, scope: &str, key: &str) -> anyhow::Result<()> { + self.iii + .trigger(TriggerRequest { + function_id: "state::delete".into(), + payload: json!({ "scope": scope, "key": key }), + action: None, + timeout_ms: Some(self.timeout_ms), + }) + .await + .map_err(|e| anyhow::anyhow!("state::delete failed: {e}"))?; + Ok(()) + } + + async fn list_values(&self, scope: &str) -> anyhow::Result> { + let resp = self + .iii + .trigger(TriggerRequest { + function_id: "state::list".into(), + payload: json!({ "scope": scope }), + action: None, + timeout_ms: Some(self.timeout_ms), + }) + .await + .map_err(|e| anyhow::anyhow!("state::list failed: {e}"))?; + serde_json::from_value(resp).map_err(Into::into) + } +} + +#[async_trait::async_trait] +impl AuthStore for IiiStateAuthStore { + async fn get_client(&self, client_id: &str) -> anyhow::Result> { + self.get_value(CLIENTS_SCOPE, client_id) + .await? + .map(serde_json::from_value) + .transpose() + .map_err(Into::into) + } + + async fn set_client(&self, client: ClientRecord) -> anyhow::Result<()> { + let key = client.client_id.clone(); + self.set_value(CLIENTS_SCOPE, &key, serde_json::to_value(client)?) + .await + } + + async fn get_keyset(&self) -> anyhow::Result> { + self.get_value(JWKS_SCOPE, KEYSET_KEY) + .await? + .map(serde_json::from_value) + .transpose() + .map_err(Into::into) + } + + async fn set_keyset(&self, keyset: KeySet) -> anyhow::Result<()> { + self.set_value(JWKS_SCOPE, KEYSET_KEY, serde_json::to_value(keyset)?) + .await + } + + async fn create_keyset_if_absent(&self, keyset: KeySet) -> anyhow::Result { + let _guard = self.lock.lock().await; + if let Some(existing) = self.get_keyset().await? { + return Ok(existing); + } + self.set_keyset(keyset.clone()).await?; + Ok(keyset) + } + + async fn rotate_keyset( + &self, + new_key: KeyRecord, + current_time: i64, + rotation_overlap_seconds: i64, + ) -> anyhow::Result { + let _guard = self.lock.lock().await; + let mut keyset = self.get_keyset().await?.unwrap_or_else(|| KeySet { + current_kid: new_key.kid.clone(), + keys: Vec::new(), + }); + if !keyset.keys.is_empty() { + for key in &mut keyset.keys { + if key.kid == keyset.current_kid && key.retire_after.is_none() { + key.retire_after = Some(current_time + rotation_overlap_seconds); + } + } + keyset + .keys + .retain(|key| key.retire_after.is_none_or(|retire| retire > current_time)); + } + keyset.current_kid.clone_from(&new_key.kid); + keyset.keys.push(new_key); + self.set_keyset(keyset.clone()).await?; + Ok(keyset) + } + + async fn get_refresh_token( + &self, + token_id: &str, + ) -> anyhow::Result> { + self.get_value(TOKENS_SCOPE, &format!("refresh:{token_id}")) + .await? + .map(serde_json::from_value) + .transpose() + .map_err(Into::into) + } + + async fn set_refresh_token(&self, record: RefreshTokenRecord) -> anyhow::Result<()> { + self.set_value( + TOKENS_SCOPE, + &format!("refresh:{}", record.token_id), + serde_json::to_value(record)?, + ) + .await + } + + async fn rotate_refresh_token( + &self, + old_token_id: &str, + new_record: RefreshTokenRecord, + ) -> anyhow::Result<()> { + let _guard = self.lock.lock().await; + self.set_refresh_token(new_record.clone()).await?; + if let Err(err) = self.revoke(old_token_id).await { + let _ = self + .delete_value(TOKENS_SCOPE, &format!("refresh:{}", new_record.token_id)) + .await; + return Err(err); + } + Ok(()) + } + + async fn is_revoked(&self, token_id: &str) -> anyhow::Result { + Ok(self + .get_value(TOKENS_SCOPE, &format!("revoked:{token_id}")) + .await? + .is_some()) + } + + async fn revoke(&self, token_id: &str) -> anyhow::Result<()> { + self.set_value( + TOKENS_SCOPE, + &format!("revoked:{token_id}"), + serde_json::to_value(RevokedTokenRecord { + token_id: token_id.to_string(), + created_at: Utc::now().timestamp(), + })?, + ) + .await + } + + async fn cleanup_expired_tokens( + &self, + current_time: i64, + revoked_retention_seconds: i64, + ) -> anyhow::Result<()> { + let _guard = self.lock.lock().await; + let revoked_cutoff = current_time.saturating_sub(revoked_retention_seconds); + for value in self.list_values(TOKENS_SCOPE).await? { + if let Ok(record) = serde_json::from_value::(value.clone()) { + if record.expires_at <= current_time { + self.delete_value(TOKENS_SCOPE, &format!("refresh:{}", record.token_id)) + .await?; + } + continue; + } + if let Ok(record) = serde_json::from_value::(value) { + if record.created_at < revoked_cutoff { + self.delete_value(TOKENS_SCOPE, &format!("revoked:{}", record.token_id)) + .await?; + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn memory_cleanup_prunes_expired_tokens() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + store + .set_refresh_token(RefreshTokenRecord { + token_id: "expired".to_string(), + client_id: "client".to_string(), + subject: "client".to_string(), + scopes: vec!["mcp:tools".to_string()], + expires_at: Utc::now().timestamp() - 1, + }) + .await?; + store.revoke("old-revoked").await?; + store + .cleanup_expired_tokens(Utc::now().timestamp() + 1, 0) + .await?; + assert!(store.get_refresh_token("expired").await?.is_none()); + assert!(!store.is_revoked("old-revoked").await?); + Ok(()) + } +} diff --git a/auth/tests/integration.rs b/auth/tests/integration.rs new file mode 100644 index 00000000..09bf5a4a --- /dev/null +++ b/auth/tests/integration.rs @@ -0,0 +1,64 @@ +use iii_auth::config::{AuthConfig, StoreBackend}; +use iii_auth::store::InMemoryAuthStore; +use iii_auth::{register_client, token_endpoint, validate_session}; +use serde_json::json; + +fn cfg() -> AuthConfig { + let admin_env = format!( + "III_AUTH_INTEGRATION_ADMIN_{}", + uuid::Uuid::new_v4().simple() + ); + std::env::set_var(&admin_env, "admin-secret"); + AuthConfig { + issuer: "https://auth.test".to_string(), + audience: "iii-test".to_string(), + store: StoreBackend::Memory, + supported_scopes: vec![ + "mcp:tools".to_string(), + "function:demo::read".to_string(), + "trigger:http".to_string(), + ], + default_scopes: vec!["mcp:tools".to_string()], + registration_admin_token_env: admin_env, + ..AuthConfig::default() + } +} + +#[tokio::test] +async fn dcr_token_validate_flow() -> anyhow::Result<()> { + let store = InMemoryAuthStore::new(); + let cfg = cfg(); + let registration = register_client( + &store, + &cfg, + json!({ + "headers": { "authorization": "Bearer admin-secret" }, + "client_name": "integration", + "scope": "function:demo::read trigger:http" + }), + ) + .await?; + let token = token_endpoint( + &store, + &cfg, + json!({ + "grant_type": "client_credentials", + "client_id": registration["client_id"], + "client_secret": registration["client_secret"], + "scope": "function:demo::read trigger:http" + }), + ) + .await?; + let decision = validate_session( + &store, + &cfg, + json!({ "headers": { "Authorization": format!("Bearer {}", token["access_token"].as_str().unwrap()) } }), + ) + .await?; + assert_eq!(decision.allowed_functions, vec!["demo::read"]); + assert_eq!( + decision.allowed_trigger_types, + Some(vec!["http".to_string()]) + ); + Ok(()) +} diff --git a/auth/tests/manifest.rs b/auth/tests/manifest.rs new file mode 100644 index 00000000..7f115473 --- /dev/null +++ b/auth/tests/manifest.rs @@ -0,0 +1,13 @@ +use std::process::Command; + +#[test] +fn manifest_command_outputs_json() { + let output = Command::new(env!("CARGO_BIN_EXE_iii-auth")) + .arg("--manifest") + .output() + .expect("run manifest command"); + assert!(output.status.success()); + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(json["name"], "auth"); + assert!(json["default_config"].is_object()); +} diff --git a/auth/tests/skill.rs b/auth/tests/skill.rs new file mode 100644 index 00000000..a846923c --- /dev/null +++ b/auth/tests/skill.rs @@ -0,0 +1,14 @@ +#[test] +fn skill_starts_with_heading_and_lists_functions() { + let body = include_str!("../skills/index.md"); + assert!(body.starts_with("# auth\n")); + assert!(body.contains("auth::validate")); + assert!(body.contains("auth::server_metadata")); + assert!(body.contains("auth::resource_metadata")); + assert!(body.contains("auth::register")); + assert!(body.contains("auth::jwks")); + assert!(body.contains("auth::jwks_rotate")); + assert!(body.contains("auth::token")); + assert!(body.contains("auth::introspect")); + assert!(body.contains("auth::revoke")); +}