diff --git a/crates/auths-cli/src/commands/device/pair/lan_server.rs b/crates/auths-cli/src/commands/device/pair/lan_server.rs index 86a2f839..665c94f5 100644 --- a/crates/auths-cli/src/commands/device/pair/lan_server.rs +++ b/crates/auths-cli/src/commands/device/pair/lan_server.rs @@ -126,7 +126,7 @@ impl LanPairingServer { } /// Shut down the server without waiting for a response. - #[allow(dead_code)] + #[allow(dead_code)] // public API for callers that need graceful shutdown without waiting pub fn shutdown(self) { self.cancel.cancel(); } diff --git a/crates/auths-cli/src/commands/emergency.rs b/crates/auths-cli/src/commands/emergency.rs index c861ecfe..d4779b01 100644 --- a/crates/auths-cli/src/commands/emergency.rs +++ b/crates/auths-cli/src/commands/emergency.rs @@ -147,15 +147,6 @@ pub struct ReportCommand { pub repo: Option, } -/// Incident type for interactive flow. -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -pub enum IncidentType { - DeviceLostStolen, - KeyExposed, - FreezeEverything, -} - /// Incident report output. #[derive(Debug, Serialize, Deserialize)] pub struct IncidentReport { diff --git a/crates/auths-cli/src/commands/id/register.rs b/crates/auths-cli/src/commands/id/register.rs index 26524765..bdfdf789 100644 --- a/crates/auths-cli/src/commands/id/register.rs +++ b/crates/auths-cli/src/commands/id/register.rs @@ -7,6 +7,7 @@ use serde::Serialize; use auths_id::ports::registry::RegistryBackend; use auths_id::storage::attestation::AttestationSource; use auths_id::storage::identity::IdentityStorage; +use auths_infra_http::HttpRegistryClient; use auths_sdk::error::RegistrationError; pub use auths_sdk::registration::DEFAULT_REGISTRY_URL; use auths_sdk::result::RegistrationOutcome; @@ -44,12 +45,15 @@ pub fn handle_register(repo_path: &Path, registry: &str) -> Result<()> { let attestation_store = Arc::new(RegistryAttestationStorage::new(repo_path)); let attestation_source: Arc = attestation_store; + let registry_client = HttpRegistryClient::new(); + match rt.block_on(auths_sdk::registration::register_identity( identity_storage, backend, attestation_source, registry, None, + ®istry_client, )) { Ok(outcome) => display_registration_result(&outcome), Err(RegistrationError::AlreadyRegistered) => { diff --git a/crates/auths-cli/src/commands/init.rs b/crates/auths-cli/src/commands/init.rs index 1b7a63e6..48c9dfe2 100644 --- a/crates/auths-cli/src/commands/init.rs +++ b/crates/auths-cli/src/commands/init.rs @@ -17,6 +17,7 @@ use auths_id::attestation::export::AttestationSink; use auths_id::ports::registry::RegistryBackend; use auths_id::storage::attestation::AttestationSource; use auths_id::storage::identity::IdentityStorage; +use auths_infra_http::HttpRegistryClient; use auths_sdk::context::AuthsContext; use auths_sdk::ports::git_config::GitConfigProvider; use auths_sdk::registration::DEFAULT_REGISTRY_URL; @@ -634,12 +635,15 @@ fn submit_registration( let attestation_store = Arc::new(RegistryAttestationStorage::new(repo_path)); let attestation_source: Arc = attestation_store; + let registry_client = HttpRegistryClient::new(); + match rt.block_on(auths_sdk::registration::register_identity( identity_storage, backend, attestation_source, registry_url, proof_url, + ®istry_client, )) { Ok(outcome) => { out.print_success(&format!("Identity registered at {}", outcome.registry)); diff --git a/crates/auths-cli/src/errors/cli_error.rs b/crates/auths-cli/src/errors/cli_error.rs index 2a6db05f..c5b457db 100644 --- a/crates/auths-cli/src/errors/cli_error.rs +++ b/crates/auths-cli/src/errors/cli_error.rs @@ -1,7 +1,6 @@ //! Typed CLI error variants with actionable help text. /// Structured CLI errors with built-in suggestion and documentation links. -#[allow(dead_code)] #[derive(thiserror::Error, Debug)] pub enum CliError { #[error("key rotation failed — no pre-rotation commitment found")] diff --git a/crates/auths-core/src/ports/network.rs b/crates/auths-core/src/ports/network.rs index 5341b074..441e1cd9 100644 --- a/crates/auths-core/src/ports/network.rs +++ b/crates/auths-core/src/ports/network.rs @@ -250,6 +250,18 @@ pub trait WitnessClient: Send + Sync { ) -> impl Future>, NetworkError>> + Send; } +/// Response from a registry POST operation. +/// +/// Carries the HTTP status code and body so callers can dispatch on +/// status-specific business logic (e.g., 201 Created vs. 409 Conflict). +#[derive(Debug)] +pub struct RegistryResponse { + /// HTTP status code. + pub status: u16, + /// Response body bytes. + pub body: Vec, +} + /// Fetches and pushes data to a remote registry service. /// /// Implementations handle the transport protocol (e.g., HTTP, gRPC). @@ -297,4 +309,27 @@ pub trait RegistryClient: Send + Sync { path: &str, data: &[u8], ) -> impl Future> + Send; + + /// POSTs a JSON payload to a registry endpoint and returns the raw response. + /// + /// Args: + /// * `registry_url`: Base URL of the registry service. + /// * `path`: The logical path within the registry (e.g., `"v1/identities"`). + /// * `json_body`: Serialized JSON bytes to send as the request body. + /// + /// Usage: + /// ```ignore + /// let resp = client.post_json("https://registry.example.com", "v1/identities", &body).await?; + /// match resp.status { + /// 201 => { /* success */ } + /// 409 => { /* conflict */ } + /// _ => { /* error */ } + /// } + /// ``` + fn post_json( + &self, + registry_url: &str, + path: &str, + json_body: &[u8], + ) -> impl Future> + Send; } diff --git a/crates/auths-core/src/storage/android_keystore.rs b/crates/auths-core/src/storage/android_keystore.rs index e5e8aa88..59afcd95 100644 --- a/crates/auths-core/src/storage/android_keystore.rs +++ b/crates/auths-core/src/storage/android_keystore.rs @@ -20,7 +20,7 @@ use crate::storage::keychain::{IdentityDID, KeyAlias, KeyStorage}; /// - Ed25519 support (with P-256 fallback and conversion layer) /// pub struct AndroidKeystoreStorage { - #[allow(dead_code)] + #[allow(dead_code)] // stub platform impl — field required for API parity service_name: String, } diff --git a/crates/auths-core/src/storage/linux_secret_service.rs b/crates/auths-core/src/storage/linux_secret_service.rs index 11786aa3..60d00d18 100644 --- a/crates/auths-core/src/storage/linux_secret_service.rs +++ b/crates/auths-core/src/storage/linux_secret_service.rs @@ -91,22 +91,6 @@ impl LinuxSecretServiceStorage { Ok(collection) } - - /// Build attributes map for a key. - #[allow(dead_code)] - fn build_attributes<'a>( - &'a self, - alias: &'a str, - identity_did: Option<&'a str>, - ) -> HashMap<&'a str, &'a str> { - let mut attrs = HashMap::new(); - attrs.insert(ATTR_SERVICE, self.service_name.as_str()); - attrs.insert(ATTR_ALIAS, alias); - if let Some(did) = identity_did { - attrs.insert(ATTR_IDENTITY, did); - } - attrs - } } impl KeyStorage for LinuxSecretServiceStorage { @@ -357,30 +341,6 @@ impl KeyStorage for LinuxSecretServiceStorage { mod tests { use super::*; - #[test] - fn test_build_attributes() { - let storage = LinuxSecretServiceStorage { - service_name: "test.service".to_string(), - }; - - let attrs = storage.build_attributes("my-alias", Some("did:keri:test")); - assert_eq!(attrs.get(ATTR_SERVICE), Some(&"test.service")); - assert_eq!(attrs.get(ATTR_ALIAS), Some(&"my-alias")); - assert_eq!(attrs.get(ATTR_IDENTITY), Some(&"did:keri:test")); - } - - #[test] - fn test_build_attributes_without_identity() { - let storage = LinuxSecretServiceStorage { - service_name: "test.service".to_string(), - }; - - let attrs = storage.build_attributes("my-alias", None); - assert_eq!(attrs.get(ATTR_SERVICE), Some(&"test.service")); - assert_eq!(attrs.get(ATTR_ALIAS), Some(&"my-alias")); - assert!(!attrs.contains_key(ATTR_IDENTITY)); - } - #[test] fn test_backend_name() { let storage = LinuxSecretServiceStorage { diff --git a/crates/auths-core/src/witness/receipt.rs b/crates/auths-core/src/witness/receipt.rs index d608caea..d832309c 100644 --- a/crates/auths-core/src/witness/receipt.rs +++ b/crates/auths-core/src/witness/receipt.rs @@ -212,35 +212,6 @@ impl ReceiptBuilder { } } -/// The signing payload for agent commit receipts. -/// -/// Signs tree hash + parent hashes to avoid the chicken-and-egg problem -/// where embedding a receipt in the commit message would change the commit hash. -#[allow(dead_code)] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CommitReceiptPayload { - /// Git tree object hash (20 bytes). - pub tree_hash: Vec, - /// Parent commit hashes (20 bytes each). - pub parent_hashes: Vec>, -} - -#[allow(dead_code)] -impl CommitReceiptPayload { - /// Produce deterministic bytes for signing. - /// - /// Format: `tree_hash || num_parents (4 bytes LE) || parent_1 || parent_2 || ...` - pub fn signing_bytes(&self) -> Vec { - let mut buf = Vec::with_capacity(20 + 4 + self.parent_hashes.len() * 20); - buf.extend_from_slice(&self.tree_hash); - buf.extend_from_slice(&(self.parent_hashes.len() as u32).to_le_bytes()); - for parent in &self.parent_hashes { - buf.extend_from_slice(parent); - } - buf - } -} - impl From for auths_verifier::witness::WitnessReceipt { fn from(r: Receipt) -> Self { Self { @@ -407,29 +378,4 @@ mod tests { let result = Receipt::from_trailer_value(&encoded); assert!(result.is_err()); } - - #[test] - fn commit_receipt_payload_signing_bytes_deterministic() { - let payload = CommitReceiptPayload { - tree_hash: vec![0xaa; 20], - parent_hashes: vec![vec![0xbb; 20], vec![0xcc; 20]], - }; - let bytes1 = payload.signing_bytes(); - let bytes2 = payload.signing_bytes(); - assert_eq!(bytes1, bytes2); - // 20 (tree) + 4 (count) + 40 (2 parents) - assert_eq!(bytes1.len(), 64); - } - - #[test] - fn commit_receipt_payload_no_parents() { - let payload = CommitReceiptPayload { - tree_hash: vec![0xaa; 20], - parent_hashes: vec![], - }; - let bytes = payload.signing_bytes(); - assert_eq!(bytes.len(), 24); // 20 + 4 - // num_parents should be 0 - assert_eq!(&bytes[20..24], &[0, 0, 0, 0]); - } } diff --git a/crates/auths-core/src/witness/server.rs b/crates/auths-core/src/witness/server.rs index dac3ecb6..a0411271 100644 --- a/crates/auths-core/src/witness/server.rs +++ b/crates/auths-core/src/witness/server.rs @@ -265,7 +265,7 @@ pub async fn run_server(state: WitnessServerState, addr: SocketAddr) -> Result<( /// For production deployments behind a reverse proxy, prefer [`run_server`] and terminate /// TLS at the proxy layer instead. #[cfg(feature = "tls")] -#[allow(dead_code)] +#[allow(dead_code)] // feature-gated public API — available when tls feature is enabled pub async fn run_server_tls( state: WitnessServerState, addr: SocketAddr, @@ -634,27 +634,6 @@ mod tests { event } - /// Build a valid non-inception event (rotation) with proper SAID. - #[allow(dead_code)] - fn make_valid_rot_event(prefix: &str, seq: u64, prior_said: &str) -> serde_json::Value { - let mut event = serde_json::json!({ - "v": "KERI10JSON000000_", - "t": "rot", - "d": "", - "i": prefix, - "s": seq, - "p": prior_said - }); - - // Compute SAID - let said_payload = serde_json::to_vec(&event).unwrap(); - event["d"] = serde_json::Value::String( - crate::crypto::said::compute_said(&said_payload).into_inner(), - ); - - event - } - #[tokio::test(flavor = "multi_thread")] async fn health_endpoint() { let state = test_state(); diff --git a/crates/auths-crypto/Cargo.toml b/crates/auths-crypto/Cargo.toml index d2468a5a..77a39b41 100644 --- a/crates/auths-crypto/Cargo.toml +++ b/crates/auths-crypto/Cargo.toml @@ -5,6 +5,8 @@ edition = "2024" publish = true license.workspace = true description = "Cryptographic primitives for Auths: KERI key parsing and DID:key encoding" +keywords = ["cryptography", "ed25519", "did", "verification", "signing"] +categories = ["cryptography"] [features] default = ["native"] diff --git a/crates/auths-id/tests/cases/mod.rs b/crates/auths-id/tests/cases/mod.rs index 27fc4575..d6008de6 100644 --- a/crates/auths-id/tests/cases/mod.rs +++ b/crates/auths-id/tests/cases/mod.rs @@ -1,6 +1,7 @@ mod concurrent_writes; mod keri; mod lifecycle; +mod proptest_keri; mod recovery; mod registry_contract; mod rotation_edge_cases; diff --git a/crates/auths-id/tests/cases/proptest_keri.rs b/crates/auths-id/tests/cases/proptest_keri.rs new file mode 100644 index 00000000..702b2597 --- /dev/null +++ b/crates/auths-id/tests/cases/proptest_keri.rs @@ -0,0 +1,261 @@ +use auths_core::crypto::said::{compute_next_commitment, compute_said}; +use auths_id::keri::{ + Event, IcpEvent, IxnEvent, KERI_VERSION, KeriSequence, Prefix, RotEvent, Said, Seal, + ValidationError, finalize_icp_event, serialize_for_signing, validate_kel, verify_event_said, +}; +use base64::Engine; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use proptest::prelude::*; +use ring::rand::SystemRandom; +use ring::signature::{Ed25519KeyPair, KeyPair}; + +fn gen_keypair() -> Ed25519KeyPair { + let rng = SystemRandom::new(); + let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap(); + Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap() +} + +fn encode_pubkey(kp: &Ed25519KeyPair) -> String { + format!("D{}", URL_SAFE_NO_PAD.encode(kp.public_key().as_ref())) +} + +fn sign_event(event: &Event, kp: &Ed25519KeyPair) -> String { + let canonical = serialize_for_signing(event).unwrap(); + URL_SAFE_NO_PAD.encode(kp.sign(&canonical).as_ref()) +} + +fn make_signed_icp(kp: &Ed25519KeyPair, next_commitment: &str) -> IcpEvent { + let icp = IcpEvent { + v: KERI_VERSION.to_string(), + d: Said::default(), + i: Prefix::default(), + s: KeriSequence::new(0), + kt: "1".to_string(), + k: vec![encode_pubkey(kp)], + nt: "1".to_string(), + n: vec![next_commitment.to_string()], + bt: "0".to_string(), + b: vec![], + a: vec![], + x: String::new(), + }; + + let mut finalized = finalize_icp_event(icp).unwrap(); + finalized.x = sign_event(&Event::Icp(finalized.clone()), kp); + finalized +} + +fn make_signed_ixn( + prefix: &Prefix, + prev_said: &Said, + seq: u64, + kp: &Ed25519KeyPair, + seals: Vec, +) -> IxnEvent { + let mut ixn = IxnEvent { + v: KERI_VERSION.to_string(), + d: Said::default(), + i: prefix.clone(), + s: KeriSequence::new(seq), + p: prev_said.clone(), + a: seals, + x: String::new(), + }; + + let json = serde_json::to_vec(&Event::Ixn(ixn.clone())).unwrap(); + ixn.d = compute_said(&json); + ixn.x = sign_event(&Event::Ixn(ixn.clone()), kp); + ixn +} + +fn make_signed_rot( + prefix: &Prefix, + prev_said: &Said, + seq: u64, + new_kp: &Ed25519KeyPair, + next_commitment: &str, +) -> RotEvent { + let mut rot = RotEvent { + v: KERI_VERSION.to_string(), + d: Said::default(), + i: prefix.clone(), + s: KeriSequence::new(seq), + p: prev_said.clone(), + kt: "1".to_string(), + k: vec![encode_pubkey(new_kp)], + nt: "1".to_string(), + n: vec![next_commitment.to_string()], + bt: "0".to_string(), + b: vec![], + a: vec![], + x: String::new(), + }; + + let json = serde_json::to_vec(&Event::Rot(rot.clone())).unwrap(); + rot.d = compute_said(&json); + rot.x = sign_event(&Event::Rot(rot.clone()), new_kp); + rot +} + +fn build_valid_kel(ixn_count: usize) -> Vec { + let kp = gen_keypair(); + let next_kp = gen_keypair(); + let next_commitment = compute_next_commitment(next_kp.public_key().as_ref()); + + let icp = make_signed_icp(&kp, &next_commitment); + let prefix = icp.i.clone(); + let mut events: Vec = vec![Event::Icp(icp.clone())]; + let mut prev_said = icp.d.clone(); + + for i in 0..ixn_count { + let ixn = make_signed_ixn( + &prefix, + &prev_said, + (i + 1) as u64, + &kp, + vec![Seal::device_attestation(format!("EAttest{i}"))], + ); + prev_said = ixn.d.clone(); + events.push(Event::Ixn(ixn)); + } + + events +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(20))] + + #[test] + fn said_integrity_recompute_matches(ixn_count in 0..5usize) { + let events = build_valid_kel(ixn_count); + for event in &events { + prop_assert!(verify_event_said(event).is_ok(), "SAID verification failed for event s={}", event.sequence().value()); + } + } + + #[test] + fn duplicate_sequence_rejected(ixn_count in 1..4usize) { + let events = build_valid_kel(ixn_count); + // Duplicate the last event + let mut bad_events = events.clone(); + bad_events.push(events.last().unwrap().clone()); + + let result = validate_kel(&bad_events); + prop_assert!(result.is_err()); + match result.unwrap_err() { + ValidationError::InvalidSequence { .. } + | ValidationError::BrokenChain { .. } + | ValidationError::SignatureFailed { .. } => {} + e => prop_assert!(false, "Expected sequence/chain/sig error, got: {:?}", e), + } + } + + #[test] + fn broken_chain_rejected(ixn_count in 1..4usize) { + let mut events = build_valid_kel(ixn_count); + // Corrupt the `p` field of the last event + if let Some(Event::Ixn(ixn)) = events.last_mut() { + ixn.p = Said::new_unchecked("ECorruptedPreviousSaidThatDoesNotMatch00000000".to_string()); + } + + let result = validate_kel(&events); + prop_assert!(result.is_err()); + match result.unwrap_err() { + // Corrupting `p` changes serialization, so SAID check fails first + ValidationError::InvalidSaid { .. } + | ValidationError::BrokenChain { .. } + | ValidationError::SignatureFailed { .. } => {} + e => prop_assert!(false, "Expected InvalidSaid, BrokenChain, or SignatureFailed, got: {:?}", e), + } + } + + #[test] + fn multiple_inception_rejected(ixn_count in 0..3usize) { + let events = build_valid_kel(ixn_count); + // Append another inception + let extra_kp = gen_keypair(); + let extra_next = gen_keypair(); + let extra_icp = make_signed_icp(&extra_kp, &compute_next_commitment(extra_next.public_key().as_ref())); + let mut bad_events = events; + bad_events.push(Event::Icp(extra_icp)); + + let result = validate_kel(&bad_events); + prop_assert!(result.is_err()); + match result.unwrap_err() { + ValidationError::MultipleInceptions + | ValidationError::InvalidSequence { .. } => {} + e => prop_assert!(false, "Expected MultipleInceptions or InvalidSequence, got: {:?}", e), + } + } + + #[test] + fn wrong_rotation_key_rejected(_dummy in 0..5u8) { + let kp = gen_keypair(); + let next_kp = gen_keypair(); + let wrong_kp = gen_keypair(); + let next_commitment = compute_next_commitment(next_kp.public_key().as_ref()); + + let icp = make_signed_icp(&kp, &next_commitment); + let prefix = icp.i.clone(); + let prev_said = icp.d.clone(); + let mut events: Vec = vec![Event::Icp(icp)]; + + // Rotate with the WRONG key (not the pre-committed one) + let future_kp = gen_keypair(); + let rot = make_signed_rot( + &prefix, + &prev_said, + 1, + &wrong_kp, + &compute_next_commitment(future_kp.public_key().as_ref()), + ); + events.push(Event::Rot(rot)); + + let result = validate_kel(&events); + prop_assert!(result.is_err()); + match result.unwrap_err() { + ValidationError::CommitmentMismatch { .. } + | ValidationError::SignatureFailed { .. } => {} + e => prop_assert!(false, "Expected CommitmentMismatch or SignatureFailed, got: {:?}", e), + } + } + + #[test] + fn valid_kel_replay_idempotent(ixn_count in 0..5usize) { + let events = build_valid_kel(ixn_count); + let state1 = validate_kel(&events).expect("first validation should succeed"); + let state2 = validate_kel(&events).expect("second validation should succeed"); + prop_assert_eq!(state1, state2); + } + + #[test] + fn valid_kel_with_rotation_replays_correctly(_dummy in 0..5u8) { + let kp1 = gen_keypair(); + let kp2 = gen_keypair(); + let kp3 = gen_keypair(); + let commitment2 = compute_next_commitment(kp2.public_key().as_ref()); + let commitment3 = compute_next_commitment(kp3.public_key().as_ref()); + + let icp = make_signed_icp(&kp1, &commitment2); + let prefix = icp.i.clone(); + let prev_said = icp.d.clone(); + + let rot = make_signed_rot(&prefix, &prev_said, 1, &kp2, &commitment3); + let rot_said = rot.d.clone(); + + let ixn = make_signed_ixn( + &prefix, + &rot_said, + 2, + &kp2, + vec![Seal::device_attestation("EPostRotAttest")], + ); + + let events = vec![Event::Icp(icp), Event::Rot(rot), Event::Ixn(ixn)]; + let state = validate_kel(&events).expect("valid KEL should validate"); + + prop_assert_eq!(state.sequence, 2); + prop_assert_eq!(state.current_keys, vec![encode_pubkey(&kp2)]); + prop_assert_eq!(state.next_commitment, vec![commitment3]); + } +} diff --git a/crates/auths-infra-git/Cargo.toml b/crates/auths-infra-git/Cargo.toml index c1bf8c43..459877dd 100644 --- a/crates/auths-infra-git/Cargo.toml +++ b/crates/auths-infra-git/Cargo.toml @@ -4,6 +4,8 @@ version.workspace = true edition = "2024" publish = true license.workspace = true +keywords = ["git", "infrastructure", "storage", "adapter"] +categories = ["development-tools"] [dependencies] auths-core = { workspace = true } diff --git a/crates/auths-infra-git/src/lib.rs b/crates/auths-infra-git/src/lib.rs index 88796892..3bc6ab4c 100644 --- a/crates/auths-infra-git/src/lib.rs +++ b/crates/auths-infra-git/src/lib.rs @@ -1,3 +1,16 @@ +//! Git storage adapter layer for Auths. +//! +//! Implements the storage port traits defined in `auths-core` using `libgit2`. +//! Each adapter wraps a bare Git repository and provides typed access to +//! identity data stored as Git objects. +//! +//! ## Modules +//! +//! - [`GitBlobStore`] — content-addressable blob storage +//! - [`GitRefStore`] — ref-based key-value storage for identity state +//! - [`GitEventLog`] — append-only event log backed by Git refs +//! - [`audit`] — audit log helpers for registry operations + pub mod audit; mod blob_store; mod error; diff --git a/crates/auths-infra-http/Cargo.toml b/crates/auths-infra-http/Cargo.toml index ee9ec409..32cd417a 100644 --- a/crates/auths-infra-http/Cargo.toml +++ b/crates/auths-infra-http/Cargo.toml @@ -4,6 +4,8 @@ version.workspace = true edition = "2024" publish = true license.workspace = true +keywords = ["http", "infrastructure", "client", "adapter"] +categories = ["web-programming::http-client"] [dependencies] async-trait = "0.1" diff --git a/crates/auths-infra-http/src/async_witness_client.rs b/crates/auths-infra-http/src/async_witness_client.rs index fca6b6c3..645f80b7 100644 --- a/crates/auths-infra-http/src/async_witness_client.rs +++ b/crates/auths-infra-http/src/async_witness_client.rs @@ -30,7 +30,7 @@ pub struct HttpAsyncWitnessClient { #[derive(Debug, Deserialize)] struct HeadResponse { - #[allow(dead_code)] + #[allow(dead_code)] // serde deserialize target — field must exist for JSON mapping prefix: String, latest_seq: Option, } diff --git a/crates/auths-infra-http/src/lib.rs b/crates/auths-infra-http/src/lib.rs index c4f8009a..680e187a 100644 --- a/crates/auths-infra-http/src/lib.rs +++ b/crates/auths-infra-http/src/lib.rs @@ -1,3 +1,15 @@ +//! HTTP client adapter layer for Auths. +//! +//! Implements the network port traits defined in `auths-core` using `reqwest`. +//! Each client wraps HTTP endpoints for the Auths infrastructure services. +//! +//! ## Modules +//! +//! - [`HttpRegistryClient`] — registry service client for identity and attestation operations +//! - [`HttpWitnessClient`] — synchronous witness client for KERI event submission +//! - [`HttpAsyncWitnessClient`] — async witness client with quorum support +//! - [`HttpIdentityResolver`] — DID resolution over HTTP + mod async_witness_client; mod error; mod identity_resolver; diff --git a/crates/auths-infra-http/src/registry_client.rs b/crates/auths-infra-http/src/registry_client.rs index 016db0f6..37dcb395 100644 --- a/crates/auths-infra-http/src/registry_client.rs +++ b/crates/auths-infra-http/src/registry_client.rs @@ -1,6 +1,7 @@ -use auths_core::ports::network::{NetworkError, RegistryClient}; +use auths_core::ports::network::{NetworkError, RegistryClient, RegistryResponse}; use std::future::Future; +use crate::error::map_reqwest_error; use crate::request::{ build_get_request, build_post_request, execute_request, parse_response_bytes, }; @@ -65,4 +66,33 @@ impl RegistryClient for HttpRegistryClient { Ok(()) } } + + fn post_json( + &self, + registry_url: &str, + path: &str, + json_body: &[u8], + ) -> impl Future> + Send { + let url = format!("{}/{}", registry_url.trim_end_matches('/'), path); + let request = self + .client + .post(&url) + .header("Content-Type", "application/json") + .body(json_body.to_vec()); + let endpoint = registry_url.to_string(); + + async move { + let response = request + .send() + .await + .map_err(|e| map_reqwest_error(e, &endpoint))?; + let status = response.status().as_u16(); + let body = response.bytes().await.map(|b| b.to_vec()).map_err(|e| { + NetworkError::InvalidResponse { + detail: e.to_string(), + } + })?; + Ok(RegistryResponse { status, body }) + } + } } diff --git a/crates/auths-sdk/Cargo.toml b/crates/auths-sdk/Cargo.toml index 4ca43322..7fb3fdbc 100644 --- a/crates/auths-sdk/Cargo.toml +++ b/crates/auths-sdk/Cargo.toml @@ -6,6 +6,8 @@ description = "Application services layer for Auths identity operations" publish = true license.workspace = true repository = "https://github.com/auths-dev/auths" +keywords = ["sdk", "identity", "did", "cryptography", "attestation"] +categories = ["cryptography", "authentication"] [dependencies] auths-core.workspace = true @@ -19,7 +21,6 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" json-canon = "0.1" base64 = "0.22" -reqwest = { version = "0.13.2", features = ["json"] } chrono = "0.4" hex = "0.4" html-escape = "0.2" diff --git a/crates/auths-sdk/src/registration.rs b/crates/auths-sdk/src/registration.rs index b2dd2540..3b5f8ce9 100644 --- a/crates/auths-sdk/src/registration.rs +++ b/crates/auths-sdk/src/registration.rs @@ -1,9 +1,8 @@ use std::sync::Arc; -use std::time::Duration; use serde::{Deserialize, Serialize}; -use auths_core::ports::network::NetworkError; +use auths_core::ports::network::{NetworkError, RegistryClient}; use auths_id::keri::Prefix; use auths_id::ports::registry::RegistryBackend; use auths_id::storage::attestation::AttestationSource; @@ -36,12 +35,13 @@ struct RegistrationResponse { /// * `attestation_source`: Source for loading local attestations. /// * `registry_url`: Base URL of the target registry. /// * `proof_url`: Optional URL to a platform proof (e.g., GitHub gist). +/// * `registry_client`: Network client for communicating with the registry. /// /// Usage: /// ```ignore /// let outcome = register_identity( /// identity_storage, registry, attestation_source, -/// "https://auths-registry.fly.dev", None, +/// "https://auths-registry.fly.dev", None, &http_client, /// ).await?; /// ``` pub async fn register_identity( @@ -50,6 +50,7 @@ pub async fn register_identity( attestation_source: Arc, registry_url: &str, proof_url: Option, + registry_client: &impl RegistryClient, ) -> Result { let identity = identity_storage .load_identity() @@ -89,19 +90,23 @@ pub async fn register_identity( proof_url, }; + let json_body = serde_json::to_vec(&payload) + .map_err(|e| RegistrationError::LocalDataError(e.to_string()))?; + let registry_url = registry_url.trim_end_matches('/'); - let response = transmit_registration(registry_url, &payload) + let response = registry_client + .post_json(registry_url, "v1/identities", &json_body) .await .map_err(RegistrationError::NetworkError)?; - let status = response.status(); - match status.as_u16() { + match response.status { 201 => { - let body: RegistrationResponse = response.json().await.map_err(|e| { - RegistrationError::NetworkError(NetworkError::InvalidResponse { - detail: e.to_string(), - }) - })?; + let body: RegistrationResponse = + serde_json::from_slice(&response.body).map_err(|e| { + RegistrationError::NetworkError(NetworkError::InvalidResponse { + detail: e.to_string(), + }) + })?; Ok(RegistrationOutcome { did_prefix: body.did_prefix, @@ -112,43 +117,12 @@ pub async fn register_identity( 409 => Err(RegistrationError::AlreadyRegistered), 429 => Err(RegistrationError::QuotaExceeded), _ => { - let body = response.text().await.unwrap_or_default(); + let body = String::from_utf8_lossy(&response.body); Err(RegistrationError::NetworkError( NetworkError::InvalidResponse { - detail: format!("Registry error ({}): {}", status, body), + detail: format!("Registry error ({}): {}", response.status, body), }, )) } } } - -async fn transmit_registration( - registry: &str, - payload: &RegistryOnboardingPayload, -) -> Result { - let client = reqwest::Client::builder() - .connect_timeout(Duration::from_secs(30)) - .timeout(Duration::from_secs(60)) - .build() - .map_err(|e| NetworkError::Internal(Box::new(e)))?; - - let endpoint = format!("{}/v1/identities", registry); - client - .post(&endpoint) - .json(payload) - .send() - .await - .map_err(|e| { - if e.is_timeout() { - NetworkError::Timeout { - endpoint: endpoint.clone(), - } - } else if e.is_connect() { - NetworkError::Unreachable { - endpoint: endpoint.clone(), - } - } else { - NetworkError::Internal(Box::new(e)) - } - }) -} diff --git a/crates/auths-storage/Cargo.toml b/crates/auths-storage/Cargo.toml index ddc53e99..f9ef1b6a 100644 --- a/crates/auths-storage/Cargo.toml +++ b/crates/auths-storage/Cargo.toml @@ -4,6 +4,8 @@ version.workspace = true edition = "2024" publish = true license.workspace = true +keywords = ["storage", "git", "sqlite", "identity", "adapter"] +categories = ["database"] [dependencies] auths-id = { workspace = true } diff --git a/crates/auths-storage/src/git/adapter.rs b/crates/auths-storage/src/git/adapter.rs index 8e5c292b..691f2cb5 100644 --- a/crates/auths-storage/src/git/adapter.rs +++ b/crates/auths-storage/src/git/adapter.rs @@ -360,22 +360,6 @@ impl GitRegistryBackend { Ok((commit, tree)) } - /// Get the current registry tree, or None if not initialized. - #[allow(dead_code)] - fn current_tree_opt<'a>( - &self, - repo: &'a Repository, - ) -> Result>, RegistryError> { - match repo.find_reference(REGISTRY_REF) { - Ok(reference) => { - let commit = reference.peel_to_commit().map_err(from_git2)?; - Ok(Some(commit.tree().map_err(from_git2)?)) - } - Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(None), - Err(e) => Err(from_git2(e)), - } - } - /// Create a commit and update the registry ref atomically. /// /// Uses CAS to detect concurrent modifications. CAS failure aborts @@ -535,7 +519,7 @@ impl GitRegistryBackend { /// /// Walks the `v1/orgs/` tree. Calls `visitor` for each org prefix string. /// Return `ControlFlow::Break(())` to stop early. - #[allow(dead_code)] + #[allow(dead_code)] // called from rebuild_org_members_from_registry (indexed-storage feature) fn visit_orgs(&self, mut visitor: F) -> Result<(), RegistryError> where F: FnMut(&str) -> ControlFlow<()>, @@ -1477,7 +1461,7 @@ impl AttestationSink for GitRegistryBackend { /// * `index` - The SQLite index to populate /// * `backend` - The registry backend to read identity data from #[cfg(feature = "indexed-storage")] -#[allow(dead_code)] +#[allow(dead_code)] // feature-gated public API — used when indexed-storage is enabled pub fn rebuild_identities_from_registry( index: &auths_index::AttestationIndex, backend: &GitRegistryBackend, @@ -1536,7 +1520,7 @@ pub fn rebuild_identities_from_registry( /// * `index` - The SQLite index to populate /// * `backend` - The registry backend to read org member data from #[cfg(feature = "indexed-storage")] -#[allow(dead_code)] +#[allow(dead_code)] // feature-gated public API — used when indexed-storage is enabled pub fn rebuild_org_members_from_registry( index: &auths_index::AttestationIndex, backend: &GitRegistryBackend, diff --git a/crates/auths-storage/src/git/tree_ops.rs b/crates/auths-storage/src/git/tree_ops.rs index 7cd5e89c..2a54f0a1 100644 --- a/crates/auths-storage/src/git/tree_ops.rs +++ b/crates/auths-storage/src/git/tree_ops.rs @@ -71,14 +71,6 @@ impl<'a> TreeNavigator<'a> { Some((entry.id(), entry.kind().unwrap_or(git2::ObjectType::Blob))) } - /// Navigate to a path and return the entry OID if it exists. - /// - /// Returns `None` if any component along the path doesn't exist. - #[allow(dead_code)] - pub fn get_entry(&self, path: &[&str]) -> Option { - self.get_entry_info(path).map(|(oid, _)| oid) - } - /// Read a blob at the given path and return its content. /// /// # Errors @@ -157,40 +149,6 @@ impl<'a> TreeNavigator<'a> { Ok(()) } - - /// Visit entries in a directory at a path string. - #[allow(dead_code)] - pub fn visit_dir_path(&self, path: &str, visitor: F) -> Result<(), RegistryError> - where - F: FnMut(&str) -> ControlFlow<()>, - { - let parts = path_parts(path); - self.visit_dir(&parts, visitor) - } - - /// Get the tree at the given path. - #[allow(dead_code)] - pub fn get_tree(&self, path: &[&str]) -> Result, RegistryError> { - if path.is_empty() { - return Ok(self.root.clone()); - } - - let (oid, kind) = self - .get_entry_info(path) - .ok_or_else(|| RegistryError::NotFound { - entity_type: "directory".into(), - id: path.join("/"), - })?; - - if kind != git2::ObjectType::Tree { - return Err(RegistryError::NotFound { - entity_type: "directory".into(), - id: path.join("/"), - }); - } - - self.repo.find_tree(oid).map_err(from_git2) - } } /// Efficiently mutates a Git tree by only rebuilding modified paths. @@ -234,12 +192,6 @@ impl TreeMutator { self.pending_deletes.insert(path.to_string()); } - /// Check if there are any pending mutations. - #[allow(dead_code)] - pub fn has_mutations(&self) -> bool { - !self.pending_writes.is_empty() || !self.pending_deletes.is_empty() - } - /// Build a new tree from base + mutations. /// /// # Algorithm @@ -276,7 +228,6 @@ impl TreeMutator { ChildEntry { oid: entry.id(), kind: entry.kind().unwrap_or(git2::ObjectType::Blob), - modified: false, }, ); } @@ -308,7 +259,6 @@ impl TreeMutator { ChildEntry { oid: blob_oid, kind: git2::ObjectType::Blob, - modified: true, }, ); } else { @@ -323,7 +273,6 @@ impl TreeMutator { ChildEntry { oid: blob_oid, kind: git2::ObjectType::Blob, - modified: true, }, ); } else if prefix.is_empty() { @@ -382,7 +331,6 @@ impl TreeMutator { ChildEntry { oid: child_oid, kind: git2::ObjectType::Tree, - modified: true, }, ); } else { @@ -417,8 +365,6 @@ impl Default for TreeMutator { struct ChildEntry { oid: Oid, kind: git2::ObjectType, - #[allow(dead_code)] - modified: bool, } #[cfg(test)] @@ -432,13 +378,6 @@ mod tests { (dir, repo) } - #[allow(dead_code)] - fn create_empty_tree(repo: &Repository) -> Tree<'_> { - let builder = repo.treebuilder(None).unwrap(); - let oid = builder.write().unwrap(); - repo.find_tree(oid).unwrap() - } - fn create_test_tree(repo: &Repository) -> Tree<'_> { // Create a tree with structure: // foo/bar.txt = "hello" @@ -655,8 +594,6 @@ mod tests { let base = create_test_tree(&repo); let mutator = TreeMutator::new(); - assert!(!mutator.has_mutations()); - let oid = mutator.build_tree(&repo, Some(&base)).unwrap(); // The tree content should be identical diff --git a/crates/auths-telemetry/Cargo.toml b/crates/auths-telemetry/Cargo.toml index c4cd93e9..cd8e62f9 100644 --- a/crates/auths-telemetry/Cargo.toml +++ b/crates/auths-telemetry/Cargo.toml @@ -4,6 +4,8 @@ version.workspace = true edition = "2024" publish = true license.workspace = true +keywords = ["telemetry", "security", "events", "observability"] +categories = ["development-tools"] [lib] name = "auths_telemetry" diff --git a/deny.toml b/deny.toml index 5481a0f6..7a256e27 100644 --- a/deny.toml +++ b/deny.toml @@ -26,11 +26,9 @@ allow = [ multiple-versions = "warn" deny = [ - # reqwest is confined to adapter/infrastructure layer; auths-sdk usage is a tracked refactor debt { crate = "reqwest", wrappers = [ "auths-infra-http", "auths-cli", - "auths-sdk", ], reason = "HTTP clients must be confined to adapter layer" }, # dialoguer is a terminal UX dependency — CLI only