Skip to main content

chio_listing/
lib.rs

1pub use chio_core_types::capability::MonetaryAmount;
2pub use chio_core_types::{canonical_json_bytes, crypto, receipt};
3
4pub mod discovery;
5pub use discovery::{
6    compare, provider_signing_key, resolve_admissible_listing, search, Listing, ListingComparison,
7    ListingComparisonRow, ListingPricingHint, ListingQuery, ListingSearchResponse, ListingSla,
8    SignedListingPricingHint, LISTING_COMPARISON_SCHEMA, LISTING_PRICING_HINT_SCHEMA,
9    LISTING_SEARCH_SCHEMA, MAX_MARKETPLACE_SEARCH_LIMIT,
10};
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15
16use crate::crypto::{sha256_hex, PublicKey};
17use crate::receipt::SignedExportEnvelope;
18
19pub const GENERIC_NAMESPACE_ARTIFACT_SCHEMA: &str = "chio.registry.namespace.v1";
20pub const GENERIC_LISTING_ARTIFACT_SCHEMA: &str = "chio.registry.listing.v1";
21pub const GENERIC_LISTING_REPORT_SCHEMA: &str = "chio.registry.listing-report.v1";
22pub const GENERIC_LISTING_NETWORK_SEARCH_SCHEMA: &str = "chio.registry.search.v1";
23pub const GENERIC_TRUST_ACTIVATION_ARTIFACT_SCHEMA: &str = "chio.registry.trust-activation.v1";
24pub const GENERIC_LISTING_SEARCH_ALGORITHM_V1: &str = "freshness-status-kind-actor-published-at-v1";
25pub const MAX_GENERIC_LISTING_LIMIT: usize = 200;
26pub const DEFAULT_GENERIC_LISTING_REPORT_MAX_AGE_SECS: u64 = 300;
27
28#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
29#[serde(rename_all = "snake_case")]
30pub enum GenericListingActorKind {
31    ToolServer,
32    CredentialIssuer,
33    CredentialVerifier,
34    LiabilityProvider,
35}
36
37#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
38#[serde(rename_all = "snake_case")]
39pub enum GenericListingStatus {
40    Active,
41    Suspended,
42    Superseded,
43    Revoked,
44    Retired,
45}
46
47#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
48#[serde(rename_all = "snake_case")]
49pub enum GenericNamespaceLifecycleState {
50    Active,
51    Transferred,
52    Retired,
53}
54
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
56#[serde(rename_all = "snake_case")]
57pub enum GenericRegistryPublisherRole {
58    Origin,
59    Mirror,
60    Indexer,
61}
62
63#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "snake_case")]
65pub enum GenericListingFreshnessState {
66    Fresh,
67    Stale,
68    Divergent,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(rename_all = "camelCase")]
73pub struct GenericListingBoundary {
74    pub visibility_only: bool,
75    pub explicit_trust_activation_required: bool,
76    pub automatic_trust_admission: bool,
77}
78
79impl Default for GenericListingBoundary {
80    fn default() -> Self {
81        Self {
82            visibility_only: true,
83            explicit_trust_activation_required: true,
84            automatic_trust_admission: false,
85        }
86    }
87}
88
89impl GenericListingBoundary {
90    pub fn validate(&self) -> Result<(), String> {
91        if !self.visibility_only {
92            return Err("generic listings must remain visibility-only".to_string());
93        }
94        if !self.explicit_trust_activation_required {
95            return Err(
96                "generic listings must require explicit trust activation outside the listing surface"
97                    .to_string(),
98            );
99        }
100        if self.automatic_trust_admission {
101            return Err("generic listings must not auto-admit trust".to_string());
102        }
103        Ok(())
104    }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(rename_all = "camelCase")]
109pub struct GenericNamespaceOwnership {
110    pub namespace: String,
111    pub owner_id: String,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub owner_name: Option<String>,
114    pub registry_url: String,
115    pub signer_public_key: PublicKey,
116    pub registered_at: u64,
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub transferred_from_owner_id: Option<String>,
119}
120
121impl GenericNamespaceOwnership {
122    pub fn validate(&self) -> Result<(), String> {
123        validate_non_empty(&self.namespace, "namespace")?;
124        validate_non_empty(&self.owner_id, "owner_id")?;
125        validate_http_url(&self.registry_url, "registry_url")?;
126        Ok(())
127    }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131#[serde(rename_all = "camelCase")]
132pub struct GenericRegistryPublisher {
133    pub role: GenericRegistryPublisherRole,
134    pub operator_id: String,
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub operator_name: Option<String>,
137    pub registry_url: String,
138    #[serde(default, skip_serializing_if = "Vec::is_empty")]
139    pub upstream_registry_urls: Vec<String>,
140}
141
142impl GenericRegistryPublisher {
143    pub fn validate(&self) -> Result<(), String> {
144        validate_non_empty(&self.operator_id, "publisher.operator_id")?;
145        validate_http_url(&self.registry_url, "publisher.registry_url")?;
146        for (index, upstream) in self.upstream_registry_urls.iter().enumerate() {
147            validate_http_url(
148                upstream,
149                &format!("publisher.upstream_registry_urls[{index}]"),
150            )?;
151        }
152        Ok(())
153    }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
157#[serde(rename_all = "camelCase")]
158pub struct GenericNamespaceArtifact {
159    pub schema: String,
160    pub namespace_id: String,
161    pub lifecycle_state: GenericNamespaceLifecycleState,
162    pub ownership: GenericNamespaceOwnership,
163    pub boundary: GenericListingBoundary,
164}
165
166impl GenericNamespaceArtifact {
167    pub fn validate(&self) -> Result<(), String> {
168        if self.schema != GENERIC_NAMESPACE_ARTIFACT_SCHEMA {
169            return Err(format!(
170                "unsupported generic namespace schema: {}",
171                self.schema
172            ));
173        }
174        validate_non_empty(&self.namespace_id, "namespace_id")?;
175        self.ownership.validate()?;
176        self.boundary.validate()?;
177        Ok(())
178    }
179}
180
181pub type SignedGenericNamespace = SignedExportEnvelope<GenericNamespaceArtifact>;
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
184#[serde(rename_all = "camelCase")]
185pub struct GenericListingCompatibilityReference {
186    pub source_schema: String,
187    pub source_artifact_id: String,
188    pub source_artifact_sha256: String,
189}
190
191impl GenericListingCompatibilityReference {
192    pub fn validate(&self) -> Result<(), String> {
193        validate_non_empty(&self.source_schema, "compatibility.source_schema")?;
194        validate_non_empty(&self.source_artifact_id, "compatibility.source_artifact_id")?;
195        validate_non_empty(
196            &self.source_artifact_sha256,
197            "compatibility.source_artifact_sha256",
198        )?;
199        Ok(())
200    }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204#[serde(rename_all = "camelCase")]
205pub struct GenericListingSubject {
206    pub actor_kind: GenericListingActorKind,
207    pub actor_id: String,
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub display_name: Option<String>,
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub metadata_url: Option<String>,
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub resolution_url: Option<String>,
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub homepage_url: Option<String>,
216}
217
218impl GenericListingSubject {
219    pub fn validate(&self) -> Result<(), String> {
220        validate_non_empty(&self.actor_id, "subject.actor_id")?;
221        validate_optional_http_url(self.metadata_url.as_deref(), "subject.metadata_url")?;
222        validate_optional_http_url(self.resolution_url.as_deref(), "subject.resolution_url")?;
223        validate_optional_http_url(self.homepage_url.as_deref(), "subject.homepage_url")?;
224        Ok(())
225    }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
229#[serde(rename_all = "camelCase")]
230pub struct GenericListingArtifact {
231    pub schema: String,
232    pub listing_id: String,
233    pub namespace: String,
234    pub published_at: u64,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub expires_at: Option<u64>,
237    pub status: GenericListingStatus,
238    pub namespace_ownership: GenericNamespaceOwnership,
239    pub subject: GenericListingSubject,
240    pub compatibility: GenericListingCompatibilityReference,
241    pub boundary: GenericListingBoundary,
242}
243
244impl GenericListingArtifact {
245    pub fn validate(&self) -> Result<(), String> {
246        if self.schema != GENERIC_LISTING_ARTIFACT_SCHEMA {
247            return Err(format!(
248                "unsupported generic listing schema: {}",
249                self.schema
250            ));
251        }
252        validate_non_empty(&self.listing_id, "listing_id")?;
253        validate_non_empty(&self.namespace, "namespace")?;
254        if self.namespace.trim_end_matches('/')
255            != self.namespace_ownership.namespace.trim_end_matches('/')
256        {
257            return Err(format!(
258                "listing namespace `{}` does not match namespace ownership `{}`",
259                self.namespace, self.namespace_ownership.namespace
260            ));
261        }
262        if let Some(expires_at) = self.expires_at {
263            if expires_at <= self.published_at {
264                return Err("generic listing expiry must be greater than published_at".to_string());
265            }
266        }
267        self.namespace_ownership.validate()?;
268        self.subject.validate()?;
269        self.compatibility.validate()?;
270        self.boundary.validate()?;
271        Ok(())
272    }
273}
274
275pub type SignedGenericListing = SignedExportEnvelope<GenericListingArtifact>;
276
277#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
278#[serde(rename_all = "camelCase")]
279pub struct GenericListingQuery {
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub namespace: Option<String>,
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub actor_kind: Option<GenericListingActorKind>,
284    #[serde(default, skip_serializing_if = "Option::is_none")]
285    pub actor_id: Option<String>,
286    #[serde(default, skip_serializing_if = "Option::is_none")]
287    pub status: Option<GenericListingStatus>,
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub limit: Option<usize>,
290}
291
292impl GenericListingQuery {
293    #[must_use]
294    pub fn limit_or_default(&self) -> usize {
295        self.limit
296            .unwrap_or(100)
297            .clamp(1, MAX_GENERIC_LISTING_LIMIT)
298    }
299
300    #[must_use]
301    pub fn normalized(&self) -> Self {
302        let mut normalized = self.clone();
303        normalized.limit = Some(self.limit_or_default());
304        normalized.namespace = normalized
305            .namespace
306            .as_deref()
307            .map(normalize_namespace)
308            .filter(|value| !value.is_empty());
309        normalized.actor_id = normalized
310            .actor_id
311            .as_deref()
312            .map(str::trim)
313            .map(str::to_string)
314            .filter(|value| !value.is_empty());
315        normalized
316    }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320#[serde(rename_all = "camelCase")]
321pub struct GenericListingSummary {
322    pub matching_listings: u64,
323    pub returned_listings: u64,
324    pub active_listings: u64,
325    pub suspended_listings: u64,
326    pub superseded_listings: u64,
327    pub revoked_listings: u64,
328    pub retired_listings: u64,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
332#[serde(rename_all = "camelCase")]
333pub struct GenericListingReport {
334    pub schema: String,
335    pub generated_at: u64,
336    pub query: GenericListingQuery,
337    pub namespace: GenericNamespaceOwnership,
338    pub publisher: GenericRegistryPublisher,
339    pub freshness: GenericListingFreshnessWindow,
340    pub search_policy: GenericListingSearchPolicy,
341    pub summary: GenericListingSummary,
342    pub listings: Vec<SignedGenericListing>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
346#[serde(rename_all = "camelCase")]
347pub struct GenericListingFreshnessWindow {
348    pub max_age_secs: u64,
349    pub valid_until: u64,
350}
351
352impl GenericListingFreshnessWindow {
353    pub fn validate(&self, generated_at: u64) -> Result<(), String> {
354        if self.max_age_secs == 0 {
355            return Err("freshness.max_age_secs must be greater than zero".to_string());
356        }
357        if self.valid_until <= generated_at {
358            return Err("freshness.valid_until must be greater than generated_at".to_string());
359        }
360        Ok(())
361    }
362
363    #[must_use]
364    pub fn assess(&self, generated_at: u64, now: u64) -> GenericListingReplicaFreshness {
365        let age_secs = now.saturating_sub(generated_at);
366        let state = if age_secs > self.max_age_secs || now > self.valid_until {
367            GenericListingFreshnessState::Stale
368        } else {
369            GenericListingFreshnessState::Fresh
370        };
371        GenericListingReplicaFreshness {
372            state,
373            age_secs,
374            max_age_secs: self.max_age_secs,
375            valid_until: self.valid_until,
376            generated_at,
377        }
378    }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
382#[serde(rename_all = "camelCase")]
383pub struct GenericListingSearchPolicy {
384    pub algorithm: String,
385    pub reproducible_ordering: bool,
386    pub freshness_affects_ranking: bool,
387    pub visibility_only: bool,
388    pub explicit_trust_activation_required: bool,
389    #[serde(default, skip_serializing_if = "Vec::is_empty")]
390    pub ranking_inputs: Vec<String>,
391}
392
393impl Default for GenericListingSearchPolicy {
394    fn default() -> Self {
395        Self {
396            algorithm: GENERIC_LISTING_SEARCH_ALGORITHM_V1.to_string(),
397            reproducible_ordering: true,
398            freshness_affects_ranking: true,
399            visibility_only: true,
400            explicit_trust_activation_required: true,
401            ranking_inputs: vec![
402                "freshness".to_string(),
403                "status".to_string(),
404                "actor_kind".to_string(),
405                "actor_id".to_string(),
406                "published_at_desc".to_string(),
407                "publisher_role".to_string(),
408                "listing_id".to_string(),
409            ],
410        }
411    }
412}
413
414impl GenericListingSearchPolicy {
415    pub fn validate(&self) -> Result<(), String> {
416        validate_non_empty(&self.algorithm, "search_policy.algorithm")?;
417        if !self.reproducible_ordering {
418            return Err("generic listing search must remain reproducible".to_string());
419        }
420        if !self.visibility_only {
421            return Err("generic listing search must remain visibility-only".to_string());
422        }
423        if !self.explicit_trust_activation_required {
424            return Err(
425                "generic listing search must require explicit trust activation outside search"
426                    .to_string(),
427            );
428        }
429        Ok(())
430    }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
434#[serde(rename_all = "camelCase")]
435pub struct GenericListingReplicaFreshness {
436    pub state: GenericListingFreshnessState,
437    pub age_secs: u64,
438    pub max_age_secs: u64,
439    pub valid_until: u64,
440    pub generated_at: u64,
441}
442
443impl GenericListingReplicaFreshness {
444    pub fn validate(&self) -> Result<(), String> {
445        if self.max_age_secs == 0 {
446            return Err("freshness.max_age_secs must be greater than zero".to_string());
447        }
448        if self.valid_until <= self.generated_at {
449            return Err("freshness.valid_until must be greater than generated_at".to_string());
450        }
451        Ok(())
452    }
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
456#[serde(rename_all = "camelCase")]
457pub struct GenericListingSearchResult {
458    pub rank: u64,
459    pub listing: SignedGenericListing,
460    pub publisher: GenericRegistryPublisher,
461    pub freshness: GenericListingReplicaFreshness,
462    #[serde(default, skip_serializing_if = "Vec::is_empty")]
463    pub replica_operator_ids: Vec<String>,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
467#[serde(rename_all = "camelCase")]
468pub struct GenericListingSearchError {
469    pub operator_id: String,
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub operator_name: Option<String>,
472    pub registry_url: String,
473    pub error: String,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
477#[serde(rename_all = "camelCase")]
478pub struct GenericListingDivergence {
479    pub divergence_key: String,
480    pub actor_id: String,
481    pub actor_kind: GenericListingActorKind,
482    pub publisher_operator_ids: Vec<String>,
483    pub reason: String,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
487#[serde(rename_all = "camelCase")]
488pub struct GenericListingSearchResponse {
489    pub schema: String,
490    pub generated_at: u64,
491    pub query: GenericListingQuery,
492    pub search_policy: GenericListingSearchPolicy,
493    pub peer_count: u64,
494    pub reachable_count: u64,
495    pub stale_peer_count: u64,
496    pub divergence_count: u64,
497    pub result_count: u64,
498    pub results: Vec<GenericListingSearchResult>,
499    pub divergences: Vec<GenericListingDivergence>,
500    pub errors: Vec<GenericListingSearchError>,
501}
502
503#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
504#[serde(rename_all = "snake_case")]
505pub enum GenericTrustAdmissionClass {
506    PublicUntrusted,
507    Reviewable,
508    BondBacked,
509    RoleGated,
510}
511
512#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
513#[serde(rename_all = "snake_case")]
514pub enum GenericTrustActivationDisposition {
515    PendingReview,
516    Approved,
517    Denied,
518}
519
520#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
521#[serde(rename_all = "snake_case")]
522pub enum GenericTrustActivationFindingCode {
523    MissingActivation,
524    ListingUnverifiable,
525    ActivationUnverifiable,
526    ListingMismatch,
527    ListingStale,
528    ListingDivergent,
529    ActivationExpired,
530    ActivationPendingReview,
531    ActivationDenied,
532    AdmissionClassUntrusted,
533    ActorKindIneligible,
534    PublisherRoleIneligible,
535    ListingStatusIneligible,
536    ListingOperatorIneligible,
537    BondBackingRequired,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
541#[serde(rename_all = "camelCase")]
542pub struct GenericTrustActivationEligibility {
543    #[serde(default, skip_serializing_if = "Vec::is_empty")]
544    pub allowed_actor_kinds: Vec<GenericListingActorKind>,
545    #[serde(default, skip_serializing_if = "Vec::is_empty")]
546    pub allowed_publisher_roles: Vec<GenericRegistryPublisherRole>,
547    #[serde(default, skip_serializing_if = "Vec::is_empty")]
548    pub allowed_statuses: Vec<GenericListingStatus>,
549    #[serde(default)]
550    pub require_fresh_listing: bool,
551    #[serde(default)]
552    pub require_bond_backing: bool,
553    #[serde(default, skip_serializing_if = "Vec::is_empty")]
554    pub required_listing_operator_ids: Vec<String>,
555    #[serde(default, skip_serializing_if = "Option::is_none")]
556    pub policy_reference: Option<String>,
557}
558
559impl GenericTrustActivationEligibility {
560    pub fn validate(&self, admission_class: GenericTrustAdmissionClass) -> Result<(), String> {
561        for (index, operator_id) in self.required_listing_operator_ids.iter().enumerate() {
562            validate_non_empty(
563                operator_id,
564                &format!("eligibility.required_listing_operator_ids[{index}]"),
565            )?;
566        }
567        if matches!(admission_class, GenericTrustAdmissionClass::RoleGated)
568            && self.required_listing_operator_ids.is_empty()
569        {
570            return Err(
571                "role_gated trust activation requires required_listing_operator_ids".to_string(),
572            );
573        }
574        if matches!(admission_class, GenericTrustAdmissionClass::BondBacked)
575            && !self.require_bond_backing
576        {
577            return Err("bond_backed trust activation must require bond backing".to_string());
578        }
579        if !matches!(admission_class, GenericTrustAdmissionClass::BondBacked)
580            && self.require_bond_backing
581        {
582            return Err(
583                "require_bond_backing is only valid for bond_backed trust activation".to_string(),
584            );
585        }
586        Ok(())
587    }
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
591#[serde(rename_all = "camelCase")]
592pub struct GenericTrustActivationReviewContext {
593    pub publisher: GenericRegistryPublisher,
594    pub freshness: GenericListingReplicaFreshness,
595}
596
597impl GenericTrustActivationReviewContext {
598    pub fn validate(&self) -> Result<(), String> {
599        self.publisher.validate()?;
600        self.freshness.validate()?;
601        Ok(())
602    }
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
606#[serde(rename_all = "camelCase")]
607pub struct GenericTrustActivationArtifact {
608    pub schema: String,
609    pub activation_id: String,
610    pub local_operator_id: String,
611    #[serde(default, skip_serializing_if = "Option::is_none")]
612    pub local_operator_name: Option<String>,
613    pub listing_id: String,
614    pub namespace: String,
615    pub listing_sha256: String,
616    pub listing_published_at: u64,
617    pub admission_class: GenericTrustAdmissionClass,
618    pub disposition: GenericTrustActivationDisposition,
619    pub eligibility: GenericTrustActivationEligibility,
620    pub review_context: GenericTrustActivationReviewContext,
621    pub requested_at: u64,
622    #[serde(default, skip_serializing_if = "Option::is_none")]
623    pub reviewed_at: Option<u64>,
624    #[serde(default, skip_serializing_if = "Option::is_none")]
625    pub expires_at: Option<u64>,
626    pub requested_by: String,
627    #[serde(default, skip_serializing_if = "Option::is_none")]
628    pub reviewed_by: Option<String>,
629    #[serde(default, skip_serializing_if = "Option::is_none")]
630    pub note: Option<String>,
631}
632
633impl GenericTrustActivationArtifact {
634    pub fn validate(&self) -> Result<(), String> {
635        if self.schema != GENERIC_TRUST_ACTIVATION_ARTIFACT_SCHEMA {
636            return Err(format!(
637                "unsupported generic trust activation schema: {}",
638                self.schema
639            ));
640        }
641        validate_non_empty(&self.activation_id, "activation_id")?;
642        validate_non_empty(&self.local_operator_id, "local_operator_id")?;
643        validate_non_empty(&self.listing_id, "listing_id")?;
644        validate_non_empty(&self.namespace, "namespace")?;
645        validate_non_empty(&self.listing_sha256, "listing_sha256")?;
646        validate_non_empty(&self.requested_by, "requested_by")?;
647        self.eligibility.validate(self.admission_class)?;
648        self.review_context.validate()?;
649        if let Some(reviewed_at) = self.reviewed_at {
650            if reviewed_at < self.requested_at {
651                return Err("reviewed_at must be greater than or equal to requested_at".to_string());
652            }
653        }
654        if let Some(expires_at) = self.expires_at {
655            if expires_at <= self.requested_at {
656                return Err("expires_at must be greater than requested_at".to_string());
657            }
658        }
659        match self.disposition {
660            GenericTrustActivationDisposition::PendingReview => {
661                if self.reviewed_at.is_some() || self.reviewed_by.is_some() {
662                    return Err(
663                        "pending_review trust activation must not carry review completion fields"
664                            .to_string(),
665                    );
666                }
667            }
668            GenericTrustActivationDisposition::Approved
669            | GenericTrustActivationDisposition::Denied => {
670                if self.reviewed_at.is_none() || self.reviewed_by.as_deref().is_none() {
671                    return Err(
672                        "approved or denied trust activation requires reviewed_at and reviewed_by"
673                            .to_string(),
674                    );
675                }
676            }
677        }
678        Ok(())
679    }
680}
681
682pub type SignedGenericTrustActivation = SignedExportEnvelope<GenericTrustActivationArtifact>;
683
684#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
685#[serde(rename_all = "camelCase")]
686pub struct GenericTrustActivationIssueRequest {
687    pub listing: SignedGenericListing,
688    pub admission_class: GenericTrustAdmissionClass,
689    pub disposition: GenericTrustActivationDisposition,
690    pub eligibility: GenericTrustActivationEligibility,
691    pub review_context: GenericTrustActivationReviewContext,
692    pub requested_by: String,
693    #[serde(default, skip_serializing_if = "Option::is_none")]
694    pub reviewed_by: Option<String>,
695    #[serde(default, skip_serializing_if = "Option::is_none")]
696    pub requested_at: Option<u64>,
697    #[serde(default, skip_serializing_if = "Option::is_none")]
698    pub reviewed_at: Option<u64>,
699    #[serde(default, skip_serializing_if = "Option::is_none")]
700    pub expires_at: Option<u64>,
701    #[serde(default, skip_serializing_if = "Option::is_none")]
702    pub note: Option<String>,
703}
704
705impl GenericTrustActivationIssueRequest {
706    pub fn validate(&self) -> Result<(), String> {
707        self.listing.body.validate()?;
708        if !self
709            .listing
710            .verify_signature()
711            .map_err(|error| error.to_string())?
712        {
713            return Err("trust activation listing signature is invalid".to_string());
714        }
715        self.review_context.validate()?;
716        self.eligibility.validate(self.admission_class)?;
717        validate_non_empty(&self.requested_by, "requested_by")?;
718        if matches!(
719            self.disposition,
720            GenericTrustActivationDisposition::Approved
721        ) && self.review_context.freshness.state != GenericListingFreshnessState::Fresh
722        {
723            return Err(
724                "approved trust activation requires fresh listing review context".to_string(),
725            );
726        }
727        Ok(())
728    }
729}
730
731#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
732#[serde(rename_all = "camelCase")]
733pub struct GenericTrustActivationEvaluationRequest {
734    pub listing: SignedGenericListing,
735    pub current_publisher: GenericRegistryPublisher,
736    pub current_freshness: GenericListingReplicaFreshness,
737    #[serde(default, skip_serializing_if = "Option::is_none")]
738    pub activation: Option<SignedGenericTrustActivation>,
739    #[serde(default, skip_serializing_if = "Option::is_none")]
740    pub evaluated_at: Option<u64>,
741}
742
743impl GenericTrustActivationEvaluationRequest {
744    pub fn validate(&self) -> Result<(), String> {
745        self.listing.body.validate()?;
746        self.current_publisher.validate()?;
747        self.current_freshness.validate()?;
748        Ok(())
749    }
750}
751
752#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
753#[serde(rename_all = "camelCase")]
754pub struct GenericTrustActivationFinding {
755    pub code: GenericTrustActivationFindingCode,
756    pub message: String,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
760#[serde(rename_all = "camelCase")]
761pub struct GenericTrustActivationEvaluation {
762    pub listing_id: String,
763    pub namespace: String,
764    pub evaluated_at: u64,
765    #[serde(default, skip_serializing_if = "Option::is_none")]
766    pub local_operator_id: Option<String>,
767    #[serde(default, skip_serializing_if = "Option::is_none")]
768    pub admission_class: Option<GenericTrustAdmissionClass>,
769    #[serde(default, skip_serializing_if = "Option::is_none")]
770    pub disposition: Option<GenericTrustActivationDisposition>,
771    pub admitted: bool,
772    #[serde(default, skip_serializing_if = "Vec::is_empty")]
773    pub findings: Vec<GenericTrustActivationFinding>,
774}
775
776pub fn build_generic_trust_activation_artifact(
777    local_operator_id: &str,
778    local_operator_name: Option<String>,
779    request: &GenericTrustActivationIssueRequest,
780    issued_at: u64,
781) -> Result<GenericTrustActivationArtifact, String> {
782    request.validate()?;
783    validate_non_empty(local_operator_id, "local_operator_id")?;
784    let requested_at = request.requested_at.unwrap_or(issued_at);
785    let reviewed_at = request.reviewed_at.or(match request.disposition {
786        GenericTrustActivationDisposition::PendingReview => None,
787        GenericTrustActivationDisposition::Approved | GenericTrustActivationDisposition::Denied => {
788            Some(issued_at)
789        }
790    });
791    let listing_sha256 = generic_listing_body_sha256(&request.listing)?;
792    let activation_id = format!(
793        "activation-{}",
794        sha256_hex(
795            &canonical_json_bytes(&(
796                local_operator_id,
797                &request.listing.body.listing_id,
798                &listing_sha256,
799                request.admission_class,
800                request.disposition,
801                requested_at,
802            ))
803            .map_err(|error| error.to_string())?
804        )
805    );
806    let artifact = GenericTrustActivationArtifact {
807        schema: GENERIC_TRUST_ACTIVATION_ARTIFACT_SCHEMA.to_string(),
808        activation_id,
809        local_operator_id: local_operator_id.to_string(),
810        local_operator_name,
811        listing_id: request.listing.body.listing_id.clone(),
812        namespace: request.listing.body.namespace.clone(),
813        listing_sha256,
814        listing_published_at: request.listing.body.published_at,
815        admission_class: request.admission_class,
816        disposition: request.disposition,
817        eligibility: request.eligibility.clone(),
818        review_context: request.review_context.clone(),
819        requested_at,
820        reviewed_at,
821        expires_at: request.expires_at,
822        requested_by: request.requested_by.clone(),
823        reviewed_by: request.reviewed_by.clone(),
824        note: request.note.clone(),
825    };
826    artifact.validate()?;
827    Ok(artifact)
828}
829
830pub fn evaluate_generic_trust_activation(
831    request: &GenericTrustActivationEvaluationRequest,
832    now: u64,
833) -> Result<GenericTrustActivationEvaluation, String> {
834    request.validate()?;
835    let mut evaluation = GenericTrustActivationEvaluation {
836        listing_id: request.listing.body.listing_id.clone(),
837        namespace: request.listing.body.namespace.clone(),
838        evaluated_at: request.evaluated_at.unwrap_or(now),
839        local_operator_id: None,
840        admission_class: None,
841        disposition: None,
842        admitted: false,
843        findings: Vec::new(),
844    };
845
846    if !request
847        .listing
848        .verify_signature()
849        .map_err(|error| error.to_string())?
850    {
851        evaluation.findings.push(GenericTrustActivationFinding {
852            code: GenericTrustActivationFindingCode::ListingUnverifiable,
853            message: "listing signature is invalid".to_string(),
854        });
855        return Ok(evaluation);
856    }
857
858    let Some(activation) = request.activation.as_ref() else {
859        evaluation.findings.push(GenericTrustActivationFinding {
860            code: GenericTrustActivationFindingCode::MissingActivation,
861            message: "listing visibility requires an explicit local trust activation artifact"
862                .to_string(),
863        });
864        return Ok(evaluation);
865    };
866
867    if !activation
868        .verify_signature()
869        .map_err(|error| error.to_string())?
870    {
871        evaluation.findings.push(GenericTrustActivationFinding {
872            code: GenericTrustActivationFindingCode::ActivationUnverifiable,
873            message: "trust activation signature is invalid".to_string(),
874        });
875        return Ok(evaluation);
876    }
877
878    if let Err(error) = activation.body.validate() {
879        evaluation.findings.push(GenericTrustActivationFinding {
880            code: GenericTrustActivationFindingCode::ActivationUnverifiable,
881            message: error,
882        });
883        return Ok(evaluation);
884    }
885
886    evaluation.local_operator_id = Some(activation.body.local_operator_id.clone());
887    evaluation.admission_class = Some(activation.body.admission_class);
888    evaluation.disposition = Some(activation.body.disposition);
889
890    let listing_sha256 = generic_listing_body_sha256(&request.listing)?;
891    if activation.body.listing_id != request.listing.body.listing_id
892        || normalize_namespace(&activation.body.namespace)
893            != normalize_namespace(&request.listing.body.namespace)
894        || activation.body.listing_sha256 != listing_sha256
895        || activation.body.listing_published_at != request.listing.body.published_at
896    {
897        evaluation.findings.push(GenericTrustActivationFinding {
898            code: GenericTrustActivationFindingCode::ListingMismatch,
899            message:
900                "trust activation does not match the current listing identity, namespace, or body hash"
901                    .to_string(),
902        });
903        return Ok(evaluation);
904    }
905
906    match request.current_freshness.state {
907        GenericListingFreshnessState::Stale => {
908            evaluation.findings.push(GenericTrustActivationFinding {
909                code: GenericTrustActivationFindingCode::ListingStale,
910                message:
911                    "current listing report is stale and cannot be activated for runtime trust"
912                        .to_string(),
913            });
914            return Ok(evaluation);
915        }
916        GenericListingFreshnessState::Divergent => {
917            evaluation.findings.push(GenericTrustActivationFinding {
918                code: GenericTrustActivationFindingCode::ListingDivergent,
919                message:
920                    "current listing report is divergent and cannot be activated for runtime trust"
921                        .to_string(),
922            });
923            return Ok(evaluation);
924        }
925        GenericListingFreshnessState::Fresh => {}
926    }
927
928    if activation
929        .body
930        .expires_at
931        .is_some_and(|expires_at| expires_at <= evaluation.evaluated_at)
932    {
933        evaluation.findings.push(GenericTrustActivationFinding {
934            code: GenericTrustActivationFindingCode::ActivationExpired,
935            message: "trust activation has expired".to_string(),
936        });
937        return Ok(evaluation);
938    }
939
940    match activation.body.disposition {
941        GenericTrustActivationDisposition::PendingReview => {
942            evaluation.findings.push(GenericTrustActivationFinding {
943                code: GenericTrustActivationFindingCode::ActivationPendingReview,
944                message: "trust activation remains pending review".to_string(),
945            });
946            return Ok(evaluation);
947        }
948        GenericTrustActivationDisposition::Denied => {
949            evaluation.findings.push(GenericTrustActivationFinding {
950                code: GenericTrustActivationFindingCode::ActivationDenied,
951                message: "trust activation was explicitly denied".to_string(),
952            });
953            return Ok(evaluation);
954        }
955        GenericTrustActivationDisposition::Approved => {}
956    }
957
958    if activation.body.eligibility.require_fresh_listing
959        && request.current_freshness.state != GenericListingFreshnessState::Fresh
960    {
961        evaluation.findings.push(GenericTrustActivationFinding {
962            code: GenericTrustActivationFindingCode::ListingStale,
963            message: "trust activation requires fresh listing evidence".to_string(),
964        });
965        return Ok(evaluation);
966    }
967
968    if !activation.body.eligibility.allowed_actor_kinds.is_empty()
969        && !activation
970            .body
971            .eligibility
972            .allowed_actor_kinds
973            .contains(&request.listing.body.subject.actor_kind)
974    {
975        evaluation.findings.push(GenericTrustActivationFinding {
976            code: GenericTrustActivationFindingCode::ActorKindIneligible,
977            message: "listing actor kind is not eligible under the activation policy".to_string(),
978        });
979        return Ok(evaluation);
980    }
981
982    if !activation
983        .body
984        .eligibility
985        .allowed_publisher_roles
986        .is_empty()
987        && !activation
988            .body
989            .eligibility
990            .allowed_publisher_roles
991            .contains(&request.current_publisher.role)
992    {
993        evaluation.findings.push(GenericTrustActivationFinding {
994            code: GenericTrustActivationFindingCode::PublisherRoleIneligible,
995            message: "listing publisher role is not eligible under the activation policy"
996                .to_string(),
997        });
998        return Ok(evaluation);
999    }
1000
1001    if !activation.body.eligibility.allowed_statuses.is_empty()
1002        && !activation
1003            .body
1004            .eligibility
1005            .allowed_statuses
1006            .contains(&request.listing.body.status)
1007    {
1008        evaluation.findings.push(GenericTrustActivationFinding {
1009            code: GenericTrustActivationFindingCode::ListingStatusIneligible,
1010            message: "listing lifecycle status is not eligible under the activation policy"
1011                .to_string(),
1012        });
1013        return Ok(evaluation);
1014    }
1015
1016    if !activation
1017        .body
1018        .eligibility
1019        .required_listing_operator_ids
1020        .is_empty()
1021        && !activation
1022            .body
1023            .eligibility
1024            .required_listing_operator_ids
1025            .contains(&request.current_publisher.operator_id)
1026    {
1027        evaluation.findings.push(GenericTrustActivationFinding {
1028            code: GenericTrustActivationFindingCode::ListingOperatorIneligible,
1029            message: "listing operator is not eligible under the activation policy".to_string(),
1030        });
1031        return Ok(evaluation);
1032    }
1033
1034    if matches!(
1035        activation.body.admission_class,
1036        GenericTrustAdmissionClass::PublicUntrusted
1037    ) {
1038        evaluation.findings.push(GenericTrustActivationFinding {
1039            code: GenericTrustActivationFindingCode::AdmissionClassUntrusted,
1040            message: "public_untrusted admission class preserves visibility without runtime trust"
1041                .to_string(),
1042        });
1043        return Ok(evaluation);
1044    }
1045
1046    if activation.body.eligibility.require_bond_backing {
1047        evaluation.findings.push(GenericTrustActivationFinding {
1048            code: GenericTrustActivationFindingCode::BondBackingRequired,
1049            message:
1050                "bond_backed activation remains review-visible only until bond backing is proven"
1051                    .to_string(),
1052        });
1053        return Ok(evaluation);
1054    }
1055
1056    evaluation.admitted = true;
1057    Ok(evaluation)
1058}
1059
1060pub fn normalize_namespace(namespace: &str) -> String {
1061    namespace.trim().trim_end_matches('/').to_string()
1062}
1063
1064fn generic_listing_body_sha256(listing: &SignedGenericListing) -> Result<String, String> {
1065    Ok(sha256_hex(
1066        &canonical_json_bytes(&listing.body).map_err(|error| error.to_string())?,
1067    ))
1068}
1069
1070pub fn ensure_generic_listing_namespace_consistency<'a>(
1071    listings: impl IntoIterator<Item = &'a GenericListingArtifact>,
1072) -> Result<(), String> {
1073    let mut namespaces = BTreeMap::<String, GenericNamespaceOwnership>::new();
1074    for listing in listings {
1075        let namespace = normalize_namespace(&listing.namespace);
1076        if namespace.is_empty() {
1077            return Err("generic listing namespace must not be empty".to_string());
1078        }
1079        let ownership = listing.namespace_ownership.clone();
1080        if let Some(existing) = namespaces.get(&namespace) {
1081            if existing.owner_id != ownership.owner_id
1082                || existing.registry_url != ownership.registry_url
1083                || existing.signer_public_key != ownership.signer_public_key
1084            {
1085                return Err(format!(
1086                    "generic listing namespace `{namespace}` has conflicting ownership claims"
1087                ));
1088            }
1089        } else {
1090            namespaces.insert(namespace, ownership);
1091        }
1092    }
1093    Ok(())
1094}
1095
1096pub fn aggregate_generic_listing_reports(
1097    reports: &[GenericListingReport],
1098    query: &GenericListingQuery,
1099    now: u64,
1100) -> GenericListingSearchResponse {
1101    let normalized_query = query.normalized();
1102    let mut reachable_count = 0_u64;
1103    let mut stale_peer_count = 0_u64;
1104    let mut errors = Vec::<GenericListingSearchError>::new();
1105    let mut candidates = Vec::<(
1106        SignedGenericListing,
1107        GenericRegistryPublisher,
1108        GenericListingReplicaFreshness,
1109    )>::new();
1110
1111    for report in reports {
1112        if let Err(error) = validate_generic_listing_report(report) {
1113            errors.push(GenericListingSearchError {
1114                operator_id: report.publisher.operator_id.clone(),
1115                operator_name: report.publisher.operator_name.clone(),
1116                registry_url: report.publisher.registry_url.clone(),
1117                error,
1118            });
1119            continue;
1120        }
1121
1122        let freshness = report.freshness.assess(report.generated_at, now);
1123        if freshness.state == GenericListingFreshnessState::Stale {
1124            stale_peer_count += 1;
1125            errors.push(GenericListingSearchError {
1126                operator_id: report.publisher.operator_id.clone(),
1127                operator_name: report.publisher.operator_name.clone(),
1128                registry_url: report.publisher.registry_url.clone(),
1129                error: format!(
1130                    "generic registry report is stale: age {}s exceeds max {}s",
1131                    freshness.age_secs, freshness.max_age_secs
1132                ),
1133            });
1134            continue;
1135        }
1136
1137        reachable_count += 1;
1138        for listing in &report.listings {
1139            if normalized_query
1140                .namespace
1141                .as_deref()
1142                .is_some_and(|namespace| normalize_namespace(&listing.body.namespace) != namespace)
1143            {
1144                continue;
1145            }
1146            if normalized_query
1147                .actor_kind
1148                .is_some_and(|actor_kind| listing.body.subject.actor_kind != actor_kind)
1149            {
1150                continue;
1151            }
1152            if normalized_query
1153                .actor_id
1154                .as_deref()
1155                .is_some_and(|actor_id| listing.body.subject.actor_id != actor_id)
1156            {
1157                continue;
1158            }
1159            if normalized_query
1160                .status
1161                .is_some_and(|status| listing.body.status != status)
1162            {
1163                continue;
1164            }
1165            candidates.push((listing.clone(), report.publisher.clone(), freshness.clone()));
1166        }
1167    }
1168
1169    let mut groups = BTreeMap::<
1170        String,
1171        Vec<(
1172            SignedGenericListing,
1173            GenericRegistryPublisher,
1174            GenericListingReplicaFreshness,
1175        )>,
1176    >::new();
1177    for candidate in candidates {
1178        let divergence_key = generic_listing_divergence_key(&candidate.0.body);
1179        groups.entry(divergence_key).or_default().push(candidate);
1180    }
1181
1182    let mut divergences = Vec::<GenericListingDivergence>::new();
1183    let mut results = Vec::<GenericListingSearchResult>::new();
1184
1185    for (divergence_key, mut group) in groups {
1186        let first = &group[0].0.body;
1187        let canonical_fingerprint = (
1188            first.compatibility.source_artifact_sha256.clone(),
1189            first.status,
1190            first.namespace_ownership.owner_id.clone(),
1191            first.namespace_ownership.registry_url.clone(),
1192        );
1193        let is_divergent = group.iter().skip(1).any(|(listing, _, _)| {
1194            (
1195                listing.body.compatibility.source_artifact_sha256.clone(),
1196                listing.body.status,
1197                listing.body.namespace_ownership.owner_id.clone(),
1198                listing.body.namespace_ownership.registry_url.clone(),
1199            ) != canonical_fingerprint
1200        });
1201        if is_divergent {
1202            divergences.push(GenericListingDivergence {
1203                divergence_key,
1204                actor_id: first.subject.actor_id.clone(),
1205                actor_kind: first.subject.actor_kind,
1206                publisher_operator_ids: group
1207                    .iter()
1208                    .map(|(_, publisher, _)| publisher.operator_id.clone())
1209                    .collect(),
1210                reason:
1211                    "conflicting source artifact, lifecycle state, or namespace ownership across publishers"
1212                        .to_string(),
1213            });
1214            continue;
1215        }
1216
1217        group.sort_by(|left, right| {
1218            freshness_state_rank(&left.2.state)
1219                .cmp(&freshness_state_rank(&right.2.state))
1220                .then(publisher_role_rank(left.1.role).cmp(&publisher_role_rank(right.1.role)))
1221                .then(left.2.age_secs.cmp(&right.2.age_secs))
1222                .then((u64::MAX - left.2.generated_at).cmp(&(u64::MAX - right.2.generated_at)))
1223                .then(status_rank(left.0.body.status).cmp(&status_rank(right.0.body.status)))
1224                .then(
1225                    left.0
1226                        .body
1227                        .subject
1228                        .actor_kind
1229                        .cmp(&right.0.body.subject.actor_kind),
1230                )
1231                .then(
1232                    left.0
1233                        .body
1234                        .subject
1235                        .actor_id
1236                        .cmp(&right.0.body.subject.actor_id),
1237                )
1238                .then(right.0.body.published_at.cmp(&left.0.body.published_at))
1239                .then(left.1.operator_id.cmp(&right.1.operator_id))
1240                .then(left.0.body.listing_id.cmp(&right.0.body.listing_id))
1241        });
1242
1243        let (listing, publisher, freshness) = group.remove(0);
1244        results.push(GenericListingSearchResult {
1245            rank: 0,
1246            listing,
1247            publisher,
1248            freshness,
1249            replica_operator_ids: group
1250                .iter()
1251                .map(|(_, publisher, _)| publisher.operator_id.clone())
1252                .collect(),
1253        });
1254    }
1255
1256    results.sort_by(|left, right| {
1257        freshness_state_rank(&left.freshness.state)
1258            .cmp(&freshness_state_rank(&right.freshness.state))
1259            .then(
1260                publisher_role_rank(left.publisher.role)
1261                    .cmp(&publisher_role_rank(right.publisher.role)),
1262            )
1263            .then(left.freshness.age_secs.cmp(&right.freshness.age_secs))
1264            .then(
1265                (u64::MAX - left.freshness.generated_at)
1266                    .cmp(&(u64::MAX - right.freshness.generated_at)),
1267            )
1268            .then(
1269                status_rank(left.listing.body.status).cmp(&status_rank(right.listing.body.status)),
1270            )
1271            .then(
1272                left.listing
1273                    .body
1274                    .subject
1275                    .actor_kind
1276                    .cmp(&right.listing.body.subject.actor_kind),
1277            )
1278            .then(
1279                left.listing
1280                    .body
1281                    .subject
1282                    .actor_id
1283                    .cmp(&right.listing.body.subject.actor_id),
1284            )
1285            .then(
1286                right
1287                    .listing
1288                    .body
1289                    .published_at
1290                    .cmp(&left.listing.body.published_at),
1291            )
1292            .then(left.publisher.operator_id.cmp(&right.publisher.operator_id))
1293            .then(
1294                left.listing
1295                    .body
1296                    .listing_id
1297                    .cmp(&right.listing.body.listing_id),
1298            )
1299    });
1300
1301    for (index, result) in results.iter_mut().enumerate() {
1302        result.rank = (index + 1) as u64;
1303    }
1304    results.truncate(normalized_query.limit_or_default());
1305
1306    GenericListingSearchResponse {
1307        schema: GENERIC_LISTING_NETWORK_SEARCH_SCHEMA.to_string(),
1308        generated_at: now,
1309        query: normalized_query,
1310        search_policy: GenericListingSearchPolicy::default(),
1311        peer_count: reports.len() as u64,
1312        reachable_count,
1313        stale_peer_count,
1314        divergence_count: divergences.len() as u64,
1315        result_count: results.len() as u64,
1316        results,
1317        divergences,
1318        errors,
1319    }
1320}
1321
1322fn validate_generic_listing_report(report: &GenericListingReport) -> Result<(), String> {
1323    if report.schema != GENERIC_LISTING_REPORT_SCHEMA {
1324        return Err(format!(
1325            "unsupported generic listing report schema: {}",
1326            report.schema
1327        ));
1328    }
1329    report.namespace.validate()?;
1330    report.publisher.validate()?;
1331    report.freshness.validate(report.generated_at)?;
1332    report.search_policy.validate()?;
1333    ensure_generic_listing_namespace_consistency(
1334        report.listings.iter().map(|listing| &listing.body),
1335    )?;
1336    for listing in &report.listings {
1337        listing.body.validate()?;
1338        if !listing
1339            .verify_signature()
1340            .map_err(|error| error.to_string())?
1341        {
1342            return Err(format!(
1343                "listing `{}` signature is invalid in generic registry report",
1344                listing.body.listing_id
1345            ));
1346        }
1347        if normalize_namespace(&listing.body.namespace)
1348            != normalize_namespace(&report.namespace.namespace)
1349        {
1350            return Err(format!(
1351                "listing namespace `{}` falls outside report namespace `{}`",
1352                listing.body.namespace, report.namespace.namespace
1353            ));
1354        }
1355    }
1356    Ok(())
1357}
1358
1359fn generic_listing_divergence_key(listing: &GenericListingArtifact) -> String {
1360    format!(
1361        "{:?}:{}:{}:{}",
1362        listing.subject.actor_kind,
1363        listing.subject.actor_id,
1364        listing.compatibility.source_schema,
1365        listing.compatibility.source_artifact_id
1366    )
1367}
1368
1369fn publisher_role_rank(role: GenericRegistryPublisherRole) -> u8 {
1370    match role {
1371        GenericRegistryPublisherRole::Origin => 0,
1372        GenericRegistryPublisherRole::Mirror => 1,
1373        GenericRegistryPublisherRole::Indexer => 2,
1374    }
1375}
1376
1377fn status_rank(status: GenericListingStatus) -> u8 {
1378    match status {
1379        GenericListingStatus::Active => 0,
1380        GenericListingStatus::Suspended => 1,
1381        GenericListingStatus::Superseded => 2,
1382        GenericListingStatus::Revoked => 3,
1383        GenericListingStatus::Retired => 4,
1384    }
1385}
1386
1387fn freshness_state_rank(state: &GenericListingFreshnessState) -> u8 {
1388    match state {
1389        GenericListingFreshnessState::Fresh => 0,
1390        GenericListingFreshnessState::Stale => 1,
1391        GenericListingFreshnessState::Divergent => 2,
1392    }
1393}
1394
1395fn validate_non_empty(value: &str, field: &str) -> Result<(), String> {
1396    if value.trim().is_empty() {
1397        return Err(format!("{field} must not be empty"));
1398    }
1399    Ok(())
1400}
1401
1402fn validate_http_url(value: &str, field: &str) -> Result<(), String> {
1403    validate_non_empty(value, field)?;
1404    if !(value.starts_with("http://") || value.starts_with("https://")) {
1405        return Err(format!("{field} must start with http:// or https://"));
1406    }
1407    Ok(())
1408}
1409
1410fn validate_optional_http_url(value: Option<&str>, field: &str) -> Result<(), String> {
1411    if let Some(value) = value {
1412        validate_http_url(value, field)?;
1413    }
1414    Ok(())
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419    use super::*;
1420    use crate::crypto::Keypair;
1421
1422    fn sample_namespace(owner_id: &str, keypair: &Keypair) -> GenericNamespaceOwnership {
1423        GenericNamespaceOwnership {
1424            namespace: "https://registry.chio.example".to_string(),
1425            owner_id: owner_id.to_string(),
1426            owner_name: Some("Chio Registry".to_string()),
1427            registry_url: "https://registry.chio.example".to_string(),
1428            signer_public_key: keypair.public_key(),
1429            registered_at: 1,
1430            transferred_from_owner_id: None,
1431        }
1432    }
1433
1434    fn sample_listing(
1435        owner_id: &str,
1436        keypair: &Keypair,
1437        artifact_id: &str,
1438        source_sha256: &str,
1439    ) -> GenericListingArtifact {
1440        GenericListingArtifact {
1441            schema: GENERIC_LISTING_ARTIFACT_SCHEMA.to_string(),
1442            listing_id: format!("listing-{artifact_id}"),
1443            namespace: "https://registry.chio.example".to_string(),
1444            published_at: 10,
1445            expires_at: Some(20),
1446            status: GenericListingStatus::Active,
1447            namespace_ownership: sample_namespace(owner_id, keypair),
1448            subject: GenericListingSubject {
1449                actor_kind: GenericListingActorKind::ToolServer,
1450                actor_id: "demo-server".to_string(),
1451                display_name: Some("Demo Server".to_string()),
1452                metadata_url: Some("https://registry.chio.example/metadata".to_string()),
1453                resolution_url: Some(
1454                    "https://registry.chio.example/v1/public/certifications/resolve/demo-server"
1455                        .to_string(),
1456                ),
1457                homepage_url: Some("https://demo.chio.example".to_string()),
1458            },
1459            compatibility: GenericListingCompatibilityReference {
1460                source_schema: "chio.certify.check.v1".to_string(),
1461                source_artifact_id: artifact_id.to_string(),
1462                source_artifact_sha256: source_sha256.to_string(),
1463            },
1464            boundary: GenericListingBoundary::default(),
1465        }
1466    }
1467
1468    fn signed_sample_listing(
1469        owner_id: &str,
1470        signing_keypair: &Keypair,
1471        artifact_id: &str,
1472        source_sha256: &str,
1473    ) -> SignedGenericListing {
1474        SignedGenericListing::sign(
1475            sample_listing(owner_id, signing_keypair, artifact_id, source_sha256),
1476            signing_keypair,
1477        )
1478        .expect("sign sample listing")
1479    }
1480
1481    fn sample_publisher(
1482        role: GenericRegistryPublisherRole,
1483        operator_id: &str,
1484    ) -> GenericRegistryPublisher {
1485        GenericRegistryPublisher {
1486            role,
1487            operator_id: operator_id.to_string(),
1488            operator_name: Some(format!("Operator {operator_id}")),
1489            registry_url: format!("https://{operator_id}.chio.example"),
1490            upstream_registry_urls: Vec::new(),
1491        }
1492    }
1493
1494    fn sample_report(
1495        role: GenericRegistryPublisherRole,
1496        operator_id: &str,
1497        generated_at: u64,
1498        max_age_secs: u64,
1499        listings: Vec<SignedGenericListing>,
1500    ) -> GenericListingReport {
1501        let keypair = Keypair::generate();
1502        GenericListingReport {
1503            schema: GENERIC_LISTING_REPORT_SCHEMA.to_string(),
1504            generated_at,
1505            query: GenericListingQuery::default(),
1506            namespace: sample_namespace("https://registry.chio.example", &keypair),
1507            publisher: sample_publisher(role, operator_id),
1508            freshness: GenericListingFreshnessWindow {
1509                max_age_secs,
1510                valid_until: generated_at + max_age_secs,
1511            },
1512            search_policy: GenericListingSearchPolicy::default(),
1513            summary: GenericListingSummary {
1514                matching_listings: listings.len() as u64,
1515                returned_listings: listings.len() as u64,
1516                active_listings: listings.len() as u64,
1517                suspended_listings: 0,
1518                superseded_listings: 0,
1519                revoked_listings: 0,
1520                retired_listings: 0,
1521            },
1522            listings,
1523        }
1524    }
1525
1526    fn sample_review_context(
1527        role: GenericRegistryPublisherRole,
1528        operator_id: &str,
1529        freshness_state: GenericListingFreshnessState,
1530    ) -> GenericTrustActivationReviewContext {
1531        GenericTrustActivationReviewContext {
1532            publisher: sample_publisher(role, operator_id),
1533            freshness: GenericListingReplicaFreshness {
1534                state: freshness_state,
1535                age_secs: 5,
1536                max_age_secs: 300,
1537                valid_until: 400,
1538                generated_at: 100,
1539            },
1540        }
1541    }
1542
1543    fn sample_activation_issue_request(
1544        listing: SignedGenericListing,
1545        admission_class: GenericTrustAdmissionClass,
1546        disposition: GenericTrustActivationDisposition,
1547    ) -> GenericTrustActivationIssueRequest {
1548        GenericTrustActivationIssueRequest {
1549            listing,
1550            admission_class,
1551            disposition,
1552            eligibility: GenericTrustActivationEligibility {
1553                allowed_actor_kinds: vec![GenericListingActorKind::ToolServer],
1554                allowed_publisher_roles: vec![GenericRegistryPublisherRole::Origin],
1555                allowed_statuses: vec![GenericListingStatus::Active],
1556                require_fresh_listing: true,
1557                require_bond_backing: false,
1558                required_listing_operator_ids: Vec::new(),
1559                policy_reference: Some("policy/open-registry/default".to_string()),
1560            },
1561            review_context: sample_review_context(
1562                GenericRegistryPublisherRole::Origin,
1563                "origin-a",
1564                GenericListingFreshnessState::Fresh,
1565            ),
1566            requested_by: "ops@chio.example".to_string(),
1567            reviewed_by: Some("reviewer@chio.example".to_string()),
1568            requested_at: Some(120),
1569            reviewed_at: Some(130),
1570            expires_at: Some(200),
1571            note: Some("reviewed under default local activation policy".to_string()),
1572        }
1573    }
1574
1575    fn issue_request_for(
1576        listing: SignedGenericListing,
1577        admission_class: GenericTrustAdmissionClass,
1578        disposition: GenericTrustActivationDisposition,
1579    ) -> GenericTrustActivationIssueRequest {
1580        GenericTrustActivationIssueRequest {
1581            reviewed_by: match disposition {
1582                GenericTrustActivationDisposition::PendingReview => None,
1583                GenericTrustActivationDisposition::Approved
1584                | GenericTrustActivationDisposition::Denied => {
1585                    Some("reviewer@chio.example".to_string())
1586                }
1587            },
1588            reviewed_at: match disposition {
1589                GenericTrustActivationDisposition::PendingReview => None,
1590                GenericTrustActivationDisposition::Approved
1591                | GenericTrustActivationDisposition::Denied => Some(130),
1592            },
1593            ..sample_activation_issue_request(listing, admission_class, disposition)
1594        }
1595    }
1596
1597    fn signed_activation(
1598        listing: SignedGenericListing,
1599        admission_class: GenericTrustAdmissionClass,
1600        disposition: GenericTrustActivationDisposition,
1601    ) -> SignedGenericTrustActivation {
1602        let authority_keypair = Keypair::generate();
1603        let artifact = build_generic_trust_activation_artifact(
1604            "https://operator.chio.example",
1605            Some("Chio Operator".to_string()),
1606            &issue_request_for(listing, admission_class, disposition),
1607            130,
1608        )
1609        .expect("build activation artifact");
1610        SignedGenericTrustActivation::sign(artifact, &authority_keypair).expect("sign activation")
1611    }
1612
1613    fn evaluation_request(
1614        listing: SignedGenericListing,
1615        activation: Option<SignedGenericTrustActivation>,
1616        freshness_state: GenericListingFreshnessState,
1617        publisher_role: GenericRegistryPublisherRole,
1618        publisher_operator_id: &str,
1619        evaluated_at: u64,
1620    ) -> GenericTrustActivationEvaluationRequest {
1621        GenericTrustActivationEvaluationRequest {
1622            listing,
1623            current_publisher: sample_publisher(publisher_role, publisher_operator_id),
1624            current_freshness: GenericListingReplicaFreshness {
1625                state: freshness_state,
1626                age_secs: 5,
1627                max_age_secs: 300,
1628                valid_until: 400,
1629                generated_at: 100,
1630            },
1631            activation,
1632            evaluated_at: Some(evaluated_at),
1633        }
1634    }
1635
1636    #[test]
1637    fn generic_listing_boundary_rejects_automatic_trust_admission() {
1638        let boundary = GenericListingBoundary {
1639            visibility_only: true,
1640            explicit_trust_activation_required: true,
1641            automatic_trust_admission: true,
1642        };
1643        assert!(boundary
1644            .validate()
1645            .expect_err("automatic trust admission rejected")
1646            .contains("must not auto-admit trust"));
1647    }
1648
1649    #[test]
1650    fn generic_listing_boundary_rejects_missing_explicit_activation_gate() {
1651        let boundary = GenericListingBoundary {
1652            visibility_only: true,
1653            explicit_trust_activation_required: false,
1654            automatic_trust_admission: false,
1655        };
1656        assert!(boundary
1657            .validate()
1658            .expect_err("missing explicit trust activation gate rejected")
1659            .contains("must require explicit trust activation"));
1660    }
1661
1662    #[test]
1663    fn generic_namespace_artifact_rejects_wrong_schema() {
1664        let keypair = Keypair::generate();
1665        let artifact = GenericNamespaceArtifact {
1666            schema: "chio.registry.namespace.v0".to_string(),
1667            namespace_id: "registry.chio.example".to_string(),
1668            lifecycle_state: GenericNamespaceLifecycleState::Active,
1669            ownership: sample_namespace("operator-a", &keypair),
1670            boundary: GenericListingBoundary::default(),
1671        };
1672
1673        assert!(artifact
1674            .validate()
1675            .expect_err("wrong namespace schema rejected")
1676            .contains("unsupported generic namespace schema"));
1677    }
1678
1679    #[test]
1680    fn generic_listing_rejects_namespace_mismatch() {
1681        let keypair = Keypair::generate();
1682        let mut listing = sample_listing("operator-a", &keypair, "artifact-1", "deadbeef");
1683        listing.namespace = "https://other.chio.example".to_string();
1684        assert!(listing
1685            .validate()
1686            .expect_err("namespace mismatch rejected")
1687            .contains("does not match namespace ownership"));
1688    }
1689
1690    #[test]
1691    fn generic_listing_rejects_non_increasing_expiry() {
1692        let keypair = Keypair::generate();
1693        let mut listing = sample_listing("operator-a", &keypair, "artifact-1", "deadbeef");
1694        listing.expires_at = Some(listing.published_at);
1695
1696        assert!(listing
1697            .validate()
1698            .expect_err("non-increasing expiry rejected")
1699            .contains("expiry must be greater"));
1700    }
1701
1702    #[test]
1703    fn generic_listing_query_normalizes_namespace_actor_and_limit() {
1704        let normalized = GenericListingQuery {
1705            namespace: Some(" https://registry.chio.example/ ".to_string()),
1706            actor_kind: Some(GenericListingActorKind::ToolServer),
1707            actor_id: Some("   ".to_string()),
1708            status: Some(GenericListingStatus::Active),
1709            limit: Some(999),
1710        }
1711        .normalized();
1712
1713        assert_eq!(
1714            normalized.namespace.as_deref(),
1715            Some("https://registry.chio.example")
1716        );
1717        assert_eq!(normalized.actor_id, None);
1718        assert_eq!(normalized.limit, Some(MAX_GENERIC_LISTING_LIMIT));
1719    }
1720
1721    #[test]
1722    fn generic_listing_freshness_window_rejects_invalid_bounds_and_assesses_stale() {
1723        assert!(GenericListingFreshnessWindow {
1724            max_age_secs: 0,
1725            valid_until: 200,
1726        }
1727        .validate(100)
1728        .expect_err("zero max age rejected")
1729        .contains("greater than zero"));
1730
1731        assert!(GenericListingFreshnessWindow {
1732            max_age_secs: 30,
1733            valid_until: 100,
1734        }
1735        .validate(100)
1736        .expect_err("non-increasing valid_until rejected")
1737        .contains("greater than generated_at"));
1738
1739        let freshness = GenericListingFreshnessWindow {
1740            max_age_secs: 30,
1741            valid_until: 150,
1742        }
1743        .assess(100, 200);
1744        assert_eq!(freshness.state, GenericListingFreshnessState::Stale);
1745        assert_eq!(freshness.age_secs, 100);
1746    }
1747
1748    #[test]
1749    fn generic_listing_search_policy_rejects_non_reproducible_modes() {
1750        let mut policy = GenericListingSearchPolicy::default();
1751        policy.reproducible_ordering = false;
1752        assert!(policy
1753            .validate()
1754            .expect_err("non-reproducible policy rejected")
1755            .contains("must remain reproducible"));
1756
1757        let mut policy = GenericListingSearchPolicy::default();
1758        policy.visibility_only = false;
1759        assert!(policy
1760            .validate()
1761            .expect_err("non-visibility-only policy rejected")
1762            .contains("must remain visibility-only"));
1763
1764        let mut policy = GenericListingSearchPolicy::default();
1765        policy.explicit_trust_activation_required = false;
1766        assert!(policy
1767            .validate()
1768            .expect_err("missing explicit trust activation rejected")
1769            .contains("must require explicit trust activation"));
1770    }
1771
1772    #[test]
1773    fn generic_listing_replica_freshness_rejects_invalid_window() {
1774        let freshness = GenericListingReplicaFreshness {
1775            state: GenericListingFreshnessState::Fresh,
1776            age_secs: 5,
1777            max_age_secs: 0,
1778            valid_until: 100,
1779            generated_at: 100,
1780        };
1781        assert!(freshness
1782            .validate()
1783            .expect_err("invalid freshness rejected")
1784            .contains("greater than zero"));
1785    }
1786
1787    #[test]
1788    fn generic_trust_activation_eligibility_rejects_invalid_role_and_bond_rules() {
1789        assert!(GenericTrustActivationEligibility {
1790            required_listing_operator_ids: vec![],
1791            ..GenericTrustActivationEligibility {
1792                allowed_actor_kinds: vec![],
1793                allowed_publisher_roles: vec![],
1794                allowed_statuses: vec![],
1795                require_fresh_listing: true,
1796                require_bond_backing: false,
1797                required_listing_operator_ids: vec![],
1798                policy_reference: None,
1799            }
1800        }
1801        .validate(GenericTrustAdmissionClass::RoleGated)
1802        .expect_err("role-gated operators required")
1803        .contains("requires required_listing_operator_ids"));
1804
1805        assert!(GenericTrustActivationEligibility {
1806            require_bond_backing: false,
1807            ..GenericTrustActivationEligibility {
1808                allowed_actor_kinds: vec![],
1809                allowed_publisher_roles: vec![],
1810                allowed_statuses: vec![],
1811                require_fresh_listing: true,
1812                require_bond_backing: false,
1813                required_listing_operator_ids: vec![],
1814                policy_reference: None,
1815            }
1816        }
1817        .validate(GenericTrustAdmissionClass::BondBacked)
1818        .expect_err("bond-backed admission must require bonds")
1819        .contains("must require bond backing"));
1820
1821        assert!(GenericTrustActivationEligibility {
1822            require_bond_backing: true,
1823            ..GenericTrustActivationEligibility {
1824                allowed_actor_kinds: vec![],
1825                allowed_publisher_roles: vec![],
1826                allowed_statuses: vec![],
1827                require_fresh_listing: true,
1828                require_bond_backing: true,
1829                required_listing_operator_ids: vec![],
1830                policy_reference: None,
1831            }
1832        }
1833        .validate(GenericTrustAdmissionClass::Reviewable)
1834        .expect_err("non-bond admission cannot require bonds")
1835        .contains("only valid for bond_backed"));
1836    }
1837
1838    #[test]
1839    fn generic_trust_activation_artifact_validate_rejects_review_field_misconfigurations() {
1840        let keypair = Keypair::generate();
1841        let listing = signed_sample_listing(
1842            "https://registry.chio.example",
1843            &keypair,
1844            "artifact-1",
1845            "deadbeef",
1846        );
1847        let mut artifact = build_generic_trust_activation_artifact(
1848            "https://operator.chio.example",
1849            Some("Chio Operator".to_string()),
1850            &issue_request_for(
1851                listing.clone(),
1852                GenericTrustAdmissionClass::Reviewable,
1853                GenericTrustActivationDisposition::Approved,
1854            ),
1855            130,
1856        )
1857        .expect("build activation");
1858        artifact.reviewed_at = Some(100);
1859        assert!(artifact
1860            .validate()
1861            .expect_err("reviewed_at before requested_at rejected")
1862            .contains("reviewed_at must be greater"));
1863
1864        let mut artifact = build_generic_trust_activation_artifact(
1865            "https://operator.chio.example",
1866            Some("Chio Operator".to_string()),
1867            &issue_request_for(
1868                listing.clone(),
1869                GenericTrustAdmissionClass::Reviewable,
1870                GenericTrustActivationDisposition::Approved,
1871            ),
1872            130,
1873        )
1874        .expect("build activation");
1875        artifact.expires_at = Some(120);
1876        assert!(artifact
1877            .validate()
1878            .expect_err("expiry before requested_at rejected")
1879            .contains("expires_at must be greater"));
1880
1881        let mut artifact = build_generic_trust_activation_artifact(
1882            "https://operator.chio.example",
1883            Some("Chio Operator".to_string()),
1884            &issue_request_for(
1885                listing.clone(),
1886                GenericTrustAdmissionClass::Reviewable,
1887                GenericTrustActivationDisposition::Approved,
1888            ),
1889            130,
1890        )
1891        .expect("build activation");
1892        artifact.disposition = GenericTrustActivationDisposition::PendingReview;
1893        artifact.reviewed_by = Some("reviewer@chio.example".to_string());
1894        artifact.reviewed_at = Some(130);
1895        assert!(artifact
1896            .validate()
1897            .expect_err("pending review cannot carry review completion")
1898            .contains("must not carry review completion fields"));
1899
1900        let mut artifact = build_generic_trust_activation_artifact(
1901            "https://operator.chio.example",
1902            Some("Chio Operator".to_string()),
1903            &issue_request_for(
1904                listing,
1905                GenericTrustAdmissionClass::Reviewable,
1906                GenericTrustActivationDisposition::Approved,
1907            ),
1908            130,
1909        )
1910        .expect("build activation");
1911        artifact.reviewed_by = None;
1912        assert!(artifact
1913            .validate()
1914            .expect_err("approved activation requires reviewer")
1915            .contains("requires reviewed_at and reviewed_by"));
1916    }
1917
1918    #[test]
1919    fn generic_trust_activation_issue_request_validate_rejects_stale_approved_context() {
1920        let signing_keypair = Keypair::generate();
1921        let listing = signed_sample_listing(
1922            "https://registry.chio.example",
1923            &signing_keypair,
1924            "artifact-1",
1925            "deadbeef",
1926        );
1927        let mut request = issue_request_for(
1928            listing,
1929            GenericTrustAdmissionClass::Reviewable,
1930            GenericTrustActivationDisposition::Approved,
1931        );
1932        request.review_context.freshness.state = GenericListingFreshnessState::Stale;
1933
1934        assert!(request
1935            .validate()
1936            .expect_err("approved activation requires fresh context")
1937            .contains("requires fresh listing review context"));
1938    }
1939
1940    #[test]
1941    fn build_generic_trust_activation_artifact_defaults_reviewed_at_for_approved() {
1942        let signing_keypair = Keypair::generate();
1943        let listing = signed_sample_listing(
1944            "https://registry.chio.example",
1945            &signing_keypair,
1946            "artifact-1",
1947            "deadbeef",
1948        );
1949        let mut request = issue_request_for(
1950            listing,
1951            GenericTrustAdmissionClass::Reviewable,
1952            GenericTrustActivationDisposition::Approved,
1953        );
1954        request.reviewed_at = None;
1955        let artifact = build_generic_trust_activation_artifact(
1956            "https://operator.chio.example",
1957            Some("Chio Operator".to_string()),
1958            &request,
1959            130,
1960        )
1961        .expect("build activation");
1962
1963        assert_eq!(artifact.reviewed_at, Some(130));
1964    }
1965
1966    #[test]
1967    fn generic_listing_namespace_consistency_rejects_conflicting_owners() {
1968        let keypair_a = Keypair::generate();
1969        let keypair_b = Keypair::generate();
1970        let listing_a = sample_listing("operator-a", &keypair_a, "artifact-1", "deadbeef");
1971        let listing_b = sample_listing("operator-b", &keypair_b, "artifact-1", "deadbeef");
1972        assert!(
1973            ensure_generic_listing_namespace_consistency([&listing_a, &listing_b])
1974                .expect_err("conflicting namespace ownership rejected")
1975                .contains("conflicting ownership")
1976        );
1977    }
1978
1979    #[test]
1980    fn generic_listing_search_prefers_fresh_origin_and_collapses_identical_replicas() {
1981        let signing_keypair = Keypair::generate();
1982        let origin = sample_report(
1983            GenericRegistryPublisherRole::Origin,
1984            "origin-a",
1985            100,
1986            300,
1987            vec![signed_sample_listing(
1988                "https://registry.chio.example",
1989                &signing_keypair,
1990                "artifact-1",
1991                "deadbeef",
1992            )],
1993        );
1994        let mirror = sample_report(
1995            GenericRegistryPublisherRole::Mirror,
1996            "mirror-a",
1997            105,
1998            300,
1999            vec![signed_sample_listing(
2000                "https://registry.chio.example",
2001                &signing_keypair,
2002                "artifact-1",
2003                "deadbeef",
2004            )],
2005        );
2006        let indexer = sample_report(
2007            GenericRegistryPublisherRole::Indexer,
2008            "indexer-a",
2009            106,
2010            300,
2011            vec![signed_sample_listing(
2012                "https://registry.chio.example",
2013                &signing_keypair,
2014                "artifact-1",
2015                "deadbeef",
2016            )],
2017        );
2018
2019        let response = aggregate_generic_listing_reports(
2020            &[origin, mirror, indexer],
2021            &GenericListingQuery::default(),
2022            120,
2023        );
2024        assert_eq!(response.peer_count, 3);
2025        assert_eq!(response.reachable_count, 3);
2026        assert_eq!(response.result_count, 1);
2027        assert_eq!(response.divergence_count, 0);
2028        assert_eq!(
2029            response.results[0].publisher.role,
2030            GenericRegistryPublisherRole::Origin
2031        );
2032        assert_eq!(response.results[0].replica_operator_ids.len(), 2);
2033    }
2034
2035    #[test]
2036    fn generic_listing_search_rejects_stale_reports() {
2037        let signing_keypair = Keypair::generate();
2038        let stale = sample_report(
2039            GenericRegistryPublisherRole::Mirror,
2040            "mirror-a",
2041            100,
2042            10,
2043            vec![signed_sample_listing(
2044                "https://registry.chio.example",
2045                &signing_keypair,
2046                "artifact-1",
2047                "deadbeef",
2048            )],
2049        );
2050
2051        let response =
2052            aggregate_generic_listing_reports(&[stale], &GenericListingQuery::default(), 200);
2053        assert_eq!(response.peer_count, 1);
2054        assert_eq!(response.reachable_count, 0);
2055        assert_eq!(response.stale_peer_count, 1);
2056        assert_eq!(response.result_count, 0);
2057        assert_eq!(response.errors.len(), 1);
2058        assert!(response.errors[0].error.contains("stale"));
2059    }
2060
2061    #[test]
2062    fn generic_listing_search_excludes_divergent_results() {
2063        let signing_keypair = Keypair::generate();
2064        let origin = sample_report(
2065            GenericRegistryPublisherRole::Origin,
2066            "origin-a",
2067            100,
2068            300,
2069            vec![signed_sample_listing(
2070                "https://registry.chio.example",
2071                &signing_keypair,
2072                "artifact-1",
2073                "deadbeef",
2074            )],
2075        );
2076        let mirror = sample_report(
2077            GenericRegistryPublisherRole::Mirror,
2078            "mirror-a",
2079            101,
2080            300,
2081            vec![signed_sample_listing(
2082                "https://registry.chio.example",
2083                &signing_keypair,
2084                "artifact-1",
2085                "cafebabe",
2086            )],
2087        );
2088
2089        let response = aggregate_generic_listing_reports(
2090            &[origin, mirror],
2091            &GenericListingQuery::default(),
2092            120,
2093        );
2094        assert_eq!(response.result_count, 0);
2095        assert_eq!(response.divergence_count, 1);
2096        assert_eq!(response.divergences[0].publisher_operator_ids.len(), 2);
2097    }
2098
2099    #[test]
2100    fn generic_listing_search_rejects_reports_with_invalid_listing_signatures() {
2101        let signing_keypair = Keypair::generate();
2102        let mut report = sample_report(
2103            GenericRegistryPublisherRole::Mirror,
2104            "mirror-a",
2105            100,
2106            300,
2107            vec![signed_sample_listing(
2108                "https://registry.chio.example",
2109                &signing_keypair,
2110                "artifact-1",
2111                "deadbeef",
2112            )],
2113        );
2114        report.listings[0].body.status = GenericListingStatus::Revoked;
2115
2116        let response =
2117            aggregate_generic_listing_reports(&[report], &GenericListingQuery::default(), 120);
2118        assert_eq!(response.peer_count, 1);
2119        assert_eq!(response.reachable_count, 0);
2120        assert_eq!(response.result_count, 0);
2121        assert_eq!(response.errors.len(), 1);
2122        assert!(response.errors[0].error.contains("signature is invalid"));
2123    }
2124
2125    #[test]
2126    fn generic_trust_activation_requires_explicit_artifact() {
2127        let signing_keypair = Keypair::generate();
2128        let listing = signed_sample_listing(
2129            "https://registry.chio.example",
2130            &signing_keypair,
2131            "artifact-1",
2132            "deadbeef",
2133        );
2134        let report = evaluate_generic_trust_activation(
2135            &GenericTrustActivationEvaluationRequest {
2136                listing,
2137                current_publisher: sample_publisher(
2138                    GenericRegistryPublisherRole::Origin,
2139                    "origin-a",
2140                ),
2141                current_freshness: GenericListingReplicaFreshness {
2142                    state: GenericListingFreshnessState::Fresh,
2143                    age_secs: 5,
2144                    max_age_secs: 300,
2145                    valid_until: 400,
2146                    generated_at: 100,
2147                },
2148                activation: None,
2149                evaluated_at: Some(150),
2150            },
2151            150,
2152        )
2153        .expect("evaluate missing activation");
2154        assert!(!report.admitted);
2155        assert_eq!(report.findings.len(), 1);
2156        assert_eq!(
2157            report.findings[0].code,
2158            GenericTrustActivationFindingCode::MissingActivation
2159        );
2160    }
2161
2162    #[test]
2163    fn generic_trust_activation_admits_reviewable_activation() {
2164        let signing_keypair = Keypair::generate();
2165        let authority_keypair = Keypair::generate();
2166        let listing = signed_sample_listing(
2167            "https://registry.chio.example",
2168            &signing_keypair,
2169            "artifact-1",
2170            "deadbeef",
2171        );
2172        let issue_request = sample_activation_issue_request(
2173            listing.clone(),
2174            GenericTrustAdmissionClass::Reviewable,
2175            GenericTrustActivationDisposition::Approved,
2176        );
2177        let activation = SignedGenericTrustActivation::sign(
2178            build_generic_trust_activation_artifact(
2179                "https://operator.chio.example",
2180                Some("Chio Operator".to_string()),
2181                &issue_request,
2182                130,
2183            )
2184            .expect("build activation artifact"),
2185            &authority_keypair,
2186        )
2187        .expect("sign activation");
2188
2189        let report = evaluate_generic_trust_activation(
2190            &GenericTrustActivationEvaluationRequest {
2191                listing,
2192                current_publisher: sample_publisher(
2193                    GenericRegistryPublisherRole::Origin,
2194                    "origin-a",
2195                ),
2196                current_freshness: GenericListingReplicaFreshness {
2197                    state: GenericListingFreshnessState::Fresh,
2198                    age_secs: 5,
2199                    max_age_secs: 300,
2200                    valid_until: 400,
2201                    generated_at: 100,
2202                },
2203                activation: Some(activation),
2204                evaluated_at: Some(150),
2205            },
2206            150,
2207        )
2208        .expect("evaluate activation");
2209        assert!(report.admitted);
2210        assert!(report.findings.is_empty());
2211        assert_eq!(
2212            report.admission_class,
2213            Some(GenericTrustAdmissionClass::Reviewable)
2214        );
2215    }
2216
2217    #[test]
2218    fn generic_trust_activation_fails_closed_on_stale_listing() {
2219        let signing_keypair = Keypair::generate();
2220        let authority_keypair = Keypair::generate();
2221        let listing = signed_sample_listing(
2222            "https://registry.chio.example",
2223            &signing_keypair,
2224            "artifact-1",
2225            "deadbeef",
2226        );
2227        let issue_request = sample_activation_issue_request(
2228            listing.clone(),
2229            GenericTrustAdmissionClass::Reviewable,
2230            GenericTrustActivationDisposition::Approved,
2231        );
2232        let activation = SignedGenericTrustActivation::sign(
2233            build_generic_trust_activation_artifact(
2234                "https://operator.chio.example",
2235                Some("Chio Operator".to_string()),
2236                &issue_request,
2237                130,
2238            )
2239            .expect("build activation artifact"),
2240            &authority_keypair,
2241        )
2242        .expect("sign activation");
2243
2244        let report = evaluate_generic_trust_activation(
2245            &GenericTrustActivationEvaluationRequest {
2246                listing,
2247                current_publisher: sample_publisher(
2248                    GenericRegistryPublisherRole::Origin,
2249                    "origin-a",
2250                ),
2251                current_freshness: GenericListingReplicaFreshness {
2252                    state: GenericListingFreshnessState::Stale,
2253                    age_secs: 500,
2254                    max_age_secs: 300,
2255                    valid_until: 400,
2256                    generated_at: 100,
2257                },
2258                activation: Some(activation),
2259                evaluated_at: Some(700),
2260            },
2261            700,
2262        )
2263        .expect("evaluate stale listing");
2264        assert!(!report.admitted);
2265        assert_eq!(
2266            report.findings[0].code,
2267            GenericTrustActivationFindingCode::ListingStale
2268        );
2269    }
2270
2271    #[test]
2272    fn generic_trust_activation_public_untrusted_never_admits() {
2273        let signing_keypair = Keypair::generate();
2274        let authority_keypair = Keypair::generate();
2275        let listing = signed_sample_listing(
2276            "https://registry.chio.example",
2277            &signing_keypair,
2278            "artifact-1",
2279            "deadbeef",
2280        );
2281        let issue_request = sample_activation_issue_request(
2282            listing.clone(),
2283            GenericTrustAdmissionClass::PublicUntrusted,
2284            GenericTrustActivationDisposition::Approved,
2285        );
2286        let activation = SignedGenericTrustActivation::sign(
2287            build_generic_trust_activation_artifact(
2288                "https://operator.chio.example",
2289                Some("Chio Operator".to_string()),
2290                &issue_request,
2291                130,
2292            )
2293            .expect("build activation artifact"),
2294            &authority_keypair,
2295        )
2296        .expect("sign activation");
2297
2298        let report = evaluate_generic_trust_activation(
2299            &GenericTrustActivationEvaluationRequest {
2300                listing,
2301                current_publisher: sample_publisher(
2302                    GenericRegistryPublisherRole::Origin,
2303                    "origin-a",
2304                ),
2305                current_freshness: GenericListingReplicaFreshness {
2306                    state: GenericListingFreshnessState::Fresh,
2307                    age_secs: 5,
2308                    max_age_secs: 300,
2309                    valid_until: 400,
2310                    generated_at: 100,
2311                },
2312                activation: Some(activation),
2313                evaluated_at: Some(150),
2314            },
2315            150,
2316        )
2317        .expect("evaluate public_untrusted");
2318        assert!(!report.admitted);
2319        assert_eq!(
2320            report.findings[0].code,
2321            GenericTrustActivationFindingCode::AdmissionClassUntrusted
2322        );
2323    }
2324
2325    #[test]
2326    fn generic_trust_activation_flags_unverifiable_listing_signature() {
2327        let signing_keypair = Keypair::generate();
2328        let mut listing = signed_sample_listing(
2329            "https://registry.chio.example",
2330            &signing_keypair,
2331            "artifact-1",
2332            "deadbeef",
2333        );
2334        listing.body.status = GenericListingStatus::Revoked;
2335
2336        let report = evaluate_generic_trust_activation(
2337            &evaluation_request(
2338                listing,
2339                None,
2340                GenericListingFreshnessState::Fresh,
2341                GenericRegistryPublisherRole::Origin,
2342                "origin-a",
2343                150,
2344            ),
2345            150,
2346        )
2347        .expect("evaluate invalid listing signature");
2348
2349        assert_eq!(
2350            report.findings[0].code,
2351            GenericTrustActivationFindingCode::ListingUnverifiable
2352        );
2353    }
2354
2355    #[test]
2356    fn generic_trust_activation_flags_unverifiable_activation_signature() {
2357        let signing_keypair = Keypair::generate();
2358        let listing = signed_sample_listing(
2359            "https://registry.chio.example",
2360            &signing_keypair,
2361            "artifact-1",
2362            "deadbeef",
2363        );
2364        let mut activation = signed_activation(
2365            listing.clone(),
2366            GenericTrustAdmissionClass::Reviewable,
2367            GenericTrustActivationDisposition::Approved,
2368        );
2369        activation.body.local_operator_id = "https://tampered.chio.example".to_string();
2370
2371        let report = evaluate_generic_trust_activation(
2372            &evaluation_request(
2373                listing,
2374                Some(activation),
2375                GenericListingFreshnessState::Fresh,
2376                GenericRegistryPublisherRole::Origin,
2377                "origin-a",
2378                150,
2379            ),
2380            150,
2381        )
2382        .expect("evaluate invalid activation signature");
2383
2384        assert_eq!(
2385            report.findings[0].code,
2386            GenericTrustActivationFindingCode::ActivationUnverifiable
2387        );
2388    }
2389
2390    #[test]
2391    fn generic_trust_activation_flags_invalid_activation_body() {
2392        let signing_keypair = Keypair::generate();
2393        let authority_keypair = Keypair::generate();
2394        let listing = signed_sample_listing(
2395            "https://registry.chio.example",
2396            &signing_keypair,
2397            "artifact-1",
2398            "deadbeef",
2399        );
2400        let mut artifact = build_generic_trust_activation_artifact(
2401            "https://operator.chio.example",
2402            Some("Chio Operator".to_string()),
2403            &issue_request_for(
2404                listing.clone(),
2405                GenericTrustAdmissionClass::Reviewable,
2406                GenericTrustActivationDisposition::Approved,
2407            ),
2408            130,
2409        )
2410        .expect("build activation");
2411        artifact.reviewed_by = None;
2412        let activation = SignedGenericTrustActivation::sign(artifact, &authority_keypair)
2413            .expect("sign activation");
2414
2415        let report = evaluate_generic_trust_activation(
2416            &evaluation_request(
2417                listing,
2418                Some(activation),
2419                GenericListingFreshnessState::Fresh,
2420                GenericRegistryPublisherRole::Origin,
2421                "origin-a",
2422                150,
2423            ),
2424            150,
2425        )
2426        .expect("evaluate invalid activation body");
2427
2428        assert_eq!(
2429            report.findings[0].code,
2430            GenericTrustActivationFindingCode::ActivationUnverifiable
2431        );
2432    }
2433
2434    #[test]
2435    fn generic_trust_activation_rejects_listing_mismatch() {
2436        let signing_keypair = Keypair::generate();
2437        let authority_keypair = Keypair::generate();
2438        let listing = signed_sample_listing(
2439            "https://registry.chio.example",
2440            &signing_keypair,
2441            "artifact-1",
2442            "deadbeef",
2443        );
2444        let mut artifact = build_generic_trust_activation_artifact(
2445            "https://operator.chio.example",
2446            Some("Chio Operator".to_string()),
2447            &issue_request_for(
2448                listing.clone(),
2449                GenericTrustAdmissionClass::Reviewable,
2450                GenericTrustActivationDisposition::Approved,
2451            ),
2452            130,
2453        )
2454        .expect("build activation");
2455        artifact.listing_sha256 = "different".to_string();
2456        let activation = SignedGenericTrustActivation::sign(artifact, &authority_keypair)
2457            .expect("sign activation");
2458
2459        let report = evaluate_generic_trust_activation(
2460            &evaluation_request(
2461                listing,
2462                Some(activation),
2463                GenericListingFreshnessState::Fresh,
2464                GenericRegistryPublisherRole::Origin,
2465                "origin-a",
2466                150,
2467            ),
2468            150,
2469        )
2470        .expect("evaluate mismatched activation");
2471
2472        assert_eq!(
2473            report.findings[0].code,
2474            GenericTrustActivationFindingCode::ListingMismatch
2475        );
2476    }
2477
2478    #[test]
2479    fn generic_trust_activation_rejects_divergent_listing_context() {
2480        let signing_keypair = Keypair::generate();
2481        let listing = signed_sample_listing(
2482            "https://registry.chio.example",
2483            &signing_keypair,
2484            "artifact-1",
2485            "deadbeef",
2486        );
2487        let activation = signed_activation(
2488            listing.clone(),
2489            GenericTrustAdmissionClass::Reviewable,
2490            GenericTrustActivationDisposition::Approved,
2491        );
2492
2493        let report = evaluate_generic_trust_activation(
2494            &evaluation_request(
2495                listing,
2496                Some(activation),
2497                GenericListingFreshnessState::Divergent,
2498                GenericRegistryPublisherRole::Origin,
2499                "origin-a",
2500                150,
2501            ),
2502            150,
2503        )
2504        .expect("evaluate divergent listing");
2505
2506        assert_eq!(
2507            report.findings[0].code,
2508            GenericTrustActivationFindingCode::ListingDivergent
2509        );
2510    }
2511
2512    #[test]
2513    fn generic_trust_activation_rejects_expired_pending_and_denied_activations() {
2514        let signing_keypair = Keypair::generate();
2515        let authority_keypair = Keypair::generate();
2516        let listing = signed_sample_listing(
2517            "https://registry.chio.example",
2518            &signing_keypair,
2519            "artifact-1",
2520            "deadbeef",
2521        );
2522
2523        let mut expired_artifact = build_generic_trust_activation_artifact(
2524            "https://operator.chio.example",
2525            Some("Chio Operator".to_string()),
2526            &issue_request_for(
2527                listing.clone(),
2528                GenericTrustAdmissionClass::Reviewable,
2529                GenericTrustActivationDisposition::Approved,
2530            ),
2531            130,
2532        )
2533        .expect("build activation");
2534        expired_artifact.expires_at = Some(140);
2535        let expired = SignedGenericTrustActivation::sign(expired_artifact, &authority_keypair)
2536            .expect("sign expired activation");
2537        let expired_report = evaluate_generic_trust_activation(
2538            &evaluation_request(
2539                listing.clone(),
2540                Some(expired),
2541                GenericListingFreshnessState::Fresh,
2542                GenericRegistryPublisherRole::Origin,
2543                "origin-a",
2544                150,
2545            ),
2546            150,
2547        )
2548        .expect("evaluate expired activation");
2549        assert_eq!(
2550            expired_report.findings[0].code,
2551            GenericTrustActivationFindingCode::ActivationExpired
2552        );
2553
2554        let pending = signed_activation(
2555            listing.clone(),
2556            GenericTrustAdmissionClass::Reviewable,
2557            GenericTrustActivationDisposition::PendingReview,
2558        );
2559        let pending_report = evaluate_generic_trust_activation(
2560            &evaluation_request(
2561                listing.clone(),
2562                Some(pending),
2563                GenericListingFreshnessState::Fresh,
2564                GenericRegistryPublisherRole::Origin,
2565                "origin-a",
2566                150,
2567            ),
2568            150,
2569        )
2570        .expect("evaluate pending activation");
2571        assert_eq!(
2572            pending_report.findings[0].code,
2573            GenericTrustActivationFindingCode::ActivationPendingReview
2574        );
2575
2576        let denied = signed_activation(
2577            listing,
2578            GenericTrustAdmissionClass::Reviewable,
2579            GenericTrustActivationDisposition::Denied,
2580        );
2581        let denied_report = evaluate_generic_trust_activation(
2582            &evaluation_request(
2583                signed_sample_listing(
2584                    "https://registry.chio.example",
2585                    &signing_keypair,
2586                    "artifact-1",
2587                    "deadbeef",
2588                ),
2589                Some(denied),
2590                GenericListingFreshnessState::Fresh,
2591                GenericRegistryPublisherRole::Origin,
2592                "origin-a",
2593                150,
2594            ),
2595            150,
2596        )
2597        .expect("evaluate denied activation");
2598        assert_eq!(
2599            denied_report.findings[0].code,
2600            GenericTrustActivationFindingCode::ActivationDenied
2601        );
2602    }
2603
2604    #[test]
2605    fn generic_trust_activation_rejects_ineligible_actor_publisher_status_and_operator() {
2606        let signing_keypair = Keypair::generate();
2607        let authority_keypair = Keypair::generate();
2608        let listing = signed_sample_listing(
2609            "https://registry.chio.example",
2610            &signing_keypair,
2611            "artifact-1",
2612            "deadbeef",
2613        );
2614
2615        let mut actor_artifact = build_generic_trust_activation_artifact(
2616            "https://operator.chio.example",
2617            Some("Chio Operator".to_string()),
2618            &issue_request_for(
2619                listing.clone(),
2620                GenericTrustAdmissionClass::Reviewable,
2621                GenericTrustActivationDisposition::Approved,
2622            ),
2623            130,
2624        )
2625        .expect("build activation");
2626        actor_artifact.eligibility.allowed_actor_kinds =
2627            vec![GenericListingActorKind::CredentialIssuer];
2628        let actor_activation =
2629            SignedGenericTrustActivation::sign(actor_artifact, &authority_keypair)
2630                .expect("sign actor-limited activation");
2631        let actor_report = evaluate_generic_trust_activation(
2632            &evaluation_request(
2633                listing.clone(),
2634                Some(actor_activation),
2635                GenericListingFreshnessState::Fresh,
2636                GenericRegistryPublisherRole::Origin,
2637                "origin-a",
2638                150,
2639            ),
2640            150,
2641        )
2642        .expect("evaluate actor ineligible");
2643        assert_eq!(
2644            actor_report.findings[0].code,
2645            GenericTrustActivationFindingCode::ActorKindIneligible
2646        );
2647
2648        let mut publisher_artifact = build_generic_trust_activation_artifact(
2649            "https://operator.chio.example",
2650            Some("Chio Operator".to_string()),
2651            &issue_request_for(
2652                listing.clone(),
2653                GenericTrustAdmissionClass::Reviewable,
2654                GenericTrustActivationDisposition::Approved,
2655            ),
2656            130,
2657        )
2658        .expect("build activation");
2659        publisher_artifact.eligibility.allowed_publisher_roles =
2660            vec![GenericRegistryPublisherRole::Mirror];
2661        let publisher_activation =
2662            SignedGenericTrustActivation::sign(publisher_artifact, &authority_keypair)
2663                .expect("sign publisher-limited activation");
2664        let publisher_report = evaluate_generic_trust_activation(
2665            &evaluation_request(
2666                listing.clone(),
2667                Some(publisher_activation),
2668                GenericListingFreshnessState::Fresh,
2669                GenericRegistryPublisherRole::Origin,
2670                "origin-a",
2671                150,
2672            ),
2673            150,
2674        )
2675        .expect("evaluate publisher ineligible");
2676        assert_eq!(
2677            publisher_report.findings[0].code,
2678            GenericTrustActivationFindingCode::PublisherRoleIneligible
2679        );
2680
2681        let status_listing = SignedGenericListing::sign(
2682            GenericListingArtifact {
2683                status: GenericListingStatus::Suspended,
2684                ..sample_listing(
2685                    "https://registry.chio.example",
2686                    &signing_keypair,
2687                    "artifact-1",
2688                    "deadbeef",
2689                )
2690            },
2691            &signing_keypair,
2692        )
2693        .expect("sign suspended listing");
2694        let status_activation = signed_activation(
2695            status_listing.clone(),
2696            GenericTrustAdmissionClass::Reviewable,
2697            GenericTrustActivationDisposition::Approved,
2698        );
2699        let status_report = evaluate_generic_trust_activation(
2700            &evaluation_request(
2701                status_listing,
2702                Some(status_activation),
2703                GenericListingFreshnessState::Fresh,
2704                GenericRegistryPublisherRole::Origin,
2705                "origin-a",
2706                150,
2707            ),
2708            150,
2709        )
2710        .expect("evaluate status ineligible");
2711        assert_eq!(
2712            status_report.findings[0].code,
2713            GenericTrustActivationFindingCode::ListingStatusIneligible
2714        );
2715
2716        let mut operator_artifact = build_generic_trust_activation_artifact(
2717            "https://operator.chio.example",
2718            Some("Chio Operator".to_string()),
2719            &issue_request_for(
2720                listing.clone(),
2721                GenericTrustAdmissionClass::Reviewable,
2722                GenericTrustActivationDisposition::Approved,
2723            ),
2724            130,
2725        )
2726        .expect("build activation");
2727        operator_artifact.eligibility.required_listing_operator_ids = vec!["mirror-a".to_string()];
2728        let operator_activation =
2729            SignedGenericTrustActivation::sign(operator_artifact, &authority_keypair)
2730                .expect("sign operator-limited activation");
2731        let operator_report = evaluate_generic_trust_activation(
2732            &evaluation_request(
2733                listing,
2734                Some(operator_activation),
2735                GenericListingFreshnessState::Fresh,
2736                GenericRegistryPublisherRole::Origin,
2737                "origin-a",
2738                150,
2739            ),
2740            150,
2741        )
2742        .expect("evaluate operator ineligible");
2743        assert_eq!(
2744            operator_report.findings[0].code,
2745            GenericTrustActivationFindingCode::ListingOperatorIneligible
2746        );
2747    }
2748
2749    #[test]
2750    fn generic_trust_activation_bond_backed_policy_remains_review_visible_only() {
2751        let signing_keypair = Keypair::generate();
2752        let authority_keypair = Keypair::generate();
2753        let listing = signed_sample_listing(
2754            "https://registry.chio.example",
2755            &signing_keypair,
2756            "artifact-1",
2757            "deadbeef",
2758        );
2759        let mut request = issue_request_for(
2760            listing.clone(),
2761            GenericTrustAdmissionClass::BondBacked,
2762            GenericTrustActivationDisposition::Approved,
2763        );
2764        request.eligibility.require_bond_backing = true;
2765        let artifact = build_generic_trust_activation_artifact(
2766            "https://operator.chio.example",
2767            Some("Chio Operator".to_string()),
2768            &request,
2769            130,
2770        )
2771        .expect("build activation");
2772        let activation = SignedGenericTrustActivation::sign(artifact, &authority_keypair)
2773            .expect("sign activation");
2774
2775        let report = evaluate_generic_trust_activation(
2776            &evaluation_request(
2777                listing,
2778                Some(activation),
2779                GenericListingFreshnessState::Fresh,
2780                GenericRegistryPublisherRole::Origin,
2781                "origin-a",
2782                150,
2783            ),
2784            150,
2785        )
2786        .expect("evaluate bond-backed activation");
2787
2788        assert_eq!(
2789            report.findings[0].code,
2790            GenericTrustActivationFindingCode::BondBackingRequired
2791        );
2792    }
2793}