Skip to main content

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}