1use 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}