diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c325a5..1079f15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added Infisical provider support via `infisical` feature flag for integration with Infisical secrets management platform + ### Changed - Made keyring provider optional via `keyring` feature flag (enabled by default) diff --git a/Cargo.lock b/Cargo.lock index 459aba1..4b31d1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -108,6 +114,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -165,12 +177,27 @@ dependencies = [ "cipher", ] +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "cipher" version = "0.4.4" @@ -249,6 +276,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -283,7 +320,7 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -334,7 +371,7 @@ dependencies = [ "hkdf", "num", "once_cell", - "rand", + "rand 0.8.5", "sha2", ] @@ -399,6 +436,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "envy" version = "0.4.2" @@ -436,6 +482,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -445,12 +506,28 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-macro" version = "0.3.31" @@ -462,6 +539,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -475,8 +558,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -499,8 +585,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -510,9 +598,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -527,6 +617,25 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.4" @@ -568,6 +677,114 @@ dependencies = [ "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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -685,6 +902,20 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "infisical" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4db449c303e3aad086d71efcdcc2a0b71dda29ad88328a8f465b14c6a0af17" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "url", +] + [[package]] name = "inout" version = "0.1.4" @@ -722,6 +953,33 @@ dependencies = [ "similar", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -759,7 +1017,7 @@ dependencies = [ "byteorder", "dbus-secret-service", "log", - "security-framework", + "security-framework 3.2.0", "windows-sys 0.59.0", ] @@ -842,6 +1100,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.7.5" @@ -878,6 +1142,12 @@ dependencies = [ "syn", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -899,6 +1169,34 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "newline-converter" version = "0.2.2" @@ -1002,6 +1300,50 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -1088,6 +1430,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -1110,8 +1507,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1121,7 +1528,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "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.3", ] [[package]] @@ -1133,6 +1550,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "redox_syscall" version = "0.5.13" @@ -1154,8 +1580,68 @@ dependencies = [ ] [[package]] -name = "rpassword" -version = "7.4.0" +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "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.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rpassword" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" dependencies = [ @@ -1180,6 +1666,12 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.0.7" @@ -1193,12 +1685,62 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1214,6 +1756,7 @@ dependencies = [ "directories", "dotenvy", "http", + "infisical", "inquire", "keyring", "linkme", @@ -1224,6 +1767,7 @@ dependencies = [ "serde_json", "tempfile", "thiserror 1.0.69", + "tokio", "toml", "url", "whoami", @@ -1257,6 +1801,19 @@ dependencies = [ "url", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.2.0" @@ -1264,7 +1821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags 2.9.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -1334,6 +1891,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1345,6 +1914,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.18" @@ -1362,7 +1937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] @@ -1393,6 +1968,26 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1443,6 +2038,15 @@ dependencies = [ "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" @@ -1454,6 +2058,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "target-triple" version = "0.1.4" @@ -1552,6 +2177,85 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +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.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio 1.0.4", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.5.10", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -1593,6 +2297,76 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "trybuild" version = "1.0.105" @@ -1644,6 +2418,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[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.4" @@ -1667,12 +2447,27 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1702,6 +2497,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] @@ -1719,6 +2515,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -1761,6 +2570,25 @@ dependencies = [ "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "whoami" version = "1.6.0" @@ -1803,6 +2631,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2113,6 +2976,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index b65cf18..060537a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ proc-macro2 = "1.0" trybuild = "1.0" insta = "1.34" linkme = "0.3" +infisical = "0.0.1" +tokio = { version = "1.46", features = ["rt", "rt-multi-thread"] } secretspec-derive = { version = "0.2.0", path = "./secretspec-derive" } secretspec = { version = "0.2.0", path = "./secretspec" } diff --git a/devenv.nix b/devenv.nix index f5d2108..e761eec 100644 --- a/devenv.nix +++ b/devenv.nix @@ -15,6 +15,8 @@ pkgs.cargo-tarpaulin # installers pkgs.cargo-dist + # openssl for infisical provider + pkgs.openssl ]; git-hooks.hooks = { diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 7c285c6..87abd60 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -47,6 +47,7 @@ export default defineConfig({ { label: "Keyring", slug: "providers/keyring" }, { label: "Dotenv", slug: "providers/dotenv" }, { label: "Environment Variables", slug: "providers/env" }, + { label: "Infisical", slug: "providers/infisical" }, { label: "LastPass", slug: "providers/lastpass" }, { label: "1Password", slug: "providers/onepassword" }, ], diff --git a/docs/src/content/docs/concepts/providers.md b/docs/src/content/docs/concepts/providers.md index 8b83c35..40b3751 100644 --- a/docs/src/content/docs/concepts/providers.md +++ b/docs/src/content/docs/concepts/providers.md @@ -12,6 +12,7 @@ Providers are pluggable storage backends that handle the storage and retrieval o | **keyring** | System credential storage (macOS Keychain, Windows Credential Manager, Linux Secret Service) | ✓ | ✓ | ✓ | | **dotenv** | Traditional `.env` file in your project directory | ✓ | ✓ | ✗ | | **env** | Read-only access to existing environment variables | ✓ | ✗ | ✗ | +| **infisical** | Integration with Infisical secrets management platform | ✓ | ✓ | ✓ | | **onepassword** | Integration with OnePassword password manager | ✓ | ✓ | ✓ | | **lastpass** | Integration with LastPass password manager | ✓ | ✓ | ✓ | diff --git a/docs/src/content/docs/providers/infisical.md b/docs/src/content/docs/providers/infisical.md new file mode 100644 index 0000000..69f2bc6 --- /dev/null +++ b/docs/src/content/docs/providers/infisical.md @@ -0,0 +1,90 @@ +--- +title: Infisical Provider +description: Infisical secrets management platform integration +--- + +The Infisical provider integrates with Infisical for centralized secrets management with advanced access controls and audit logging. + +## Prerequisites + +- Infisical account (cloud or self-hosted) +- Machine identity with Universal Auth configured +- Client ID and Client Secret from Infisical dashboard + +## Configuration + +### URI Format + +``` +infisical://[host/]project-id[/path]?client_id=xxx&client_secret=yyy +``` + +- `host`: Optional custom Infisical instance URL (defaults to app.infisical.com) +- `project-id`: Your Infisical project ID +- `path`: Optional path prefix for organizing secrets +- `client_id`: Universal Auth client ID +- `client_secret`: Universal Auth client secret + +### Examples + +```bash +# Basic usage with cloud Infisical +$ secretspec set API_KEY --provider "infisical://project-id?client_id=xxx&client_secret=yyy" + +# Self-hosted Infisical instance +$ secretspec set DATABASE_URL --provider "infisical://infisical.company.com/project-id?client_id=xxx&client_secret=yyy" + +# With path prefix for organization +$ secretspec set SECRET --provider "infisical://project-id/backend?client_id=xxx&client_secret=yyy" +``` + +## Usage + +### Basic Commands + +```bash +# Set a secret +$ secretspec set DATABASE_URL +Enter value for DATABASE_URL: postgresql://localhost/mydb +✓ Secret DATABASE_URL saved to Infisical + +# Get a secret +$ secretspec get DATABASE_URL + +# Run with secrets +$ secretspec run -- npm start +``` + +### Profile Configuration + +```toml +# secretspec.toml +[development] +provider = "infisical://dev-project-id?client_id=dev-id&client_secret=dev-secret" + +[production] +provider = "infisical://prod-project-id?client_id=prod-id&client_secret=prod-secret" +``` + +## Environment Mapping + +SecretSpec profiles map to Infisical environments: + +- `default` → `dev` +- Other profiles map directly (e.g., `production` → `production`) + +## Secret Naming + +Secrets are stored in Infisical with the naming convention: +`SECRETSPEC_{PROJECT}_{KEY}` + +For example, if your project is named "myapp" and you store "API_KEY", it will be saved as "SECRETSPEC_MYAPP_API_KEY" in Infisical. + +## Self-Hosted Infisical + +For self-hosted instances, specify your instance URL: + +```bash +# Via URI +$ secretspec set KEY --provider "infisical://infisical.internal.com/project-id?client_id=xxx&client_secret=yyy" +``` \ No newline at end of file diff --git a/secretspec/Cargo.toml b/secretspec/Cargo.toml index cc556d8..768da54 100644 --- a/secretspec/Cargo.toml +++ b/secretspec/Cargo.toml @@ -34,8 +34,11 @@ http.workspace = true url.workspace = true whoami = { workspace = true, optional = true } linkme.workspace = true +infisical = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } [features] default = ["cli", "keyring"] cli = [] keyring = ["dep:keyring", "dep:whoami"] +infisical = ["dep:infisical", "dep:tokio"] diff --git a/secretspec/README.md b/secretspec/README.md index 249aa22..2ea6bea 100644 --- a/secretspec/README.md +++ b/secretspec/README.md @@ -14,7 +14,13 @@ SecretSpec separates the declaration of what secrets an application needs from w ## Features - **[Declarative Configuration](https://secretspec.dev/reference/configuration/)**: Define your secrets in `secretspec.toml` with descriptions and requirements -- **[Multiple Provider Backends](https://secretspec.dev/concepts/providers/)**: [Keyring](https://secretspec.dev/providers/keyring), [.env](https://secretspec.dev/providers/dotenv), [OnePassword](https://secretspec.dev/providers/onepassword), [LastPass](https://secretspec.dev/providers/lastpass), and [environment variables](https://secretspec.dev/providers/env) +- **[Multiple Provider Backends](https://secretspec.dev/concepts/providers/)**: + - [Keyring](https://secretspec.dev/providers/keyring) - System keychain (macOS, Windows, Linux) + - [.env](https://secretspec.dev/providers/dotenv) - Traditional .env files + - [Infisical](https://secretspec.dev/providers/infisical) - Infisical secrets management platform + - [OnePassword](https://secretspec.dev/providers/onepassword) - 1Password password manager + - [LastPass](https://secretspec.dev/providers/lastpass) - LastPass password manager + - [Environment variables](https://secretspec.dev/providers/env) - Read-only environment variables - **[Type-Safe Rust SDK](https://secretspec.dev/sdk/rust/)**: Generate strongly-typed structs from your `secretspec.toml` for compile-time safety - **[Profile Support](https://secretspec.dev/concepts/profiles/)**: Override secret requirements and defaults per profile (development, production, etc.) - **Configuration Inheritance**: Extend and override shared configurations using the `extends` feature @@ -38,6 +44,7 @@ $ secretspec config init > onepassword: OnePassword password manager dotenv: Traditional .env files env: Read-only environment variables + infisical: Infisical secrets management platform keyring: Uses system keychain (Recommended) lastpass: LastPass password manager ? Select your default profile: diff --git a/secretspec/src/cli/mod.rs b/secretspec/src/cli/mod.rs index 9e69129..b8f78d3 100644 --- a/secretspec/src/cli/mod.rs +++ b/secretspec/src/cli/mod.rs @@ -224,7 +224,7 @@ pub fn main() -> Result<()> { // Create dotenv provider and reflect secrets let dotenv_config = (&uri).try_into().into_diagnostic()?; - let dotenv_provider = DotEnvProvider::new(dotenv_config); + let dotenv_provider = DotEnvProvider::new(dotenv_config).into_diagnostic()?; let secrets = dotenv_provider.reflect().into_diagnostic()?; // Create a new project config diff --git a/secretspec/src/provider/dotenv.rs b/secretspec/src/provider/dotenv.rs index a905417..c87f9ac 100644 --- a/secretspec/src/provider/dotenv.rs +++ b/secretspec/src/provider/dotenv.rs @@ -150,10 +150,10 @@ impl DotEnvProvider { /// use secretspec::provider::dotenv::{DotEnvProvider, DotEnvConfig}; /// /// let config = DotEnvConfig::default(); - /// let provider = DotEnvProvider::new(config); + /// let provider = DotEnvProvider::new(config)?; /// ``` - pub fn new(config: DotEnvConfig) -> Self { - Self { config } + pub fn new(config: DotEnvConfig) -> Result { + Ok(Self { config }) } /// Reflects all secrets available in the .env file as Secret entries. @@ -359,7 +359,7 @@ mod tests { path: env_file.clone(), }); - let secrets = provider.reflect().unwrap(); + let secrets = provider.unwrap().reflect().unwrap(); assert_eq!(secrets.len(), 2); assert!(secrets.contains_key("API_KEY")); assert!(secrets.contains_key("DATABASE_URL")); @@ -379,7 +379,7 @@ mod tests { path: PathBuf::from("/tmp/nonexistent/.env"), }); - let secrets = provider.reflect().unwrap(); + let secrets = provider.unwrap().reflect().unwrap(); assert!(secrets.is_empty()); } } diff --git a/secretspec/src/provider/env.rs b/secretspec/src/provider/env.rs index 25563b0..2bab7c6 100644 --- a/secretspec/src/provider/env.rs +++ b/secretspec/src/provider/env.rs @@ -99,10 +99,10 @@ impl EnvProvider { /// ```ignore /// # use secretspec::provider::env::{EnvProvider, EnvConfig}; /// let config = EnvConfig::default(); - /// let provider = EnvProvider::new(config); + /// let provider = EnvProvider::new(config)?; /// ``` - pub fn new(config: EnvConfig) -> Self { - Self { config } + pub fn new(config: EnvConfig) -> Result { + Ok(Self { config }) } } diff --git a/secretspec/src/provider/infisical.rs b/secretspec/src/provider/infisical.rs new file mode 100644 index 0000000..de533bc --- /dev/null +++ b/secretspec/src/provider/infisical.rs @@ -0,0 +1,448 @@ +use super::Provider; +use crate::{Result, SecretSpecError}; +use infisical::{ + auth::AuthMethod, + client::Client, + resources::secrets::{CreateSecretRequest, GetSecretRequest, UpdateSecretRequest}, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::runtime::Runtime; +use url::Url; + +/// Configuration for the Infisical provider. +/// +/// This struct contains all the necessary configuration options for +/// interacting with Infisical API. It currently supports Universal Auth +/// for authentication. +/// +/// # Examples +/// +/// ```ignore +/// # use secretspec::provider::infisical::InfisicalConfig; +/// // Using universal auth (client ID and secret) +/// let config = InfisicalConfig { +/// client_id: Some("your-client-id".to_string()), +/// client_secret: Some("your-client-secret".to_string()), +/// project_id: "your-project-id".to_string(), +/// ..Default::default() +/// }; +/// +/// // With custom API URL +/// let config = InfisicalConfig { +/// client_id: Some("your-client-id".to_string()), +/// client_secret: Some("your-client-secret".to_string()), +/// project_id: "your-project-id".to_string(), +/// api_url: Some("https://custom.infisical.com".to_string()), +/// ..Default::default() +/// }; +/// ``` +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct InfisicalConfig { + /// Client ID for universal authentication. + /// + /// This should be obtained from the Infisical dashboard + /// when creating machine identities. + pub client_id: Option, + + /// Client secret for universal authentication. + /// + /// This should be obtained from the Infisical dashboard + /// when creating machine identities. + pub client_secret: Option, + + /// Optional custom API URL. + /// + /// Defaults to "https://app.infisical.com" if not specified. + /// Useful for self-hosted Infisical instances. + pub api_url: Option, + + /// Project ID where secrets are stored. + /// + /// Required for secret operations. + pub project_id: Option, + + /// Optional path prefix for organizing secrets. + /// + /// Defaults to "/" if not specified. + pub path_prefix: Option, +} + +impl TryFrom<&Url> for InfisicalConfig { + type Error = SecretSpecError; + + fn try_from(url: &Url) -> std::result::Result { + if url.scheme() != "infisical" { + return Err(SecretSpecError::ProviderOperationFailed(format!( + "Invalid scheme '{}' for infisical provider", + url.scheme() + ))); + } + + let mut config = Self::default(); + + // Parse URL components + // Format: infisical://[api_url]/[project_id]/[path]?client_id=xxx&client_secret=yyy + if let Some(host) = url.host_str() { + if !host.is_empty() { + // If there's a port, include it in the API URL + if let Some(port) = url.port() { + config.api_url = Some(format!("https://{}:{}", host, port)); + } else { + config.api_url = Some(format!("https://{}", host)); + } + } + } + + // Extract project ID and path from URL path + let path = url.path(); + if !path.is_empty() && path != "/" { + let parts: Vec<&str> = path.trim_start_matches('/').split('/').collect(); + if !parts.is_empty() && !parts[0].is_empty() { + config.project_id = Some(parts[0].to_string()); + if parts.len() > 1 { + config.path_prefix = Some(format!("/{}", parts[1..].join("/"))); + } + } + } + + // Parse query parameters + for (key, value) in url.query_pairs() { + match key.as_ref() { + "client_id" => config.client_id = Some(value.to_string()), + "client_secret" => config.client_secret = Some(value.to_string()), + _ => {} + } + } + + Ok(config) + } +} + +/// Provider for storing secrets in Infisical. +/// +/// The InfisicalProvider uses the Infisical API to store and retrieve +/// secrets securely. It requires authentication via Universal Auth (client ID and secret). +/// +/// Secrets are organized using the following structure: +/// - Project: Maps to Infisical project ID +/// - Environment: Maps to secretspec profile (e.g., "development", "production") +/// - Path: `/secretspec/{project}/{key}` +/// +/// This ensures secrets are properly namespaced by project and profile, +/// preventing conflicts between different projects or environments. +pub struct InfisicalProvider { + config: InfisicalConfig, + client: Arc, + runtime: Runtime, +} + +crate::register_provider! { + struct: InfisicalProvider, + config: InfisicalConfig, + name: "infisical", + description: "Infisical secrets management platform", + schemes: ["infisical"], + examples: ["infisical://project-id?client_id=xxx&client_secret=yyy", "infisical://app.infisical.com/project-id?client_id=xxx&client_secret=yyy"], +} + +impl InfisicalProvider { + /// Creates a new InfisicalProvider with the given configuration. + /// + /// # Arguments + /// + /// * `config` - The configuration for the infisical provider + /// + /// # Returns + /// + /// A Result containing the new instance or an error + pub fn new(config: InfisicalConfig) -> Result { + let client_id = config.client_id.clone().ok_or_else(|| { + SecretSpecError::ProviderOperationFailed( + "Infisical client ID not provided. Set via URL parameter".to_string(), + ) + })?; + + let client_secret = config.client_secret.clone().ok_or_else(|| { + SecretSpecError::ProviderOperationFailed( + "Infisical client secret not provided. Set via URL parameter".to_string(), + ) + })?; + + let api_url = config.api_url.clone(); + + // Create a tokio runtime for async operations + let runtime = Runtime::new().map_err(|e| { + SecretSpecError::ProviderOperationFailed(format!("Failed to create runtime: {}", e)) + })?; + + // Build and authenticate the client + let client = runtime.block_on(async { + let mut builder = Client::builder(); + + if let Some(url) = api_url { + builder = builder.base_url(url); + } + + let mut client = builder.build().await.map_err(|e| { + SecretSpecError::ProviderOperationFailed(format!("Failed to build client: {}", e)) + })?; + + // Authenticate with universal auth + let auth_method = AuthMethod::new_universal_auth(client_id, client_secret); + client.login(auth_method).await.map_err(|e| { + SecretSpecError::ProviderOperationFailed(format!("Failed to authenticate: {}", e)) + })?; + + Ok::<_, SecretSpecError>(client) + })?; + + Ok(Self { + config, + client: Arc::new(client), + runtime, + }) + } + + /// Constructs the secret key name for a given project and key. + /// In Infisical, we'll use a naming convention: SECRETSPEC_{PROJECT}_{KEY} + fn secret_key(&self, project: &str, key: &str) -> String { + format!( + "SECRETSPEC_{}_{}", + project.to_uppercase().replace("-", "_"), + key + ) + } + + /// Gets the path for secrets, defaulting to the configured path prefix or "/" + fn get_path(&self) -> &str { + self.config.path_prefix.as_deref().unwrap_or("/") + } + + /// Gets the project ID from config. + fn get_project_id(&self) -> Result { + self.config.project_id.clone().ok_or_else(|| { + SecretSpecError::ProviderOperationFailed( + "Project ID not specified. Set via URL path".to_string(), + ) + }) + } +} + +impl Provider for InfisicalProvider { + fn name(&self) -> &'static str { + Self::PROVIDER_NAME + } + + /// Retrieves a secret from Infisical. + /// + /// The secret is looked up using the project ID, environment (profile), + /// and a naming convention: SECRETSPEC_{PROJECT}_{KEY} + /// + /// # Arguments + /// + /// * `project` - The project namespace for the secret + /// * `key` - The secret key/name to retrieve + /// * `profile` - The profile/environment (e.g., "development", "production") + /// + /// # Returns + /// + /// - `Ok(Some(value))` if the secret exists + /// - `Ok(None)` if the secret doesn't exist + /// - `Err` if there was an error accessing Infisical + fn get(&self, project: &str, key: &str, profile: &str) -> Result> { + let project_id = self.get_project_id()?; + let secret_key = self.secret_key(project, key); + let path = self.get_path(); + + // Map profile names to Infisical environments + let environment = match profile { + "default" => "dev", + _ => profile, + }; + + let client = Arc::clone(&self.client); + + self.runtime.block_on(async move { + let request = GetSecretRequest::builder(&secret_key, &project_id, environment) + .path(path) + .build(); + + match client.secrets().get(request).await { + Ok(secret) => Ok(Some(secret.secret_value)), + Err(e) => { + // Check if it's a not found error + let error_msg = e.to_string(); + if error_msg.contains("not found") + || error_msg.contains("404") + || error_msg.contains("does not exist") + { + Ok(None) + } else { + Err(SecretSpecError::ProviderOperationFailed(format!( + "Failed to get secret from Infisical: {}", + e + ))) + } + } + } + }) + } + + /// Stores a secret in Infisical. + /// + /// The secret is stored with the project ID, environment (profile), + /// and a naming convention: SECRETSPEC_{PROJECT}_{KEY} + /// + /// # Arguments + /// + /// * `project` - The project namespace for the secret + /// * `key` - The secret key/name to store + /// * `value` - The secret value to store + /// * `profile` - The profile/environment (e.g., "development", "production") + /// + /// # Returns + /// + /// - `Ok(())` if the secret was successfully stored + /// - `Err` if there was an error + fn set(&self, project: &str, key: &str, value: &str, profile: &str) -> Result<()> { + let project_id = self.get_project_id()?; + let secret_key = self.secret_key(project, key); + let path = self.get_path(); + + // Map profile names to Infisical environments + let environment = match profile { + "default" => "dev", + _ => profile, + }; + + let client = Arc::clone(&self.client); + let value = value.to_string(); + + self.runtime.block_on(async move { + // First, check if the secret exists + let get_request = GetSecretRequest::builder(&secret_key, &project_id, environment) + .path(path) + .build(); + + match client.secrets().get(get_request).await { + Ok(_) => { + // Secret exists, update it + // Based on the GitHub example, environment should be in the builder + let update_request = + UpdateSecretRequest::builder(&secret_key, &value, &project_id) + .path(path) + .build(); + + client.secrets().update(update_request).await.map_err(|e| { + SecretSpecError::ProviderOperationFailed(format!( + "Failed to update secret in Infisical: {}", + e + )) + })?; + } + Err(_) => { + // Secret doesn't exist, create it + let create_request = + CreateSecretRequest::builder(&secret_key, &value, &project_id, environment) + .path(path) + .secret_comment(&format!( + "SecretSpec managed secret for {}/{}", + project, key + )) + .build(); + + client.secrets().create(create_request).await.map_err(|e| { + SecretSpecError::ProviderOperationFailed(format!( + "Failed to create secret in Infisical: {}", + e + )) + })?; + } + } + + Ok(()) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_infisical_config_from_url() { + // Test basic client credentials + let url = Url::parse("infisical://?client_id=xxx&client_secret=yyy").unwrap(); + let config = InfisicalConfig::try_from(&url).unwrap(); + assert_eq!(config.client_id, Some("xxx".to_string())); + assert_eq!(config.client_secret, Some("yyy".to_string())); + assert_eq!(config.api_url, None); + assert_eq!(config.project_id, None); + + // Test with custom API URL and project + let url = + Url::parse("infisical://app.infisical.com/project-123?client_id=aaa&client_secret=bbb") + .unwrap(); + let config = InfisicalConfig::try_from(&url).unwrap(); + assert_eq!(config.client_id, Some("aaa".to_string())); + assert_eq!(config.client_secret, Some("bbb".to_string())); + assert_eq!( + config.api_url, + Some("https://app.infisical.com".to_string()) + ); + assert_eq!(config.project_id, Some("project-123".to_string())); + + // Test with path prefix + let url = Url::parse("infisical://app.infisical.com/project-123/production/backend?client_id=ccc&client_secret=ddd").unwrap(); + let config = InfisicalConfig::try_from(&url).unwrap(); + assert_eq!(config.client_id, Some("ccc".to_string())); + assert_eq!(config.client_secret, Some("ddd".to_string())); + assert_eq!(config.project_id, Some("project-123".to_string())); + assert_eq!(config.path_prefix, Some("/production/backend".to_string())); + + // Test with port + let url = + Url::parse("infisical://localhost:8080/project-456?client_id=eee&client_secret=fff") + .unwrap(); + let config = InfisicalConfig::try_from(&url).unwrap(); + assert_eq!(config.api_url, Some("https://localhost:8080".to_string())); + assert_eq!(config.project_id, Some("project-456".to_string())); + } + + #[test] + fn test_secret_key_construction() { + // We can't easily create a real provider without authentication, + // so we'll test the key construction logic separately + + // Test basic key construction + assert_eq!( + format!( + "SECRETSPEC_{}_{}", + "myapp".to_uppercase().replace("-", "_"), + "API_KEY" + ), + "SECRETSPEC_MYAPP_API_KEY" + ); + + // Test with hyphens in project name + assert_eq!( + format!( + "SECRETSPEC_{}_{}", + "my-app".to_uppercase().replace("-", "_"), + "DATABASE_URL" + ), + "SECRETSPEC_MY_APP_DATABASE_URL" + ); + + // Test with lowercase key + assert_eq!( + format!( + "SECRETSPEC_{}_{}", + "service".to_uppercase().replace("-", "_"), + "api_key" + ), + "SECRETSPEC_SERVICE_api_key" + ); + } +} diff --git a/secretspec/src/provider/keyring.rs b/secretspec/src/provider/keyring.rs index 846c8ee..c44cecf 100644 --- a/secretspec/src/provider/keyring.rs +++ b/secretspec/src/provider/keyring.rs @@ -80,8 +80,8 @@ impl KeyringProvider { /// # Returns /// /// A new instance of KeyringProvider - pub fn new(config: KeyringConfig) -> Self { - Self { config } + pub fn new(config: KeyringConfig) -> crate::Result { + Ok(Self { config }) } } diff --git a/secretspec/src/provider/lastpass.rs b/secretspec/src/provider/lastpass.rs index 7eacd0f..7e5b36a 100644 --- a/secretspec/src/provider/lastpass.rs +++ b/secretspec/src/provider/lastpass.rs @@ -144,8 +144,8 @@ impl LastPassProvider { /// # Arguments /// /// * `config` - The LastPass configuration to use - pub fn new(config: LastPassConfig) -> Self { - Self { config } + pub fn new(config: LastPassConfig) -> crate::Result { + Ok(Self { config }) } /// Executes a LastPass CLI command and returns its output. @@ -430,6 +430,6 @@ impl Default for LastPassProvider { /// /// This is equivalent to calling `LastPassProvider::new(LastPassConfig::default())`. fn default() -> Self { - Self::new(LastPassConfig::default()) + Self::new(LastPassConfig::default()).expect("Failed to create default LastPassProvider") } } diff --git a/secretspec/src/provider/macros.rs b/secretspec/src/provider/macros.rs index b16e0b9..9b110fb 100644 --- a/secretspec/src/provider/macros.rs +++ b/secretspec/src/provider/macros.rs @@ -57,7 +57,7 @@ macro_rules! register_provider { schemes: &[$($scheme,)*], factory: |url| { let config = <$config_type>::try_from(url)?; - Ok(Box::new(<$struct_name>::new(config))) + <$struct_name>::new(config).map(|p| Box::new(p) as Box) }, }; }; diff --git a/secretspec/src/provider/mod.rs b/secretspec/src/provider/mod.rs index 73649a5..f3ac9d3 100644 --- a/secretspec/src/provider/mod.rs +++ b/secretspec/src/provider/mod.rs @@ -18,6 +18,7 @@ //! - [`KeyringProvider`]: System keyring integration (default) //! - [`DotEnvProvider`]: `.env` file support //! - [`EnvProvider`]: Environment variables (read-only) +//! - [`InfisicalProvider`]: Infisical secrets management platform //! - [`OnePasswordProvider`]: OnePassword integration //! - [`LastPassProvider`]: LastPass integration //! @@ -28,6 +29,7 @@ //! ```text //! keyring:// //! dotenv://.env.production +//! infisical://project-id?client_id=xxx&client_secret=yyy //! onepassword://vault/items //! lastpass://folder //! ``` @@ -56,6 +58,8 @@ use url::Url; pub mod dotenv; pub mod env; +#[cfg(feature = "infisical")] +pub mod infisical; #[cfg(feature = "keyring")] pub mod keyring; pub mod lastpass; diff --git a/secretspec/src/provider/onepassword.rs b/secretspec/src/provider/onepassword.rs index 97904f9..217610d 100644 --- a/secretspec/src/provider/onepassword.rs +++ b/secretspec/src/provider/onepassword.rs @@ -232,8 +232,8 @@ impl OnePasswordProvider { /// # Arguments /// /// * `config` - The configuration for the provider - pub fn new(config: OnePasswordConfig) -> Self { - Self { config } + pub fn new(config: OnePasswordConfig) -> crate::Result { + Ok(Self { config }) } /// Executes a OnePassword CLI command with proper error handling. @@ -570,5 +570,6 @@ impl Default for OnePasswordProvider { /// Uses interactive authentication and the "Private" vault by default. fn default() -> Self { Self::new(OnePasswordConfig::default()) + .expect("Failed to create default OnePasswordProvider") } } diff --git a/secretspec/src/provider/tests.rs b/secretspec/src/provider/tests.rs index 1aafbc8..bcc60d4 100644 --- a/secretspec/src/provider/tests.rs +++ b/secretspec/src/provider/tests.rs @@ -155,6 +155,13 @@ fn test_documentation_examples() { let provider = Box::::try_from("lastpass://folder").unwrap(); assert_eq!(provider.name(), "lastpass"); + // Test infisical provider + #[cfg(feature = "infisical")] + { + let provider = Box::::try_from("infisical://?token=st.xxxx").unwrap(); + assert_eq!(provider.name(), "infisical"); + } + // Test dotenv examples from provider list let provider = Box::::try_from("dotenv://path").unwrap(); assert_eq!(provider.name(), "dotenv");