acdp_server/registry/
lifecycle.rs1use acdp_primitives::error::AcdpError;
18use acdp_types::lifecycle::LifecycleEvent;
19
20const BODY_FIELD_NAMES: &[&str] = &[
27 "body",
28 "ctx_id",
30 "lineage_id",
31 "origin_registry",
32 "created_at",
33 "content_hash",
35 "signature",
36 "version",
38 "supersedes",
39 "agent_id",
40 "contributors",
41 "title",
42 "type",
43 "data_refs",
44 "derived_from",
45 "visibility",
46 "audience",
47 "acdp_version",
48 "description",
49 "summary",
50 "tags",
51 "domain",
52 "expires_at",
53 "data_period",
54 "metadata",
55 "schema_uri",
56 "anchors",
57 "key_attestation",
59];
60
61pub fn parse_lifecycle_request(
79 raw: &serde_json::Value,
80) -> Result<(LifecycleEvent, serde_json::Value), AcdpError> {
81 let map = raw.as_object().ok_or_else(|| {
82 AcdpError::SchemaViolation("lifecycle request body must be a JSON object".into())
83 })?;
84
85 for key in map.keys() {
86 if key == "event" {
87 continue;
88 }
89 if BODY_FIELD_NAMES.contains(&key.as_str()) {
90 return Err(AcdpError::ImmutableField(format!(
91 "lifecycle request member '{key}' attempts to supply or alter immutable \
92 body content — bodies are immutable and lifecycle endpoints mutate \
93 registry state only (RFC-ACDP-0013 §6 step 2)"
94 )));
95 }
96 return Err(AcdpError::SchemaViolation(format!(
97 "lifecycle request has unknown member '{key}' — the request body is a closed \
98 object with exactly one member, 'event' (RFC-ACDP-0013 §6)"
99 )));
100 }
101
102 let raw_event = map.get("event").ok_or_else(|| {
103 AcdpError::SchemaViolation(
104 "lifecycle request is missing the required 'event' member (RFC-ACDP-0013 §6)".into(),
105 )
106 })?;
107 let event = LifecycleEvent::from_value(raw_event)?;
108 Ok((event, raw_event.clone()))
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use serde_json::json;
115
116 fn valid_event() -> serde_json::Value {
117 json!({
118 "event_id": "018f6d0a-7b2e-4c4d-9e1f-3a5b7c9d1e2f",
119 "ctx_id": "acdp://registry.example.com/12345678-1234-4321-8123-123456781234",
120 "event_type": "retracted",
121 "occurred_at": "2026-07-04T09:15:42.000Z",
122 "actor": "did:web:agents.example.com:test-producer",
123 "reason": "underlying data source found to be fabricated",
124 "signature": {
125 "algorithm": "ed25519",
126 "key_id": "did:web:agents.example.com:test-producer#key-1",
127 "value": "AA=="
128 }
129 })
130 }
131
132 #[test]
133 fn valid_envelope_parses() {
134 let (event, raw) = parse_lifecycle_request(&json!({ "event": valid_event() })).unwrap();
135 assert_eq!(event.event_id, "018f6d0a-7b2e-4c4d-9e1f-3a5b7c9d1e2f");
136 assert_eq!(raw, valid_event());
137 }
138
139 #[test]
141 fn body_member_is_immutable_field() {
142 let err = parse_lifecycle_request(&json!({
143 "event": valid_event(),
144 "body": { "title": "Corrected title" }
145 }))
146 .unwrap_err();
147 assert!(matches!(err, AcdpError::ImmutableField(_)), "got {err:?}");
148 }
149
150 #[test]
153 fn body_field_named_member_is_immutable_field() {
154 let err = parse_lifecycle_request(&json!({
155 "event": valid_event(),
156 "summary": "please update the summary too"
157 }))
158 .unwrap_err();
159 assert!(matches!(err, AcdpError::ImmutableField(_)), "got {err:?}");
160 }
161
162 #[test]
167 fn anchors_field_named_member_is_immutable_field() {
168 let err = parse_lifecycle_request(&json!({
169 "event": valid_event(),
170 "anchors": [{"scheme": "macp.commitment", "content_hash": "sha256:aa"}]
171 }))
172 .unwrap_err();
173 assert!(matches!(err, AcdpError::ImmutableField(_)), "got {err:?}");
174 }
175
176 #[test]
179 fn non_body_unknown_member_is_schema_violation() {
180 let err = parse_lifecycle_request(&json!({
181 "event": valid_event(),
182 "note": "hello"
183 }))
184 .unwrap_err();
185 assert!(matches!(err, AcdpError::SchemaViolation(_)), "got {err:?}");
186 }
187
188 #[test]
189 fn missing_event_member_rejected() {
190 let err = parse_lifecycle_request(&json!({})).unwrap_err();
191 assert!(matches!(err, AcdpError::SchemaViolation(_)));
192 let err = parse_lifecycle_request(&json!([1, 2])).unwrap_err();
193 assert!(matches!(err, AcdpError::SchemaViolation(_)));
194 }
195}