Skip to main content

ans_client/
models.rs

1//! API request and response models for the ANS Registry.
2//!
3//! These types map to the `OpenAPI` specification for the ANS API.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use uuid::Uuid;
9
10/// Communication protocol used by agents.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[non_exhaustive]
13pub enum Protocol {
14    /// Agent-to-Agent protocol.
15    #[serde(rename = "A2A")]
16    A2A,
17    /// Model Context Protocol.
18    #[serde(rename = "MCP")]
19    Mcp,
20    /// HTTP-based API.
21    #[serde(rename = "HTTP-API", alias = "HTTP_API")]
22    HttpApi,
23}
24
25impl fmt::Display for Protocol {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Self::A2A => write!(f, "A2A"),
29            Self::Mcp => write!(f, "MCP"),
30            Self::HttpApi => write!(f, "HTTP-API"),
31        }
32    }
33}
34
35/// Transport mechanism for agent communication.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[non_exhaustive]
38pub enum Transport {
39    /// Streamable HTTP transport.
40    #[serde(rename = "STREAMABLE-HTTP", alias = "STREAMABLE_HTTP")]
41    StreamableHttp,
42    /// Server-Sent Events.
43    #[serde(rename = "SSE")]
44    Sse,
45    /// JSON-RPC transport.
46    #[serde(rename = "JSON-RPC", alias = "JSON_RPC")]
47    JsonRpc,
48    /// gRPC transport.
49    #[serde(rename = "GRPC")]
50    Grpc,
51    /// REST transport.
52    #[serde(rename = "REST")]
53    Rest,
54    /// Generic HTTP transport.
55    #[serde(rename = "HTTP")]
56    Http,
57}
58
59/// A function/capability provided by an agent endpoint.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct AgentFunction {
63    /// Unique identifier for the function.
64    pub id: String,
65    /// Human-readable name.
66    pub name: String,
67    /// Tags for categorization.
68    #[serde(default, skip_serializing_if = "Vec::is_empty")]
69    pub tags: Vec<String>,
70}
71
72impl AgentFunction {
73    /// Create a new agent function.
74    pub fn new(id: impl Into<String>, name: impl Into<String>, tags: Vec<String>) -> Self {
75        Self {
76            id: id.into(),
77            name: name.into(),
78            tags,
79        }
80    }
81}
82
83/// An agent endpoint configuration.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86#[non_exhaustive]
87pub struct AgentEndpoint {
88    /// URL where the agent accepts requests.
89    pub agent_url: String,
90    /// URL for agent metadata.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub meta_data_url: Option<String>,
93    /// URL for agent documentation.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub documentation_url: Option<String>,
96    /// Communication protocol.
97    pub protocol: Protocol,
98    /// Supported transport mechanisms.
99    #[serde(default, skip_serializing_if = "Vec::is_empty")]
100    pub transports: Vec<Transport>,
101    /// Functions provided by this endpoint.
102    #[serde(default, skip_serializing_if = "Vec::is_empty")]
103    pub functions: Vec<AgentFunction>,
104}
105
106impl AgentEndpoint {
107    /// Create a new endpoint with required fields.
108    pub fn new(agent_url: impl Into<String>, protocol: Protocol) -> Self {
109        Self {
110            agent_url: agent_url.into(),
111            meta_data_url: None,
112            documentation_url: None,
113            protocol,
114            transports: Vec::new(),
115            functions: Vec::new(),
116        }
117    }
118
119    /// Set the transport mechanisms.
120    pub fn with_transports(mut self, transports: Vec<Transport>) -> Self {
121        self.transports = transports;
122        self
123    }
124
125    /// Set the functions.
126    pub fn with_functions(mut self, functions: Vec<AgentFunction>) -> Self {
127        self.functions = functions;
128        self
129    }
130}
131
132/// Request to register a new agent.
133#[derive(Debug, Clone, Serialize)]
134#[serde(rename_all = "camelCase")]
135#[non_exhaustive]
136pub struct AgentRegistrationRequest {
137    /// Human-readable agent name.
138    pub agent_display_name: String,
139    /// FQDN where the agent is hosted.
140    pub agent_host: String,
141    /// Semantic version (e.g., "1.0.0").
142    pub version: String,
143    /// Optional description.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub agent_description: Option<String>,
146    /// CSR for identity certificate (required).
147    #[serde(rename = "identityCsrPEM")]
148    pub identity_csr_pem: String,
149    /// CSR for server certificate (mutually exclusive with `server_certificate_pem`).
150    #[serde(rename = "serverCsrPEM", skip_serializing_if = "Option::is_none")]
151    pub server_csr_pem: Option<String>,
152    /// BYOC server certificate (mutually exclusive with `server_csr_pem`).
153    #[serde(
154        rename = "serverCertificatePEM",
155        skip_serializing_if = "Option::is_none"
156    )]
157    pub server_certificate_pem: Option<String>,
158    /// Certificate chain for BYOC server certificate.
159    #[serde(
160        rename = "serverCertificateChainPEM",
161        skip_serializing_if = "Option::is_none"
162    )]
163    pub server_certificate_chain_pem: Option<String>,
164    /// Agent endpoints.
165    pub endpoints: Vec<AgentEndpoint>,
166}
167
168impl AgentRegistrationRequest {
169    /// Create a new registration request with required fields.
170    pub fn new(
171        agent_display_name: impl Into<String>,
172        agent_host: impl Into<String>,
173        version: impl Into<String>,
174        identity_csr_pem: impl Into<String>,
175        endpoints: Vec<AgentEndpoint>,
176    ) -> Self {
177        Self {
178            agent_display_name: agent_display_name.into(),
179            agent_host: agent_host.into(),
180            version: version.into(),
181            agent_description: None,
182            identity_csr_pem: identity_csr_pem.into(),
183            server_csr_pem: None,
184            server_certificate_pem: None,
185            server_certificate_chain_pem: None,
186            endpoints,
187        }
188    }
189
190    /// Set the agent description.
191    pub fn with_description(mut self, description: impl Into<String>) -> Self {
192        self.agent_description = Some(description.into());
193        self
194    }
195
196    /// Set the server CSR PEM.
197    pub fn with_server_csr_pem(mut self, csr: impl Into<String>) -> Self {
198        self.server_csr_pem = Some(csr.into());
199        self
200    }
201
202    /// Set the server certificate PEM (BYOC).
203    pub fn with_server_certificate_pem(mut self, cert: impl Into<String>) -> Self {
204        self.server_certificate_pem = Some(cert.into());
205        self
206    }
207
208    /// Set the server certificate chain PEM (BYOC).
209    pub fn with_server_certificate_chain_pem(mut self, chain: impl Into<String>) -> Self {
210        self.server_certificate_chain_pem = Some(chain.into());
211        self
212    }
213}
214
215/// Registration status.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
218#[non_exhaustive]
219pub enum RegistrationStatus {
220    /// Waiting for domain validation.
221    PendingValidation,
222    /// Waiting for certificates.
223    PendingCerts,
224    /// Waiting for DNS configuration.
225    PendingDns,
226}
227
228/// Agent lifecycle status.
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
231#[non_exhaustive]
232pub enum AgentLifecycleStatus {
233    /// Waiting for validation.
234    PendingValidation,
235    /// Waiting for DNS.
236    PendingDns,
237    /// Agent is active.
238    Active,
239    /// Registration failed.
240    Failed,
241    /// Registration expired.
242    Expired,
243    /// Agent was revoked.
244    Revoked,
245}
246
247/// HATEOAS link.
248#[derive(Debug, Clone, Serialize, Deserialize)]
249#[non_exhaustive]
250pub struct Link {
251    /// Link relation type.
252    pub rel: String,
253    /// Link URL.
254    pub href: String,
255}
256
257/// DNS record to be configured.
258#[derive(Debug, Clone, Serialize, Deserialize)]
259#[non_exhaustive]
260pub struct DnsRecord {
261    /// Full DNS record name.
262    pub name: String,
263    /// Record type (HTTPS, TLSA, TXT).
264    #[serde(rename = "type")]
265    pub record_type: String,
266    /// Record value.
267    pub value: String,
268    /// Purpose of this record.
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub purpose: Option<String>,
271    /// TTL in seconds.
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub ttl: Option<i32>,
274    /// Priority for HTTPS records.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub priority: Option<i32>,
277    /// Whether this record is required.
278    #[serde(default = "default_true")]
279    pub required: bool,
280}
281
282fn default_true() -> bool {
283    true
284}
285
286/// ACME challenge type.
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[non_exhaustive]
289pub enum ChallengeType {
290    /// DNS-01 challenge.
291    #[serde(rename = "DNS_01")]
292    Dns01,
293    /// HTTP-01 challenge.
294    #[serde(rename = "HTTP_01")]
295    Http01,
296}
297
298/// DNS record details for ACME challenge.
299#[derive(Debug, Clone, Serialize, Deserialize)]
300#[non_exhaustive]
301pub struct DnsRecordDetails {
302    /// Record name.
303    pub name: String,
304    /// Record type.
305    #[serde(rename = "type")]
306    pub record_type: String,
307    /// Record value.
308    pub value: String,
309}
310
311/// ACME challenge information.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314#[non_exhaustive]
315pub struct ChallengeInfo {
316    /// Challenge type.
317    #[serde(rename = "type")]
318    pub challenge_type: ChallengeType,
319    /// Challenge token.
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub token: Option<String>,
322    /// Key authorization string.
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub key_authorization: Option<String>,
325    /// HTTP path for HTTP-01 challenge.
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub http_path: Option<String>,
328    /// DNS record for DNS-01 challenge.
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub dns_record: Option<DnsRecordDetails>,
331    /// Challenge expiration.
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub expires_at: Option<DateTime<Utc>>,
334}
335
336/// Action to take in next step.
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
338#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
339#[non_exhaustive]
340pub enum NextStepAction {
341    /// Configure DNS records.
342    ConfigureDns,
343    /// Configure HTTP challenge.
344    ConfigureHttp,
345    /// Verify DNS records.
346    VerifyDns,
347    /// Validate domain ownership.
348    ValidateDomain,
349    /// Wait for processing.
350    Wait,
351}
352
353/// A required action to continue registration.
354#[derive(Debug, Clone, Serialize, Deserialize)]
355#[serde(rename_all = "camelCase")]
356#[non_exhaustive]
357pub struct NextStep {
358    /// Action to take.
359    pub action: NextStepAction,
360    /// Description of the step.
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub description: Option<String>,
363    /// API endpoint for the action.
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub endpoint: Option<String>,
366    /// Estimated time in minutes.
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub estimated_time_minutes: Option<i32>,
369}
370
371/// Response for pending registration.
372#[derive(Debug, Clone, Serialize, Deserialize)]
373#[serde(rename_all = "camelCase")]
374#[non_exhaustive]
375pub struct RegistrationPending {
376    /// Current registration status.
377    pub status: RegistrationStatus,
378    /// ANS name being registered.
379    pub ans_name: String,
380    /// Agent ID (when available).
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub agent_id: Option<String>,
383    /// Required actions.
384    pub next_steps: Vec<NextStep>,
385    /// ACME challenges.
386    #[serde(default)]
387    pub challenges: Vec<ChallengeInfo>,
388    /// DNS records to configure.
389    #[serde(default)]
390    pub dns_records: Vec<DnsRecord>,
391    /// Registration expiration.
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub expires_at: Option<DateTime<Utc>>,
394    /// HATEOAS links.
395    #[serde(default)]
396    pub links: Vec<Link>,
397}
398
399impl RegistrationPending {
400    /// Gets the agent ID, either from the field or by parsing the self link.
401    ///
402    /// The API may not include `agent_id` in the response body, but it's
403    /// available in the `self` link (e.g., `/v1/agents/{agent_id}`).
404    pub fn get_agent_id(&self) -> Option<String> {
405        // First try the direct field
406        if let Some(ref id) = self.agent_id {
407            return Some(id.clone());
408        }
409
410        // Fall back to parsing from self link
411        self.links
412            .iter()
413            .find(|link| link.rel == "self")
414            .and_then(|link| {
415                // Parse agent ID from href like "/v1/agents/{agent_id}" or full URL
416                link.href
417                    .trim_end_matches('/')
418                    .rsplit('/')
419                    .next()
420                    .filter(|s| !s.is_empty() && *s != "agents")
421                    .map(String::from)
422            })
423    }
424}
425
426/// Registration phase.
427#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
429#[non_exhaustive]
430pub enum RegistrationPhase {
431    /// Initial setup.
432    Initialization,
433    /// Validating domain ownership.
434    DomainValidation,
435    /// Issuing certificates.
436    CertificateIssuance,
437    /// Provisioning DNS.
438    DnsProvisioning,
439    /// Registration complete.
440    Completed,
441}
442
443/// Agent status information.
444#[derive(Debug, Clone, Serialize, Deserialize)]
445#[serde(rename_all = "camelCase")]
446#[non_exhaustive]
447pub struct AgentStatus {
448    /// Lifecycle status.
449    pub status: AgentLifecycleStatus,
450    /// Current phase.
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub phase: Option<RegistrationPhase>,
453    /// Completed steps.
454    #[serde(default)]
455    pub completed_steps: Vec<String>,
456    /// Pending steps.
457    #[serde(default)]
458    pub pending_steps: Vec<String>,
459    /// When created.
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub created_at: Option<DateTime<Utc>>,
462    /// Last updated.
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub updated_at: Option<DateTime<Utc>>,
465    /// Registration expiration.
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub expires_at: Option<DateTime<Utc>>,
468}
469
470/// Detailed agent information.
471#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase")]
473#[non_exhaustive]
474pub struct AgentDetails {
475    /// Unique agent identifier.
476    pub agent_id: String,
477    /// Display name.
478    pub agent_display_name: String,
479    /// Hosting domain.
480    pub agent_host: String,
481    /// Description.
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub agent_description: Option<String>,
484    /// Full ANS name.
485    pub ans_name: String,
486    /// Version.
487    pub version: String,
488    /// Lifecycle status.
489    pub agent_status: AgentLifecycleStatus,
490    /// Endpoints.
491    pub endpoints: Vec<AgentEndpoint>,
492    /// Registration timestamp.
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub registration_timestamp: Option<DateTime<Utc>>,
495    /// Last renewal timestamp.
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub last_renewal_timestamp: Option<DateTime<Utc>>,
498    /// Pending registration details.
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub registration_pending: Option<RegistrationPending>,
501    /// HATEOAS links.
502    #[serde(default)]
503    pub links: Vec<Link>,
504}
505
506/// Search criteria.
507#[derive(Debug, Clone, Default, Serialize, Deserialize)]
508#[serde(rename_all = "camelCase")]
509#[non_exhaustive]
510pub struct SearchCriteria {
511    /// Filter by protocol.
512    #[serde(skip_serializing_if = "Option::is_none")]
513    pub protocol: Option<Protocol>,
514    /// Filter by display name.
515    #[serde(skip_serializing_if = "Option::is_none")]
516    pub agent_display_name: Option<String>,
517    /// Filter by version.
518    #[serde(skip_serializing_if = "Option::is_none")]
519    pub version: Option<String>,
520    /// Filter by host.
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub agent_host: Option<String>,
523}
524
525/// Agent search result.
526#[derive(Debug, Clone, Serialize, Deserialize)]
527#[serde(rename_all = "camelCase")]
528#[non_exhaustive]
529pub struct AgentSearchResult {
530    /// ANS name.
531    pub ans_name: String,
532    /// Agent ID.
533    pub agent_id: String,
534    /// Display name.
535    pub agent_display_name: String,
536    /// Description.
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub agent_description: Option<String>,
539    /// Version.
540    pub version: String,
541    /// Hosting domain.
542    pub agent_host: String,
543    /// TTL in seconds.
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub ttl: Option<i32>,
546    /// Registration timestamp.
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub registration_timestamp: Option<DateTime<Utc>>,
549    /// Endpoints.
550    pub endpoints: Vec<AgentEndpoint>,
551    /// HATEOAS links.
552    #[serde(default)]
553    pub links: Vec<Link>,
554}
555
556/// Search results response.
557#[derive(Debug, Clone, Serialize, Deserialize)]
558#[serde(rename_all = "camelCase")]
559#[non_exhaustive]
560pub struct AgentSearchResponse {
561    /// Matching agents.
562    pub agents: Vec<AgentSearchResult>,
563    /// Total matching count.
564    pub total_count: i32,
565    /// Count returned in this response.
566    pub returned_count: i32,
567    /// Pagination limit.
568    pub limit: i32,
569    /// Pagination offset.
570    pub offset: i32,
571    /// Whether more results are available.
572    pub has_more: bool,
573    /// Search criteria used.
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub search_criteria: Option<SearchCriteria>,
576}
577
578/// Revocation reason (RFC 5280).
579#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
580#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
581#[non_exhaustive]
582pub enum RevocationReason {
583    /// Private key compromised.
584    KeyCompromise,
585    /// Agent decommissioned.
586    CessationOfOperation,
587    /// Affiliation changed.
588    AffiliationChanged,
589    /// Superseded by new certificate.
590    Superseded,
591    /// Temporarily on hold.
592    CertificateHold,
593    /// Privileges withdrawn.
594    PrivilegeWithdrawn,
595    /// AA compromised.
596    AaCompromise,
597}
598
599/// Request to revoke an agent.
600#[derive(Debug, Clone, Serialize)]
601#[non_exhaustive]
602pub struct AgentRevocationRequest {
603    /// Reason for revocation.
604    pub reason: RevocationReason,
605    /// Additional comments.
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub comments: Option<String>,
608}
609
610/// Revocation response.
611#[derive(Debug, Clone, Serialize, Deserialize)]
612#[serde(rename_all = "camelCase")]
613#[non_exhaustive]
614pub struct AgentRevocationResponse {
615    /// Agent ID.
616    pub agent_id: Uuid,
617    /// ANS name.
618    pub ans_name: String,
619    /// Status (will be REVOKED).
620    pub status: AgentLifecycleStatus,
621    /// When revocation occurred.
622    pub revoked_at: DateTime<Utc>,
623    /// Revocation reason.
624    pub reason: RevocationReason,
625    /// HATEOAS links.
626    pub links: Vec<Link>,
627}
628
629/// Certificate information.
630#[derive(Debug, Clone, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632#[non_exhaustive]
633pub struct CertificateResponse {
634    /// CSR ID that generated this certificate.
635    pub csr_id: Uuid,
636    /// Certificate subject DN.
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub certificate_subject: Option<String>,
639    /// Certificate issuer DN.
640    #[serde(skip_serializing_if = "Option::is_none")]
641    pub certificate_issuer: Option<String>,
642    /// Serial number.
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub certificate_serial_number: Option<String>,
645    /// Validity start.
646    pub certificate_valid_from: DateTime<Utc>,
647    /// Validity end.
648    pub certificate_valid_to: DateTime<Utc>,
649    /// PEM-encoded certificate.
650    #[serde(rename = "certificatePEM")]
651    pub certificate_pem: String,
652    /// PEM-encoded certificate chain.
653    #[serde(rename = "chainPEM", skip_serializing_if = "Option::is_none")]
654    pub chain_pem: Option<String>,
655    /// Public key algorithm.
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub certificate_public_key_algorithm: Option<String>,
658    /// Signature algorithm.
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub certificate_signature_algorithm: Option<String>,
661}
662
663/// CSR submission request.
664#[derive(Debug, Clone, Serialize)]
665#[serde(rename_all = "camelCase")]
666#[non_exhaustive]
667pub struct CsrSubmissionRequest {
668    /// PEM-encoded CSR.
669    #[serde(rename = "csrPEM")]
670    pub csr_pem: String,
671}
672
673/// CSR submission response.
674#[derive(Debug, Clone, Deserialize)]
675#[serde(rename_all = "camelCase")]
676#[non_exhaustive]
677pub struct CsrSubmissionResponse {
678    /// Assigned CSR ID.
679    pub csr_id: Uuid,
680    /// Optional message.
681    #[serde(skip_serializing_if = "Option::is_none")]
682    pub message: Option<String>,
683}
684
685/// CSR type.
686#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
687#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
688#[non_exhaustive]
689pub enum CsrType {
690    /// Server certificate CSR.
691    Server,
692    /// Identity certificate CSR.
693    Identity,
694}
695
696/// CSR status.
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
699#[non_exhaustive]
700pub enum CsrStatus {
701    /// Pending processing.
702    Pending,
703    /// Signed and ready.
704    Signed,
705    /// Rejected.
706    Rejected,
707}
708
709/// CSR status response.
710#[derive(Debug, Clone, Deserialize)]
711#[serde(rename_all = "camelCase")]
712#[non_exhaustive]
713pub struct CsrStatusResponse {
714    /// CSR ID.
715    pub csr_id: Uuid,
716    /// CSR type.
717    #[serde(rename = "type")]
718    pub csr_type: CsrType,
719    /// Current status.
720    pub status: CsrStatus,
721    /// Submission time.
722    pub submitted_at: DateTime<Utc>,
723    /// Last update time.
724    pub updated_at: DateTime<Utc>,
725    /// Rejection reason (when rejected).
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub failure_reason: Option<String>,
728}
729
730/// Resolution request.
731#[derive(Debug, Clone, Serialize)]
732#[serde(rename_all = "camelCase")]
733#[non_exhaustive]
734pub struct AgentResolutionRequest {
735    /// Agent host domain.
736    pub agent_host: String,
737    /// Version pattern (e.g., "*", "^1.0.0").
738    pub version: String,
739}
740
741/// Resolution response.
742#[derive(Debug, Clone, Serialize, Deserialize)]
743#[serde(rename_all = "camelCase")]
744#[non_exhaustive]
745pub struct AgentResolutionResponse {
746    /// Resolved ANS name.
747    pub ans_name: String,
748    /// HATEOAS links.
749    pub links: Vec<Link>,
750}
751
752// =========================================================================
753// Event Types
754// =========================================================================
755
756/// Event type for agent lifecycle events.
757#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
758#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
759#[non_exhaustive]
760pub enum EventType {
761    /// Agent was registered.
762    AgentRegistered,
763    /// Agent registration was renewed.
764    AgentRenewed,
765    /// Agent was revoked.
766    AgentRevoked,
767    /// Agent version was updated.
768    AgentVersionUpdated,
769}
770
771impl std::fmt::Display for EventType {
772    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
773        match self {
774            Self::AgentRegistered => write!(f, "AGENT_REGISTERED"),
775            Self::AgentRenewed => write!(f, "AGENT_RENEWED"),
776            Self::AgentRevoked => write!(f, "AGENT_REVOKED"),
777            Self::AgentVersionUpdated => write!(f, "AGENT_VERSION_UPDATED"),
778        }
779    }
780}
781
782/// An individual agent event.
783#[derive(Debug, Clone, Serialize, Deserialize)]
784#[serde(rename_all = "camelCase")]
785#[non_exhaustive]
786pub struct EventItem {
787    /// Log entry ID (used for pagination continuation).
788    pub log_id: String,
789    /// Type of event.
790    pub event_type: EventType,
791    /// When the event occurred.
792    pub created_at: DateTime<Utc>,
793    /// When the agent expires (if applicable).
794    #[serde(skip_serializing_if = "Option::is_none")]
795    pub expires_at: Option<DateTime<Utc>>,
796    /// Agent ID.
797    pub agent_id: Uuid,
798    /// ANS name (e.g., `ans://v1.0.0.agent.example.com`).
799    pub ans_name: String,
800    /// Agent host domain.
801    pub agent_host: String,
802    /// Human-readable agent name.
803    #[serde(skip_serializing_if = "Option::is_none")]
804    pub agent_display_name: Option<String>,
805    /// Agent description.
806    #[serde(skip_serializing_if = "Option::is_none")]
807    pub agent_description: Option<String>,
808    /// Agent version.
809    pub version: String,
810    /// Provider ID (for AHP filtering).
811    #[serde(skip_serializing_if = "Option::is_none")]
812    pub provider_id: Option<String>,
813    /// Agent endpoints at time of event.
814    #[serde(default, skip_serializing_if = "Vec::is_empty")]
815    pub endpoints: Vec<AgentEndpoint>,
816}
817
818/// Paginated events response.
819#[derive(Debug, Clone, Serialize, Deserialize)]
820#[serde(rename_all = "camelCase")]
821#[non_exhaustive]
822pub struct EventPageResponse {
823    /// List of events in this page.
824    pub items: Vec<EventItem>,
825    /// Last log ID for pagination (pass as `last_log_id` to get next page).
826    /// None if this is the last page.
827    #[serde(skip_serializing_if = "Option::is_none")]
828    pub last_log_id: Option<String>,
829}
830
831#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
832#[cfg(test)]
833mod tests {
834    use super::*;
835
836    fn make_pending_with_links(
837        agent_id: Option<&str>,
838        links: Vec<(&str, &str)>,
839    ) -> RegistrationPending {
840        RegistrationPending {
841            status: RegistrationStatus::PendingValidation,
842            ans_name: "ans://v1.0.0.agent.example.com".to_string(),
843            agent_id: agent_id.map(String::from),
844            next_steps: vec![],
845            challenges: vec![],
846            dns_records: vec![],
847            expires_at: None,
848            links: links
849                .into_iter()
850                .map(|(rel, href)| Link {
851                    rel: rel.to_string(),
852                    href: href.to_string(),
853                })
854                .collect(),
855        }
856    }
857
858    #[test]
859    fn test_get_agent_id_from_field() {
860        let pending = make_pending_with_links(Some("direct-id"), vec![]);
861        assert_eq!(pending.get_agent_id(), Some("direct-id".to_string()));
862    }
863
864    #[test]
865    fn test_get_agent_id_from_self_link_path() {
866        let pending = make_pending_with_links(None, vec![("self", "/v1/agents/uuid-from-link")]);
867        assert_eq!(pending.get_agent_id(), Some("uuid-from-link".to_string()));
868    }
869
870    #[test]
871    fn test_get_agent_id_from_self_link_full_url() {
872        let pending = make_pending_with_links(
873            None,
874            vec![("self", "https://api.example.com/v1/agents/uuid-full-url")],
875        );
876        assert_eq!(pending.get_agent_id(), Some("uuid-full-url".to_string()));
877    }
878
879    #[test]
880    fn test_get_agent_id_from_self_link_trailing_slash() {
881        let pending = make_pending_with_links(None, vec![("self", "/v1/agents/uuid-trailing/")]);
882        assert_eq!(pending.get_agent_id(), Some("uuid-trailing".to_string()));
883    }
884
885    #[test]
886    fn test_get_agent_id_prefers_field_over_link() {
887        let pending =
888            make_pending_with_links(Some("field-id"), vec![("self", "/v1/agents/link-id")]);
889        assert_eq!(pending.get_agent_id(), Some("field-id".to_string()));
890    }
891
892    #[test]
893    fn test_get_agent_id_no_self_link() {
894        let pending = make_pending_with_links(None, vec![("other", "/v1/something/else")]);
895        assert_eq!(pending.get_agent_id(), None);
896    }
897
898    #[test]
899    fn test_get_agent_id_empty_links() {
900        let pending = make_pending_with_links(None, vec![]);
901        assert_eq!(pending.get_agent_id(), None);
902    }
903}