1#[derive(
21 std::fmt::Debug,
22 std::clone::Clone,
23 std::cmp::PartialEq,
24 std::cmp::Eq,
25 serde::Serialize,
26 serde::Deserialize,
27)]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct NotarizationEnvelope {
30 pub aph_version: String,
32 #[serde(rename = "@context")]
34 pub context: Vec<String>,
35 #[serde(rename = "type")]
38 pub r#type: Vec<String>,
39 pub id: String,
41 pub issuer: String,
43 pub valid_from: String,
45 pub valid_until: String,
47 pub credential_subject: CredentialSubject,
49 #[serde(default)]
51 pub linked_mandate: std::option::Option<LinkedMandate>,
52 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
68 pub credential_status:
69 std::option::Option<crate::credential_status::CredentialStatusEntry>,
70 pub proof: EnvelopeProofs,
73}
74
75#[derive(
80 std::fmt::Debug,
81 std::clone::Clone,
82 std::cmp::PartialEq,
83 std::cmp::Eq,
84 serde::Serialize,
85 serde::Deserialize,
86)]
87#[serde(untagged)]
88pub enum EnvelopeProofs {
89 Single(EnvelopeProof),
91 Chain(std::vec::Vec<EnvelopeProof>),
93}
94
95impl EnvelopeProofs {
96 pub fn all(&self) -> &[EnvelopeProof] {
98 match self {
99 Self::Single(proof) => std::slice::from_ref(proof),
102 Self::Chain(proofs) => proofs.as_slice(),
103 }
104 }
105
106 pub fn principal(&self) -> std::option::Option<&EnvelopeProof> {
114 match self {
115 Self::Single(_) => std::option::Option::None,
116 Self::Chain(proofs) => {
117 if proofs.len() == 2 {
118 proofs.first()
119 } else {
120 std::option::Option::None
121 }
122 }
123 }
124 }
125
126 pub fn notary(&self) -> std::option::Option<&EnvelopeProof> {
128 match self {
129 Self::Single(proof) => std::option::Option::Some(proof),
130 Self::Chain(proofs) => {
131 if proofs.len() == 2 {
132 proofs.get(1)
133 } else {
134 std::option::Option::None
135 }
136 }
137 }
138 }
139
140 pub fn is_chain(&self) -> bool {
142 std::matches!(self, Self::Chain(_))
143 }
144}
145
146#[derive(
151 std::fmt::Debug,
152 std::clone::Clone,
153 std::marker::Copy,
154 std::cmp::PartialEq,
155 std::cmp::Eq,
156 serde::Serialize,
157 serde::Deserialize,
158)]
159pub enum AttestationMode {
160 PrincipalSigned,
162 NotaryAttested,
164}
165
166impl AttestationMode {
167 pub fn label(&self) -> &'static str {
173 match self {
174 Self::PrincipalSigned => "PrincipalSigned",
175 Self::NotaryAttested => "NotaryAttested",
176 }
177 }
178}
179
180impl std::fmt::Display for AttestationMode {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 std::write!(f, "{}", self.label())
183 }
184}
185
186impl std::str::FromStr for AttestationMode {
187 type Err = std::string::String;
188
189 fn from_str(label: &str) -> std::result::Result<Self, Self::Err> {
202 match label {
203 "PrincipalSigned" => std::result::Result::Ok(Self::PrincipalSigned),
204 "NotaryAttested" => std::result::Result::Ok(Self::NotaryAttested),
205 other => std::result::Result::Err(std::format!(
206 "unknown attestation mode `{}`: expected `PrincipalSigned` or `NotaryAttested`",
207 other
208 )),
209 }
210 }
211}
212
213#[derive(std::fmt::Debug, std::clone::Clone, std::marker::Copy, std::cmp::PartialEq, std::cmp::Eq)]
229pub enum ChannelKind {
230 Slack,
232 Email,
234 Discord,
236 Teams,
238 Whatsapp,
240 GoogleChat,
243 Imessage,
245 Service,
250 Squillo,
256}
257
258impl ChannelKind {
259 pub const ALL: [Self; 9] = [
262 Self::Slack,
263 Self::Email,
264 Self::Discord,
265 Self::Teams,
266 Self::Whatsapp,
267 Self::GoogleChat,
268 Self::Imessage,
269 Self::Service,
270 Self::Squillo,
271 ];
272
273 pub fn label(&self) -> &'static str {
276 match self {
277 Self::Slack => "slack",
278 Self::Email => "email",
279 Self::Discord => "discord",
280 Self::Teams => "teams",
281 Self::Whatsapp => "whatsapp",
282 Self::GoogleChat => "google_chat",
283 Self::Imessage => "imessage",
284 Self::Service => "service",
285 Self::Squillo => "squillo",
286 }
287 }
288
289 fn labels_for_error() -> std::string::String {
296 Self::ALL
297 .iter()
298 .map(Self::label)
299 .collect::<std::vec::Vec<&'static str>>()
300 .join(", ")
301 }
302}
303
304impl std::fmt::Display for ChannelKind {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 std::write!(f, "{}", self.label())
307 }
308}
309
310impl std::str::FromStr for ChannelKind {
311 type Err = std::string::String;
312
313 fn from_str(label: &str) -> std::result::Result<Self, Self::Err> {
321 match label {
322 "slack" => std::result::Result::Ok(Self::Slack),
323 "email" => std::result::Result::Ok(Self::Email),
324 "discord" => std::result::Result::Ok(Self::Discord),
325 "teams" => std::result::Result::Ok(Self::Teams),
326 "whatsapp" => std::result::Result::Ok(Self::Whatsapp),
327 "google_chat" => std::result::Result::Ok(Self::GoogleChat),
328 "imessage" => std::result::Result::Ok(Self::Imessage),
329 "service" => std::result::Result::Ok(Self::Service),
330 "squillo" => std::result::Result::Ok(Self::Squillo),
331 other => std::result::Result::Err(std::format!(
332 "`{}` is not in the closed set {{{}}}",
333 other,
334 Self::labels_for_error()
335 )),
336 }
337 }
338}
339
340impl serde::Serialize for ChannelKind {
348 fn serialize<S: serde::Serializer>(
349 &self,
350 serializer: S,
351 ) -> std::result::Result<S::Ok, S::Error> {
352 serializer.serialize_str(self.label())
353 }
354}
355
356impl<'de> serde::Deserialize<'de> for ChannelKind {
357 fn deserialize<D: serde::Deserializer<'de>>(
358 deserializer: D,
359 ) -> std::result::Result<Self, D::Error> {
360 let raw = <std::string::String as serde::Deserialize>::deserialize(deserializer)?;
361 <Self as std::str::FromStr>::from_str(&raw).map_err(serde::de::Error::custom)
362 }
363}
364
365#[derive(std::fmt::Debug, std::clone::Clone, std::marker::Copy, std::cmp::PartialEq, std::cmp::Eq)]
371pub enum ContentClass {
372 Reply,
374 New,
376 Mention,
378 Dm,
380 Channel,
382 BulkSend,
384 Broadcast,
386 Mutation,
391}
392
393impl ContentClass {
394 pub const ALL: [Self; 8] = [
396 Self::Reply,
397 Self::New,
398 Self::Mention,
399 Self::Dm,
400 Self::Channel,
401 Self::BulkSend,
402 Self::Broadcast,
403 Self::Mutation,
404 ];
405
406 pub fn label(&self) -> &'static str {
408 match self {
409 Self::Reply => "Reply",
410 Self::New => "New",
411 Self::Mention => "Mention",
412 Self::Dm => "DM",
413 Self::Channel => "Channel",
414 Self::BulkSend => "BulkSend",
415 Self::Broadcast => "Broadcast",
416 Self::Mutation => "Mutation",
417 }
418 }
419
420 fn labels_for_error() -> std::string::String {
427 Self::ALL
428 .iter()
429 .map(Self::label)
430 .collect::<std::vec::Vec<&'static str>>()
431 .join(", ")
432 }
433}
434
435impl std::fmt::Display for ContentClass {
436 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437 std::write!(f, "{}", self.label())
438 }
439}
440
441impl std::str::FromStr for ContentClass {
442 type Err = std::string::String;
443
444 fn from_str(label: &str) -> std::result::Result<Self, Self::Err> {
447 match label {
448 "Reply" => std::result::Result::Ok(Self::Reply),
449 "New" => std::result::Result::Ok(Self::New),
450 "Mention" => std::result::Result::Ok(Self::Mention),
451 "DM" => std::result::Result::Ok(Self::Dm),
452 "Channel" => std::result::Result::Ok(Self::Channel),
453 "BulkSend" => std::result::Result::Ok(Self::BulkSend),
454 "Broadcast" => std::result::Result::Ok(Self::Broadcast),
455 "Mutation" => std::result::Result::Ok(Self::Mutation),
456 other => std::result::Result::Err(std::format!(
457 "`{}` is not in the closed set {{{}}}",
458 other,
459 Self::labels_for_error()
460 )),
461 }
462 }
463}
464
465impl serde::Serialize for ContentClass {
473 fn serialize<S: serde::Serializer>(
474 &self,
475 serializer: S,
476 ) -> std::result::Result<S::Ok, S::Error> {
477 serializer.serialize_str(self.label())
478 }
479}
480
481impl<'de> serde::Deserialize<'de> for ContentClass {
482 fn deserialize<D: serde::Deserializer<'de>>(
483 deserializer: D,
484 ) -> std::result::Result<Self, D::Error> {
485 let raw = <std::string::String as serde::Deserialize>::deserialize(deserializer)?;
486 <Self as std::str::FromStr>::from_str(&raw).map_err(serde::de::Error::custom)
487 }
488}
489
490#[derive(
493 std::fmt::Debug,
494 std::clone::Clone,
495 std::cmp::PartialEq,
496 std::cmp::Eq,
497 serde::Serialize,
498 serde::Deserialize,
499)]
500#[serde(rename_all = "camelCase", deny_unknown_fields)]
501pub struct CredentialSubject {
502 pub human_principal: HumanPrincipalRef,
504 pub agent: AgentRef,
506 pub channel: ChannelDescriptor,
508 pub communication: CommunicationDescriptor,
510 pub policy: PolicyDescriptor,
512 pub notarization: NotarizationMetadata,
514 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
520 pub apple_aur_acceptance: std::option::Option<AppleAurAcceptanceClaim>,
521 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
533 pub audience: std::option::Option<Audience>,
534 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
540 pub sealed_payload: std::option::Option<SealedPayload>,
541 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
558 pub act_classification: std::option::Option<ActClassification>,
559}
560
561#[derive(
563 std::fmt::Debug,
564 std::clone::Clone,
565 std::cmp::PartialEq,
566 std::cmp::Eq,
567 serde::Serialize,
568 serde::Deserialize,
569)]
570#[serde(rename_all = "camelCase", deny_unknown_fields)]
571pub struct HumanPrincipalRef {
572 pub id: String,
574 pub display_name: String,
576}
577
578#[derive(
580 std::fmt::Debug,
581 std::clone::Clone,
582 std::cmp::PartialEq,
583 std::cmp::Eq,
584 serde::Serialize,
585 serde::Deserialize,
586)]
587#[serde(rename_all = "camelCase", deny_unknown_fields)]
588pub struct AgentRef {
589 pub id: String,
591 #[serde(default)]
593 pub agent_card_uri: std::option::Option<String>,
594 pub display_name: String,
596 pub version: String,
598}
599
600#[derive(
602 std::fmt::Debug,
603 std::clone::Clone,
604 std::cmp::PartialEq,
605 std::cmp::Eq,
606 serde::Serialize,
607 serde::Deserialize,
608)]
609#[serde(rename_all = "camelCase", deny_unknown_fields)]
610pub struct ChannelDescriptor {
611 pub kind: ChannelKind,
617 pub recipient_addressing: serde_json::Value,
619 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
628 pub recipient_class: std::option::Option<RecipientClass>,
629}
630
631#[derive(
634 std::fmt::Debug,
635 std::clone::Clone,
636 std::cmp::PartialEq,
637 std::cmp::Eq,
638 serde::Serialize,
639 serde::Deserialize,
640)]
641#[serde(rename_all = "camelCase", deny_unknown_fields)]
642pub struct CommunicationDescriptor {
643 pub content_class: ContentClass,
648 pub body_sha256: String,
650 pub body_size: u64,
652 pub preview_lines: u32,
654 pub preview: String,
656}
657
658#[derive(
660 std::fmt::Debug,
661 std::clone::Clone,
662 std::cmp::PartialEq,
663 std::cmp::Eq,
664 serde::Serialize,
665 serde::Deserialize,
666)]
667#[serde(rename_all = "camelCase", deny_unknown_fields)]
668pub struct PolicyDescriptor {
669 pub decision: PolicyDecision,
671 pub matched_scope: String,
673 #[serde(default)]
675 pub delegation_mandate_id: std::option::Option<String>,
676 #[serde(default)]
679 pub act_chain: Vec<String>,
680 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
682 pub attestation_mode: std::option::Option<AttestationMode>,
683 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
686 pub delegation_mandate: std::option::Option<crate::delegation_mandate::DelegationMandate>,
687}
688
689impl PolicyDescriptor {
690 pub fn effective_attestation_mode(&self) -> AttestationMode {
697 match self.attestation_mode {
698 std::option::Option::Some(mode) => mode,
699 std::option::Option::None => AttestationMode::NotaryAttested,
700 }
701 }
702}
703
704#[derive(
706 std::fmt::Debug,
707 std::clone::Clone,
708 std::cmp::PartialEq,
709 std::cmp::Eq,
710 serde::Serialize,
711 serde::Deserialize,
712)]
713#[serde(rename_all = "camelCase", deny_unknown_fields)]
714pub struct NotarizationMetadata {
715 pub notary_service: NotaryServiceRef,
717 pub decision_timestamp: String,
719 pub decision_latency_ms: u64,
722}
723
724#[derive(
726 std::fmt::Debug,
727 std::clone::Clone,
728 std::cmp::PartialEq,
729 std::cmp::Eq,
730 serde::Serialize,
731 serde::Deserialize,
732)]
733#[serde(rename_all = "camelCase", deny_unknown_fields)]
734pub struct NotaryServiceRef {
735 pub id: String,
737 pub name: String,
739 pub version: String,
741 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
747 pub attested_digest: std::option::Option<String>,
748 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
752 pub attestation_uri: std::option::Option<String>,
753}
754
755#[derive(
757 std::fmt::Debug,
758 std::clone::Clone,
759 std::cmp::PartialEq,
760 std::cmp::Eq,
761 serde::Serialize,
762 serde::Deserialize,
763)]
764#[serde(rename_all = "camelCase", deny_unknown_fields)]
765pub struct LinkedMandate {
766 #[serde(default)]
768 pub ap2_intent_mandate_uri: std::option::Option<String>,
769 #[serde(default)]
774 pub ap2_signed_payload_b64: std::option::Option<String>,
775 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
780 pub vault_mutation: std::option::Option<
781 crate::vault_mutation::VaultMutationMandate,
782 >,
783}
784
785#[derive(
786 std::fmt::Debug,
787 std::clone::Clone,
788 std::cmp::PartialEq,
789 std::cmp::Eq,
790 serde::Serialize,
791 serde::Deserialize,
792)]
793#[serde(rename_all = "camelCase", deny_unknown_fields)]
796pub struct EnvelopeProof {
797 #[serde(rename = "type")]
800 pub r#type: String,
801 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
811 pub cryptosuite: std::option::Option<String>,
812 pub verification_method: String,
815 pub created: String,
817 pub proof_purpose: String,
820 pub proof_value: String,
822 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
825 pub id: std::option::Option<String>,
826 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
829 pub previous_proof: std::option::Option<String>,
830}
831
832#[derive(
839 std::fmt::Debug,
840 std::clone::Clone,
841 std::cmp::PartialEq,
842 std::cmp::Eq,
843 serde::Serialize,
844 serde::Deserialize,
845)]
846#[serde(rename_all = "camelCase", deny_unknown_fields)]
847pub struct AppleAurAcceptanceClaim {
848 pub user_id: String,
850 pub device_id: String,
853 pub aur_version_hash: String,
855 pub accepted_at: String,
857 pub document_kind: String,
859}
860
861#[derive(std::fmt::Debug, std::clone::Clone, std::marker::Copy, std::cmp::PartialEq, std::cmp::Eq)]
877pub enum PolicyDecision {
878 AlwaysAllow,
880 AskEveryTime,
884 NeverAllow,
886}
887
888impl PolicyDecision {
889 pub const ALL: [Self; 3] = [Self::AlwaysAllow, Self::AskEveryTime, Self::NeverAllow];
891
892 pub fn label(&self) -> &'static str {
894 match self {
895 Self::AlwaysAllow => "AlwaysAllow",
896 Self::AskEveryTime => "AskEveryTime",
897 Self::NeverAllow => "NeverAllow",
898 }
899 }
900
901 fn labels_for_error() -> std::string::String {
904 Self::ALL
905 .iter()
906 .map(Self::label)
907 .collect::<std::vec::Vec<&'static str>>()
908 .join(", ")
909 }
910}
911
912impl std::fmt::Display for PolicyDecision {
913 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
914 std::write!(f, "{}", self.label())
915 }
916}
917
918impl std::str::FromStr for PolicyDecision {
919 type Err = std::string::String;
920
921 fn from_str(label: &str) -> std::result::Result<Self, Self::Err> {
926 match label {
927 "AlwaysAllow" => std::result::Result::Ok(Self::AlwaysAllow),
928 "AskEveryTime" => std::result::Result::Ok(Self::AskEveryTime),
929 "NeverAllow" => std::result::Result::Ok(Self::NeverAllow),
930 other => std::result::Result::Err(std::format!(
931 "`{}` is not in the closed set {{{}}}",
932 other,
933 Self::labels_for_error()
934 )),
935 }
936 }
937}
938
939impl serde::Serialize for PolicyDecision {
940 fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
941 serializer.serialize_str(self.label())
942 }
943}
944
945impl<'de> serde::Deserialize<'de> for PolicyDecision {
946 fn deserialize<D: serde::Deserializer<'de>>(
947 deserializer: D,
948 ) -> std::result::Result<Self, D::Error> {
949 let raw = <std::string::String as serde::Deserialize>::deserialize(deserializer)?;
950 <Self as std::str::FromStr>::from_str(&raw).map_err(serde::de::Error::custom)
951 }
952}
953
954#[derive(std::fmt::Debug, std::clone::Clone, std::marker::Copy, std::cmp::PartialEq, std::cmp::Eq)]
969pub enum RecipientClass {
970 Human,
972 Agent,
974}
975
976impl RecipientClass {
977 pub const ALL: [Self; 2] = [Self::Human, Self::Agent];
979
980 pub fn label(&self) -> &'static str {
982 match self {
983 Self::Human => "human",
984 Self::Agent => "agent",
985 }
986 }
987
988 fn labels_for_error() -> std::string::String {
991 Self::ALL
992 .iter()
993 .map(Self::label)
994 .collect::<std::vec::Vec<&'static str>>()
995 .join(", ")
996 }
997}
998
999impl std::fmt::Display for RecipientClass {
1000 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1001 std::write!(f, "{}", self.label())
1002 }
1003}
1004
1005impl std::str::FromStr for RecipientClass {
1006 type Err = std::string::String;
1007
1008 fn from_str(label: &str) -> std::result::Result<Self, Self::Err> {
1013 match label {
1014 "human" => std::result::Result::Ok(Self::Human),
1015 "agent" => std::result::Result::Ok(Self::Agent),
1016 other => std::result::Result::Err(std::format!(
1017 "`{}` is not in the closed set {{{}}}",
1018 other,
1019 Self::labels_for_error()
1020 )),
1021 }
1022 }
1023}
1024
1025impl serde::Serialize for RecipientClass {
1026 fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
1027 serializer.serialize_str(self.label())
1028 }
1029}
1030
1031impl<'de> serde::Deserialize<'de> for RecipientClass {
1032 fn deserialize<D: serde::Deserializer<'de>>(
1033 deserializer: D,
1034 ) -> std::result::Result<Self, D::Error> {
1035 let raw = <std::string::String as serde::Deserialize>::deserialize(deserializer)?;
1036 <Self as std::str::FromStr>::from_str(&raw).map_err(serde::de::Error::custom)
1037 }
1038}
1039
1040#[derive(
1054 std::fmt::Debug,
1055 std::clone::Clone,
1056 std::cmp::PartialEq,
1057 std::cmp::Eq,
1058 serde::Serialize,
1059 serde::Deserialize,
1060)]
1061#[serde(rename_all = "camelCase", deny_unknown_fields)]
1062pub struct ActClassification {
1063 #[serde(deserialize_with = "non_empty_vocabularies")]
1076 pub vocabularies: std::vec::Vec<VocabularyRef>,
1077 pub labels: std::vec::Vec<ActLabel>,
1084}
1085
1086#[derive(
1095 std::fmt::Debug,
1096 std::clone::Clone,
1097 std::cmp::PartialEq,
1098 std::cmp::Eq,
1099 serde::Serialize,
1100 serde::Deserialize,
1101)]
1102#[serde(rename_all = "camelCase", deny_unknown_fields)]
1103pub struct Audience {
1104 pub id: String,
1106 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
1111 pub channel_binding: std::option::Option<AudienceChannelBinding>,
1112}
1113
1114#[derive(
1127 std::fmt::Debug,
1128 std::clone::Clone,
1129 std::cmp::PartialEq,
1130 std::cmp::Eq,
1131 serde::Serialize,
1132 serde::Deserialize,
1133)]
1134#[serde(rename_all = "camelCase")]
1135pub struct AudienceChannelBinding {
1136 pub kind: ChannelKind,
1138 #[serde(flatten)]
1144 pub coordinates: std::collections::BTreeMap<String, serde_json::Value>,
1145}
1146
1147#[derive(
1152 std::fmt::Debug,
1153 std::clone::Clone,
1154 std::cmp::PartialEq,
1155 std::cmp::Eq,
1156 serde::Serialize,
1157 serde::Deserialize,
1158)]
1159#[serde(rename_all = "camelCase", deny_unknown_fields)]
1160pub struct SealedReader {
1161 pub id: String,
1164 pub kid: String,
1166}
1167
1168#[derive(
1178 std::fmt::Debug,
1179 std::clone::Clone,
1180 std::cmp::PartialEq,
1181 std::cmp::Eq,
1182 serde::Serialize,
1183 serde::Deserialize,
1184)]
1185#[serde(rename_all = "camelCase", deny_unknown_fields)]
1186pub struct SealedPayload {
1187 pub suite: String,
1191 pub reader: SealedReader,
1193 pub enc: String,
1195 pub ciphertext: String,
1197}
1198
1199pub fn sealed_payload_is_declared(
1207 envelope: &NotarizationEnvelope,
1208) -> std::result::Result<(), String> {
1209 if envelope.credential_subject.sealed_payload.is_some() && envelope.aph_version != "0.2" {
1210 return std::result::Result::Err(std::format!(
1211 "`sealedPayload` is not declared by aphVersion `{}`: the member exists from aphVersion 0.2 (spec/aph-0.2.md), and an earlier version carrying it is malformed for the version it claims",
1212 envelope.aph_version
1213 ));
1214 }
1215 std::result::Result::Ok(())
1216}
1217
1218pub fn parse_envelope_json(
1226 json: &str,
1227) -> std::result::Result<NotarizationEnvelope, String> {
1228 let envelope: NotarizationEnvelope =
1229 serde_json::from_str(json).map_err(|e| std::format!("{}", e))?;
1230 sealed_payload_is_declared(&envelope)?;
1231 std::result::Result::Ok(envelope)
1232}
1233
1234fn non_empty_vocabularies<'de, D: serde::Deserializer<'de>>(
1239 deserializer: D,
1240) -> std::result::Result<std::vec::Vec<VocabularyRef>, D::Error> {
1241 let refs = <std::vec::Vec<VocabularyRef> as serde::Deserialize>::deserialize(deserializer)?;
1242 if refs.is_empty() {
1243 return std::result::Result::Err(serde::de::Error::custom(
1244 "`vocabularies` is empty; a classification must name every vocabulary \
1245 the verdict depended on, and one that names none claims nothing",
1246 ));
1247 }
1248 std::result::Result::Ok(refs)
1249}
1250
1251#[derive(std::fmt::Debug, std::clone::Clone, std::cmp::PartialEq, std::cmp::Eq)]
1269pub struct ActLabel {
1270 family: String,
1271 label: String,
1272}
1273
1274impl ActLabel {
1275 pub fn family(&self) -> &str {
1277 &self.family
1278 }
1279
1280 pub fn label(&self) -> &str {
1282 &self.label
1283 }
1284
1285 pub fn qualified(&self) -> std::string::String {
1287 std::format!("{}/{}", self.family, self.label)
1288 }
1289}
1290
1291impl std::fmt::Display for ActLabel {
1292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1293 std::write!(f, "{}", self.qualified())
1294 }
1295}
1296
1297impl std::str::FromStr for ActLabel {
1298 type Err = std::string::String;
1299
1300 fn from_str(raw: &str) -> std::result::Result<Self, Self::Err> {
1306 let (family, label) = match raw.split_once('/') {
1307 std::option::Option::Some(parts) => parts,
1308 std::option::Option::None => {
1309 return std::result::Result::Err(std::format!(
1310 "`{}` is not family-qualified: a label is written `FAMILY/LABEL`",
1311 raw
1312 ));
1313 }
1314 };
1315 if family.is_empty() || label.is_empty() {
1316 return std::result::Result::Err(std::format!(
1317 "`{}` has an empty family or label; both sides of the `/` are required",
1318 raw
1319 ));
1320 }
1321 if label.contains('/') {
1322 return std::result::Result::Err(std::format!(
1323 "`{}` has more than one `/`; a label names exactly one family",
1324 raw
1325 ));
1326 }
1327 std::result::Result::Ok(Self {
1328 family: family.to_string(),
1329 label: label.to_string(),
1330 })
1331 }
1332}
1333
1334impl serde::Serialize for ActLabel {
1335 fn serialize<S: serde::Serializer>(
1336 &self,
1337 serializer: S,
1338 ) -> std::result::Result<S::Ok, S::Error> {
1339 serializer.serialize_str(&self.qualified())
1340 }
1341}
1342
1343impl<'de> serde::Deserialize<'de> for ActLabel {
1344 fn deserialize<D: serde::Deserializer<'de>>(
1345 deserializer: D,
1346 ) -> std::result::Result<Self, D::Error> {
1347 let raw = <std::string::String as serde::Deserialize>::deserialize(deserializer)?;
1348 <Self as std::str::FromStr>::from_str(&raw).map_err(serde::de::Error::custom)
1349 }
1350}
1351
1352#[derive(
1360 std::fmt::Debug,
1361 std::clone::Clone,
1362 std::cmp::PartialEq,
1363 std::cmp::Eq,
1364 serde::Serialize,
1365 serde::Deserialize,
1366)]
1367#[serde(rename_all = "camelCase", deny_unknown_fields)]
1368pub struct VocabularyRef {
1369 pub name: String,
1371 pub version: String,
1373 pub digest: String,
1384}
1385
1386#[cfg(test)]
1387mod tests {
1388 fn sample_human_principal() -> super::HumanPrincipalRef {
1391 super::HumanPrincipalRef {
1392 id: "did:key:z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy".to_string(),
1393 display_name: "Scott Wyatt".to_string(),
1394 }
1395 }
1396
1397 fn sample_agent() -> super::AgentRef {
1398 super::AgentRef {
1399 id: "did:web:agent.squillo.com".to_string(),
1400 agent_card_uri: std::option::Option::Some(
1401 "https://agent.squillo.com/.well-known/agent-card.json".to_string(),
1402 ),
1403 display_name: "Squillo Concierge".to_string(),
1404 version: "1.0".to_string(),
1405 }
1406 }
1407
1408 fn sample_channel() -> super::ChannelDescriptor {
1409 super::ChannelDescriptor {
1410 kind: super::ChannelKind::Slack,
1411 recipient_addressing: serde_json::json!({
1412 "teamId": "T01234567",
1413 "channelId": "C01234567",
1414 "parentTs": "1716249600.000100"
1415 }),
1416 recipient_class: std::option::Option::None,
1417 }
1418 }
1419
1420 fn sample_communication() -> super::CommunicationDescriptor {
1421 super::CommunicationDescriptor {
1422 content_class: super::ContentClass::Reply,
1423 body_sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(),
1424 body_size: 1842,
1425 preview_lines: 3,
1426 preview: "hello world".to_string(),
1427 }
1428 }
1429
1430 fn sample_policy() -> super::PolicyDescriptor {
1431 super::PolicyDescriptor {
1432 decision: super::PolicyDecision::AskEveryTime,
1433 matched_scope: "per-channel".to_string(),
1434 delegation_mandate_id: std::option::Option::None,
1435 act_chain: std::vec::Vec::new(),
1436 attestation_mode: std::option::Option::None,
1437 delegation_mandate: std::option::Option::None,
1438 }
1439 }
1440
1441 #[test]
1442 fn attestation_fields_are_omitted_when_absent_and_accepted_when_present() {
1443 let bare = sample_notary_service();
1451 let json = serde_json::to_string(&bare).unwrap();
1452 std::assert!(
1453 !json.contains("attestedDigest") && !json.contains("attestationUri"),
1454 "absent attestation fields must not appear on the wire: {}",
1455 json
1456 );
1457
1458 let with_claim: super::NotaryServiceRef = serde_json::from_str(
1459 r#"{"id":"did:web:notary.squillo.com","name":"Squillo Notary Service","version":"0.1.0","attestedDigest":"sha256:abc","attestationUri":"https://notary.squillo.com/.well-known/aph-attestation.json"}"#,
1460 )
1461 .expect("a notary advertising an attestation must parse");
1462 std::assert_eq!(
1463 with_claim.attested_digest.as_deref(),
1464 std::option::Option::Some("sha256:abc")
1465 );
1466 }
1467
1468 fn sample_notary_service() -> super::NotaryServiceRef {
1469 super::NotaryServiceRef {
1470 id: "did:web:notary.squillo.com".to_string(),
1471 name: "Squillo Notary Service".to_string(),
1472 version: "0.1.0".to_string(),
1473 attested_digest: std::option::Option::None,
1474 attestation_uri: std::option::Option::None,
1475 }
1476 }
1477
1478 fn sample_notarization_metadata() -> super::NotarizationMetadata {
1479 super::NotarizationMetadata {
1480 notary_service: sample_notary_service(),
1481 decision_timestamp: "2026-05-21T00:00:01Z".to_string(),
1482 decision_latency_ms: 1834,
1483 }
1484 }
1485
1486 fn sample_credential_subject() -> super::CredentialSubject {
1487 super::CredentialSubject {
1488 human_principal: sample_human_principal(),
1489 agent: sample_agent(),
1490 channel: sample_channel(),
1491 communication: sample_communication(),
1492 policy: sample_policy(),
1493 notarization: sample_notarization_metadata(),
1494 apple_aur_acceptance: std::option::Option::None,
1495 audience: std::option::Option::None,
1496 sealed_payload: std::option::Option::None,
1497 act_classification: std::option::Option::None,
1498 }
1499 }
1500
1501 fn sample_linked_mandate() -> super::LinkedMandate {
1502 super::LinkedMandate {
1503 ap2_intent_mandate_uri: std::option::Option::Some(
1504 "urn:uuid:11111111-1111-4111-8111-111111111111".to_string(),
1505 ),
1506 ap2_signed_payload_b64: std::option::Option::None,
1507 vault_mutation: std::option::Option::None,
1508 }
1509 }
1510
1511 fn sample_proof() -> super::EnvelopeProof {
1512 super::EnvelopeProof {
1513 r#type: "DataIntegrityProof".to_string(),
1514 cryptosuite: std::option::Option::Some("eddsa-jcs-2022".to_string()),
1515 verification_method:
1516 "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV#z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"
1517 .to_string(),
1518 created: "2026-05-21T00:00:01Z".to_string(),
1519 proof_purpose: "assertionMethod".to_string(),
1520 proof_value:
1521 "z3WgvA9JHkbV3qLZHcM4FxBp4xHfQVnVnPKKDdyazQwQGdGzxsRdmZWBxXwQvN6P2sLZbLP4HnRy9LcZdpFLLM6h"
1522 .to_string(),
1523 id: std::option::Option::None,
1524 previous_proof: std::option::Option::None,
1525 }
1526 }
1527
1528 fn sample_envelope() -> super::NotarizationEnvelope {
1529 super::NotarizationEnvelope {
1530 aph_version: "0.1".to_string(),
1531 context: std::vec![
1532 "https://www.w3.org/ns/credentials/v2".to_string(),
1533 "https://w3id.org/aph/v1".to_string(),
1534 ],
1535 r#type: std::vec![
1536 "VerifiableCredential".to_string(),
1537 "AgentSendAuthorizationCredential".to_string(),
1538 ],
1539 id: "urn:uuid:00000000-0000-4000-8000-000000000001".to_string(),
1540 issuer: "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV".to_string(),
1541 valid_from: "2026-05-21T00:00:00Z".to_string(),
1542 valid_until: "2026-05-22T00:00:00Z".to_string(),
1543 credential_subject: sample_credential_subject(),
1544 linked_mandate: std::option::Option::None,
1545 credential_status: std::option::Option::None,
1549 proof: super::EnvelopeProofs::Single(sample_proof()),
1550 }
1551 }
1552
1553 fn sample_principal_proof() -> super::EnvelopeProof {
1554 super::EnvelopeProof {
1555 r#type: "DataIntegrityProof".to_string(),
1556 cryptosuite: std::option::Option::Some("eddsa-jcs-2022".to_string()),
1557 verification_method:
1560 "did:key:z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy#z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy"
1561 .to_string(),
1562 created: "2026-05-21T00:00:02Z".to_string(),
1563 proof_purpose: "assertionMethod".to_string(),
1564 proof_value: "z-illustrative-principal-proof-value".to_string(),
1565 id: std::option::Option::Some("urn:uuid:00000000-0000-4000-8000-0000000000a1".to_string()),
1566 previous_proof: std::option::Option::None,
1567 }
1568 }
1569
1570 fn sample_notary_countersignature() -> super::EnvelopeProof {
1571 super::EnvelopeProof {
1572 r#type: "DataIntegrityProof".to_string(),
1573 cryptosuite: std::option::Option::Some("eddsa-jcs-2022".to_string()),
1574 verification_method:
1575 "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV#z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"
1576 .to_string(),
1577 created: "2026-05-21T00:00:03Z".to_string(),
1578 proof_purpose: "authentication".to_string(),
1579 proof_value: "z-illustrative-notary-countersignature".to_string(),
1580 id: std::option::Option::Some("urn:uuid:00000000-0000-4000-8000-0000000000b1".to_string()),
1581 previous_proof: std::option::Option::Some(
1582 "urn:uuid:00000000-0000-4000-8000-0000000000a1".to_string(),
1583 ),
1584 }
1585 }
1586
1587 #[test]
1590 fn round_trip_human_principal_ref() {
1591 let v = sample_human_principal();
1596 let s = serde_json::to_string(&v).unwrap();
1597 let v2: super::HumanPrincipalRef = serde_json::from_str(&s).unwrap();
1598 std::assert_eq!(v, v2);
1599 }
1600
1601 #[test]
1602 fn round_trip_agent_ref() {
1603 let v = sample_agent();
1607 let s = serde_json::to_string(&v).unwrap();
1608 let v2: super::AgentRef = serde_json::from_str(&s).unwrap();
1609 std::assert_eq!(v, v2);
1610 }
1611
1612 #[test]
1613 fn round_trip_channel_descriptor() {
1614 let v = sample_channel();
1618 let s = serde_json::to_string(&v).unwrap();
1619 let v2: super::ChannelDescriptor = serde_json::from_str(&s).unwrap();
1620 std::assert_eq!(v, v2);
1621 }
1622
1623 #[test]
1624 fn round_trip_communication_descriptor() {
1625 let v = sample_communication();
1629 let s = serde_json::to_string(&v).unwrap();
1630 let v2: super::CommunicationDescriptor = serde_json::from_str(&s).unwrap();
1631 std::assert_eq!(v, v2);
1632 }
1633
1634 #[test]
1635 fn round_trip_policy_descriptor() {
1636 let v = sample_policy();
1640 let s = serde_json::to_string(&v).unwrap();
1641 let v2: super::PolicyDescriptor = serde_json::from_str(&s).unwrap();
1642 std::assert_eq!(v, v2);
1643 }
1644
1645 #[test]
1646 fn round_trip_notary_service_ref() {
1647 let v = sample_notary_service();
1650 let s = serde_json::to_string(&v).unwrap();
1651 let v2: super::NotaryServiceRef = serde_json::from_str(&s).unwrap();
1652 std::assert_eq!(v, v2);
1653 }
1654
1655 #[test]
1656 fn round_trip_notarization_metadata() {
1657 let v = sample_notarization_metadata();
1660 let s = serde_json::to_string(&v).unwrap();
1661 let v2: super::NotarizationMetadata = serde_json::from_str(&s).unwrap();
1662 std::assert_eq!(v, v2);
1663 }
1664
1665 #[test]
1666 fn round_trip_credential_subject() {
1667 let v = sample_credential_subject();
1671 let s = serde_json::to_string(&v).unwrap();
1672 let v2: super::CredentialSubject = serde_json::from_str(&s).unwrap();
1673 std::assert_eq!(v, v2);
1674 }
1675
1676 #[test]
1677 fn round_trip_linked_mandate() {
1678 let v = sample_linked_mandate();
1682 let s = serde_json::to_string(&v).unwrap();
1683 let v2: super::LinkedMandate = serde_json::from_str(&s).unwrap();
1684 std::assert_eq!(v, v2);
1685 }
1686
1687 #[test]
1688 fn round_trip_envelope_proof() {
1689 let v = sample_proof();
1692 let s = serde_json::to_string(&v).unwrap();
1693 let v2: super::EnvelopeProof = serde_json::from_str(&s).unwrap();
1694 std::assert_eq!(v, v2);
1695 }
1696
1697 #[test]
1698 fn round_trip_notarization_envelope() {
1699 let v = sample_envelope();
1703 let s = serde_json::to_string(&v).unwrap();
1704 let v2: super::NotarizationEnvelope = serde_json::from_str(&s).unwrap();
1705 std::assert_eq!(v, v2);
1706 }
1707
1708 #[test]
1711 fn envelope_serializes_type_as_type_and_context_as_at_context() {
1712 let v = sample_envelope();
1717 let s = serde_json::to_string(&v).unwrap();
1718 std::assert!(
1719 s.contains("\"type\":"),
1720 "envelope must serialize r#type as \"type\": {}",
1721 s
1722 );
1723 std::assert!(
1724 s.contains("\"@context\":"),
1725 "envelope must serialize context as \"@context\": {}",
1726 s
1727 );
1728 std::assert!(
1729 !s.contains("\"rType\""),
1730 "envelope must NOT serialize as \"rType\": {}",
1731 s
1732 );
1733 std::assert!(
1734 !s.contains("\"r#type\""),
1735 "envelope must NOT serialize as \"r#type\": {}",
1736 s
1737 );
1738 }
1739
1740 #[test]
1741 fn proof_serializes_type_as_type() {
1742 let v = sample_proof();
1746 let s = serde_json::to_string(&v).unwrap();
1747 std::assert!(
1748 s.contains("\"type\":"),
1749 "proof must serialize r#type as \"type\": {}",
1750 s
1751 );
1752 std::assert!(
1753 !s.contains("\"rType\""),
1754 "proof must NOT serialize as \"rType\": {}",
1755 s
1756 );
1757 }
1758
1759 #[test]
1762 fn envelope_rejects_unknown_field() {
1763 let s = serde_json::json!({
1767 "aphVersion": "0.1",
1768 "@context": [
1769 "https://www.w3.org/ns/credentials/v2",
1770 "https://w3id.org/aph/v1"
1771 ],
1772 "type": ["VerifiableCredential", "AgentSendAuthorizationCredential"],
1773 "id": "urn:uuid:00000000-0000-4000-8000-000000000001",
1774 "issuer": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV",
1775 "validFrom": "2026-05-21T00:00:00Z",
1776 "validUntil": "2026-05-22T00:00:00Z",
1777 "credentialSubject": {
1778 "humanPrincipal": {
1779 "id": "did:key:abc",
1780 "displayName": "X"
1781 },
1782 "agent": {
1783 "id": "did:web:agent.squillo.com",
1784 "displayName": "X",
1785 "version": "1.0"
1786 },
1787 "channel": {
1788 "kind": "slack",
1789 "recipientAddressing": {}
1790 },
1791 "communication": {
1792 "contentClass": "Reply",
1793 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1794 "bodySize": 0,
1795 "previewLines": 0,
1796 "preview": ""
1797 },
1798 "policy": {
1799 "decision": "AskEveryTime",
1800 "matchedScope": "per-channel"
1801 },
1802 "notarization": {
1803 "notaryService": {
1804 "id": "did:web:notary.squillo.com",
1805 "name": "Squillo Notary Service",
1806 "version": "0.1.0"
1807 },
1808 "decisionTimestamp": "2026-05-21T00:00:01Z",
1809 "decisionLatencyMs": 0
1810 }
1811 },
1812 "proof": {
1813 "type": "DataIntegrityProof",
1814 "verificationMethod": "did:key:abc#abc",
1815 "created": "2026-05-21T00:00:01Z",
1816 "proofPurpose": "assertionMethod",
1817 "proofValue": "z..."
1818 },
1819 "extraKey": "x"
1820 })
1821 .to_string();
1822 let r: std::result::Result<super::NotarizationEnvelope, _> = serde_json::from_str(&s);
1823 std::assert!(
1824 r.is_err(),
1825 "deny_unknown_fields must reject extraKey: {:?}",
1826 r
1827 );
1828 }
1829
1830 #[test]
1831 fn human_principal_ref_rejects_unknown_field() {
1832 let s = serde_json::json!({
1836 "id": "did:key:abc",
1837 "displayName": "X",
1838 "rogue": true
1839 })
1840 .to_string();
1841 let r: std::result::Result<super::HumanPrincipalRef, _> = serde_json::from_str(&s);
1842 std::assert!(
1843 r.is_err(),
1844 "deny_unknown_fields must reject rogue field: {:?}",
1845 r
1846 );
1847 }
1848
1849 #[test]
1852 fn envelope_deserializes_without_linked_mandate() {
1853 let s = serde_json::json!({
1857 "aphVersion": "0.1",
1858 "@context": [
1859 "https://www.w3.org/ns/credentials/v2",
1860 "https://w3id.org/aph/v1"
1861 ],
1862 "type": ["VerifiableCredential", "AgentSendAuthorizationCredential"],
1863 "id": "urn:uuid:00000000-0000-4000-8000-000000000001",
1864 "issuer": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV",
1865 "validFrom": "2026-05-21T00:00:00Z",
1866 "validUntil": "2026-05-22T00:00:00Z",
1867 "credentialSubject": {
1868 "humanPrincipal": {
1869 "id": "did:key:abc",
1870 "displayName": "X"
1871 },
1872 "agent": {
1873 "id": "did:web:agent.squillo.com",
1874 "displayName": "X",
1875 "version": "1.0"
1876 },
1877 "channel": {
1878 "kind": "slack",
1879 "recipientAddressing": {}
1880 },
1881 "communication": {
1882 "contentClass": "Reply",
1883 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1884 "bodySize": 0,
1885 "previewLines": 0,
1886 "preview": ""
1887 },
1888 "policy": {
1889 "decision": "AskEveryTime",
1890 "matchedScope": "per-channel"
1891 },
1892 "notarization": {
1893 "notaryService": {
1894 "id": "did:web:notary.squillo.com",
1895 "name": "Squillo Notary Service",
1896 "version": "0.1.0"
1897 },
1898 "decisionTimestamp": "2026-05-21T00:00:01Z",
1899 "decisionLatencyMs": 0
1900 }
1901 },
1902 "proof": {
1903 "type": "DataIntegrityProof",
1904 "verificationMethod": "did:key:abc#abc",
1905 "created": "2026-05-21T00:00:01Z",
1906 "proofPurpose": "assertionMethod",
1907 "proofValue": "z..."
1908 }
1909 })
1910 .to_string();
1911 let v: super::NotarizationEnvelope =
1912 serde_json::from_str(&s).expect("must deserialize with linkedMandate omitted");
1913 std::assert!(v.linked_mandate.is_none());
1914 }
1915
1916 #[test]
1917 fn envelope_round_trips_a_credential_status_under_the_camel_case_wire_key() {
1918 let s = serde_json::json!({
1933 "aphVersion": "0.1",
1934 "@context": [
1935 "https://www.w3.org/ns/credentials/v2",
1936 "https://w3id.org/aph/v1"
1937 ],
1938 "type": ["VerifiableCredential", "AgentSendAuthorizationCredential"],
1939 "id": "urn:uuid:00000000-0000-4000-8000-000000000001",
1940 "issuer": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV",
1941 "validFrom": "2026-05-21T00:00:00Z",
1942 "validUntil": "2026-05-22T00:00:00Z",
1943 "credentialSubject": {
1944 "humanPrincipal": {
1945 "id": "did:key:abc",
1946 "displayName": "X"
1947 },
1948 "agent": {
1949 "id": "did:web:agent.squillo.com",
1950 "displayName": "X",
1951 "version": "1.0"
1952 },
1953 "channel": {
1954 "kind": "slack",
1955 "recipientAddressing": {}
1956 },
1957 "communication": {
1958 "contentClass": "Reply",
1959 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1960 "bodySize": 0,
1961 "previewLines": 0,
1962 "preview": ""
1963 },
1964 "policy": {
1965 "decision": "AskEveryTime",
1966 "matchedScope": "per-channel",
1967 "delegationMandateId": "urn:uuid:00000000-0000-4000-8000-0000000000d1"
1971 },
1972 "notarization": {
1973 "notaryService": {
1974 "id": "did:web:aph-notary.squillo.com",
1975 "name": "Squillo Notary Service",
1976 "version": "0.1.0"
1977 },
1978 "decisionTimestamp": "2026-05-21T00:00:01Z",
1979 "decisionLatencyMs": 0
1980 }
1981 },
1982 "credentialStatus": {
1983 "type": "BitstringStatusListEntry",
1984 "statusPurpose": "revocation",
1985 "statusListIndex": "94567",
1986 "statusListCredential": "https://aph-notary.squillo.com/.well-known/aph-status.json"
1987 },
1988 "proof": {
1989 "type": "DataIntegrityProof",
1990 "verificationMethod": "did:key:abc#abc",
1991 "created": "2026-05-21T00:00:01Z",
1992 "proofPurpose": "assertionMethod",
1993 "proofValue": "z..."
1994 }
1995 })
1996 .to_string();
1997 let v: super::NotarizationEnvelope = serde_json::from_str(&s)
1998 .expect("deny_unknown_fields must ACCEPT the credentialStatus key");
1999 let entry = v
2000 .credential_status
2001 .as_ref()
2002 .expect("the wire key must land in the typed field");
2003 std::assert_eq!(
2004 entry.r#type,
2005 crate::credential_status::StatusEntryType::BitstringStatusListEntry
2006 );
2007 std::assert_eq!(
2008 entry.status_purpose,
2009 crate::credential_status::StatusPurpose::Revocation
2010 );
2011 std::assert_eq!(entry.index().expect("the index is readable"), 94567u64);
2012
2013 let back = serde_json::to_string(&v).expect("envelope serializes");
2017 let reparsed: serde_json::Value = serde_json::from_str(&back).expect("round-trip is JSON");
2018 let emitted = reparsed
2019 .get("credentialStatus")
2020 .expect("the member must be re-emitted as `credentialStatus`");
2021 std::assert_eq!(
2022 emitted,
2023 &serde_json::json!({
2024 "type": "BitstringStatusListEntry",
2025 "statusPurpose": "revocation",
2026 "statusListIndex": "94567",
2027 "statusListCredential": "https://aph-notary.squillo.com/.well-known/aph-status.json"
2028 }),
2029 "the absent optional `id` must not appear, as a null or otherwise"
2030 );
2031 let base = crate::crypto::proof_base::signing_base(
2038 &v,
2039 crate::crypto::proof_base::ProofRole::Notary,
2040 )
2041 .expect("a single notary proof builds a notary base");
2042 std::assert!(
2043 base.contains("\"credentialStatus\""),
2044 "the status reference must be covered by the notary signature"
2045 );
2046 }
2047
2048 #[test]
2049 fn agent_ref_deserializes_without_agent_card_uri() {
2050 let s = serde_json::json!({
2053 "id": "did:web:agent.squillo.com",
2054 "displayName": "X",
2055 "version": "1.0"
2056 })
2057 .to_string();
2058 let v: super::AgentRef =
2059 serde_json::from_str(&s).expect("must deserialize with agentCardUri omitted");
2060 std::assert!(v.agent_card_uri.is_none());
2061 }
2062
2063 #[test]
2064 fn policy_descriptor_deserializes_without_optionals() {
2065 let s = serde_json::json!({
2069 "decision": "AskEveryTime",
2070 "matchedScope": "per-channel"
2071 })
2072 .to_string();
2073 let v: super::PolicyDescriptor =
2074 serde_json::from_str(&s).expect("must deserialize with optionals omitted");
2075 std::assert!(v.delegation_mandate_id.is_none());
2076 std::assert!(v.act_chain.is_empty());
2077 std::assert!(v.attestation_mode.is_none());
2078 std::assert!(v.delegation_mandate.is_none());
2079 }
2080
2081 #[test]
2082 fn linked_mandate_deserializes_without_ap2_uri() {
2083 let s = serde_json::json!({}).to_string();
2086 let v: super::LinkedMandate =
2087 serde_json::from_str(&s).expect("must deserialize with ap2IntentMandateUri omitted");
2088 std::assert!(v.ap2_intent_mandate_uri.is_none());
2089 }
2090
2091 #[test]
2092 fn envelope_proof_deserializes_without_cryptosuite() {
2093 let s = serde_json::json!({
2097 "type": "JsonWebSignature2020",
2098 "verificationMethod": "did:key:abc#abc",
2099 "created": "2026-05-21T00:00:01Z",
2100 "proofPurpose": "assertionMethod",
2101 "proofValue": "z..."
2102 })
2103 .to_string();
2104 let v: super::EnvelopeProof =
2105 serde_json::from_str(&s).expect("must deserialize with cryptosuite omitted");
2106 std::assert!(v.cryptosuite.is_none());
2107 }
2108
2109 #[test]
2112 fn human_principal_ref_serializes_camel_case() {
2113 let v = super::HumanPrincipalRef {
2117 id: "did:key:abc".to_string(),
2118 display_name: "Scott".to_string(),
2119 };
2120 let s = serde_json::to_string(&v).unwrap();
2121 std::assert!(
2122 s.contains("\"displayName\""),
2123 "must serialize display_name as displayName: {}",
2124 s
2125 );
2126 std::assert!(
2127 !s.contains("\"display_name\""),
2128 "must NOT serialize as display_name: {}",
2129 s
2130 );
2131 }
2132
2133 #[test]
2136 fn apple_aur_acceptance_claim_round_trip() {
2137 let claim = super::AppleAurAcceptanceClaim {
2141 user_id: "did:key:z6MkUserAbc123".to_string(),
2142 device_id: "device-opaque-id-001".to_string(),
2143 aur_version_hash:
2144 "a3b4c5d6e7f8091011121314151617181920212223242526272829303132333435".to_string(),
2145 accepted_at: "2026-06-09T00:00:00Z".to_string(),
2146 document_kind: "foundation_models_framework_aur".to_string(),
2147 };
2148 let subject = super::CredentialSubject {
2149 human_principal: sample_human_principal(),
2150 agent: sample_agent(),
2151 channel: sample_channel(),
2152 communication: sample_communication(),
2153 policy: sample_policy(),
2154 notarization: sample_notarization_metadata(),
2155 apple_aur_acceptance: std::option::Option::Some(claim.clone()),
2156 audience: std::option::Option::None,
2157 sealed_payload: std::option::Option::None,
2158 act_classification: std::option::Option::None,
2159 };
2160 let s = serde_json::to_string(&subject).unwrap();
2161 std::assert!(
2163 s.contains("\"appleAurAcceptance\""),
2164 "must serialize apple_aur_acceptance as appleAurAcceptance: {}",
2165 s
2166 );
2167 let v2: super::CredentialSubject = serde_json::from_str(&s).unwrap();
2168 std::assert_eq!(subject, v2);
2169 let recovered = v2.apple_aur_acceptance.expect("must be Some after round-trip");
2170 std::assert_eq!(recovered.document_kind, "foundation_models_framework_aur");
2171 std::assert_eq!(recovered.user_id, claim.user_id);
2172 std::assert_eq!(recovered.device_id, claim.device_id);
2173 std::assert_eq!(recovered.aur_version_hash, claim.aur_version_hash);
2174 std::assert_eq!(recovered.accepted_at, claim.accepted_at);
2175 }
2176
2177 #[test]
2180 fn credential_subject_legacy_omit_apple_aur_acceptance() {
2181 let s = serde_json::json!({
2185 "humanPrincipal": {
2186 "id": "did:key:z6MkLegacyUser",
2187 "displayName": "Legacy User"
2188 },
2189 "agent": {
2190 "id": "did:web:agent.squillo.com",
2191 "displayName": "Squillo Concierge",
2192 "version": "1.0"
2193 },
2194 "channel": {
2195 "kind": "slack",
2196 "recipientAddressing": {}
2197 },
2198 "communication": {
2199 "contentClass": "Reply",
2200 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
2201 "bodySize": 0,
2202 "previewLines": 0,
2203 "preview": ""
2204 },
2205 "policy": {
2206 "decision": "AskEveryTime",
2207 "matchedScope": "per-channel"
2208 },
2209 "notarization": {
2210 "notaryService": {
2211 "id": "did:web:notary.squillo.com",
2212 "name": "Squillo Notary Service",
2213 "version": "0.1.0"
2214 },
2215 "decisionTimestamp": "2026-05-21T00:00:01Z",
2216 "decisionLatencyMs": 0
2217 }
2218 })
2219 .to_string();
2220 let v: super::CredentialSubject = serde_json::from_str(&s)
2221 .expect("legacy payload without appleAurAcceptance must deserialize cleanly");
2222 std::assert!(
2223 v.apple_aur_acceptance.is_none(),
2224 "apple_aur_acceptance must be None when absent from legacy wire payload"
2225 );
2226 }
2227
2228 #[test]
2231 fn envelope_proofs_round_trips_the_single_object_wire_form() {
2232 let single = super::EnvelopeProofs::Single(sample_proof());
2237 let s = serde_json::to_string(&single).unwrap();
2238 std::assert!(
2239 s.starts_with('{'),
2240 "the single form must serialize as a JSON OBJECT, not an array: {}",
2241 s
2242 );
2243 let back: super::EnvelopeProofs = serde_json::from_str(&s).unwrap();
2244 std::assert_eq!(single, back);
2245 }
2246
2247 #[test]
2248 fn envelope_proofs_round_trips_the_array_wire_form() {
2249 let chain = super::EnvelopeProofs::Chain(std::vec![
2255 sample_principal_proof(),
2256 sample_notary_countersignature(),
2257 ]);
2258 let s = serde_json::to_string(&chain).unwrap();
2259 std::assert!(
2260 s.starts_with('['),
2261 "the chain form must serialize as a JSON ARRAY: {}",
2262 s
2263 );
2264 let back: super::EnvelopeProofs = serde_json::from_str(&s).unwrap();
2265 std::assert_eq!(chain, back);
2266 }
2267
2268 #[test]
2269 fn envelope_with_absent_additive_fields_emits_none_of_their_keys() {
2270 let json = serde_json::to_string(&sample_envelope()).unwrap();
2275 std::assert!(
2276 !json.contains("attestationMode"),
2277 "absent attestationMode must not appear on the wire: {}",
2278 json
2279 );
2280 std::assert!(
2281 !json.contains("delegationMandate\""),
2282 "absent embedded delegationMandate must not appear on the wire: {}",
2283 json
2284 );
2285 std::assert!(
2286 !json.contains("previousProof"),
2287 "absent previousProof must not appear on the wire: {}",
2288 json
2289 );
2290 std::assert!(
2291 !json.contains("\"id\":null"),
2292 "absent proof id must be omitted, never emitted as null: {}",
2293 json
2294 );
2295 }
2296
2297 #[test]
2298 fn all_returns_every_proof_for_both_wire_forms() {
2299 std::assert_eq!(
2303 super::EnvelopeProofs::Single(sample_proof()).all().len(),
2304 1
2305 );
2306 std::assert_eq!(
2307 super::EnvelopeProofs::Chain(std::vec![
2308 sample_principal_proof(),
2309 sample_notary_countersignature(),
2310 ])
2311 .all()
2312 .len(),
2313 2
2314 );
2315 }
2316
2317 #[test]
2318 fn principal_is_none_for_a_single_proof() {
2319 std::assert!(
2323 super::EnvelopeProofs::Single(sample_proof())
2324 .principal()
2325 .is_none()
2326 );
2327 }
2328
2329 #[test]
2330 fn notary_is_the_lone_proof_and_the_tail_of_a_chain() {
2331 let single = super::EnvelopeProofs::Single(sample_proof());
2334 std::assert_eq!(
2335 single.notary().map(|p| p.proof_purpose.as_str()),
2336 std::option::Option::Some("assertionMethod")
2337 );
2338 let chain = super::EnvelopeProofs::Chain(std::vec![
2339 sample_principal_proof(),
2340 sample_notary_countersignature(),
2341 ]);
2342 std::assert_eq!(
2343 chain.notary().map(|p| p.proof_purpose.as_str()),
2344 std::option::Option::Some("authentication")
2345 );
2346 }
2347
2348 #[test]
2349 fn accessors_refuse_to_name_proofs_inside_a_malformed_chain() {
2350 let over_long = super::EnvelopeProofs::Chain(std::vec![
2356 sample_principal_proof(),
2357 sample_notary_countersignature(),
2358 sample_notary_countersignature(),
2359 ]);
2360 std::assert!(over_long.principal().is_none());
2361 std::assert!(over_long.notary().is_none());
2362 }
2363
2364 #[test]
2365 fn is_chain_reports_the_array_form_at_any_length() {
2366 std::assert!(
2371 super::EnvelopeProofs::Chain(std::vec![sample_principal_proof()]).is_chain()
2372 );
2373 std::assert!(!super::EnvelopeProofs::Single(sample_proof()).is_chain());
2374 }
2375
2376 #[test]
2377 fn absent_attestation_mode_resolves_to_notary_attested() {
2378 std::assert_eq!(
2383 sample_policy().effective_attestation_mode(),
2384 super::AttestationMode::NotaryAttested
2385 );
2386 }
2387
2388 #[test]
2389 fn declared_attestation_mode_is_returned_verbatim() {
2390 let mut policy = sample_policy();
2394 policy.attestation_mode = std::option::Option::Some(super::AttestationMode::PrincipalSigned);
2395 std::assert_eq!(
2396 policy.effective_attestation_mode(),
2397 super::AttestationMode::PrincipalSigned
2398 );
2399 }
2400
2401 #[test]
2402 fn attestation_mode_wire_spelling_matches_the_closed_enum() {
2403 std::assert_eq!(
2408 serde_json::to_string(&super::AttestationMode::PrincipalSigned).unwrap(),
2409 "\"PrincipalSigned\""
2410 );
2411 std::assert_eq!(
2412 serde_json::to_string(&super::AttestationMode::NotaryAttested).unwrap(),
2413 "\"NotaryAttested\""
2414 );
2415 std::assert_eq!(super::AttestationMode::PrincipalSigned.label(), "PrincipalSigned");
2416 std::assert_eq!(super::AttestationMode::NotaryAttested.label(), "NotaryAttested");
2417 }
2418
2419 #[test]
2420 fn envelope_parses_a_chain_and_the_principal_signed_label_from_the_wire() {
2421 let mut envelope = sample_envelope();
2426 envelope.credential_subject.policy.attestation_mode =
2427 std::option::Option::Some(super::AttestationMode::PrincipalSigned);
2428 envelope.proof = super::EnvelopeProofs::Chain(std::vec![
2429 sample_principal_proof(),
2430 sample_notary_countersignature(),
2431 ]);
2432 let s = serde_json::to_string(&envelope).unwrap();
2433 let back: super::NotarizationEnvelope =
2434 serde_json::from_str(&s).expect("a PrincipalSigned chain envelope must parse");
2435 std::assert_eq!(envelope, back);
2436 }
2437}
2438
2439#[cfg(test)]
2440mod act_classification_tests {
2441 fn golden() -> std::string::String {
2450 std::include_str!("../../../../examples/slack_reply_envelope.json").to_string()
2451 }
2452
2453 #[test]
2454 fn an_envelope_without_the_claim_round_trips_byte_identically() {
2455 let raw = golden();
2464 let parsed: super::NotarizationEnvelope =
2465 serde_json::from_str(&raw).expect("a published golden must parse");
2466 std::assert!(
2467 parsed.credential_subject.act_classification.is_none(),
2468 "a published golden predates this field and must carry no claim"
2469 );
2470 let reserialized = serde_json::to_string(&parsed).expect("it serializes");
2471 std::assert!(
2472 !reserialized.contains("actClassification"),
2473 "an absent claim must not appear in output at all: {}",
2474 reserialized
2475 );
2476 }
2477
2478 #[test]
2479 fn a_classification_round_trips_through_serde() {
2480 let claim = super::ActClassification {
2484 vocabularies: std::vec![super::VocabularyRef {
2485 name: "aph_guardrails".to_string(),
2486 version: "0.1.0-alpha.1".to_string(),
2487 digest: "sha256-y6E/EGldCz2ogpVB7wlnS5orbnAjcCpoUBaDietJmXA=".to_string(),
2488 }],
2489 labels: std::vec![
2490 "APH_ACT_ACCESS/ACCESS_GRANT".parse().expect("a qualified label parses"),
2491 ],
2492 };
2493 let json = serde_json::to_string(&claim).expect("it serializes");
2494 std::assert!(
2495 json.contains("\"APH_ACT_ACCESS/ACCESS_GRANT\""),
2496 "a label rides the wire as its qualified STRING, not as a struct: {}",
2497 json
2498 );
2499 let back: super::ActClassification =
2500 serde_json::from_str(&json).expect("it round-trips");
2501 std::assert_eq!(claim, back);
2502 }
2503
2504 #[test]
2505 fn a_claim_citing_no_vocabulary_is_refused_at_parse() {
2506 let refused = serde_json::from_str::<super::ActClassification>(
2514 r#"{"vocabularies":[],"labels":["A/B"]}"#,
2515 );
2516 std::assert!(refused.is_err(), "a claim citing no vocabulary must be refused");
2517 std::assert!(
2518 refused.unwrap_err().to_string().contains("claims nothing"),
2519 "the refusal must say why an empty citation is meaningless"
2520 );
2521 }
2522
2523 #[test]
2524 fn an_unqualified_label_is_refused_at_parse() {
2525 let refused = "ACCESS_GRANT".parse::<super::ActLabel>();
2530 std::assert!(refused.is_err(), "an unqualified label must be refused");
2531 let message = refused.unwrap_err();
2532 std::assert!(
2533 message.contains("family-qualified"),
2534 "the refusal must say what shape was expected: {}",
2535 message
2536 );
2537 }
2538
2539 #[test]
2540 fn a_label_with_an_empty_side_or_two_separators_is_refused() {
2541 for raw in ["/ACCESS_GRANT", "APH_ACT_ACCESS/", "A/B/C"] {
2544 std::assert!(
2545 raw.parse::<super::ActLabel>().is_err(),
2546 "`{}` is structurally meaningless and must be refused",
2547 raw
2548 );
2549 }
2550 }
2551
2552 #[test]
2553 fn a_third_party_spelling_is_accepted_because_only_structure_is_ours() {
2554 let parsed: super::ActLabel = "acme.finance/wire-transfer.initiate"
2560 .parse()
2561 .expect("an unfamiliar but well-formed spelling must be accepted");
2562 std::assert_eq!(parsed.family(), "acme.finance");
2563 std::assert_eq!(parsed.label(), "wire-transfer.initiate");
2564 }
2565}
2566
2567#[cfg(test)]
2568mod closed_vocabulary_tests {
2569 #[test]
2577 fn every_channel_kind_wire_spelling_round_trips() {
2578 for kind in super::ChannelKind::ALL {
2584 let back: super::ChannelKind = kind
2585 .label()
2586 .parse()
2587 .expect("every published wire spelling must parse");
2588 std::assert_eq!(kind, back);
2589 }
2590 }
2591
2592 #[test]
2593 fn every_content_class_wire_spelling_round_trips() {
2594 for class in super::ContentClass::ALL {
2598 let back: super::ContentClass = class
2599 .label()
2600 .parse()
2601 .expect("every published wire spelling must parse");
2602 std::assert_eq!(class, back);
2603 }
2604 }
2605
2606 #[test]
2607 fn every_policy_decision_wire_spelling_round_trips() {
2608 for decision in super::PolicyDecision::ALL {
2614 let back: super::PolicyDecision = decision
2615 .label()
2616 .parse()
2617 .expect("every published wire spelling must parse");
2618 std::assert_eq!(decision, back);
2619 }
2620 }
2621
2622 #[test]
2623 fn an_unrecognized_policy_decision_is_refused_and_names_the_closed_set() {
2624 let err = "Sometimes"
2630 .parse::<super::PolicyDecision>()
2631 .expect_err("a value outside the closed set must be refused");
2632 std::assert!(err.contains("closed set"), "must name the closed set: {err}");
2633 std::assert!(err.contains("AskEveryTime"), "must list the members: {err}");
2634 }
2635
2636 #[test]
2637 fn an_unrecognized_channel_kind_is_refused_and_names_the_closed_set() {
2638 let err = "carrier_pigeon"
2647 .parse::<super::ChannelKind>()
2648 .expect_err("a value outside the closed set must be refused");
2649 std::assert!(err.contains("closed set"), "error must name the closed set: {err}");
2650 std::assert!(err.contains("google_chat"), "error must list the members: {err}");
2651 }
2652
2653 #[test]
2654 fn an_unrecognized_content_class_is_refused() {
2655 std::assert!("Digest".parse::<super::ContentClass>().is_err());
2660 std::assert!("reply".parse::<super::ContentClass>().is_err(), "case matters: wire is `Reply`");
2661 }
2662
2663 #[test]
2664 fn the_closed_sets_hold_their_declared_census() {
2665 std::assert_eq!(super::ChannelKind::ALL.len(), 9);
2671 std::assert_eq!(super::ContentClass::ALL.len(), 8);
2672 std::assert_eq!(super::PolicyDecision::ALL.len(), 3);
2673 }
2674}
2675
2676#[cfg(test)]
2677mod closed_vocabulary_deserialization_tests {
2678 fn golden() -> std::string::String {
2689 std::include_str!("../tests/golden/slack_reply_envelope.json").to_string()
2690 }
2691
2692 #[test]
2693 fn a_conformant_envelope_still_parses_after_the_types_closed() {
2694 let parsed: std::result::Result<super::NotarizationEnvelope, _> =
2698 serde_json::from_str(&golden());
2699 std::assert!(parsed.is_ok(), "the slack golden must still parse: {parsed:?}");
2700 }
2701
2702 #[test]
2703 fn an_envelope_naming_an_unrecognized_channel_is_refused_at_parse() {
2704 let doc = golden().replace("\"kind\": \"slack\"", "\"kind\": \"carrier_pigeon\"");
2710 let parsed: std::result::Result<super::NotarizationEnvelope, _> =
2711 serde_json::from_str(&doc);
2712 let err = parsed.expect_err("an unrecognized channel kind must be refused at parse");
2713 let msg = err.to_string();
2714 std::assert!(msg.contains("closed set"), "must name the closed set: {msg}");
2715 }
2716
2717 #[test]
2718 fn an_envelope_naming_an_unrecognized_content_class_is_refused_at_parse() {
2719 let doc = golden().replace("\"contentClass\": \"Reply\"", "\"contentClass\": \"Digest\"");
2720 let parsed: std::result::Result<super::NotarizationEnvelope, _> =
2721 serde_json::from_str(&doc);
2722 std::assert!(parsed.is_err(), "an unrecognized content class must be refused at parse");
2723 }
2724
2725 #[test]
2726 fn the_wire_spellings_survive_a_round_trip_through_serde() {
2727 for kind in super::ChannelKind::ALL {
2731 let json = serde_json::to_string(&kind).expect("a channel kind serializes");
2732 std::assert_eq!(json, std::format!("\"{}\"", kind.label()));
2733 let back: super::ChannelKind = serde_json::from_str(&json).expect("and parses back");
2734 std::assert_eq!(kind, back);
2735 }
2736 for class in super::ContentClass::ALL {
2737 let json = serde_json::to_string(&class).expect("a content class serializes");
2738 std::assert_eq!(json, std::format!("\"{}\"", class.label()));
2739 let back: super::ContentClass = serde_json::from_str(&json).expect("and parses back");
2740 std::assert_eq!(class, back);
2741 }
2742 }
2743}