From df6f7b4a4ef669714ece0f632bd0673e6e95f8f4 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:50:10 +0700 Subject: [PATCH 1/4] fix(auth): enable real OS keyring backends for secure-storage keyring 3.x has no default platform stores and falls back to an in-memory mock, so --features secure-storage did not persist secrets on macOS/Windows/Linux (issue #33). Wire apple-native, windows-native, and linux-native-sync-persistent, reject the mock backend on writes, and stop blaming code-signing for mock readback failures. Closes #33 --- Cargo.lock | 721 ++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 +- README.md | 2 +- src/commands/auth.rs | 11 +- src/keyring.rs | 175 ++++++----- 5 files changed, 831 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 347a922..d32266a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -76,6 +87,123 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[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" @@ -109,6 +237,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -121,12 +271,27 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.51" @@ -163,6 +328,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.53" @@ -228,6 +403,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.11" @@ -276,6 +460,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + [[package]] name = "crypto-common" version = "0.1.7" @@ -307,6 +497,35 @@ dependencies = [ "memchr", ] +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "aes", + "block-padding", + "cbc", + "dbus", + "fastrand", + "hkdf", + "num", + "once_cell", + "sha2", + "zeroize", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -378,6 +597,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -394,6 +640,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -469,6 +736,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -578,12 +858,27 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -833,6 +1128,16 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -896,7 +1201,14 @@ version = "3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" dependencies = [ + "byteorder", + "dbus-secret-service", + "linux-keyutils", "log", + "secret-service", + "security-framework 2.11.1", + "security-framework 3.6.0", + "windows-sys 0.60.2", "zeroize", ] @@ -912,6 +1224,15 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libredox" version = "0.1.12" @@ -953,6 +1274,16 @@ dependencies = [ "url", ] +[[package]] +name = "linux-keyutils" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -992,6 +1323,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1009,6 +1349,82 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[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.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1053,6 +1469,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "papergrid" version = "0.17.0" @@ -1064,6 +1490,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1111,6 +1543,37 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1129,6 +1592,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.6+spec-1.1.0", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1438,7 +1910,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.6.0", ] [[package]] @@ -1489,6 +1961,38 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand 0.8.6", + "serde", + "sha2", + "zbus", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.6.0" @@ -1555,6 +2059,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1576,6 +2091,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1637,6 +2163,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -1871,8 +2403,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -1884,6 +2416,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -1893,9 +2434,30 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.14", +] + +[[package]] +name = "toml_edit" +version = "0.25.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0db3bae107c9522f86d361697dee1d7386a2ddcf659d5aea5159819a21a3c4a7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.4", ] [[package]] @@ -1956,9 +2518,21 @@ 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" @@ -1980,6 +2554,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.60.2", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -2453,6 +3038,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -2465,6 +3059,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "yoke" version = "0.8.1" @@ -2488,6 +3092,62 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand 0.8.6", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.38" @@ -2534,6 +3194,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -2573,3 +3247,40 @@ name = "zmij" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 2e28078..b84a3bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ chrono = { version = "0.4", features = ["serde"] } regex = "1" rand = "0.8" csv = "1" -keyring = { version = "3", optional = true } +keyring = { version = "3", optional = true, default-features = false, features = ["apple-native", "windows-native", "linux-native-sync-persistent", "crypto-rust"] } sha2 = "0.10" open = "5" url = "2" diff --git a/README.md b/README.md index ed8c9f4..22aed2b 100644 --- a/README.md +++ b/README.md @@ -439,7 +439,7 @@ linear-cli auth revoke # Revoke OAuth tokens linear-cli auth logout # Remove stored credentials ``` -> On macOS, `--secure` works best with an official signed release binary. Locally built or frequently rebuilt CLI binaries can trigger repeated Keychain prompts and may fail keychain readback verification. If that happens, use plain `linear-cli auth oauth` or `LINEAR_API_KEY` instead. +> `--secure` needs a build with `--features secure-storage` (enables OS backends: Keychain, Credential Manager, Secret Service). On macOS Keychain, unsigned local builds can still prompt repeatedly or fail readback — if that happens, use an official release, plain `linear-cli auth oauth`, or `LINEAR_API_KEY`. **Auth priority:** `LINEAR_API_KEY` env var > OS keyring > OAuth tokens > config file API key. diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 6b47142..96d1a71 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -544,9 +544,14 @@ async fn oauth_login( #[cfg(feature = "secure-storage")] if secure { - config::save_oauth_config_secure(&profile, &oauth_config).context( - "Secure OAuth storage failed. On macOS, locally built or unsigned binaries may not be able to read back Keychain items. Try the official signed release or fall back to plain `linear-cli auth oauth`.", - )?; + config::save_oauth_config_secure(&profile, &oauth_config).with_context(|| { + format!( + "Secure OAuth storage failed using {}. \ + If this build is on keyring's mock backend, rebuild with platform keyring features. \ + On macOS Keychain only: unsigned local builds can also fail readback — try an official release or plain `linear-cli auth oauth`.", + crate::keyring::backend_name() + ) + })?; if output.is_json() || output.has_template() { print_json_owned( diff --git a/src/keyring.rs b/src/keyring.rs index 40b3413..5fa3376 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -1,34 +1,77 @@ //! Secure API key storage using OS keyring. //! //! This module provides cross-platform credential storage: -//! - macOS: Keychain -//! - Windows: Credential Manager -//! - Linux: Secret Service (requires D-Bus and a keyring daemon) +//! - macOS: Keychain (`apple-native`) +//! - Windows: Credential Manager (`windows-native`) +//! - Linux: keyutils + Secret Service (`linux-native-sync-persistent`) +//! +//! keyring 3.x has no default platform backends. Without those features it +//! silently uses an in-memory mock store — see issue #33. -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; const SERVICE_NAME: &str = "linear-cli"; +const OAUTH_SERVICE_NAME: &str = "linear-cli-oauth"; -/// Get an API key from the keyring for a profile. -/// Returns Ok(None) if no key is stored, Ok(Some(key)) if found. -pub fn get_key(profile: &str) -> Result> { - let entry = - keyring::Entry::new(SERVICE_NAME, profile).context("Failed to create keyring entry")?; +fn entry(service: &str, profile: &str) -> Result { + keyring::Entry::new(service, profile).context("Failed to create keyring entry") +} + +fn is_mock_entry(entry: &keyring::Entry) -> bool { + entry + .get_credential() + .downcast_ref::() + .is_some() +} + +/// Human-readable name of the expected OS backend on this target. +pub fn backend_name() -> &'static str { + if cfg!(target_os = "macos") { + "macOS Keychain" + } else if cfg!(target_os = "windows") { + "Windows Credential Manager" + } else if cfg!(target_os = "linux") { + "Linux keyutils + Secret Service" + } else { + "platform credential store" + } +} + +/// Reject keyring's in-memory mock backend (non-persistent). +pub fn require_persistent_backend() -> Result<()> { + let probe = entry(SERVICE_NAME, "__linear_cli_backend_probe__")?; + if is_mock_entry(&probe) { + bail!( + "secure-storage is using keyring's in-memory mock backend instead of {backend}. \ + Credentials will not persist. Rebuild with `--features secure-storage` after \ + enabling keyring platform features (apple-native, windows-native, \ + linux-native-sync-persistent).", + backend = backend_name() + ); + } + Ok(()) +} + +fn get_secret(service: &str, profile: &str, fallback_label: &str) -> Result> { + let entry = entry(service, profile)?; + if is_mock_entry(&entry) { + // Treat mock as missing so auth falls through; writes must call require_persistent_backend. + return Ok(None); + } match entry.get_password() { Ok(password) => Ok(Some(password)), Err(keyring::Error::NoEntry) => Ok(None), Err(keyring::Error::NoStorageAccess(_)) => { if !crate::output::is_quiet() { - eprintln!("Warning: Keyring not available, falling back to config file"); + eprintln!("Warning: Keyring not available, falling back to {fallback_label}"); } Ok(None) } Err(e) => { if !crate::output::is_quiet() { eprintln!( - "Warning: Keyring error ({}), falling back to config file", - e + "Warning: Keyring error ({e}), falling back to {fallback_label}" ); } Ok(None) @@ -36,88 +79,69 @@ pub fn get_key(profile: &str) -> Result> { } } -/// Store an API key in the keyring for a profile. -pub fn set_key(profile: &str, api_key: &str) -> Result<()> { - let entry = - keyring::Entry::new(SERVICE_NAME, profile).context("Failed to create keyring entry")?; +fn set_secret(service: &str, profile: &str, secret: &str) -> Result<()> { + require_persistent_backend()?; + entry(service, profile)? + .set_password(secret) + .with_context(|| format!("Failed to store secret in {}", backend_name()))?; + Ok(()) +} - entry - .set_password(api_key) - .context("Failed to store API key in keyring")?; +fn delete_secret(service: &str, profile: &str) -> Result<()> { + let entry = entry(service, profile)?; + if is_mock_entry(&entry) { + return Ok(()); + } + match entry.delete_credential() { + Ok(()) => Ok(()), + Err(keyring::Error::NoEntry) => Ok(()), + Err(e) => Err(e).with_context(|| format!("Failed to delete secret from {}", backend_name())), + } +} - Ok(()) +/// Get an API key from the keyring for a profile. +/// Returns Ok(None) if no key is stored, Ok(Some(key)) if found. +pub fn get_key(profile: &str) -> Result> { + get_secret(SERVICE_NAME, profile, "config file") +} + +/// Store an API key in the keyring for a profile. +pub fn set_key(profile: &str, api_key: &str) -> Result<()> { + set_secret(SERVICE_NAME, profile, api_key) } /// Delete an API key from the keyring for a profile. /// Returns Ok(()) even if no key was stored. pub fn delete_key(profile: &str) -> Result<()> { - let entry = - keyring::Entry::new(SERVICE_NAME, profile).context("Failed to create keyring entry")?; - - match entry.delete_credential() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => Ok(()), // Already gone, that's fine - Err(e) => Err(e).context("Failed to delete API key from keyring"), - } + delete_secret(SERVICE_NAME, profile) } -/// Check if keyring is available on this system. +/// Check if a durable OS keyring backend is available. pub fn is_available() -> bool { - // Try to create an entry and check if we can access it - match keyring::Entry::new(SERVICE_NAME, "__test__") { + match entry(SERVICE_NAME, "__test__") { Ok(entry) => { - // Try a non-destructive operation - match entry.get_password() { - Err(keyring::Error::NoStorageAccess(_)) => false, - _ => true, // NoEntry or Ok means storage is accessible + if is_mock_entry(&entry) { + return false; } + !matches!(entry.get_password(), Err(keyring::Error::NoStorageAccess(_))) } Err(_) => false, } } -const OAUTH_SERVICE_NAME: &str = "linear-cli-oauth"; - /// Get OAuth tokens JSON from keyring for a profile pub fn get_oauth_tokens(profile: &str) -> Result> { - let entry = keyring::Entry::new(OAUTH_SERVICE_NAME, profile) - .context("Failed to create keyring entry")?; - - match entry.get_password() { - Ok(json) => Ok(Some(json)), - Err(keyring::Error::NoEntry) => Ok(None), - Err(keyring::Error::NoStorageAccess(_)) => Ok(None), - Err(e) => { - if !crate::output::is_quiet() { - eprintln!( - "Warning: Keyring OAuth error ({}), falling back to config", - e - ); - } - Ok(None) - } - } + get_secret(OAUTH_SERVICE_NAME, profile, "config") } /// Store OAuth tokens JSON in keyring for a profile pub fn set_oauth_tokens(profile: &str, json: &str) -> Result<()> { - let entry = keyring::Entry::new(OAUTH_SERVICE_NAME, profile) - .context("Failed to create keyring entry")?; - entry - .set_password(json) - .context("Failed to store OAuth tokens in keyring")?; - Ok(()) + set_secret(OAUTH_SERVICE_NAME, profile, json) } /// Delete OAuth tokens from keyring for a profile pub fn delete_oauth_tokens(profile: &str) -> Result<()> { - let entry = keyring::Entry::new(OAUTH_SERVICE_NAME, profile) - .context("Failed to create keyring entry")?; - match entry.delete_credential() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => Ok(()), - Err(e) => Err(e).context("Failed to delete OAuth tokens from keyring"), - } + delete_secret(OAUTH_SERVICE_NAME, profile) } #[cfg(test)] @@ -127,11 +151,22 @@ mod tests { const TEST_PROFILE: &str = "linear-cli-test-profile"; const TEST_KEY: &str = "lin_api_test_key_12345"; + #[test] + fn default_backend_is_not_mock() { + let probe = entry(SERVICE_NAME, "__backend_test__").expect("create probe entry"); + assert!( + !is_mock_entry(&probe), + "secure-storage must not use keyring's mock backend; enable apple-native, windows-native, and linux-native-sync-persistent on the keyring dependency" + ); + } + #[test] fn test_is_available() { - // Just check it doesn't panic - availability depends on system - let available = is_available(); - println!("Keyring available: {}", available); + // Availability depends on OS services; mock must never count as available. + if is_mock_entry(&entry(SERVICE_NAME, "__test__").expect("probe")) { + assert!(!is_available()); + } + let _ = is_available(); } #[test] From 8a3f0a523fbb4def0114b6c0f2606522f147726d Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:53:53 +0700 Subject: [PATCH 2/4] fix(ci): build and test secure-storage with platform keyring deps CI and release never enabled --features secure-storage, so the mock-backend regression guard could not run and official binaries lacked OS keyring support. Install libdbus on Linux, add Cross.toml for aarch64, and enable the feature on test/clippy/release builds. --- .github/workflows/ci.yml | 18 +++++++++++++++--- .github/workflows/release.yml | 8 ++++++-- Cross.toml | 5 +++++ docs/manual-release.md | 16 +++++++++++----- src/commands/auth.rs | 3 +-- src/keyring.rs | 2 +- 6 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 Cross.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88cf73d..d76b4ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Linux keyring build deps + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config + - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -27,13 +31,18 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Run tests - run: cargo test --verbose + run: cargo test --verbose --features secure-storage - name: Check formatting run: cargo fmt --check - name: Run clippy - run: cargo clippy -- -D warnings + run: cargo clippy --features secure-storage -- -D warnings + + # Keep a default-features build green so crates.io installs without + # secure-storage still compile. + - name: Build without secure-storage + run: cargo build --verbose build: name: Build @@ -41,6 +50,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Linux keyring build deps + run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config + - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -49,4 +61,4 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Build release - run: cargo build --release + run: cargo build --release --features secure-storage diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a724c0d..a773c83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,10 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Install Linux keyring build deps + if: runner.os == 'Linux' && !matrix.use_cross + run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config + - name: Install Rust uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable @@ -47,11 +51,11 @@ jobs: - name: Build (cross) if: matrix.use_cross - run: cross build --release --target ${{ matrix.target }} + run: cross build --release --features secure-storage --target ${{ matrix.target }} - name: Build (native) if: ${{ !matrix.use_cross }} - run: cargo build --release --target ${{ matrix.target }} + run: cargo build --release --features secure-storage --target ${{ matrix.target }} - name: Package (Unix) if: runner.os != 'Windows' diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..428c24f --- /dev/null +++ b/Cross.toml @@ -0,0 +1,5 @@ +[target.aarch64-unknown-linux-gnu] +pre-build = [ + "dpkg --add-architecture $CROSS_DEB_ARCH", + "apt-get update && apt-get --assume-yes install libdbus-1-dev:$CROSS_DEB_ARCH pkg-config", +] diff --git a/docs/manual-release.md b/docs/manual-release.md index 48da67d..750992d 100644 --- a/docs/manual-release.md +++ b/docs/manual-release.md @@ -86,22 +86,28 @@ Confirm crates.io shows `0.3.16` before moving on. ## Releasing `v0.3.17` and newer -Current `master` uses `reqwest` with `rustls`, so the Linux aarch64 build no longer depends on target OpenSSL packages. +Current `master` uses `reqwest` with `rustls`. Official binaries should build with +`--features secure-storage` so Keychain / Credential Manager / Secret Service work. + +Linux builds need `libdbus-1-dev` (and `pkg-config`). For aarch64 cross builds, use the +repo-root `Cross.toml` which installs `libdbus-1-dev:$CROSS_DEB_ARCH`. ### Local builds ```bash -cargo build --release --target x86_64-unknown-linux-gnu +sudo apt-get install -y libdbus-1-dev pkg-config # Linux hosts only + +cargo build --release --features secure-storage --target x86_64-unknown-linux-gnu tar -C target/x86_64-unknown-linux-gnu/release -czf linear-cli-x86_64-unknown-linux-gnu.tar.gz linear-cli -cross build --release --target aarch64-unknown-linux-gnu +cross build --release --features secure-storage --target aarch64-unknown-linux-gnu tar -C target/aarch64-unknown-linux-gnu/release -czf linear-cli-aarch64-unknown-linux-gnu.tar.gz linear-cli -cargo xwin build --release --target x86_64-pc-windows-msvc +cargo xwin build --release --features secure-storage --target x86_64-pc-windows-msvc cd target/x86_64-pc-windows-msvc/release && 7z a ../../../linear-cli-x86_64-pc-windows-msvc.zip linear-cli.exe ``` -Build both Apple targets on a Mac with the same commands from the previous section. +Build both Apple targets on a Mac with `--features secure-storage` as well. ### Publish first, then release diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 96d1a71..6805151 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -547,8 +547,7 @@ async fn oauth_login( config::save_oauth_config_secure(&profile, &oauth_config).with_context(|| { format!( "Secure OAuth storage failed using {}. \ - If this build is on keyring's mock backend, rebuild with platform keyring features. \ - On macOS Keychain only: unsigned local builds can also fail readback — try an official release or plain `linear-cli auth oauth`.", + On macOS Keychain, unsigned local builds can fail readback — try an official release or plain `linear-cli auth oauth`.", crate::keyring::backend_name() ) })?; diff --git a/src/keyring.rs b/src/keyring.rs index 5fa3376..985d129 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -38,7 +38,7 @@ pub fn backend_name() -> &'static str { } /// Reject keyring's in-memory mock backend (non-persistent). -pub fn require_persistent_backend() -> Result<()> { +fn require_persistent_backend() -> Result<()> { let probe = entry(SERVICE_NAME, "__linear_cli_backend_probe__")?; if is_mock_entry(&probe) { bail!( From 931f7ca88bf2bcc68f3db9e7294917394bcd11d7 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:55:45 +0700 Subject: [PATCH 3/4] style: cargo fmt --- src/commands/setup.rs | 5 +---- src/config.rs | 11 +++++------ src/keyring.rs | 13 ++++++++----- src/main.rs | 32 +++++--------------------------- 4 files changed, 19 insertions(+), 42 deletions(-) diff --git a/src/commands/setup.rs b/src/commands/setup.rs index f064556..96dbba4 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -75,10 +75,7 @@ pub async fn handle(output: &OutputOptions) -> Result<()> { if let Some(ref key) = saved_team { config::set_default_team(Some(key))?; println!(" Default team saved: {}", key); - println!( - " Tip: Override with -t {} or LINEAR_CLI_TEAM={}", - key, key - ); + println!(" Tip: Override with -t {} or LINEAR_CLI_TEAM={}", key, key); } else { println!(" Invalid selection, skipping."); } diff --git a/src/config.rs b/src/config.rs index 0745cec..3d42561 100644 --- a/src/config.rs +++ b/src/config.rs @@ -806,9 +806,7 @@ mod tests { token_type: "Bearer".to_string(), scopes: vec!["read".to_string(), "write".to_string()], }); - config - .workspaces - .insert("oauth-test".to_string(), oauth_ws); + config.workspaces.insert("oauth-test".to_string(), oauth_ws); let toml_str = toml::to_string_pretty(&config).unwrap(); let parsed: Config = toml::from_str(&toml_str).unwrap(); @@ -829,9 +827,10 @@ mod tests { current: Some("default".to_string()), ..Default::default() }; - config - .workspaces - .insert("default".to_string(), Workspace::with_api_key("lin_api_key")); + config.workspaces.insert( + "default".to_string(), + Workspace::with_api_key("lin_api_key"), + ); let toml_str = toml::to_string_pretty(&config).unwrap(); assert!( diff --git a/src/keyring.rs b/src/keyring.rs index 985d129..d71adcf 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -70,9 +70,7 @@ fn get_secret(service: &str, profile: &str, fallback_label: &str) -> Result { if !crate::output::is_quiet() { - eprintln!( - "Warning: Keyring error ({e}), falling back to {fallback_label}" - ); + eprintln!("Warning: Keyring error ({e}), falling back to {fallback_label}"); } Ok(None) } @@ -95,7 +93,9 @@ fn delete_secret(service: &str, profile: &str) -> Result<()> { match entry.delete_credential() { Ok(()) => Ok(()), Err(keyring::Error::NoEntry) => Ok(()), - Err(e) => Err(e).with_context(|| format!("Failed to delete secret from {}", backend_name())), + Err(e) => { + Err(e).with_context(|| format!("Failed to delete secret from {}", backend_name())) + } } } @@ -123,7 +123,10 @@ pub fn is_available() -> bool { if is_mock_entry(&entry) { return false; } - !matches!(entry.get_password(), Err(keyring::Error::NoStorageAccess(_))) + !matches!( + entry.get_password(), + Err(keyring::Error::NoStorageAccess(_)) + ) } Err(_) => false, } diff --git a/src/main.rs b/src/main.rs index d0a29bc..900be8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1162,9 +1162,7 @@ async fn run_command( Commands::Templates { action } => templates::handle(action, output).await?, Commands::Time { action } => time::handle(action, output).await?, Commands::Uploads { action } => uploads::handle(action).await?, - Commands::Interactive { team } => { - interactive::run(config::resolve_team_arg(team)).await? - } + Commands::Interactive { team } => interactive::run(config::resolve_team_arg(team)).await?, Commands::Context => handle_context(output, agent_opts, retry).await?, Commands::Favorites { action } => favorites::handle(action, output).await?, Commands::Roadmaps { action } => { @@ -1592,31 +1590,11 @@ mod tests { #[test] fn test_should_use_pager_disabled_for_interactive_commands() { - assert!(!should_use_pager( - false, - &OutputFormat::Table, - false, - true - )); + assert!(!should_use_pager(false, &OutputFormat::Table, false, true)); // Non-interactive table on TTY would still depend on is_terminal; quiet/json off. - assert!(!should_use_pager( - true, - &OutputFormat::Table, - false, - false - )); - assert!(!should_use_pager( - false, - &OutputFormat::Json, - false, - false - )); - assert!(!should_use_pager( - false, - &OutputFormat::Table, - true, - false - )); + assert!(!should_use_pager(true, &OutputFormat::Table, false, false)); + assert!(!should_use_pager(false, &OutputFormat::Json, false, false)); + assert!(!should_use_pager(false, &OutputFormat::Table, true, false)); } #[test] From 721533b77bd28511ce74d685152804047f0a5267 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:59:16 +0700 Subject: [PATCH 4/4] fix(keyring): treat missing-delete platform errors as success --- src/keyring.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/keyring.rs b/src/keyring.rs index d71adcf..9408e6a 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -91,10 +91,15 @@ fn delete_secret(service: &str, profile: &str) -> Result<()> { return Ok(()); } match entry.delete_credential() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => Ok(()), + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), Err(e) => { - Err(e).with_context(|| format!("Failed to delete secret from {}", backend_name())) + // Some backends (notably Linux keyutils) error with a platform code + // instead of NoEntry when nothing is stored. Treat "already gone" as success. + match entry.get_password() { + Err(keyring::Error::NoEntry) => Ok(()), + _ => Err(e) + .with_context(|| format!("Failed to delete secret from {}", backend_name())), + } } } }