Skip to content

Commit fa452dd

Browse files
authored
Merge pull request #88 from auths-dev/fn-70
chore: lint sync guard, WitnessConfig versioning, new_unchecked audit, error boundary fixes
2 parents f0f982e + ab400e2 commit fa452dd

61 files changed

Lines changed: 628 additions & 130 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
key: ${{ runner.os }}-clippy-${{ hashFiles('**/Cargo.lock') }}
5353
restore-keys: ${{ runner.os }}-clippy-
5454
- run: cargo clippy --all-targets --all-features -- -D warnings
55+
- run: cargo run -p xtask -- check-clippy-sync
5556

5657
schemas:
5758
name: Schema validation

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ repos:
4242
files: (crates/auths-cli/src/|crates/xtask/src/gen_docs|docs/cli/commands/)
4343
pass_filenames: false
4444

45+
- id: check-clippy-sync
46+
name: cargo xtask check-clippy-sync
47+
entry: cargo run --package xtask -- check-clippy-sync
48+
language: system
49+
files: clippy\.toml$
50+
pass_filenames: false
51+
4552
- id: cargo-deny
4653
name: cargo deny (licenses + bans)
4754
entry: bash -c 'cargo deny check > .cargo/cargo-deny.log 2>&1; exit $?'

crates/auths-cli/src/commands/device/authorization.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ pub fn handle_device(
272272
&ctx,
273273
&auths_core::ports::clock::SystemClock,
274274
)
275-
.map_err(|e| anyhow!("{e}"))?;
275+
.map_err(anyhow::Error::new)?;
276276

277277
display_link_result(&result, &device_did)
278278
}
@@ -301,7 +301,7 @@ pub fn handle_device(
301301
note,
302302
&auths_core::ports::clock::SystemClock,
303303
)
304-
.map_err(|e| anyhow!("{e}"))?;
304+
.map_err(anyhow::Error::new)?;
305305

306306
display_revoke_result(&device_did, &repo_path)
307307
}

crates/auths-cli/src/commands/init/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,8 @@ mod tests {
437437
registry: DEFAULT_REGISTRY_URL.to_string(),
438438
skip_registration: false,
439439
};
440-
// In test context, stdin is not a TTY
441-
assert!(!resolve_interactive(&cmd).unwrap());
440+
// Auto-detect returns is_terminal() — result depends on environment
441+
let result = resolve_interactive(&cmd).unwrap();
442+
assert_eq!(result, std::io::stdin().is_terminal());
442443
}
443444
}

