use serde::{Deserialize, Serialize};
pub const CHALLENGE_TYPE: &str = "openagent-challenge-v1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdentityChallenge {
#[serde(rename = "type")]
pub message_type: String,
pub nonce: String,
pub timestamp: String,
pub origin: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub realm: Option<String>,
}
impl IdentityChallenge {
pub fn new(nonce: String, timestamp: String, origin: String, realm: Option<String>) -> Self {
Self {
message_type: CHALLENGE_TYPE.to_string(),
nonce,
timestamp,
origin,
realm,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KeyType {
Ed25519,
Secp256k1,
}
impl std::fmt::Display for KeyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ed25519 => write!(f, "ed25519"),
Self::Secp256k1 => write!(f, "secp256k1"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdentityProof {
pub signature: String,
pub public_key: String,
pub key_type: KeyType,
pub nonce: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TrustTier {
Anonymous,
Identified,
Sovereign,
}
impl std::fmt::Display for TrustTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Anonymous => write!(f, "anonymous"),
Self::Identified => write!(f, "identified"),
Self::Sovereign => write!(f, "sovereign"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdentityVerified {
pub did: String,
pub trust_tier: TrustTier,
pub session_token: String,
pub session_expires: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<Vec<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn challenge_serializes_with_type_literal() {
let challenge = IdentityChallenge::new(
"a".repeat(64),
"2026-03-21T12:00:00Z".to_string(),
"https://api.example.com".to_string(),
None,
);
let json = serde_json::to_string(&challenge).expect("serialize");
assert!(json.contains(r#""type":"openagent-challenge-v1""#));
assert!(!json.contains("realm"));
}
#[test]
fn challenge_includes_realm_only_when_present() {
let challenge = IdentityChallenge::new(
"a".repeat(64),
"2026-03-21T12:00:00Z".to_string(),
"https://api.example.com".to_string(),
Some("admin".to_string()),
);
let json = serde_json::to_string(&challenge).expect("serialize");
assert!(json.contains(r#""realm":"admin""#));
}
#[test]
fn key_type_uses_spec_spelling() {
let json = serde_json::to_string(&KeyType::Ed25519).expect("serialize");
assert_eq!(json, r#""ed25519""#);
let json = serde_json::to_string(&KeyType::Secp256k1).expect("serialize");
assert_eq!(json, r#""secp256k1""#);
}
#[test]
fn trust_tier_uses_spec_spelling() {
let json = serde_json::to_string(&TrustTier::Sovereign).expect("serialize");
assert_eq!(json, r#""sovereign""#);
}
}