use auths_core::error::AuthsErrorInfo;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct SignedAuthChallenge {
pub signature_hex: String,
pub public_key_hex: String,
pub did: String,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AuthChallengeError {
#[error("nonce must not be empty")]
EmptyNonce,
#[error("domain must not be empty")]
EmptyDomain,
#[error("canonical JSON serialization failed: {0}")]
Canonicalization(String),
}
impl AuthsErrorInfo for AuthChallengeError {
fn error_code(&self) -> &'static str {
match self {
Self::EmptyNonce => "AUTHS-E6001",
Self::EmptyDomain => "AUTHS-E6002",
Self::Canonicalization(_) => "AUTHS-E6003",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::EmptyNonce => Some("Provide the nonce from the authentication challenge"),
Self::EmptyDomain => Some("Provide the domain (e.g. auths.dev)"),
Self::Canonicalization(_) => {
Some("This is an internal error; please report it as a bug")
}
}
}
}
pub fn build_auth_challenge_message(
nonce: &str,
domain: &str,
) -> Result<String, AuthChallengeError> {
if nonce.is_empty() {
return Err(AuthChallengeError::EmptyNonce);
}
if domain.is_empty() {
return Err(AuthChallengeError::EmptyDomain);
}
let payload = serde_json::json!({
"domain": domain,
"nonce": nonce,
});
json_canon::to_string(&payload).map_err(|e| AuthChallengeError::Canonicalization(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_json_sorts_keys_alphabetically() {
let payload = serde_json::json!({
"nonce": "abc",
"domain": "xyz",
});
let canonical = json_canon::to_string(&payload).expect("canonical");
assert_eq!(canonical, r#"{"domain":"xyz","nonce":"abc"}"#);
}
#[test]
fn build_auth_challenge_message_produces_canonical_json() {
let msg = build_auth_challenge_message("abc123", "auths.dev").unwrap();
assert_eq!(msg, r#"{"domain":"auths.dev","nonce":"abc123"}"#);
}
#[test]
fn build_auth_challenge_message_rejects_empty_nonce() {
let err = build_auth_challenge_message("", "auths.dev").unwrap_err();
assert!(matches!(err, AuthChallengeError::EmptyNonce));
}
#[test]
fn build_auth_challenge_message_rejects_empty_domain() {
let err = build_auth_challenge_message("abc", "").unwrap_err();
assert!(matches!(err, AuthChallengeError::EmptyDomain));
}
}