crates/auths-cli/src/commands/verify_commit.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,8 @@ mod tests {
919919

920920
#[tokio::test]
921921
async fn verify_bundle_chain_empty_chain() {
922+
#[allow(clippy::disallowed_methods)]
923+
// INVARIANT: test-only hardcoded DID and hex string literals
922924
let bundle = IdentityBundle {
923925
identity_did: auths_verifier::IdentityDID::new_unchecked("did:keri:test"),
924926
public_key_hex: auths_verifier::PublicKeyHex::new_unchecked("aa".repeat(32)),
@@ -935,6 +937,8 @@ mod tests {
935937

936938
#[tokio::test]
937939
async fn verify_bundle_chain_invalid_hex() {
940+
#[allow(clippy::disallowed_methods)]
941+
// INVARIANT: test-only hardcoded DID, hex, and canonical DID string literals
938942
let bundle = IdentityBundle {
939943
identity_did: auths_verifier::IdentityDID::new_unchecked("did:keri:test"),
940944
public_key_hex: auths_verifier::PublicKeyHex::new_unchecked("not_hex"),

crates/auths-cli/src/errors/renderer.rs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ pub fn render_error(err: &Error, json_mode: bool) {
2828
}
2929

3030
/// Try to extract `AuthsErrorInfo` from an `anyhow::Error` by downcasting
31-
/// through all known error types.
31+
/// through all known error types. Walks the full error chain so that
32+
/// `.with_context()` wrapping doesn't hide typed errors.
3233
fn extract_error_info(err: &Error) -> Option<(&str, &str, Option<&str>)> {
3334
macro_rules! try_downcast {
34-
($err:expr, $($ty:ty),+ $(,)?) => {
35+
($source:expr, $($ty:ty),+ $(,)?) => {
3536
$(
36-
if let Some(e) = $err.downcast_ref::<$ty>() {
37+
if let Some(e) = $source.downcast_ref::<$ty>() {
3738
let code = AuthsErrorInfo::error_code(e);
3839
let msg = format!("{e}");
3940
// SAFETY: we leak the String to get a &'static str because
@@ -46,21 +47,23 @@ fn extract_error_info(err: &Error) -> Option<(&str, &str, Option<&str>)> {
4647
};
4748
}
4849

49-
try_downcast!(
50-
err,
51-
AgentError,
52-
AttestationError,
53-
SetupError,
54-
DeviceError,
55-
DeviceExtensionError,
56-
RotationError,
57-
RegistrationError,
58-
McpAuthError,
59-
OrgError,
60-
ApprovalError,
61-
AllowedSignersError,
62-
SigningError,
63-
);
50+
for cause in err.chain() {
51+
try_downcast!(
52+
cause,
53+
AgentError,
54+
AttestationError,
55+
SetupError,
56+
DeviceError,
57+
DeviceExtensionError,
58+
RotationError,
59+
RegistrationError,
60+
McpAuthError,
61+
OrgError,
62+
ApprovalError,
63+
AllowedSignersError,
64+
SigningError,
65+
);
66+
}
6467

6568
None
6669
}
@@ -248,4 +251,11 @@ mod tests {
248251
assert_eq!(code, "AUTHS-E3001");
249252
assert!(suggestion.is_some());
250253
}
254+
255+
#[test]
256+
fn extract_error_info_walks_chain_through_context() {
257+
let err: Error = Error::new(AgentError::KeyNotFound).context("operation failed");
258+
let (code, _, _) = extract_error_info(&err).unwrap();
259+
assert_eq!(code, "AUTHS-E3001");
260+
}
251261
}

crates/auths-core/clippy.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ disallowed-methods = [
1212
{ path = "std::env::var", reason = "use EnvironmentConfig abstraction instead of reading env vars directly", allow-invalid = true },
1313
{ path = "uuid::Uuid::new_v4", reason = "Use UuidProvider::new_id() instead. Inject SystemUuidProvider in production and DeterministicUuidProvider in tests." },
1414

15+
# === DID/newtype construction: prefer parse() for external input ===
16+
{ path = "auths_verifier::types::IdentityDID::new_unchecked", reason = "Use IdentityDID::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true },
17+
{ path = "auths_verifier::types::DeviceDID::new_unchecked", reason = "Use DeviceDID::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true },
18+
{ path = "auths_verifier::types::CanonicalDid::new_unchecked", reason = "Use CanonicalDid::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true },
19+
{ path = "auths_verifier::core::CommitOid::new_unchecked", reason = "Use CommitOid::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true },
20+
{ path = "auths_verifier::core::PublicKeyHex::new_unchecked", reason = "Use PublicKeyHex::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true },
21+
1522
# === Sans-IO: filesystem ===
1623
{ path = "std::fs::read", reason = "sans-IO crate — use a port trait" },
1724
{ path = "std::fs::read_to_string", reason = "sans-IO crate — use a port trait" },

crates/auths-core/src/api/ffi.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ pub unsafe extern "C" fn ffi_import_key(
381381
}
382382
};
383383

384+
#[allow(clippy::disallowed_methods)]
385+
// INVARIANT: validated with starts_with("did:") guard above
384386
let did_string = IdentityDID::new_unchecked(did_str.to_string());
385387
let alias = KeyAlias::new_unchecked(alias_str);
386388

crates/auths-core/src/error.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,16 +174,27 @@ impl AuthsErrorInfo for AgentError {
174174
Some("Run the command again and provide the required input")
175175
}
176176
Self::StorageError(_) => Some("Check file permissions and disk space"),
177-
// These errors typically don't have actionable suggestions
178-
Self::SecurityError(_)
179-
| Self::CryptoError(_)
180-
| Self::KeyDeserializationError(_)
181-
| Self::SigningFailed(_)
182-
| Self::Proto(_)
183-
| Self::IO(_)
184-
| Self::InvalidInput(_)
185-
| Self::MutexError(_)
186-
| Self::CredentialTooLarge { .. } => None,
177+
Self::SecurityError(_) => Some(
178+
"Run `auths doctor` to check system keychain access and security configuration",
179+
),
180+
Self::CryptoError(_) => {
181+
Some("A cryptographic operation failed; check key material with `auths key list`")
182+
}
183+
Self::KeyDeserializationError(_) => {
184+
Some("The stored key is corrupted; re-import with `auths key import`")
185+
}
186+
Self::SigningFailed(_) => Some(
187+
"The signing operation failed; verify your key is accessible with `auths key list`",
188+
),
189+
Self::Proto(_) => Some(
190+
"A protocol error occurred; check that both sides are running compatible versions",
191+
),
192+
Self::IO(_) => Some("Check file permissions and that the filesystem is not read-only"),
193+
Self::InvalidInput(_) => Some("Check the command arguments and try again"),
194+
Self::MutexError(_) => Some("A concurrency error occurred; restart the operation"),
195+
Self::CredentialTooLarge { .. } => Some(
196+
"Reduce the credential size or use file-based storage with AUTHS_KEYCHAIN_BACKEND=file",
197+
),
187198
Self::WeakPassphrase(_) => {
188199
Some("Use at least 12 characters with uppercase, lowercase, and a digit or symbol")
189200
}
@@ -244,7 +255,12 @@ impl AuthsErrorInfo for TrustError {
244255
Self::Lock(_) => Some("Check file permissions and try again"),
245256
Self::Io(_) => Some("Check disk space and file permissions"),
246257
Self::AlreadyExists(_) => Some("Run `auths trust list` to see existing entries"),
247-
Self::InvalidData(_) | Self::Serialization(_) => None,
258+
Self::InvalidData(_) => {
259+
Some("The trust store may be corrupted; delete and re-pin with `auths trust add`")
260+
}
261+
Self::Serialization(_) => {
262+
Some("The trust store data is corrupted; delete and re-pin with `auths trust add`")
263+
}
248264
}
249265
}
250266
}

crates/auths-core/src/pairing/error.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,16 @@ impl AuthsErrorInfo for PairingError {
5555
Self::NoPeerFound => Some("Ensure both devices are on the same network"),
5656
Self::LanTimeout => Some("Check your network and try again"),
5757
Self::RelayError(_) => Some("Check your internet connection"),
58-
_ => None,
58+
Self::Protocol(_) => Some("Ensure both devices are running compatible auths versions"),
59+
Self::QrCodeFailed(_) => {
60+
Some("QR code generation failed; try `auths device pair --mode relay` instead")
61+
}
62+
Self::LocalServerError(_) => {
63+
Some("The local pairing server failed to start; check that the port is available")
64+
}
65+
Self::MdnsError(_) => {
66+
Some("mDNS discovery failed; try `auths device pair --mode relay` instead")
67+
}
5968
}
6069
}
6170
}

0 commit comments

Comments
 (0)