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/// lifecycle (RFC-ACDP-0013) and retraction recall (RFC-ACDP-0018).
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[non_exhaustive]
21pub enum Profile {
22 /// `acdp-registry-core` — minimum profile for any registry.
23 RegistryCore,
24 /// `acdp-registry-discovery` — adds keyword search.
25 RegistryDiscovery,
26 /// `acdp-registry-federated` — adds cross-registry resolution.
27 RegistryFederated,
28 /// `acdp-registry-receipts` — mints registry-signed publication
29 /// receipts (ACDP 0.2, RFC-ACDP-0010).
30 RegistryReceipts,
31 /// `acdp-registry-head-receipts` — mints registry-signed
32 /// lineage-head receipts on `/current` (ACDP 0.3, RFC-ACDP-0011).
33 /// Prerequisite: `acdp-registry-receipts` (same signing key, same
34 /// DID-document plumbing, same key lifecycle).
35 RegistryHeadReceipts,
36 /// `acdp-registry-lifecycle` — lifecycle events & retraction (ACDP
37 /// 0.3, RFC-ACDP-0013): the `/retract` + `/republish` endpoints,
38 /// §7 status derivation (`retracted` dominating), §8
39 /// retrieval/search/`/current` semantics, and the §4.1 append-only
40 /// `lifecycle_events` discipline. Prerequisite:
41 /// `acdp-registry-core`. Registries NOT advertising it MUST return
42 /// `not_implemented` on both endpoints and never emit
43 /// `lifecycle_events` or the `retracted` status.
44 RegistryLifecycle,
45 /// `acdp-registry-changes` — the `GET /changes` feed of
46 /// reliance-changing changes (0.5.0 Draft, RFC-ACDP-0018 §3–§6).
47 /// Prerequisites: `acdp-registry-lifecycle` and
48 /// `acdp-registry-receipts`.
49 RegistryChanges,
50 /// `acdp-registry-events` — change-hint webhook subscriptions (0.5.0
51 /// Draft, RFC-ACDP-0018 §8). Prerequisite: `acdp-registry-changes`.
52 RegistryEvents,
53 /// `acdp-consumer` — a consumer of contexts (not a registry).
54 Consumer,
55 /// `acdp-consumer-recall` — a consumer that verifies change entries
56 /// before acting on them (0.5.0 Draft, RFC-ACDP-0018 §7).
57 /// Prerequisite: `acdp-consumer`.
58 ConsumerRecall,
59}
60
61impl Profile {
62 /// Wire-form identifier as it appears in
63 /// `capabilities.profiles` and prose references.
64 pub fn as_str(self) -> &'static str {
65 match self {
66 Profile::RegistryCore => "acdp-registry-core",
67 Profile::RegistryDiscovery => "acdp-registry-discovery",
68 Profile::RegistryFederated => "acdp-registry-federated",
69 Profile::RegistryReceipts => "acdp-registry-receipts",
70 Profile::RegistryHeadReceipts => "acdp-registry-head-receipts",
71 Profile::RegistryLifecycle => "acdp-registry-lifecycle",
72 Profile::RegistryChanges => "acdp-registry-changes",
73 Profile::RegistryEvents => "acdp-registry-events",
74 Profile::Consumer => "acdp-consumer",
75 Profile::ConsumerRecall => "acdp-consumer-recall",
76 }
77 }
78}
79
80/// Profiles that this `acdp` crate is designed to satisfy on the
81/// consumer side. A registry implementer building on top of the
82/// crate's primitives (`PublishValidator`, `SsrfPolicy`,
83/// `CrossRegistryResolver`) MAY claim additional profiles in their
84/// own capabilities document.
85pub const CLAIMED: &[Profile] = &[Profile::Consumer, Profile::ConsumerRecall];
86
87impl CapabilitiesDocument {
88 /// Returns `true` if the registry advertises the given profile.
89 pub fn claims_profile(&self, profile: Profile) -> bool {
90 self.profiles.iter().any(|p| p == profile.as_str())
91 }
92
93 /// Returns `Ok(())` if the registry advertises every profile in
94 /// `required`. Returns the first missing profile in
95 /// [`acdp_primitives::error::AcdpError::SchemaViolation`] otherwise.
96 pub fn supports_required(
97 &self,
98 required: &[Profile],
99 ) -> Result<(), acdp_primitives::error::AcdpError> {
100 for p in required {
101 if !self.claims_profile(*p) {
102 return Err(acdp_primitives::error::AcdpError::SchemaViolation(format!(
103 "registry does not advertise required profile '{}'",
104 p.as_str()
105 )));
106 }
107 }
108 Ok(())
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::capabilities::Limits;
116
117 fn caps_with(profiles: Vec<&str>) -> CapabilitiesDocument {
118 CapabilitiesDocument {
119 acdp_version: "0.1.0".into(),
120 registry_did: "did:web:r.example.com".into(),
121 supported_signature_algorithms: vec!["ed25519".into()],
122 supported_did_methods: vec!["did:web".into()],
123 profiles: profiles.into_iter().map(String::from).collect(),
124 limits: Limits {
125 max_payload_bytes: 1_048_576,
126 max_embedded_bytes: 65_536,
127 idempotency_key_ttl_seconds: None,
128 max_publish_per_minute: None,
129 change_retention_seconds: None,
130 max_change_filters: None,
131 max_changes_page: None,
132 max_subscriptions_per_requester: None,
133 },
134 read_authentication_methods: vec![],
135 anonymous_public_reads: true,
136 supports_idempotency_key: false,
137 extensions: Default::default(),
138 }
139 }
140
141 #[test]
142 fn claimed_profile_matches() {
143 let caps = caps_with(vec!["acdp-registry-core", "acdp-registry-discovery"]);
144 assert!(caps.claims_profile(Profile::RegistryCore));
145 assert!(caps.claims_profile(Profile::RegistryDiscovery));
146 assert!(!caps.claims_profile(Profile::RegistryFederated));
147 }
148
149 #[test]
150 fn supports_required_returns_first_missing() {
151 let caps = caps_with(vec!["acdp-registry-core"]);
152 caps.supports_required(&[Profile::RegistryCore]).unwrap();
153 let err = caps
154 .supports_required(&[Profile::RegistryCore, Profile::RegistryFederated])
155 .unwrap_err();
156 match err {
157 acdp_primitives::error::AcdpError::SchemaViolation(msg) => {
158 assert!(msg.contains("acdp-registry-federated"));
159 }
160 other => panic!("got {other:?}"),
161 }
162 }
163
164 #[test]
165 fn claimed_includes_consumer() {
166 assert!(CLAIMED.contains(&Profile::Consumer));
167 assert!(CLAIMED.contains(&Profile::ConsumerRecall));
168 }
169}