diff --git a/crates/auths-cli/src/errors/renderer.rs b/crates/auths-cli/src/errors/renderer.rs index a7ba7bfb..9278b06b 100644 --- a/crates/auths-cli/src/errors/renderer.rs +++ b/crates/auths-cli/src/errors/renderer.rs @@ -229,7 +229,11 @@ fn docs_url(code: &str) -> Option { | "AUTHS_STORAGE_LOCKED" | "AUTHS_BACKEND_INIT_FAILED" | "AUTHS_AGENT_LOCKED" - | "AUTHS_VERIFICATION_ERROR" + | "AUTHS_ISSUER_SIG_FAILED" + | "AUTHS_DEVICE_SIG_FAILED" + | "AUTHS_ATTESTATION_EXPIRED" + | "AUTHS_ATTESTATION_REVOKED" + | "AUTHS_TIMESTAMP_IN_FUTURE" | "AUTHS_MISSING_CAPABILITY" | "AUTHS_DID_RESOLUTION_ERROR" | "AUTHS_ORG_VERIFICATION_FAILED" @@ -304,7 +308,7 @@ mod tests { #[test] fn render_error_attestation_error_text() { - let err = Error::new(AttestationError::VerificationError("bad sig".into())); + let err = Error::new(AttestationError::IssuerSignatureFailed("bad sig".into())); render_error(&err, false); } diff --git a/crates/auths-id/src/attestation/verify.rs b/crates/auths-id/src/attestation/verify.rs index 96ab5fe4..b5818b59 100644 --- a/crates/auths-id/src/attestation/verify.rs +++ b/crates/auths-id/src/attestation/verify.rs @@ -16,27 +16,23 @@ pub fn verify_with_resolver( // Return specific AttestationError // 1. Check revocation and expiration if att.is_revoked() { - return Err(AttestationError::VerificationError( - "Attestation revoked".to_string(), - )); + return Err(AttestationError::AttestationRevoked); } if let Some(exp) = att.expires_at && now > exp { - return Err(AttestationError::VerificationError(format!( - "Attestation expired on {}", - exp.to_rfc3339() - ))); + return Err(AttestationError::AttestationExpired { + at: exp.to_rfc3339(), + }); } // Only reject timestamps in the future (clock drift protection) // Past timestamps are valid - attestations stored in Git are verified days/months later if let Some(ts) = att.timestamp && ts > now + Duration::seconds(MAX_SKEW_SECS) { - return Err(AttestationError::VerificationError(format!( - "Attestation timestamp {} is in the future", - ts.to_rfc3339() - ))); + return Err(AttestationError::TimestampInFuture { + at: ts.to_rfc3339(), + }); } // 2. Resolve issuer's public key @@ -82,12 +78,7 @@ pub fn verify_with_resolver( let issuer_public_key_ring = UnparsedPublicKey::new(&ED25519, &issuer_pk_bytes); issuer_public_key_ring .verify(data_to_verify, att.identity_signature.as_bytes()) - .map_err(|e| { - AttestationError::VerificationError(format!( - "Issuer signature verification failed: {}", - e - )) - })?; + .map_err(|e| AttestationError::IssuerSignatureFailed(e.to_string()))?; debug!( "(Verify) Issuer signature verified successfully for {}", att.issuer @@ -97,12 +88,7 @@ pub fn verify_with_resolver( let device_public_key_ring = UnparsedPublicKey::new(&ED25519, att.device_public_key.as_bytes()); device_public_key_ring .verify(data_to_verify, att.device_signature.as_bytes()) - .map_err(|e| { - AttestationError::VerificationError(format!( - "Device signature verification failed: {}", - e - )) - })?; + .map_err(|e| AttestationError::DeviceSignatureFailed(e.to_string()))?; debug!( "(Verify) Device signature verified successfully for {}", att.subject.as_str() diff --git a/crates/auths-verifier/src/error.rs b/crates/auths-verifier/src/error.rs index 7fdc0138..5878db58 100644 --- a/crates/auths-verifier/src/error.rs +++ b/crates/auths-verifier/src/error.rs @@ -19,9 +19,31 @@ pub trait AuthsErrorInfo { /// Errors returned by attestation signing, verification, and related operations. #[derive(Error, Debug)] pub enum AttestationError { - /// Cryptographic signature verification failed. - #[error("Signature verification failed: {0}")] - VerificationError(String), + /// Issuer's Ed25519 signature did not verify. + #[error("Issuer signature verification failed: {0}")] + IssuerSignatureFailed(String), + + /// Device's Ed25519 signature did not verify. + #[error("Device signature verification failed: {0}")] + DeviceSignatureFailed(String), + + /// Attestation has passed its expiry timestamp. + #[error("Attestation expired on {at}")] + AttestationExpired { + /// RFC 3339 formatted expiry timestamp. + at: String, + }, + + /// Attestation was explicitly revoked. + #[error("Attestation revoked")] + AttestationRevoked, + + /// Attestation timestamp is in the future (clock skew). + #[error("Attestation timestamp {at} is in the future")] + TimestampInFuture { + /// RFC 3339 formatted timestamp. + at: String, + }, /// The attestation does not grant the required capability. #[error("Missing required capability: required {required:?}, available {available:?}")] @@ -85,7 +107,11 @@ pub enum AttestationError { impl AuthsErrorInfo for AttestationError { fn error_code(&self) -> &'static str { match self { - Self::VerificationError(_) => "AUTHS_VERIFICATION_ERROR", + Self::IssuerSignatureFailed(_) => "AUTHS_ISSUER_SIG_FAILED", + Self::DeviceSignatureFailed(_) => "AUTHS_DEVICE_SIG_FAILED", + Self::AttestationExpired { .. } => "AUTHS_ATTESTATION_EXPIRED", + Self::AttestationRevoked => "AUTHS_ATTESTATION_REVOKED", + Self::TimestampInFuture { .. } => "AUTHS_TIMESTAMP_IN_FUTURE", Self::MissingCapability { .. } => "AUTHS_MISSING_CAPABILITY", Self::SigningError(_) => "AUTHS_SIGNING_ERROR", Self::DidResolutionError(_) => "AUTHS_DID_RESOLUTION_ERROR", @@ -103,9 +129,15 @@ impl AuthsErrorInfo for AttestationError { fn suggestion(&self) -> Option<&'static str> { match self { - Self::VerificationError(_) => { - Some("Verify the attestation was signed with the correct key") + Self::IssuerSignatureFailed(_) => { + Some("Verify the attestation was signed with the correct issuer key") + } + Self::DeviceSignatureFailed(_) => Some("Verify the device key matches the attestation"), + Self::AttestationExpired { .. } => Some("Request a new attestation from the issuer"), + Self::AttestationRevoked => { + Some("This device has been revoked; contact the identity admin") } + Self::TimestampInFuture { .. } => Some("Check system clock synchronization"), Self::MissingCapability { .. } => { Some("Request an attestation with the required capability") } diff --git a/crates/auths-verifier/src/ffi.rs b/crates/auths-verifier/src/ffi.rs index 01ed2def..332d398c 100644 --- a/crates/auths-verifier/src/ffi.rs +++ b/crates/auths-verifier/src/ffi.rs @@ -43,11 +43,28 @@ pub const ERR_VERIFY_INSUFFICIENT_WITNESSES: c_int = -9; pub const ERR_VERIFY_WITNESS_PARSE: c_int = -10; /// Input JSON exceeded size limit. pub const ERR_VERIFY_INPUT_TOO_LARGE: c_int = -11; +/// Attestation timestamp is in the future (clock skew). +pub const ERR_VERIFY_FUTURE_TIMESTAMP: c_int = -12; /// Unclassified verification error. pub const ERR_VERIFY_OTHER: c_int = -99; /// Internal panic occurred pub const ERR_VERIFY_PANIC: c_int = -127; +fn attestation_error_to_code(e: &AttestationError) -> c_int { + match e { + AttestationError::IssuerSignatureFailed(_) => ERR_VERIFY_ISSUER_SIG_FAIL, + AttestationError::DeviceSignatureFailed(_) => ERR_VERIFY_DEVICE_SIG_FAIL, + AttestationError::AttestationExpired { .. } => ERR_VERIFY_EXPIRED, + AttestationError::AttestationRevoked => ERR_VERIFY_REVOKED, + AttestationError::TimestampInFuture { .. } => ERR_VERIFY_FUTURE_TIMESTAMP, + AttestationError::SerializationError(_) => ERR_VERIFY_SERIALIZATION, + AttestationError::InvalidInput(_) => ERR_VERIFY_INVALID_PK_LEN, + AttestationError::InputTooLarge(_) => ERR_VERIFY_INPUT_TOO_LARGE, + AttestationError::BundleExpired { .. } => ERR_VERIFY_EXPIRED, + _ => ERR_VERIFY_OTHER, + } +} + fn check_batch_sizes(sizes: &[usize], caller: &str) -> Option { for &size in sizes { if size > MAX_JSON_BATCH_SIZE { @@ -199,30 +216,7 @@ pub unsafe extern "C" fn ffi_verify_attestation_json( Ok(_) => VERIFY_SUCCESS, Err(e) => { error!("FFI verify failed: Verification logic error: {}", e); - // TECH-DEBT(fn-33): error code mapping couples to message text — - // if AttestationError message strings change, these codes break silently. - // Fix: add a structured variant or error_code() method to AttestationError. - match e { - AttestationError::VerificationError(msg) => { - let lower_msg = msg.to_lowercase(); - if lower_msg.contains("issuer signature") { - ERR_VERIFY_ISSUER_SIG_FAIL - } else if lower_msg.contains("device signature") { - ERR_VERIFY_DEVICE_SIG_FAIL - } else if lower_msg.contains("expired") { - ERR_VERIFY_EXPIRED - } else if lower_msg.contains("revoked") { - ERR_VERIFY_REVOKED - } else if lower_msg.contains("invalid length") { - ERR_VERIFY_INVALID_PK_LEN - } else { - ERR_VERIFY_OTHER - } - } - AttestationError::SerializationError(_) => ERR_VERIFY_SERIALIZATION, - AttestationError::InvalidInput(_) => ERR_VERIFY_INVALID_PK_LEN, - _ => ERR_VERIFY_OTHER, - } + attestation_error_to_code(&e) } } }); @@ -324,7 +318,7 @@ pub unsafe extern "C" fn ffi_verify_chain_with_witnesses( Ok(r) => r, Err(e) => { error!("FFI verify_chain_with_witnesses: verification error: {}", e); - return ERR_VERIFY_OTHER; + return attestation_error_to_code(&e); } }; @@ -406,7 +400,7 @@ pub unsafe extern "C" fn ffi_verify_chain_json( Ok(r) => r, Err(e) => { error!("FFI verify_chain_json: verification error: {}", e); - return ERR_VERIFY_OTHER; + return attestation_error_to_code(&e); } }; @@ -528,7 +522,7 @@ pub unsafe extern "C" fn ffi_verify_device_authorization_json( "FFI verify_device_authorization_json: verification error: {}", e ); - return ERR_VERIFY_OTHER; + return attestation_error_to_code(&e); } }; diff --git a/crates/auths-verifier/src/verify.rs b/crates/auths-verifier/src/verify.rs index 1a8e98b0..3f1509a7 100644 --- a/crates/auths-verifier/src/verify.rs +++ b/crates/auths-verifier/src/verify.rs @@ -333,19 +333,16 @@ pub(crate) async fn verify_with_keys_at( if let Some(revoked_at) = att.revoked_at && revoked_at <= reference_time { - return Err(AttestationError::VerificationError( - "Attestation revoked".to_string(), - )); + return Err(AttestationError::AttestationRevoked); } // --- 2. Check expiration against reference time --- if let Some(exp) = att.expires_at && reference_time > exp { - return Err(AttestationError::VerificationError(format!( - "Attestation expired on {}", - exp.to_rfc3339() - ))); + return Err(AttestationError::AttestationExpired { + at: exp.to_rfc3339(), + }); } // --- 3. Check timestamp skew against reference time --- @@ -353,15 +350,14 @@ pub(crate) async fn verify_with_keys_at( && let Some(ts) = att.timestamp && ts > reference_time + Duration::seconds(MAX_SKEW_SECS) { - return Err(AttestationError::VerificationError(format!( - "Attestation timestamp ({}) is in the future", - ts.to_rfc3339(), - ))); + return Err(AttestationError::TimestampInFuture { + at: ts.to_rfc3339(), + }); } // --- 4. Check provided issuer public key length --- if !att.identity_signature.is_empty() && issuer_pk_bytes.len() != ED25519_PUBLIC_KEY_LEN { - return Err(AttestationError::VerificationError(format!( + return Err(AttestationError::InvalidInput(format!( "Provided issuer public key has invalid length: {}", issuer_pk_bytes.len() ))); @@ -404,11 +400,7 @@ pub(crate) async fn verify_with_keys_at( att.identity_signature.as_bytes(), ) .await - .map_err(|_| { - AttestationError::VerificationError( - "Issuer signature verification failed".to_string(), - ) - })?; + .map_err(|e| AttestationError::IssuerSignatureFailed(e.to_string()))?; debug!("(Verify) Issuer signature verified successfully."); } else { debug!( @@ -424,9 +416,7 @@ pub(crate) async fn verify_with_keys_at( att.device_signature.as_bytes(), ) .await - .map_err(|_| { - AttestationError::VerificationError("Device signature verification failed".to_string()) - })?; + .map_err(|e| AttestationError::DeviceSignatureFailed(e.to_string()))?; debug!("(Verify) Device signature verified successfully."); Ok(()) @@ -1361,8 +1351,8 @@ mod tests { .await; assert!(result.is_err()); match result { - Err(AttestationError::VerificationError(_)) => {} - _ => panic!("Expected VerificationError, got {:?}", result), + Err(AttestationError::IssuerSignatureFailed(_)) => {} + _ => panic!("Expected IssuerSignatureFailed, got {:?}", result), } } diff --git a/crates/auths-verifier/src/wasm.rs b/crates/auths-verifier/src/wasm.rs index 43a70986..0804405c 100644 --- a/crates/auths-verifier/src/wasm.rs +++ b/crates/auths-verifier/src/wasm.rs @@ -1,5 +1,6 @@ use crate::clock::{ClockProvider, SystemClock}; use crate::core::{Attestation, MAX_ATTESTATION_JSON_SIZE, MAX_JSON_BATCH_SIZE}; +use crate::error::{AttestationError, AuthsErrorInfo}; use crate::keri; use crate::types::VerificationReport; use crate::verify; @@ -27,6 +28,9 @@ pub struct WasmVerificationResult { /// Human-readable error message if verification failed. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, + /// Structured error code for programmatic handling. + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, } /// Verifies an attestation provided as a JSON string against an explicit issuer public key hex string. @@ -67,7 +71,7 @@ pub async fn wasm_verify_attestation_json( } Err(e) => { console_log!("WASM: Verification failed: {}", e); - Err(JsValue::from_str(&e.to_string())) + Err(JsValue::from_str(&format!("[{}] {}", e.error_code(), e))) } } } @@ -83,10 +87,12 @@ pub async fn wasm_verify_attestation_with_result( Ok(()) => WasmVerificationResult { valid: true, error: None, + error_code: None, }, Err(e) => WasmVerificationResult { valid: false, - error: Some(e), + error: Some(e.to_string()), + error_code: Some(e.error_code().to_string()), }, }; serde_json::to_string(&result) @@ -97,32 +103,32 @@ async fn verify_attestation_internal( attestation_json_str: &str, issuer_pk_hex: &str, provider: &dyn CryptoProvider, -) -> Result<(), String> { +) -> Result<(), AttestationError> { if attestation_json_str.len() > MAX_ATTESTATION_JSON_SIZE { - return Err(format!( + return Err(AttestationError::InputTooLarge(format!( "Attestation JSON too large: {} bytes, max {}", attestation_json_str.len(), MAX_ATTESTATION_JSON_SIZE - )); + ))); } - let issuer_pk_bytes = - hex::decode(issuer_pk_hex).map_err(|e| format!("Invalid issuer public key hex: {}", e))?; + let issuer_pk_bytes = hex::decode(issuer_pk_hex).map_err(|e| { + AttestationError::InvalidInput(format!("Invalid issuer public key hex: {}", e)) + })?; if issuer_pk_bytes.len() != ED25519_PUBLIC_KEY_LEN { - return Err(format!( + return Err(AttestationError::InvalidInput(format!( "Invalid issuer public key length: expected {}, got {}", ED25519_PUBLIC_KEY_LEN, issuer_pk_bytes.len() - )); + ))); } - let att: Attestation = serde_json::from_str(attestation_json_str) - .map_err(|e| format!("Failed to parse attestation JSON: {}", e))?; + let att: Attestation = serde_json::from_str(attestation_json_str).map_err(|e| { + AttestationError::SerializationError(format!("Failed to parse attestation JSON: {}", e)) + })?; - verify::verify_with_keys_at(&att, &issuer_pk_bytes, SystemClock.now(), true, provider) - .await - .map_err(|e| e.to_string()) + verify::verify_with_keys_at(&att, &issuer_pk_bytes, SystemClock.now(), true, provider).await } /// Verifies a detached Ed25519 signature over a file hash (all inputs hex-encoded). @@ -159,7 +165,13 @@ pub async fn wasm_verify_chain_json(attestations_json_array: &str, root_pk_hex: Ok(report) => serde_json::to_string(&report) .unwrap_or_else(|_| r#"{"status":{"type":"BrokenChain","missingLink":"Serialization failed"},"chain":[],"warnings":[]}"#.to_string()), Err(e) => { - format!(r#"{{"status":{{"type":"BrokenChain","missing_link":"{}"}},"chain":[],"warnings":[]}}"#, e.replace('"', "\\\"")) + let error_response = serde_json::json!({ + "status": { "type": "BrokenChain", "missing_link": e.to_string() }, + "chain": [], + "warnings": [], + "error_code": e.error_code(), + }); + error_response.to_string() } } } @@ -168,32 +180,36 @@ async fn verify_chain_internal( attestations_json_array: &str, root_pk_hex: &str, provider: &dyn CryptoProvider, -) -> Result { +) -> Result { if attestations_json_array.len() > MAX_JSON_BATCH_SIZE { - return Err(format!( + return Err(AttestationError::InputTooLarge(format!( "Attestations JSON too large: {} bytes, max {}", attestations_json_array.len(), MAX_JSON_BATCH_SIZE - )); + ))); } - let root_pk_bytes = - hex::decode(root_pk_hex).map_err(|e| format!("Invalid root public key hex: {}", e))?; + let root_pk_bytes = hex::decode(root_pk_hex).map_err(|e| { + AttestationError::InvalidInput(format!("Invalid root public key hex: {}", e)) + })?; if root_pk_bytes.len() != ED25519_PUBLIC_KEY_LEN { - return Err(format!( + return Err(AttestationError::InvalidInput(format!( "Invalid root public key length: expected {}, got {}", ED25519_PUBLIC_KEY_LEN, root_pk_bytes.len() - )); + ))); } - let attestations: Vec = serde_json::from_str(attestations_json_array) - .map_err(|e| format!("Failed to parse attestations JSON array: {}", e))?; + let attestations: Vec = + serde_json::from_str(attestations_json_array).map_err(|e| { + AttestationError::SerializationError(format!( + "Failed to parse attestations JSON array: {}", + e + )) + })?; - verify::verify_chain_inner(&attestations, &root_pk_bytes, provider, SystemClock.now()) - .await - .map_err(|e| e.to_string()) + verify::verify_chain_inner(&attestations, &root_pk_bytes, provider, SystemClock.now()).await } /// Verifies a chain of attestations with witness quorum checking. @@ -218,7 +234,13 @@ pub async fn wasm_verify_chain_with_witnesses_json( Ok(report) => serde_json::to_string(&report) .unwrap_or_else(|_| r#"{"status":{"type":"BrokenChain","missing_link":"Serialization failed"},"chain":[],"warnings":[]}"#.to_string()), Err(e) => { - format!(r#"{{"status":{{"type":"BrokenChain","missing_link":"{}"}},"chain":[],"warnings":[]}}"#, e.replace('"', "\\\"")) + let error_response = serde_json::json!({ + "status": { "type": "BrokenChain", "missing_link": e.to_string() }, + "chain": [], + "warnings": [], + "error_code": e.error_code(), + }); + error_response.to_string() } } } @@ -230,60 +252,68 @@ async fn verify_chain_with_witnesses_internal( witness_keys_json: &str, threshold: u32, provider: &dyn CryptoProvider, -) -> Result { +) -> Result { if chain_json.len() > MAX_JSON_BATCH_SIZE { - return Err(format!( + return Err(AttestationError::InputTooLarge(format!( "Chain JSON too large: {} bytes, max {}", chain_json.len(), MAX_JSON_BATCH_SIZE - )); + ))); } if receipts_json.len() > MAX_JSON_BATCH_SIZE { - return Err(format!( + return Err(AttestationError::InputTooLarge(format!( "Receipts JSON too large: {} bytes, max {}", receipts_json.len(), MAX_JSON_BATCH_SIZE - )); + ))); } if witness_keys_json.len() > MAX_JSON_BATCH_SIZE { - return Err(format!( + return Err(AttestationError::InputTooLarge(format!( "Witness keys JSON too large: {} bytes, max {}", witness_keys_json.len(), MAX_JSON_BATCH_SIZE - )); + ))); } - let root_pk_bytes = - hex::decode(root_pk_hex).map_err(|e| format!("Invalid root public key hex: {}", e))?; + let root_pk_bytes = hex::decode(root_pk_hex).map_err(|e| { + AttestationError::InvalidInput(format!("Invalid root public key hex: {}", e)) + })?; if root_pk_bytes.len() != ED25519_PUBLIC_KEY_LEN { - return Err(format!( + return Err(AttestationError::InvalidInput(format!( "Invalid root public key length: expected {}, got {}", ED25519_PUBLIC_KEY_LEN, root_pk_bytes.len() - )); + ))); } - let attestations: Vec = serde_json::from_str(chain_json) - .map_err(|e| format!("Failed to parse attestations JSON: {}", e))?; + let attestations: Vec = serde_json::from_str(chain_json).map_err(|e| { + AttestationError::SerializationError(format!("Failed to parse attestations JSON: {}", e)) + })?; - let receipts: Vec = serde_json::from_str(receipts_json) - .map_err(|e| format!("Failed to parse receipts JSON: {}", e))?; + let receipts: Vec = serde_json::from_str(receipts_json).map_err(|e| { + AttestationError::SerializationError(format!("Failed to parse receipts JSON: {}", e)) + })?; #[derive(Deserialize)] struct WitnessKeyEntry { did: String, pk_hex: String, } - let key_entries: Vec = serde_json::from_str(witness_keys_json) - .map_err(|e| format!("Failed to parse witness keys JSON: {}", e))?; + let key_entries: Vec = + serde_json::from_str(witness_keys_json).map_err(|e| { + AttestationError::SerializationError(format!( + "Failed to parse witness keys JSON: {}", + e + )) + })?; let witness_keys: Vec<(String, Vec)> = key_entries .into_iter() .map(|e| { - hex::decode(&e.pk_hex) - .map(|pk| (e.did, pk)) - .map_err(|err| format!("Invalid witness key hex: {}", err)) + hex::decode(&e.pk_hex).map(|pk| (e.did, pk)).map_err(|err| { + AttestationError::InvalidInput(format!("Invalid witness key hex: {}", err)) + }) }) .collect::, _>>()?; @@ -295,8 +325,7 @@ async fn verify_chain_with_witnesses_internal( let mut report = verify::verify_chain_inner(&attestations, &root_pk_bytes, provider, SystemClock.now()) - .await - .map_err(|e| e.to_string())?; + .await?; if report.is_valid() { let quorum = crate::witness::verify_witness_receipts(&config, provider).await; diff --git a/docs/plans/gemini_feedback.md b/docs/plans/gemini_feedback.md new file mode 100644 index 00000000..3e7f9ff0 --- /dev/null +++ b/docs/plans/gemini_feedback.md @@ -0,0 +1,143 @@ +# Gemini Feedback: A CTO's Playbook for the Auths v0.1.0 Launch + +**To:** Auths Leadership +**From:** Gemini (CTO / DX Lead Persona) +**Date:** 2026-03-06 +**Subject:** An Actionable Roadmap for the Auths v0.1.0 Launch + +## 1. Executive Summary & Restructured Plan + +Our goal for the v0.1.0 launch is to establish `auths` as the most polished, trustworthy, and developer-obsessed identity platform on the market. Our current codebase is functionally powerful but lacks the stability and seamless developer experience (DX) required for a public launch. + +This document has been restructured from a simple list of issues into a **chronological, dependency-aware roadmap**. It is organized into four distinct phases of work. Each phase builds upon the last, ensuring that we solidify our foundation before building upon it. This is the critical path to a successful v0.1.0 launch. + +--- + +## Phase 1: Solidify the Core (Rust SDK & Verifier) + +**Objective:** Create a stable, predictable, and secure foundation. All work in this phase is a prerequisite for subsequent phases. + +### 1.1. Implement Native Commit Verification +* **Why:** The current Python-based commit verification shells out to `ssh-keygen`, which is slow, brittle, and not portable. This is our biggest reliability risk. +* **The Problem:** + ```python + # in packages/auths-python/python/auths/git.py + proc = subprocess.run( + ["ssh-keygen", "-Y", "verify", ...], ... + ) + ``` +* **Action:** Implement the entire commit signature verification logic in pure Rust within the `crates/auths-verifier` crate. This single change will dramatically improve performance and reliability for a key feature. + +### 1.2. Refactor SDK Configuration for Compile-Time Safety +* **Why:** The SDK must be impossible to misconfigure. We can prevent entire classes of runtime errors at compile time. +* **The Problem:** The `AuthsContextBuilder` in `crates/auths-sdk/src/context.rs` uses a `NoopPassphraseProvider` that causes a runtime error if signing is attempted without a real provider. + ```rust + // This defers a configuration error to a runtime crash. + passphrase_provider: self + .passphrase_provider + .unwrap_or_else(|| Arc::new(NoopPassphraseProvider)), + ``` +* **Action:** Eliminate the `Noop` providers. Use the typestate pattern to create distinct `AuthsContext` types, such as `AuthsContext` and `AuthsContext`. Workflows that require signing must take the `SigningReady` context as an argument, making it a compile-time error to call them without the correct configuration. + +### 1.3. Eradicate Panics from the Public API +* **Why:** A library that can `panic` is a library that cannot be trusted in production. It is the most hostile behavior an SDK can exhibit. +* **The Problem:** The codebase is littered with `.unwrap()` and `.expect()` calls that can crash the host application. + ```rust + // in crates/auths-sdk/src/workflows/mcp.rs:80 + .expect("failed to build HTTP client") // Will crash if host TLS is misconfigured. + ``` +* **Action:** Audit and refactor every `.unwrap()` and `.expect()` in the `auths-sdk` crate's public-facing workflows. Replace them with proper, descriptive error variants (e.g., `McpAuthError::HttpClientBuildFailed`). + +### 1.4. Unify and Seal the Public API Surface +* **Why:** We are making a promise of stability with `v0.1.0`. The API we launch with is the API we must support. +* **Action:** + 1. **Unify:** Refactor the `initialize_developer`, `initialize_ci`, and `initialize_agent` functions in `crates/auths-sdk/src/setup.rs` into private helpers. The single public entry point must be `pub fn initialize(config: IdentityConfig, ...)`. + 2. **Seal:** Run `cargo public-api` to generate a definitive list of our public API. Anything we are not ready to commit to for the long term must be hidden (`pub(crate)` or `#[doc(hidden)]`). + +--- + +## Phase 2: Refine the Developer Experience (Python FFI & SDK) + +**Objective:** Create an idiomatic, robust, and effortless experience for Python developers. This phase depends heavily on the stability provided by Phase 1. + +### 2.1. Implement Robust FFI Error Handling +* **Dependency:** Phase 1.3 (Eradicate Panics). The Rust layer must return errors, not panic. +* **Why:** The current error handling is based on string-matching messages from Rust, which is extremely fragile. +* **The Problem:** + ```python + # in packages/auths-python/python/auths/_client.py + def _map_verify_error(exc: Exception) -> Exception: + msg = str(exc) + if "public key" in msg.lower(): # This will break silently. + return CryptoError(msg, code="invalid_key") + ``` +* **Action:** Modify the Rust FFI layer to return a stable, machine-readable error code (a C-style enum or integer). The Python `_map_verify_error` function must be rewritten to dispatch on this reliable code. +This MUST be consistent across all such files + +### 2.2. Consume Native Commit Verification +* **Dependency:** Phase 1.1 (Native Commit Verification). +* **Why:** To eliminate the slow and brittle `subprocess` calls. +* **Action:** Remove the `verify_commit_range` implementation from `packages/auths-python/python/auths/git.py` and replace its body with a single call to the new native Rust function (exposed via the `auths._native` module). + +### 2.3. Adopt Pythonic Types and Conventions +* **Why:** The Python SDK must respect the conventions of its ecosystem to feel natural to developers. +* **The Problem:** The API uses strings for timestamps and may not return idiomatic `dataclass` instances. + ```python + # in packages/auths-python/python/auths/_client.py + def verify(self, ..., at: str | None = None) -> VerificationResult: + # ... + ``` +* **Action:** + 1. Modify methods like `verify` to accept `datetime.datetime` objects. The implementation can then convert them to Unix timestamps (integers) to pass to the Rust layer. + 2. Audit all functions that return data from Rust. Ensure they return proper `@dataclass` instances, not raw dictionaries or tuples. + 3. Ensure all public methods and parameters follow `snake_case` conventions. + +--- + +## Phase 3: Polish the Public Integrations (JS Ecosystem) + +**Objective:** Ensure our integrations are seamless, easy to use, and inspire confidence. This can run in parallel with Phase 2. + +### 3.1. Manage External Dependencies for Independent Repos +* **Correction & Context:** My previous analysis incorrectly assumed a monorepo structure. Understanding these are independent repositories makes the dependency management even more critical. The current build scripts have hardcoded relative paths that will fail in any standard CI/CD environment or for any external contributor. +* **Action (`auths-verify-widget`):** The `build:wasm` script in `package.json` (`"cd ../auths/crates/auths-verifier && wasm-pack build ..."`) is a critical flaw. It relies on a local file structure that will not exist in a clean checkout. The WASM verifier *must* be treated as a versioned, third-party dependency. + 1. The `auths/crates/auths-verifier` project must be configured to compile to WASM and be published to `npm` as a standalone package (e.g., `@auths/verifier-wasm`). + 2. The `auths-verify-widget` must remove the `build:wasm` script and add `@auths/verifier-wasm` as a standard `devDependency` in its `package.json`. + This ensures the widget can be built, tested, and released independently. +* **Action (`auths-verify-github-action`):** The action correctly treats the `auths` CLI as an external dependency by downloading it at runtime. However, for a v0.1.0 launch, this introduces too much variability. + 1. For the v0.1.0 release, the action *must bundle a specific, known-good version* of the `auths` native binary for Linux x64 (the standard GitHub runner environment). This guarantees performance and reliability. + 2. This can be accomplished by adding a script to the `auths-verify-github-action` repo that downloads a specific versioned release of the `auths` CLI from its GitHub Releases page and places it in the `dist` directory as part of the build process. + +### 3.2. Improve DX for Integrations +* **Why:** These integrations are the "front door" to our product for many developers. The experience must be flawless. +* **Action (`auths-verify-widget`):** + 1. **Clarify Variants:** The `README.md` must clearly explain the "full" vs. "slim" builds. + 2. **No-Build Option:** Create a UMD bundle and publish it to a CDN (`unpkg`, `jsdelivr`) so the widget can be used with a simple `