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#[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-consumer` — a consumer of contexts (not a registry).
35    Consumer,
36}
37
38impl Profile {
39    /// Wire-form identifier as it appears in
40    /// `capabilities.profiles` and prose references.
41    pub fn as_str(self) -> &'static str {
42        match self {
43            Profile::RegistryCore => "acdp-registry-core",
44            Profile::RegistryDiscovery => "acdp-registry-discovery",
45            Profile::RegistryFederated => "acdp-registry-federated",
46            Profile::RegistryReceipts => "acdp-registry-receipts",
47            Profile::RegistryHeadReceipts => "acdp-registry-head-receipts",
48            Profile::Consumer => "acdp-consumer",
49        }
50    }
51}
52
53/// Profiles that this `acdp` crate is designed to satisfy on the
54/// consumer side. A registry implementer building on top of the
55/// crate's primitives (`PublishValidator`, `SsrfPolicy`,
56/// `CrossRegistryResolver`) MAY claim additional profiles in their
57/// own capabilities document.
58pub const CLAIMED: &[Profile] = &[Profile::Consumer];
59
60impl CapabilitiesDocument {
61    /// Returns `true` if the registry advertises the given profile.
62    pub fn claims_profile(&self, profile: Profile) -> bool {
63        self.profiles.iter().any(|p| p == profile.as_str())
64    }
65
66    /// Returns `Ok(())` if the registry advertises every profile in
67    /// `required`. Returns the first missing profile in
68    /// [`acdp_primitives::error::AcdpError::SchemaViolation`] otherwise.
69    pub fn supports_required(
70        &self,
71        required: &[Profile],
72    ) -> Result<(), acdp_primitives::error::AcdpError> {
73        for p in required {
74            if !self.claims_profile(*p) {
75                return Err(acdp_primitives::error::AcdpError::SchemaViolation(format!(
76                    "registry does not advertise required profile '{}'",
77                    p.as_str()
78                )));
79            }
80        }
81        Ok(())
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::capabilities::Limits;
89
90    fn caps_with(profiles: Vec<&str>) -> CapabilitiesDocument {
91        CapabilitiesDocument {
92            acdp_version: "0.1.0".into(),
93            registry_did: "did:web:r.example.com".into(),
94            supported_signature_algorithms: vec!["ed25519".into()],
95            supported_did_methods: vec!["did:web".into()],
96            profiles: profiles.into_iter().map(String::from).collect(),
97            limits: Limits {
98                max_payload_bytes: 1_048_576,
99                max_embedded_bytes: 65_536,
100                idempotency_key_ttl_seconds: None,
101                max_publish_per_minute: None,
102            },
103            read_authentication_methods: vec![],
104            anonymous_public_reads: true,
105            supports_idempotency_key: false,
106            extensions: Default::default(),
107        }
108    }
109
110    #[test]
111    fn claimed_profile_matches() {
112        let caps = caps_with(vec!["acdp-registry-core", "acdp-registry-discovery"]);
113        assert!(caps.claims_profile(Profile::RegistryCore));
114        assert!(caps.claims_profile(Profile::RegistryDiscovery));
115        assert!(!caps.claims_profile(Profile::RegistryFederated));
116    }
117
118    #[test]
119    fn supports_required_returns_first_missing() {
120        let caps = caps_with(vec!["acdp-registry-core"]);
121        caps.supports_required(&[Profile::RegistryCore]).unwrap();
122        let err = caps
123            .supports_required(&[Profile::RegistryCore, Profile::RegistryFederated])
124            .unwrap_err();
125        match err {
126            acdp_primitives::error::AcdpError::SchemaViolation(msg) => {
127                assert!(msg.contains("acdp-registry-federated"));
128            }
129            other => panic!("got {other:?}"),
130        }
131    }
132
133    #[test]
134    fn claimed_includes_consumer() {
135        assert!(CLAIMED.contains(&Profile::Consumer));
136    }
137}