Skip to main content

chio_listing/
listing.rs

1//! Generic listing core types: actors, lifecycle status, namespace ownership, artifacts, and reports.
2
3use serde::{Deserialize, Serialize};
4
5use crate::crypto::PublicKey;
6use crate::receipt::lineage::SignedExportEnvelope;
7use crate::search::GenericListingFreshnessWindow;
8use crate::util::{
9    bounded_listing_limit, validate_http_url, validate_non_empty, validate_optional_http_url,
10};
11use crate::{normalize_namespace, GenericListingSearchPolicy};
12
13pub const GENERIC_NAMESPACE_ARTIFACT_SCHEMA: &str = "chio.registry.namespace.v1";
14pub const GENERIC_LISTING_ARTIFACT_SCHEMA: &str = "chio.registry.listing.v1";
15pub const GENERIC_LISTING_REPORT_SCHEMA: &str = "chio.registry.listing-report.v1";
16pub const GENERIC_LISTING_NETWORK_SEARCH_SCHEMA: &str = "chio.registry.search.v1";
17pub const GENERIC_TRUST_ACTIVATION_ARTIFACT_SCHEMA: &str = "chio.registry.trust-activation.v1";
18pub const GENERIC_LISTING_SEARCH_ALGORITHM_V1: &str = "freshness-status-kind-actor-published-at-v1";
19pub const MAX_GENERIC_LISTING_LIMIT: usize = 200;
20pub const DEFAULT_GENERIC_LISTING_REPORT_MAX_AGE_SECS: u64 = 300;
21
22#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[serde(rename_all = "snake_case")]
24pub enum GenericListingActorKind {
25    ToolServer,
26    CredentialIssuer,
27    CredentialVerifier,
28    LiabilityProvider,
29}
30
31#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
32#[serde(rename_all = "snake_case")]
33pub enum GenericListingStatus {
34    Active,
35    Suspended,
36    Superseded,
37    Revoked,
38    Retired,
39}
40
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(rename_all = "snake_case")]
43pub enum GenericNamespaceLifecycleState {
44    Active,
45    Transferred,
46    Retired,
47}
48
49#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
50#[serde(rename_all = "snake_case")]
51pub enum GenericRegistryPublisherRole {
52    Origin,
53    Mirror,
54    Indexer,
55}
56
57#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
58#[serde(rename_all = "snake_case")]
59pub enum GenericListingFreshnessState {
60    Fresh,
61    Stale,
62    Divergent,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66#[serde(rename_all = "camelCase")]
67pub struct GenericListingBoundary {
68    pub visibility_only: bool,
69    pub explicit_trust_activation_required: bool,
70    pub automatic_trust_admission: bool,
71}
72
73impl Default for GenericListingBoundary {
74    fn default() -> Self {
75        Self {
76            visibility_only: true,
77            explicit_trust_activation_required: true,
78            automatic_trust_admission: false,
79        }
80    }
81}
82
83impl GenericListingBoundary {
84    pub fn validate(&self) -> Result<(), String> {
85        if !self.visibility_only {
86            return Err("generic listings must remain visibility-only".to_string());
87        }
88        if !self.explicit_trust_activation_required {
89            return Err(
90                "generic listings must require explicit trust activation outside the listing surface"
91                    .to_string(),
92            );
93        }
94        if self.automatic_trust_admission {
95            return Err("generic listings must not auto-admit trust".to_string());
96        }
97        Ok(())
98    }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102#[serde(rename_all = "camelCase")]
103pub struct GenericNamespaceOwnership {
104    pub namespace: String,
105    pub owner_id: String,
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub owner_name: Option<String>,
108    pub registry_url: String,
109    pub signer_public_key: PublicKey,
110    pub registered_at: u64,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub transferred_from_owner_id: Option<String>,
113}
114
115impl GenericNamespaceOwnership {
116    pub fn validate(&self) -> Result<(), String> {
117        validate_non_empty(&self.namespace, "namespace")?;
118        validate_non_empty(&self.owner_id, "owner_id")?;
119        validate_http_url(&self.registry_url, "registry_url")?;
120        Ok(())
121    }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
125#[serde(rename_all = "camelCase")]
126pub struct GenericRegistryPublisher {
127    pub role: GenericRegistryPublisherRole,
128    pub operator_id: String,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub operator_name: Option<String>,
131    pub registry_url: String,
132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
133    pub upstream_registry_urls: Vec<String>,
134}
135
136impl GenericRegistryPublisher {
137    pub fn validate(&self) -> Result<(), String> {
138        validate_non_empty(&self.operator_id, "publisher.operator_id")?;
139        validate_http_url(&self.registry_url, "publisher.registry_url")?;
140        for (index, upstream) in self.upstream_registry_urls.iter().enumerate() {
141            validate_http_url(
142                upstream,
143                &format!("publisher.upstream_registry_urls[{index}]"),
144            )?;
145        }
146        Ok(())
147    }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
151#[serde(rename_all = "camelCase")]
152pub struct GenericNamespaceArtifact {
153    pub schema: String,
154    pub namespace_id: String,
155    pub lifecycle_state: GenericNamespaceLifecycleState,
156    pub ownership: GenericNamespaceOwnership,
157    pub boundary: GenericListingBoundary,
158}
159
160impl GenericNamespaceArtifact {
161    pub fn validate(&self) -> Result<(), String> {
162        if self.schema != GENERIC_NAMESPACE_ARTIFACT_SCHEMA {
163            return Err(format!(
164                "unsupported generic namespace schema: {}",
165                self.schema
166            ));
167        }
168        validate_non_empty(&self.namespace_id, "namespace_id")?;
169        self.ownership.validate()?;
170        self.boundary.validate()?;
171        Ok(())
172    }
173}
174
175pub type SignedGenericNamespace = SignedExportEnvelope<GenericNamespaceArtifact>;
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(rename_all = "camelCase")]
179pub struct GenericListingCompatibilityReference {
180    pub source_schema: String,
181    pub source_artifact_id: String,
182    pub source_artifact_sha256: String,
183}
184
185impl GenericListingCompatibilityReference {
186    pub fn validate(&self) -> Result<(), String> {
187        validate_non_empty(&self.source_schema, "compatibility.source_schema")?;
188        validate_non_empty(&self.source_artifact_id, "compatibility.source_artifact_id")?;
189        validate_non_empty(
190            &self.source_artifact_sha256,
191            "compatibility.source_artifact_sha256",
192        )?;
193        Ok(())
194    }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
198#[serde(rename_all = "camelCase")]
199pub struct GenericListingSubject {
200    pub actor_kind: GenericListingActorKind,
201    pub actor_id: String,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub display_name: Option<String>,
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub metadata_url: Option<String>,
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub resolution_url: Option<String>,
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub homepage_url: Option<String>,
210}
211
212impl GenericListingSubject {
213    pub fn validate(&self) -> Result<(), String> {
214        validate_non_empty(&self.actor_id, "subject.actor_id")?;
215        validate_optional_http_url(self.metadata_url.as_deref(), "subject.metadata_url")?;
216        validate_optional_http_url(self.resolution_url.as_deref(), "subject.resolution_url")?;
217        validate_optional_http_url(self.homepage_url.as_deref(), "subject.homepage_url")?;
218        Ok(())
219    }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223#[serde(rename_all = "camelCase")]
224pub struct GenericListingArtifact {
225    pub schema: String,
226    pub listing_id: String,
227    pub namespace: String,
228    pub published_at: u64,
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub expires_at: Option<u64>,
231    pub status: GenericListingStatus,
232    pub namespace_ownership: GenericNamespaceOwnership,
233    pub subject: GenericListingSubject,
234    pub compatibility: GenericListingCompatibilityReference,
235    pub boundary: GenericListingBoundary,
236}
237
238impl GenericListingArtifact {
239    pub fn validate(&self) -> Result<(), String> {
240        if self.schema != GENERIC_LISTING_ARTIFACT_SCHEMA {
241            return Err(format!(
242                "unsupported generic listing schema: {}",
243                self.schema
244            ));
245        }
246        validate_non_empty(&self.listing_id, "listing_id")?;
247        validate_non_empty(&self.namespace, "namespace")?;
248        if self.namespace.trim_end_matches('/')
249            != self.namespace_ownership.namespace.trim_end_matches('/')
250        {
251            return Err(format!(
252                "listing namespace `{}` does not match namespace ownership `{}`",
253                self.namespace, self.namespace_ownership.namespace
254            ));
255        }
256        if let Some(expires_at) = self.expires_at {
257            if expires_at <= self.published_at {
258                return Err("generic listing expiry must be greater than published_at".to_string());
259            }
260        }
261        self.namespace_ownership.validate()?;
262        self.subject.validate()?;
263        self.compatibility.validate()?;
264        self.boundary.validate()?;
265        Ok(())
266    }
267}
268
269pub type SignedGenericListing = SignedExportEnvelope<GenericListingArtifact>;
270
271#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
272#[serde(rename_all = "camelCase")]
273pub struct GenericListingQuery {
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub namespace: Option<String>,
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub actor_kind: Option<GenericListingActorKind>,
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub actor_id: Option<String>,
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub status: Option<GenericListingStatus>,
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub limit: Option<usize>,
284}
285
286impl GenericListingQuery {
287    #[must_use]
288    pub fn limit_or_default(&self) -> usize {
289        bounded_listing_limit(self.limit, MAX_GENERIC_LISTING_LIMIT)
290    }
291
292    #[must_use]
293    pub fn normalized(&self) -> Self {
294        let mut normalized = self.clone();
295        normalized.limit = Some(self.limit_or_default());
296        normalized.namespace = normalized
297            .namespace
298            .as_deref()
299            .map(normalize_namespace)
300            .filter(|value| !value.is_empty());
301        normalized.actor_id = normalized
302            .actor_id
303            .as_deref()
304            .map(str::trim)
305            .map(str::to_string)
306            .filter(|value| !value.is_empty());
307        normalized
308    }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
312#[serde(rename_all = "camelCase")]
313pub struct GenericListingSummary {
314    pub matching_listings: u64,
315    pub returned_listings: u64,
316    pub active_listings: u64,
317    pub suspended_listings: u64,
318    pub superseded_listings: u64,
319    pub revoked_listings: u64,
320    pub retired_listings: u64,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
324#[serde(rename_all = "camelCase")]
325pub struct GenericListingReport {
326    pub schema: String,
327    pub generated_at: u64,
328    pub query: GenericListingQuery,
329    pub namespace: GenericNamespaceOwnership,
330    pub publisher: GenericRegistryPublisher,
331    pub freshness: GenericListingFreshnessWindow,
332    pub search_policy: GenericListingSearchPolicy,
333    pub summary: GenericListingSummary,
334    pub listings: Vec<SignedGenericListing>,
335}