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}