acdp_types/profile.rs
1//! ACDP conformance profiles (RFC-ACDP-0001 §9.1).
2//!
3//! Implementations declare their profile(s) in the capabilities document
4//! `profiles` field. Each profile is a strict superset of its prerequisite.
5//!
6//! This crate's *consumer-side* claim is [`Profile::Consumer`]: it
7//! verifies producer signatures end-to-end, resolves cross-registry
8//! references, applies visibility rules client-side, and tolerates
9//! unknown fields. The validation and SSRF building blocks
10//! (`acdp::registry::PublishValidator`, `acdp::safe_http::SsrfPolicy`)
11//! are designed for consumption by `acdp-registry-core` /
12//! `acdp-registry-federated` registry implementations built on top.
13
14use crate::capabilities::CapabilitiesDocument;
15
16/// A conformance profile: the RFC-ACDP-0001 §9.1 set plus the receipt
17/// profiles added by RFC-ACDP-0010 (0.2.0) and RFC-ACDP-0011 (0.3.0).
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum Profile {
20 /// `acdp-registry-core` — minimum profile for any registry.
21 RegistryCore,
22 /// `acdp-registry-discovery` — adds keyword search.
23 RegistryDiscovery,
24 /// `acdp-registry-federated` — adds cross-registry resolution.
25 RegistryFederated,
26 /// `acdp-registry-receipts` — mints registry-signed publication
27 /// receipts (ACDP 0.2, RFC-ACDP-0010).
28 RegistryReceipts,
29 /// `acdp-registry-head-receipts` — mints registry-signed
30 /// lineage-head receipts on `/current` (ACDP 0.3, RFC-ACDP-0011).
31 /// Prerequisite: `acdp-registry-receipts` (same signing key, same
32 /// DID-document plumbing, same key lifecycle).
33 RegistryHeadReceipts,
34 /// `acdp-registry-lifecycle` — lifecycle events & retraction (ACDP
35 /// 0.3, RFC-ACDP-0013): the `/retract` + `/republish` endpoints,
36 /// §7 status derivation (`retracted` dominating), §8
37 /// retrieval/search/`/current` semantics, and the §4.1 append-only
38 /// `lifecycle_events` discipline. Prerequisite:
39 /// `acdp-registry-core`. Registries NOT advertising it MUST return
40 /// `not_implemented` on both endpoints and never emit
41 /// `lifecycle_events` or the `retracted` status.
42 RegistryLifecycle,
43 /// `acdp-consumer` — a consumer of contexts (not a registry).
44 Consumer,
45}
46
47impl Profile {
48 /// Wire-form identifier as it appears in
49 /// `capabilities.profiles` and prose references.
50 pub fn as_str(self) -> &'static str {
51 match self {
52 Profile::RegistryCore => "acdp-registry-core",
53 Profile::RegistryDiscovery => "acdp-registry-discovery",
54 Profile::RegistryFederated => "acdp-registry-federated",
55 Profile::RegistryReceipts => "acdp-registry-receipts",
56 Profile::RegistryHeadReceipts => "acdp-registry-head-receipts",
57 Profile::RegistryLifecycle => "acdp-registry-lifecycle",
58 Profile::Consumer => "acdp-consumer",
59 }
60 }
61}
62
63/// Profiles that this `acdp` crate is designed to satisfy on the
64/// consumer side. A registry implementer building on top of the
65/// crate's primitives (`PublishValidator`, `SsrfPolicy`,
66/// `CrossRegistryResolver`) MAY claim additional profiles in their
67/// own capabilities document.
68pub const CLAIMED: &[Profile] = &[Profile::Consumer];
69
70impl CapabilitiesDocument {
71 /// Returns `true` if the registry advertises the given profile.
72 pub fn claims_profile(&self, profile: Profile) -> bool {
73 self.profiles.iter().any(|p| p == profile.as_str())
74 }
75
76 /// Returns `Ok(())` if the registry advertises every profile in
77 /// `required`. Returns the first missing profile in
78 /// [`acdp_primitives::error::AcdpError::SchemaViolation`] otherwise.
79 pub fn supports_required(
80 &self,
81 required: &[Profile],
82 ) -> Result<(), acdp_primitives::error::AcdpError> {
83 for p in required {
84 if !self.claims_profile(*p) {
85 return Err(acdp_primitives::error::AcdpError::SchemaViolation(format!(
86 "registry does not advertise required profile '{}'",
87 p.as_str()
88 )));
89 }
90 }
91 Ok(())
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::capabilities::Limits;
99
100 fn caps_with(profiles: Vec<&str>) -> CapabilitiesDocument {
101 CapabilitiesDocument {
102 acdp_version: "0.1.0".into(),
103 registry_did: "did:web:r.example.com".into(),
104 supported_signature_algorithms: vec!["ed25519".into()],
105 supported_did_methods: vec!["did:web".into()],
106 profiles: profiles.into_iter().map(String::from).collect(),
107 limits: Limits {
108 max_payload_bytes: 1_048_576,
109 max_embedded_bytes: 65_536,
110 idempotency_key_ttl_seconds: None,
111 max_publish_per_minute: None,
112 },
113 read_authentication_methods: vec![],
114 anonymous_public_reads: true,
115 supports_idempotency_key: false,
116 extensions: Default::default(),
117 }
118 }
119
120 #[test]
121 fn claimed_profile_matches() {
122 let caps = caps_with(vec!["acdp-registry-core", "acdp-registry-discovery"]);
123 assert!(caps.claims_profile(Profile::RegistryCore));
124 assert!(caps.claims_profile(Profile::RegistryDiscovery));
125 assert!(!caps.claims_profile(Profile::RegistryFederated));
126 }
127
128 #[test]
129 fn supports_required_returns_first_missing() {
130 let caps = caps_with(vec!["acdp-registry-core"]);
131 caps.supports_required(&[Profile::RegistryCore]).unwrap();
132 let err = caps
133 .supports_required(&[Profile::RegistryCore, Profile::RegistryFederated])
134 .unwrap_err();
135 match err {
136 acdp_primitives::error::AcdpError::SchemaViolation(msg) => {
137 assert!(msg.contains("acdp-registry-federated"));
138 }
139 other => panic!("got {other:?}"),
140 }
141 }
142
143 #[test]
144 fn claimed_includes_consumer() {
145 assert!(CLAIMED.contains(&Profile::Consumer));
146 }
147}