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")]
551 pub act_classification: std::option::Option<ActClassification>,
552}
553
554#[derive(
556 std::fmt::Debug,
557 std::clone::Clone,
558 std::cmp::PartialEq,
559 std::cmp::Eq,
560 serde::Serialize,
561 serde::Deserialize,
562)]
563#[serde(rename_all = "camelCase", deny_unknown_fields)]
564pub struct HumanPrincipalRef {
565 pub id: String,
567 pub display_name: String,
569}
570
571#[derive(
573 std::fmt::Debug,
574 std::clone::Clone,
575 std::cmp::PartialEq,
576 std::cmp::Eq,
577 serde::Serialize,
578 serde::Deserialize,
579)]
580#[serde(rename_all = "camelCase", deny_unknown_fields)]
581pub struct AgentRef {
582 pub id: String,
584 #[serde(default)]
586 pub agent_card_uri: std::option::Option<String>,
587 pub display_name: String,
589 pub version: String,
591}
592
593#[derive(
595 std::fmt::Debug,
596 std::clone::Clone,
597 std::cmp::PartialEq,
598 std::cmp::Eq,
599 serde::Serialize,
600 serde::Deserialize,
601)]
602#[serde(rename_all = "camelCase", deny_unknown_fields)]
603pub struct ChannelDescriptor {
604 pub kind: ChannelKind,
610 pub recipient_addressing: serde_json::Value,
612 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
621 pub recipient_class: std::option::Option<RecipientClass>,
622}
623
624#[derive(
627 std::fmt::Debug,
628 std::clone::Clone,
629 std::cmp::PartialEq,
630 std::cmp::Eq,
631 serde::Serialize,
632 serde::Deserialize,
633)]
634#[serde(rename_all = "camelCase", deny_unknown_fields)]
635pub struct CommunicationDescriptor {
636 pub content_class: ContentClass,
641 pub body_sha256: String,
643 pub body_size: u64,
645 pub preview_lines: u32,
647 pub preview: String,
649}
650
651#[derive(
653 std::fmt::Debug,
654 std::clone::Clone,
655 std::cmp::PartialEq,
656 std::cmp::Eq,
657 serde::Serialize,
658 serde::Deserialize,
659)]
660#[serde(rename_all = "camelCase", deny_unknown_fields)]
661pub struct PolicyDescriptor {
662 pub decision: PolicyDecision,
664 pub matched_scope: String,
666 #[serde(default)]
668 pub delegation_mandate_id: std::option::Option<String>,
669 #[serde(default)]
672 pub act_chain: Vec<String>,
673 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
675 pub attestation_mode: std::option::Option<AttestationMode>,
676 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
679 pub delegation_mandate: std::option::Option<crate::delegation_mandate::DelegationMandate>,
680}
681
682impl PolicyDescriptor {
683 pub fn effective_attestation_mode(&self) -> AttestationMode {
690 match self.attestation_mode {
691 std::option::Option::Some(mode) => mode,
692 std::option::Option::None => AttestationMode::NotaryAttested,
693 }
694 }
695}
696
697#[derive(
699 std::fmt::Debug,
700 std::clone::Clone,
701 std::cmp::PartialEq,
702 std::cmp::Eq,
703 serde::Serialize,
704 serde::Deserialize,
705)]
706#[serde(rename_all = "camelCase", deny_unknown_fields)]
707pub struct NotarizationMetadata {
708 pub notary_service: NotaryServiceRef,
710 pub decision_timestamp: String,
712 pub decision_latency_ms: u64,
715}
716
717#[derive(
719 std::fmt::Debug,
720 std::clone::Clone,
721 std::cmp::PartialEq,
722 std::cmp::Eq,
723 serde::Serialize,
724 serde::Deserialize,
725)]
726#[serde(rename_all = "camelCase", deny_unknown_fields)]
727pub struct NotaryServiceRef {
728 pub id: String,
730 pub name: String,
732 pub version: String,
734 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
740 pub attested_digest: std::option::Option<String>,
741 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
745 pub attestation_uri: std::option::Option<String>,
746}
747
748#[derive(
750 std::fmt::Debug,
751 std::clone::Clone,
752 std::cmp::PartialEq,
753 std::cmp::Eq,
754 serde::Serialize,
755 serde::Deserialize,
756)]
757#[serde(rename_all = "camelCase", deny_unknown_fields)]
758pub struct LinkedMandate {
759 #[serde(default)]
761 pub ap2_intent_mandate_uri: std::option::Option<String>,
762 #[serde(default)]
767 pub ap2_signed_payload_b64: std::option::Option<String>,
768 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
773 pub vault_mutation: std::option::Option<
774 crate::vault_mutation::VaultMutationMandate,
775 >,
776}
777
778#[derive(
779 std::fmt::Debug,
780 std::clone::Clone,
781 std::cmp::PartialEq,
782 std::cmp::Eq,
783 serde::Serialize,
784 serde::Deserialize,
785)]
786#[serde(rename_all = "camelCase", deny_unknown_fields)]
789pub struct EnvelopeProof {
790 #[serde(rename = "type")]
793 pub r#type: String,
794 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
804 pub cryptosuite: std::option::Option<String>,
805 pub verification_method: String,
808 pub created: String,
810 pub proof_purpose: String,
813 pub proof_value: String,
815 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
818 pub id: std::option::Option<String>,
819 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
822 pub previous_proof: std::option::Option<String>,
823}
824
825#[derive(
832 std::fmt::Debug,
833 std::clone::Clone,
834 std::cmp::PartialEq,
835 std::cmp::Eq,
836 serde::Serialize,
837 serde::Deserialize,
838)]
839#[serde(rename_all = "camelCase", deny_unknown_fields)]
840pub struct AppleAurAcceptanceClaim {
841 pub user_id: String,
843 pub device_id: String,
846 pub aur_version_hash: String,
848 pub accepted_at: String,
850 pub document_kind: String,
852}
853
854#[derive(std::fmt::Debug, std::clone::Clone, std::marker::Copy, std::cmp::PartialEq, std::cmp::Eq)]
870pub enum PolicyDecision {
871 AlwaysAllow,
873 AskEveryTime,
877 NeverAllow,
879}
880
881impl PolicyDecision {
882 pub const ALL: [Self; 3] = [Self::AlwaysAllow, Self::AskEveryTime, Self::NeverAllow];
884
885 pub fn label(&self) -> &'static str {
887 match self {
888 Self::AlwaysAllow => "AlwaysAllow",
889 Self::AskEveryTime => "AskEveryTime",
890 Self::NeverAllow => "NeverAllow",
891 }
892 }
893
894 fn labels_for_error() -> std::string::String {
897 Self::ALL
898 .iter()
899 .map(Self::label)
900 .collect::<std::vec::Vec<&'static str>>()
901 .join(", ")
902 }
903}
904
905impl std::fmt::Display for PolicyDecision {
906 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
907 std::write!(f, "{}", self.label())
908 }
909}
910
911impl std::str::FromStr for PolicyDecision {
912 type Err = std::string::String;
913
914 fn from_str(label: &str) -> std::result::Result<Self, Self::Err> {
919 match label {
920 "AlwaysAllow" => std::result::Result::Ok(Self::AlwaysAllow),
921 "AskEveryTime" => std::result::Result::Ok(Self::AskEveryTime),
922 "NeverAllow" => std::result::Result::Ok(Self::NeverAllow),
923 other => std::result::Result::Err(std::format!(
924 "`{}` is not in the closed set {{{}}}",
925 other,
926 Self::labels_for_error()
927 )),
928 }
929 }
930}
931
932impl serde::Serialize for PolicyDecision {
933 fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
934 serializer.serialize_str(self.label())
935 }
936}
937
938impl<'de> serde::Deserialize<'de> for PolicyDecision {
939 fn deserialize<D: serde::Deserializer<'de>>(
940 deserializer: D,
941 ) -> std::result::Result<Self, D::Error> {
942 let raw = <std::string::String as serde::Deserialize>::deserialize(deserializer)?;
943 <Self as std::str::FromStr>::from_str(&raw).map_err(serde::de::Error::custom)
944 }
945}
946
947#[derive(std::fmt::Debug, std::clone::Clone, std::marker::Copy, std::cmp::PartialEq, std::cmp::Eq)]
962pub enum RecipientClass {
963 Human,
965 Agent,
967}
968
969impl RecipientClass {
970 pub const ALL: [Self; 2] = [Self::Human, Self::Agent];
972
973 pub fn label(&self) -> &'static str {
975 match self {
976 Self::Human => "human",
977 Self::Agent => "agent",
978 }
979 }
980
981 fn labels_for_error() -> std::string::String {
984 Self::ALL
985 .iter()
986 .map(Self::label)
987 .collect::<std::vec::Vec<&'static str>>()
988 .join(", ")
989 }
990}
991
992impl std::fmt::Display for RecipientClass {
993 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
994 std::write!(f, "{}", self.label())
995 }
996}
997
998impl std::str::FromStr for RecipientClass {
999 type Err = std::string::String;
1000
1001 fn from_str(label: &str) -> std::result::Result<Self, Self::Err> {
1006 match label {
1007 "human" => std::result::Result::Ok(Self::Human),
1008 "agent" => std::result::Result::Ok(Self::Agent),
1009 other => std::result::Result::Err(std::format!(
1010 "`{}` is not in the closed set {{{}}}",
1011 other,
1012 Self::labels_for_error()
1013 )),
1014 }
1015 }
1016}
1017
1018impl serde::Serialize for RecipientClass {
1019 fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
1020 serializer.serialize_str(self.label())
1021 }
1022}
1023
1024impl<'de> serde::Deserialize<'de> for RecipientClass {
1025 fn deserialize<D: serde::Deserializer<'de>>(
1026 deserializer: D,
1027 ) -> std::result::Result<Self, D::Error> {
1028 let raw = <std::string::String as serde::Deserialize>::deserialize(deserializer)?;
1029 <Self as std::str::FromStr>::from_str(&raw).map_err(serde::de::Error::custom)
1030 }
1031}
1032
1033#[derive(
1047 std::fmt::Debug,
1048 std::clone::Clone,
1049 std::cmp::PartialEq,
1050 std::cmp::Eq,
1051 serde::Serialize,
1052 serde::Deserialize,
1053)]
1054#[serde(rename_all = "camelCase", deny_unknown_fields)]
1055pub struct ActClassification {
1056 #[serde(deserialize_with = "non_empty_vocabularies")]
1069 pub vocabularies: std::vec::Vec<VocabularyRef>,
1070 pub labels: std::vec::Vec<ActLabel>,
1077}
1078
1079#[derive(
1088 std::fmt::Debug,
1089 std::clone::Clone,
1090 std::cmp::PartialEq,
1091 std::cmp::Eq,
1092 serde::Serialize,
1093 serde::Deserialize,
1094)]
1095#[serde(rename_all = "camelCase", deny_unknown_fields)]
1096pub struct Audience {
1097 pub id: String,
1099 #[serde(default, skip_serializing_if = "std::option::Option::is_none")]
1104 pub channel_binding: std::option::Option<AudienceChannelBinding>,
1105}
1106
1107#[derive(
1120 std::fmt::Debug,
1121 std::clone::Clone,
1122 std::cmp::PartialEq,
1123 std::cmp::Eq,
1124 serde::Serialize,
1125 serde::Deserialize,
1126)]
1127#[serde(rename_all = "camelCase")]
1128pub struct AudienceChannelBinding {
1129 pub kind: ChannelKind,
1131 #[serde(flatten)]
1137 pub coordinates: std::collections::BTreeMap<String, serde_json::Value>,
1138}
1139
1140fn non_empty_vocabularies<'de, D: serde::Deserializer<'de>>(
1145 deserializer: D,
1146) -> std::result::Result<std::vec::Vec<VocabularyRef>, D::Error> {
1147 let refs = <std::vec::Vec<VocabularyRef> as serde::Deserialize>::deserialize(deserializer)?;
1148 if refs.is_empty() {
1149 return std::result::Result::Err(serde::de::Error::custom(
1150 "`vocabularies` is empty; a classification must name every vocabulary \
1151 the verdict depended on, and one that names none claims nothing",
1152 ));
1153 }
1154 std::result::Result::Ok(refs)
1155}
1156
1157#[derive(std::fmt::Debug, std::clone::Clone, std::cmp::PartialEq, std::cmp::Eq)]
1175pub struct ActLabel {
1176 family: String,
1177 label: String,
1178}
1179
1180impl ActLabel {
1181 pub fn family(&self) -> &str {
1183 &self.family
1184 }
1185
1186 pub fn label(&self) -> &str {
1188 &self.label
1189 }
1190
1191 pub fn qualified(&self) -> std::string::String {
1193 std::format!("{}/{}", self.family, self.label)
1194 }
1195}
1196
1197impl std::fmt::Display for ActLabel {
1198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1199 std::write!(f, "{}", self.qualified())
1200 }
1201}
1202
1203impl std::str::FromStr for ActLabel {
1204 type Err = std::string::String;
1205
1206 fn from_str(raw: &str) -> std::result::Result<Self, Self::Err> {
1212 let (family, label) = match raw.split_once('/') {
1213 std::option::Option::Some(parts) => parts,
1214 std::option::Option::None => {
1215 return std::result::Result::Err(std::format!(
1216 "`{}` is not family-qualified: a label is written `FAMILY/LABEL`",
1217 raw
1218 ));
1219 }
1220 };
1221 if family.is_empty() || label.is_empty() {
1222 return std::result::Result::Err(std::format!(
1223 "`{}` has an empty family or label; both sides of the `/` are required",
1224 raw
1225 ));
1226 }
1227 if label.contains('/') {
1228 return std::result::Result::Err(std::format!(
1229 "`{}` has more than one `/`; a label names exactly one family",
1230 raw
1231 ));
1232 }
1233 std::result::Result::Ok(Self {
1234 family: family.to_string(),
1235 label: label.to_string(),
1236 })
1237 }
1238}
1239
1240impl serde::Serialize for ActLabel {
1241 fn serialize<S: serde::Serializer>(
1242 &self,
1243 serializer: S,
1244 ) -> std::result::Result<S::Ok, S::Error> {
1245 serializer.serialize_str(&self.qualified())
1246 }
1247}
1248
1249impl<'de> serde::Deserialize<'de> for ActLabel {
1250 fn deserialize<D: serde::Deserializer<'de>>(
1251 deserializer: D,
1252 ) -> std::result::Result<Self, D::Error> {
1253 let raw = <std::string::String as serde::Deserialize>::deserialize(deserializer)?;
1254 <Self as std::str::FromStr>::from_str(&raw).map_err(serde::de::Error::custom)
1255 }
1256}
1257
1258#[derive(
1266 std::fmt::Debug,
1267 std::clone::Clone,
1268 std::cmp::PartialEq,
1269 std::cmp::Eq,
1270 serde::Serialize,
1271 serde::Deserialize,
1272)]
1273#[serde(rename_all = "camelCase", deny_unknown_fields)]
1274pub struct VocabularyRef {
1275 pub name: String,
1277 pub version: String,
1279 pub digest: String,
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294 fn sample_human_principal() -> super::HumanPrincipalRef {
1297 super::HumanPrincipalRef {
1298 id: "did:key:z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy".to_string(),
1299 display_name: "Scott Wyatt".to_string(),
1300 }
1301 }
1302
1303 fn sample_agent() -> super::AgentRef {
1304 super::AgentRef {
1305 id: "did:web:agent.squillo.com".to_string(),
1306 agent_card_uri: std::option::Option::Some(
1307 "https://agent.squillo.com/.well-known/agent-card.json".to_string(),
1308 ),
1309 display_name: "Squillo Concierge".to_string(),
1310 version: "1.0".to_string(),
1311 }
1312 }
1313
1314 fn sample_channel() -> super::ChannelDescriptor {
1315 super::ChannelDescriptor {
1316 kind: super::ChannelKind::Slack,
1317 recipient_addressing: serde_json::json!({
1318 "teamId": "T01234567",
1319 "channelId": "C01234567",
1320 "parentTs": "1716249600.000100"
1321 }),
1322 recipient_class: std::option::Option::None,
1323 }
1324 }
1325
1326 fn sample_communication() -> super::CommunicationDescriptor {
1327 super::CommunicationDescriptor {
1328 content_class: super::ContentClass::Reply,
1329 body_sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(),
1330 body_size: 1842,
1331 preview_lines: 3,
1332 preview: "hello world".to_string(),
1333 }
1334 }
1335
1336 fn sample_policy() -> super::PolicyDescriptor {
1337 super::PolicyDescriptor {
1338 decision: super::PolicyDecision::AskEveryTime,
1339 matched_scope: "per-channel".to_string(),
1340 delegation_mandate_id: std::option::Option::None,
1341 act_chain: std::vec::Vec::new(),
1342 attestation_mode: std::option::Option::None,
1343 delegation_mandate: std::option::Option::None,
1344 }
1345 }
1346
1347 #[test]
1348 fn attestation_fields_are_omitted_when_absent_and_accepted_when_present() {
1349 let bare = sample_notary_service();
1357 let json = serde_json::to_string(&bare).unwrap();
1358 std::assert!(
1359 !json.contains("attestedDigest") && !json.contains("attestationUri"),
1360 "absent attestation fields must not appear on the wire: {}",
1361 json
1362 );
1363
1364 let with_claim: super::NotaryServiceRef = serde_json::from_str(
1365 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"}"#,
1366 )
1367 .expect("a notary advertising an attestation must parse");
1368 std::assert_eq!(
1369 with_claim.attested_digest.as_deref(),
1370 std::option::Option::Some("sha256:abc")
1371 );
1372 }
1373
1374 fn sample_notary_service() -> super::NotaryServiceRef {
1375 super::NotaryServiceRef {
1376 id: "did:web:notary.squillo.com".to_string(),
1377 name: "Squillo Notary Service".to_string(),
1378 version: "0.1.0".to_string(),
1379 attested_digest: std::option::Option::None,
1380 attestation_uri: std::option::Option::None,
1381 }
1382 }
1383
1384 fn sample_notarization_metadata() -> super::NotarizationMetadata {
1385 super::NotarizationMetadata {
1386 notary_service: sample_notary_service(),
1387 decision_timestamp: "2026-05-21T00:00:01Z".to_string(),
1388 decision_latency_ms: 1834,
1389 }
1390 }
1391
1392 fn sample_credential_subject() -> super::CredentialSubject {
1393 super::CredentialSubject {
1394 human_principal: sample_human_principal(),
1395 agent: sample_agent(),
1396 channel: sample_channel(),
1397 communication: sample_communication(),
1398 policy: sample_policy(),
1399 notarization: sample_notarization_metadata(),
1400 apple_aur_acceptance: std::option::Option::None,
1401 audience: std::option::Option::None,
1402 act_classification: std::option::Option::None,
1403 }
1404 }
1405
1406 fn sample_linked_mandate() -> super::LinkedMandate {
1407 super::LinkedMandate {
1408 ap2_intent_mandate_uri: std::option::Option::Some(
1409 "urn:uuid:11111111-1111-4111-8111-111111111111".to_string(),
1410 ),
1411 ap2_signed_payload_b64: std::option::Option::None,
1412 vault_mutation: std::option::Option::None,
1413 }
1414 }
1415
1416 fn sample_proof() -> super::EnvelopeProof {
1417 super::EnvelopeProof {
1418 r#type: "DataIntegrityProof".to_string(),
1419 cryptosuite: std::option::Option::Some("eddsa-jcs-2022".to_string()),
1420 verification_method:
1421 "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV#z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"
1422 .to_string(),
1423 created: "2026-05-21T00:00:01Z".to_string(),
1424 proof_purpose: "assertionMethod".to_string(),
1425 proof_value:
1426 "z3WgvA9JHkbV3qLZHcM4FxBp4xHfQVnVnPKKDdyazQwQGdGzxsRdmZWBxXwQvN6P2sLZbLP4HnRy9LcZdpFLLM6h"
1427 .to_string(),
1428 id: std::option::Option::None,
1429 previous_proof: std::option::Option::None,
1430 }
1431 }
1432
1433 fn sample_envelope() -> super::NotarizationEnvelope {
1434 super::NotarizationEnvelope {
1435 aph_version: "0.1".to_string(),
1436 context: std::vec![
1437 "https://www.w3.org/ns/credentials/v2".to_string(),
1438 "https://w3id.org/aph/v1".to_string(),
1439 ],
1440 r#type: std::vec![
1441 "VerifiableCredential".to_string(),
1442 "AgentSendAuthorizationCredential".to_string(),
1443 ],
1444 id: "urn:uuid:00000000-0000-4000-8000-000000000001".to_string(),
1445 issuer: "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV".to_string(),
1446 valid_from: "2026-05-21T00:00:00Z".to_string(),
1447 valid_until: "2026-05-22T00:00:00Z".to_string(),
1448 credential_subject: sample_credential_subject(),
1449 linked_mandate: std::option::Option::None,
1450 credential_status: std::option::Option::None,
1454 proof: super::EnvelopeProofs::Single(sample_proof()),
1455 }
1456 }
1457
1458 fn sample_principal_proof() -> super::EnvelopeProof {
1459 super::EnvelopeProof {
1460 r#type: "DataIntegrityProof".to_string(),
1461 cryptosuite: std::option::Option::Some("eddsa-jcs-2022".to_string()),
1462 verification_method:
1465 "did:key:z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy#z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy"
1466 .to_string(),
1467 created: "2026-05-21T00:00:02Z".to_string(),
1468 proof_purpose: "assertionMethod".to_string(),
1469 proof_value: "z-illustrative-principal-proof-value".to_string(),
1470 id: std::option::Option::Some("urn:uuid:00000000-0000-4000-8000-0000000000a1".to_string()),
1471 previous_proof: std::option::Option::None,
1472 }
1473 }
1474
1475 fn sample_notary_countersignature() -> super::EnvelopeProof {
1476 super::EnvelopeProof {
1477 r#type: "DataIntegrityProof".to_string(),
1478 cryptosuite: std::option::Option::Some("eddsa-jcs-2022".to_string()),
1479 verification_method:
1480 "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV#z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"
1481 .to_string(),
1482 created: "2026-05-21T00:00:03Z".to_string(),
1483 proof_purpose: "authentication".to_string(),
1484 proof_value: "z-illustrative-notary-countersignature".to_string(),
1485 id: std::option::Option::Some("urn:uuid:00000000-0000-4000-8000-0000000000b1".to_string()),
1486 previous_proof: std::option::Option::Some(
1487 "urn:uuid:00000000-0000-4000-8000-0000000000a1".to_string(),
1488 ),
1489 }
1490 }
1491
1492 #[test]
1495 fn round_trip_human_principal_ref() {
1496 let v = sample_human_principal();
1501 let s = serde_json::to_string(&v).unwrap();
1502 let v2: super::HumanPrincipalRef = serde_json::from_str(&s).unwrap();
1503 std::assert_eq!(v, v2);
1504 }
1505
1506 #[test]
1507 fn round_trip_agent_ref() {
1508 let v = sample_agent();
1512 let s = serde_json::to_string(&v).unwrap();
1513 let v2: super::AgentRef = serde_json::from_str(&s).unwrap();
1514 std::assert_eq!(v, v2);
1515 }
1516
1517 #[test]
1518 fn round_trip_channel_descriptor() {
1519 let v = sample_channel();
1523 let s = serde_json::to_string(&v).unwrap();
1524 let v2: super::ChannelDescriptor = serde_json::from_str(&s).unwrap();
1525 std::assert_eq!(v, v2);
1526 }
1527
1528 #[test]
1529 fn round_trip_communication_descriptor() {
1530 let v = sample_communication();
1534 let s = serde_json::to_string(&v).unwrap();
1535 let v2: super::CommunicationDescriptor = serde_json::from_str(&s).unwrap();
1536 std::assert_eq!(v, v2);
1537 }
1538
1539 #[test]
1540 fn round_trip_policy_descriptor() {
1541 let v = sample_policy();
1545 let s = serde_json::to_string(&v).unwrap();
1546 let v2: super::PolicyDescriptor = serde_json::from_str(&s).unwrap();
1547 std::assert_eq!(v, v2);
1548 }
1549
1550 #[test]
1551 fn round_trip_notary_service_ref() {
1552 let v = sample_notary_service();
1555 let s = serde_json::to_string(&v).unwrap();
1556 let v2: super::NotaryServiceRef = serde_json::from_str(&s).unwrap();
1557 std::assert_eq!(v, v2);
1558 }
1559
1560 #[test]
1561 fn round_trip_notarization_metadata() {
1562 let v = sample_notarization_metadata();
1565 let s = serde_json::to_string(&v).unwrap();
1566 let v2: super::NotarizationMetadata = serde_json::from_str(&s).unwrap();
1567 std::assert_eq!(v, v2);
1568 }
1569
1570 #[test]
1571 fn round_trip_credential_subject() {
1572 let v = sample_credential_subject();
1576 let s = serde_json::to_string(&v).unwrap();
1577 let v2: super::CredentialSubject = serde_json::from_str(&s).unwrap();
1578 std::assert_eq!(v, v2);
1579 }
1580
1581 #[test]
1582 fn round_trip_linked_mandate() {
1583 let v = sample_linked_mandate();
1587 let s = serde_json::to_string(&v).unwrap();
1588 let v2: super::LinkedMandate = serde_json::from_str(&s).unwrap();
1589 std::assert_eq!(v, v2);
1590 }
1591
1592 #[test]
1593 fn round_trip_envelope_proof() {
1594 let v = sample_proof();
1597 let s = serde_json::to_string(&v).unwrap();
1598 let v2: super::EnvelopeProof = serde_json::from_str(&s).unwrap();
1599 std::assert_eq!(v, v2);
1600 }
1601
1602 #[test]
1603 fn round_trip_notarization_envelope() {
1604 let v = sample_envelope();
1608 let s = serde_json::to_string(&v).unwrap();
1609 let v2: super::NotarizationEnvelope = serde_json::from_str(&s).unwrap();
1610 std::assert_eq!(v, v2);
1611 }
1612
1613 #[test]
1616 fn envelope_serializes_type_as_type_and_context_as_at_context() {
1617 let v = sample_envelope();
1622 let s = serde_json::to_string(&v).unwrap();
1623 std::assert!(
1624 s.contains("\"type\":"),
1625 "envelope must serialize r#type as \"type\": {}",
1626 s
1627 );
1628 std::assert!(
1629 s.contains("\"@context\":"),
1630 "envelope must serialize context as \"@context\": {}",
1631 s
1632 );
1633 std::assert!(
1634 !s.contains("\"rType\""),
1635 "envelope must NOT serialize as \"rType\": {}",
1636 s
1637 );
1638 std::assert!(
1639 !s.contains("\"r#type\""),
1640 "envelope must NOT serialize as \"r#type\": {}",
1641 s
1642 );
1643 }
1644
1645 #[test]
1646 fn proof_serializes_type_as_type() {
1647 let v = sample_proof();
1651 let s = serde_json::to_string(&v).unwrap();
1652 std::assert!(
1653 s.contains("\"type\":"),
1654 "proof must serialize r#type as \"type\": {}",
1655 s
1656 );
1657 std::assert!(
1658 !s.contains("\"rType\""),
1659 "proof must NOT serialize as \"rType\": {}",
1660 s
1661 );
1662 }
1663
1664 #[test]
1667 fn envelope_rejects_unknown_field() {
1668 let s = serde_json::json!({
1672 "aphVersion": "0.1",
1673 "@context": [
1674 "https://www.w3.org/ns/credentials/v2",
1675 "https://w3id.org/aph/v1"
1676 ],
1677 "type": ["VerifiableCredential", "AgentSendAuthorizationCredential"],
1678 "id": "urn:uuid:00000000-0000-4000-8000-000000000001",
1679 "issuer": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV",
1680 "validFrom": "2026-05-21T00:00:00Z",
1681 "validUntil": "2026-05-22T00:00:00Z",
1682 "credentialSubject": {
1683 "humanPrincipal": {
1684 "id": "did:key:abc",
1685 "displayName": "X"
1686 },
1687 "agent": {
1688 "id": "did:web:agent.squillo.com",
1689 "displayName": "X",
1690 "version": "1.0"
1691 },
1692 "channel": {
1693 "kind": "slack",
1694 "recipientAddressing": {}
1695 },
1696 "communication": {
1697 "contentClass": "Reply",
1698 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1699 "bodySize": 0,
1700 "previewLines": 0,
1701 "preview": ""
1702 },
1703 "policy": {
1704 "decision": "AskEveryTime",
1705 "matchedScope": "per-channel"
1706 },
1707 "notarization": {
1708 "notaryService": {
1709 "id": "did:web:notary.squillo.com",
1710 "name": "Squillo Notary Service",
1711 "version": "0.1.0"
1712 },
1713 "decisionTimestamp": "2026-05-21T00:00:01Z",
1714 "decisionLatencyMs": 0
1715 }
1716 },
1717 "proof": {
1718 "type": "DataIntegrityProof",
1719 "verificationMethod": "did:key:abc#abc",
1720 "created": "2026-05-21T00:00:01Z",
1721 "proofPurpose": "assertionMethod",
1722 "proofValue": "z..."
1723 },
1724 "extraKey": "x"
1725 })
1726 .to_string();
1727 let r: std::result::Result<super::NotarizationEnvelope, _> = serde_json::from_str(&s);
1728 std::assert!(
1729 r.is_err(),
1730 "deny_unknown_fields must reject extraKey: {:?}",
1731 r
1732 );
1733 }
1734
1735 #[test]
1736 fn human_principal_ref_rejects_unknown_field() {
1737 let s = serde_json::json!({
1741 "id": "did:key:abc",
1742 "displayName": "X",
1743 "rogue": true
1744 })
1745 .to_string();
1746 let r: std::result::Result<super::HumanPrincipalRef, _> = serde_json::from_str(&s);
1747 std::assert!(
1748 r.is_err(),
1749 "deny_unknown_fields must reject rogue field: {:?}",
1750 r
1751 );
1752 }
1753
1754 #[test]
1757 fn envelope_deserializes_without_linked_mandate() {
1758 let s = serde_json::json!({
1762 "aphVersion": "0.1",
1763 "@context": [
1764 "https://www.w3.org/ns/credentials/v2",
1765 "https://w3id.org/aph/v1"
1766 ],
1767 "type": ["VerifiableCredential", "AgentSendAuthorizationCredential"],
1768 "id": "urn:uuid:00000000-0000-4000-8000-000000000001",
1769 "issuer": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV",
1770 "validFrom": "2026-05-21T00:00:00Z",
1771 "validUntil": "2026-05-22T00:00:00Z",
1772 "credentialSubject": {
1773 "humanPrincipal": {
1774 "id": "did:key:abc",
1775 "displayName": "X"
1776 },
1777 "agent": {
1778 "id": "did:web:agent.squillo.com",
1779 "displayName": "X",
1780 "version": "1.0"
1781 },
1782 "channel": {
1783 "kind": "slack",
1784 "recipientAddressing": {}
1785 },
1786 "communication": {
1787 "contentClass": "Reply",
1788 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1789 "bodySize": 0,
1790 "previewLines": 0,
1791 "preview": ""
1792 },
1793 "policy": {
1794 "decision": "AskEveryTime",
1795 "matchedScope": "per-channel"
1796 },
1797 "notarization": {
1798 "notaryService": {
1799 "id": "did:web:notary.squillo.com",
1800 "name": "Squillo Notary Service",
1801 "version": "0.1.0"
1802 },
1803 "decisionTimestamp": "2026-05-21T00:00:01Z",
1804 "decisionLatencyMs": 0
1805 }
1806 },
1807 "proof": {
1808 "type": "DataIntegrityProof",
1809 "verificationMethod": "did:key:abc#abc",
1810 "created": "2026-05-21T00:00:01Z",
1811 "proofPurpose": "assertionMethod",
1812 "proofValue": "z..."
1813 }
1814 })
1815 .to_string();
1816 let v: super::NotarizationEnvelope =
1817 serde_json::from_str(&s).expect("must deserialize with linkedMandate omitted");
1818 std::assert!(v.linked_mandate.is_none());
1819 }
1820
1821 #[test]
1822 fn envelope_round_trips_a_credential_status_under_the_camel_case_wire_key() {
1823 let s = serde_json::json!({
1838 "aphVersion": "0.1",
1839 "@context": [
1840 "https://www.w3.org/ns/credentials/v2",
1841 "https://w3id.org/aph/v1"
1842 ],
1843 "type": ["VerifiableCredential", "AgentSendAuthorizationCredential"],
1844 "id": "urn:uuid:00000000-0000-4000-8000-000000000001",
1845 "issuer": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV",
1846 "validFrom": "2026-05-21T00:00:00Z",
1847 "validUntil": "2026-05-22T00:00:00Z",
1848 "credentialSubject": {
1849 "humanPrincipal": {
1850 "id": "did:key:abc",
1851 "displayName": "X"
1852 },
1853 "agent": {
1854 "id": "did:web:agent.squillo.com",
1855 "displayName": "X",
1856 "version": "1.0"
1857 },
1858 "channel": {
1859 "kind": "slack",
1860 "recipientAddressing": {}
1861 },
1862 "communication": {
1863 "contentClass": "Reply",
1864 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1865 "bodySize": 0,
1866 "previewLines": 0,
1867 "preview": ""
1868 },
1869 "policy": {
1870 "decision": "AskEveryTime",
1871 "matchedScope": "per-channel",
1872 "delegationMandateId": "urn:uuid:00000000-0000-4000-8000-0000000000d1"
1876 },
1877 "notarization": {
1878 "notaryService": {
1879 "id": "did:web:aph-notary.squillo.com",
1880 "name": "Squillo Notary Service",
1881 "version": "0.1.0"
1882 },
1883 "decisionTimestamp": "2026-05-21T00:00:01Z",
1884 "decisionLatencyMs": 0
1885 }
1886 },
1887 "credentialStatus": {
1888 "type": "BitstringStatusListEntry",
1889 "statusPurpose": "revocation",
1890 "statusListIndex": "94567",
1891 "statusListCredential": "https://aph-notary.squillo.com/.well-known/aph-status.json"
1892 },
1893 "proof": {
1894 "type": "DataIntegrityProof",
1895 "verificationMethod": "did:key:abc#abc",
1896 "created": "2026-05-21T00:00:01Z",
1897 "proofPurpose": "assertionMethod",
1898 "proofValue": "z..."
1899 }
1900 })
1901 .to_string();
1902 let v: super::NotarizationEnvelope = serde_json::from_str(&s)
1903 .expect("deny_unknown_fields must ACCEPT the credentialStatus key");
1904 let entry = v
1905 .credential_status
1906 .as_ref()
1907 .expect("the wire key must land in the typed field");
1908 std::assert_eq!(
1909 entry.r#type,
1910 crate::credential_status::StatusEntryType::BitstringStatusListEntry
1911 );
1912 std::assert_eq!(
1913 entry.status_purpose,
1914 crate::credential_status::StatusPurpose::Revocation
1915 );
1916 std::assert_eq!(entry.index().expect("the index is readable"), 94567u64);
1917
1918 let back = serde_json::to_string(&v).expect("envelope serializes");
1922 let reparsed: serde_json::Value = serde_json::from_str(&back).expect("round-trip is JSON");
1923 let emitted = reparsed
1924 .get("credentialStatus")
1925 .expect("the member must be re-emitted as `credentialStatus`");
1926 std::assert_eq!(
1927 emitted,
1928 &serde_json::json!({
1929 "type": "BitstringStatusListEntry",
1930 "statusPurpose": "revocation",
1931 "statusListIndex": "94567",
1932 "statusListCredential": "https://aph-notary.squillo.com/.well-known/aph-status.json"
1933 }),
1934 "the absent optional `id` must not appear, as a null or otherwise"
1935 );
1936 let base = crate::crypto::proof_base::signing_base(
1943 &v,
1944 crate::crypto::proof_base::ProofRole::Notary,
1945 )
1946 .expect("a single notary proof builds a notary base");
1947 std::assert!(
1948 base.contains("\"credentialStatus\""),
1949 "the status reference must be covered by the notary signature"
1950 );
1951 }
1952
1953 #[test]
1954 fn agent_ref_deserializes_without_agent_card_uri() {
1955 let s = serde_json::json!({
1958 "id": "did:web:agent.squillo.com",
1959 "displayName": "X",
1960 "version": "1.0"
1961 })
1962 .to_string();
1963 let v: super::AgentRef =
1964 serde_json::from_str(&s).expect("must deserialize with agentCardUri omitted");
1965 std::assert!(v.agent_card_uri.is_none());
1966 }
1967
1968 #[test]
1969 fn policy_descriptor_deserializes_without_optionals() {
1970 let s = serde_json::json!({
1974 "decision": "AskEveryTime",
1975 "matchedScope": "per-channel"
1976 })
1977 .to_string();
1978 let v: super::PolicyDescriptor =
1979 serde_json::from_str(&s).expect("must deserialize with optionals omitted");
1980 std::assert!(v.delegation_mandate_id.is_none());
1981 std::assert!(v.act_chain.is_empty());
1982 std::assert!(v.attestation_mode.is_none());
1983 std::assert!(v.delegation_mandate.is_none());
1984 }
1985
1986 #[test]
1987 fn linked_mandate_deserializes_without_ap2_uri() {
1988 let s = serde_json::json!({}).to_string();
1991 let v: super::LinkedMandate =
1992 serde_json::from_str(&s).expect("must deserialize with ap2IntentMandateUri omitted");
1993 std::assert!(v.ap2_intent_mandate_uri.is_none());
1994 }
1995
1996 #[test]
1997 fn envelope_proof_deserializes_without_cryptosuite() {
1998 let s = serde_json::json!({
2002 "type": "JsonWebSignature2020",
2003 "verificationMethod": "did:key:abc#abc",
2004 "created": "2026-05-21T00:00:01Z",
2005 "proofPurpose": "assertionMethod",
2006 "proofValue": "z..."
2007 })
2008 .to_string();
2009 let v: super::EnvelopeProof =
2010 serde_json::from_str(&s).expect("must deserialize with cryptosuite omitted");
2011 std::assert!(v.cryptosuite.is_none());
2012 }
2013
2014 #[test]
2017 fn human_principal_ref_serializes_camel_case() {
2018 let v = super::HumanPrincipalRef {
2022 id: "did:key:abc".to_string(),
2023 display_name: "Scott".to_string(),
2024 };
2025 let s = serde_json::to_string(&v).unwrap();
2026 std::assert!(
2027 s.contains("\"displayName\""),
2028 "must serialize display_name as displayName: {}",
2029 s
2030 );
2031 std::assert!(
2032 !s.contains("\"display_name\""),
2033 "must NOT serialize as display_name: {}",
2034 s
2035 );
2036 }
2037
2038 #[test]
2041 fn apple_aur_acceptance_claim_round_trip() {
2042 let claim = super::AppleAurAcceptanceClaim {
2046 user_id: "did:key:z6MkUserAbc123".to_string(),
2047 device_id: "device-opaque-id-001".to_string(),
2048 aur_version_hash:
2049 "a3b4c5d6e7f8091011121314151617181920212223242526272829303132333435".to_string(),
2050 accepted_at: "2026-06-09T00:00:00Z".to_string(),
2051 document_kind: "foundation_models_framework_aur".to_string(),
2052 };
2053 let subject = super::CredentialSubject {
2054 human_principal: sample_human_principal(),
2055 agent: sample_agent(),
2056 channel: sample_channel(),
2057 communication: sample_communication(),
2058 policy: sample_policy(),
2059 notarization: sample_notarization_metadata(),
2060 apple_aur_acceptance: std::option::Option::Some(claim.clone()),
2061 audience: std::option::Option::None,
2062 act_classification: std::option::Option::None,
2063 };
2064 let s = serde_json::to_string(&subject).unwrap();
2065 std::assert!(
2067 s.contains("\"appleAurAcceptance\""),
2068 "must serialize apple_aur_acceptance as appleAurAcceptance: {}",
2069 s
2070 );
2071 let v2: super::CredentialSubject = serde_json::from_str(&s).unwrap();
2072 std::assert_eq!(subject, v2);
2073 let recovered = v2.apple_aur_acceptance.expect("must be Some after round-trip");
2074 std::assert_eq!(recovered.document_kind, "foundation_models_framework_aur");
2075 std::assert_eq!(recovered.user_id, claim.user_id);
2076 std::assert_eq!(recovered.device_id, claim.device_id);
2077 std::assert_eq!(recovered.aur_version_hash, claim.aur_version_hash);
2078 std::assert_eq!(recovered.accepted_at, claim.accepted_at);
2079 }
2080
2081 #[test]
2084 fn credential_subject_legacy_omit_apple_aur_acceptance() {
2085 let s = serde_json::json!({
2089 "humanPrincipal": {
2090 "id": "did:key:z6MkLegacyUser",
2091 "displayName": "Legacy User"
2092 },
2093 "agent": {
2094 "id": "did:web:agent.squillo.com",
2095 "displayName": "Squillo Concierge",
2096 "version": "1.0"
2097 },
2098 "channel": {
2099 "kind": "slack",
2100 "recipientAddressing": {}
2101 },
2102 "communication": {
2103 "contentClass": "Reply",
2104 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
2105 "bodySize": 0,
2106 "previewLines": 0,
2107 "preview": ""
2108 },
2109 "policy": {
2110 "decision": "AskEveryTime",
2111 "matchedScope": "per-channel"
2112 },
2113 "notarization": {
2114 "notaryService": {
2115 "id": "did:web:notary.squillo.com",
2116 "name": "Squillo Notary Service",
2117 "version": "0.1.0"
2118 },
2119 "decisionTimestamp": "2026-05-21T00:00:01Z",
2120 "decisionLatencyMs": 0
2121 }
2122 })
2123 .to_string();
2124 let v: super::CredentialSubject = serde_json::from_str(&s)
2125 .expect("legacy payload without appleAurAcceptance must deserialize cleanly");
2126 std::assert!(
2127 v.apple_aur_acceptance.is_none(),
2128 "apple_aur_acceptance must be None when absent from legacy wire payload"
2129 );
2130 }
2131
2132 #[test]
2135 fn envelope_proofs_round_trips_the_single_object_wire_form() {
2136 let single = super::EnvelopeProofs::Single(sample_proof());
2141 let s = serde_json::to_string(&single).unwrap();
2142 std::assert!(
2143 s.starts_with('{'),
2144 "the single form must serialize as a JSON OBJECT, not an array: {}",
2145 s
2146 );
2147 let back: super::EnvelopeProofs = serde_json::from_str(&s).unwrap();
2148 std::assert_eq!(single, back);
2149 }
2150
2151 #[test]
2152 fn envelope_proofs_round_trips_the_array_wire_form() {
2153 let chain = super::EnvelopeProofs::Chain(std::vec![
2159 sample_principal_proof(),
2160 sample_notary_countersignature(),
2161 ]);
2162 let s = serde_json::to_string(&chain).unwrap();
2163 std::assert!(
2164 s.starts_with('['),
2165 "the chain form must serialize as a JSON ARRAY: {}",
2166 s
2167 );
2168 let back: super::EnvelopeProofs = serde_json::from_str(&s).unwrap();
2169 std::assert_eq!(chain, back);
2170 }
2171
2172 #[test]
2173 fn envelope_with_absent_additive_fields_emits_none_of_their_keys() {
2174 let json = serde_json::to_string(&sample_envelope()).unwrap();
2179 std::assert!(
2180 !json.contains("attestationMode"),
2181 "absent attestationMode must not appear on the wire: {}",
2182 json
2183 );
2184 std::assert!(
2185 !json.contains("delegationMandate\""),
2186 "absent embedded delegationMandate must not appear on the wire: {}",
2187 json
2188 );
2189 std::assert!(
2190 !json.contains("previousProof"),
2191 "absent previousProof must not appear on the wire: {}",
2192 json
2193 );
2194 std::assert!(
2195 !json.contains("\"id\":null"),
2196 "absent proof id must be omitted, never emitted as null: {}",
2197 json
2198 );
2199 }
2200
2201 #[test]
2202 fn all_returns_every_proof_for_both_wire_forms() {
2203 std::assert_eq!(
2207 super::EnvelopeProofs::Single(sample_proof()).all().len(),
2208 1
2209 );
2210 std::assert_eq!(
2211 super::EnvelopeProofs::Chain(std::vec![
2212 sample_principal_proof(),
2213 sample_notary_countersignature(),
2214 ])
2215 .all()
2216 .len(),
2217 2
2218 );
2219 }
2220
2221 #[test]
2222 fn principal_is_none_for_a_single_proof() {
2223 std::assert!(
2227 super::EnvelopeProofs::Single(sample_proof())
2228 .principal()
2229 .is_none()
2230 );
2231 }
2232
2233 #[test]
2234 fn notary_is_the_lone_proof_and_the_tail_of_a_chain() {
2235 let single = super::EnvelopeProofs::Single(sample_proof());
2238 std::assert_eq!(
2239 single.notary().map(|p| p.proof_purpose.as_str()),
2240 std::option::Option::Some("assertionMethod")
2241 );
2242 let chain = super::EnvelopeProofs::Chain(std::vec![
2243 sample_principal_proof(),
2244 sample_notary_countersignature(),
2245 ]);
2246 std::assert_eq!(
2247 chain.notary().map(|p| p.proof_purpose.as_str()),
2248 std::option::Option::Some("authentication")
2249 );
2250 }
2251
2252 #[test]
2253 fn accessors_refuse_to_name_proofs_inside_a_malformed_chain() {
2254 let over_long = super::EnvelopeProofs::Chain(std::vec![
2260 sample_principal_proof(),
2261 sample_notary_countersignature(),
2262 sample_notary_countersignature(),
2263 ]);
2264 std::assert!(over_long.principal().is_none());
2265 std::assert!(over_long.notary().is_none());
2266 }
2267
2268 #[test]
2269 fn is_chain_reports_the_array_form_at_any_length() {
2270 std::assert!(
2275 super::EnvelopeProofs::Chain(std::vec![sample_principal_proof()]).is_chain()
2276 );
2277 std::assert!(!super::EnvelopeProofs::Single(sample_proof()).is_chain());
2278 }
2279
2280 #[test]
2281 fn absent_attestation_mode_resolves_to_notary_attested() {
2282 std::assert_eq!(
2287 sample_policy().effective_attestation_mode(),
2288 super::AttestationMode::NotaryAttested
2289 );
2290 }
2291
2292 #[test]
2293 fn declared_attestation_mode_is_returned_verbatim() {
2294 let mut policy = sample_policy();
2298 policy.attestation_mode = std::option::Option::Some(super::AttestationMode::PrincipalSigned);
2299 std::assert_eq!(
2300 policy.effective_attestation_mode(),
2301 super::AttestationMode::PrincipalSigned
2302 );
2303 }
2304
2305 #[test]
2306 fn attestation_mode_wire_spelling_matches_the_closed_enum() {
2307 std::assert_eq!(
2312 serde_json::to_string(&super::AttestationMode::PrincipalSigned).unwrap(),
2313 "\"PrincipalSigned\""
2314 );
2315 std::assert_eq!(
2316 serde_json::to_string(&super::AttestationMode::NotaryAttested).unwrap(),
2317 "\"NotaryAttested\""
2318 );
2319 std::assert_eq!(super::AttestationMode::PrincipalSigned.label(), "PrincipalSigned");
2320 std::assert_eq!(super::AttestationMode::NotaryAttested.label(), "NotaryAttested");
2321 }
2322
2323 #[test]
2324 fn envelope_parses_a_chain_and_the_principal_signed_label_from_the_wire() {
2325 let mut envelope = sample_envelope();
2330 envelope.credential_subject.policy.attestation_mode =
2331 std::option::Option::Some(super::AttestationMode::PrincipalSigned);
2332 envelope.proof = super::EnvelopeProofs::Chain(std::vec![
2333 sample_principal_proof(),
2334 sample_notary_countersignature(),
2335 ]);
2336 let s = serde_json::to_string(&envelope).unwrap();
2337 let back: super::NotarizationEnvelope =
2338 serde_json::from_str(&s).expect("a PrincipalSigned chain envelope must parse");
2339 std::assert_eq!(envelope, back);
2340 }
2341}
2342
2343#[cfg(test)]
2344mod act_classification_tests {
2345 fn golden() -> std::string::String {
2354 std::include_str!("../../../../examples/slack_reply_envelope.json").to_string()
2355 }
2356
2357 #[test]
2358 fn an_envelope_without_the_claim_round_trips_byte_identically() {
2359 let raw = golden();
2368 let parsed: super::NotarizationEnvelope =
2369 serde_json::from_str(&raw).expect("a published golden must parse");
2370 std::assert!(
2371 parsed.credential_subject.act_classification.is_none(),
2372 "a published golden predates this field and must carry no claim"
2373 );
2374 let reserialized = serde_json::to_string(&parsed).expect("it serializes");
2375 std::assert!(
2376 !reserialized.contains("actClassification"),
2377 "an absent claim must not appear in output at all: {}",
2378 reserialized
2379 );
2380 }
2381
2382 #[test]
2383 fn a_classification_round_trips_through_serde() {
2384 let claim = super::ActClassification {
2388 vocabularies: std::vec![super::VocabularyRef {
2389 name: "aph_guardrails".to_string(),
2390 version: "0.1.0-alpha.1".to_string(),
2391 digest: "sha256-y6E/EGldCz2ogpVB7wlnS5orbnAjcCpoUBaDietJmXA=".to_string(),
2392 }],
2393 labels: std::vec![
2394 "APH_ACT_ACCESS/ACCESS_GRANT".parse().expect("a qualified label parses"),
2395 ],
2396 };
2397 let json = serde_json::to_string(&claim).expect("it serializes");
2398 std::assert!(
2399 json.contains("\"APH_ACT_ACCESS/ACCESS_GRANT\""),
2400 "a label rides the wire as its qualified STRING, not as a struct: {}",
2401 json
2402 );
2403 let back: super::ActClassification =
2404 serde_json::from_str(&json).expect("it round-trips");
2405 std::assert_eq!(claim, back);
2406 }
2407
2408 #[test]
2409 fn a_claim_citing_no_vocabulary_is_refused_at_parse() {
2410 let refused = serde_json::from_str::<super::ActClassification>(
2418 r#"{"vocabularies":[],"labels":["A/B"]}"#,
2419 );
2420 std::assert!(refused.is_err(), "a claim citing no vocabulary must be refused");
2421 std::assert!(
2422 refused.unwrap_err().to_string().contains("claims nothing"),
2423 "the refusal must say why an empty citation is meaningless"
2424 );
2425 }
2426
2427 #[test]
2428 fn an_unqualified_label_is_refused_at_parse() {
2429 let refused = "ACCESS_GRANT".parse::<super::ActLabel>();
2434 std::assert!(refused.is_err(), "an unqualified label must be refused");
2435 let message = refused.unwrap_err();
2436 std::assert!(
2437 message.contains("family-qualified"),
2438 "the refusal must say what shape was expected: {}",
2439 message
2440 );
2441 }
2442
2443 #[test]
2444 fn a_label_with_an_empty_side_or_two_separators_is_refused() {
2445 for raw in ["/ACCESS_GRANT", "APH_ACT_ACCESS/", "A/B/C"] {
2448 std::assert!(
2449 raw.parse::<super::ActLabel>().is_err(),
2450 "`{}` is structurally meaningless and must be refused",
2451 raw
2452 );
2453 }
2454 }
2455
2456 #[test]
2457 fn a_third_party_spelling_is_accepted_because_only_structure_is_ours() {
2458 let parsed: super::ActLabel = "acme.finance/wire-transfer.initiate"
2464 .parse()
2465 .expect("an unfamiliar but well-formed spelling must be accepted");
2466 std::assert_eq!(parsed.family(), "acme.finance");
2467 std::assert_eq!(parsed.label(), "wire-transfer.initiate");
2468 }
2469}
2470
2471#[cfg(test)]
2472mod closed_vocabulary_tests {
2473 #[test]
2481 fn every_channel_kind_wire_spelling_round_trips() {
2482 for kind in super::ChannelKind::ALL {
2488 let back: super::ChannelKind = kind
2489 .label()
2490 .parse()
2491 .expect("every published wire spelling must parse");
2492 std::assert_eq!(kind, back);
2493 }
2494 }
2495
2496 #[test]
2497 fn every_content_class_wire_spelling_round_trips() {
2498 for class in super::ContentClass::ALL {
2502 let back: super::ContentClass = class
2503 .label()
2504 .parse()
2505 .expect("every published wire spelling must parse");
2506 std::assert_eq!(class, back);
2507 }
2508 }
2509
2510 #[test]
2511 fn every_policy_decision_wire_spelling_round_trips() {
2512 for decision in super::PolicyDecision::ALL {
2518 let back: super::PolicyDecision = decision
2519 .label()
2520 .parse()
2521 .expect("every published wire spelling must parse");
2522 std::assert_eq!(decision, back);
2523 }
2524 }
2525
2526 #[test]
2527 fn an_unrecognized_policy_decision_is_refused_and_names_the_closed_set() {
2528 let err = "Sometimes"
2534 .parse::<super::PolicyDecision>()
2535 .expect_err("a value outside the closed set must be refused");
2536 std::assert!(err.contains("closed set"), "must name the closed set: {err}");
2537 std::assert!(err.contains("AskEveryTime"), "must list the members: {err}");
2538 }
2539
2540 #[test]
2541 fn an_unrecognized_channel_kind_is_refused_and_names_the_closed_set() {
2542 let err = "carrier_pigeon"
2551 .parse::<super::ChannelKind>()
2552 .expect_err("a value outside the closed set must be refused");
2553 std::assert!(err.contains("closed set"), "error must name the closed set: {err}");
2554 std::assert!(err.contains("google_chat"), "error must list the members: {err}");
2555 }
2556
2557 #[test]
2558 fn an_unrecognized_content_class_is_refused() {
2559 std::assert!("Digest".parse::<super::ContentClass>().is_err());
2564 std::assert!("reply".parse::<super::ContentClass>().is_err(), "case matters: wire is `Reply`");
2565 }
2566
2567 #[test]
2568 fn the_closed_sets_hold_their_declared_census() {
2569 std::assert_eq!(super::ChannelKind::ALL.len(), 9);
2575 std::assert_eq!(super::ContentClass::ALL.len(), 8);
2576 std::assert_eq!(super::PolicyDecision::ALL.len(), 3);
2577 }
2578}
2579
2580#[cfg(test)]
2581mod closed_vocabulary_deserialization_tests {
2582 fn golden() -> std::string::String {
2593 std::include_str!("../tests/golden/slack_reply_envelope.json").to_string()
2594 }
2595
2596 #[test]
2597 fn a_conformant_envelope_still_parses_after_the_types_closed() {
2598 let parsed: std::result::Result<super::NotarizationEnvelope, _> =
2602 serde_json::from_str(&golden());
2603 std::assert!(parsed.is_ok(), "the slack golden must still parse: {parsed:?}");
2604 }
2605
2606 #[test]
2607 fn an_envelope_naming_an_unrecognized_channel_is_refused_at_parse() {
2608 let doc = golden().replace("\"kind\": \"slack\"", "\"kind\": \"carrier_pigeon\"");
2614 let parsed: std::result::Result<super::NotarizationEnvelope, _> =
2615 serde_json::from_str(&doc);
2616 let err = parsed.expect_err("an unrecognized channel kind must be refused at parse");
2617 let msg = err.to_string();
2618 std::assert!(msg.contains("closed set"), "must name the closed set: {msg}");
2619 }
2620
2621 #[test]
2622 fn an_envelope_naming_an_unrecognized_content_class_is_refused_at_parse() {
2623 let doc = golden().replace("\"contentClass\": \"Reply\"", "\"contentClass\": \"Digest\"");
2624 let parsed: std::result::Result<super::NotarizationEnvelope, _> =
2625 serde_json::from_str(&doc);
2626 std::assert!(parsed.is_err(), "an unrecognized content class must be refused at parse");
2627 }
2628
2629 #[test]
2630 fn the_wire_spellings_survive_a_round_trip_through_serde() {
2631 for kind in super::ChannelKind::ALL {
2635 let json = serde_json::to_string(&kind).expect("a channel kind serializes");
2636 std::assert_eq!(json, std::format!("\"{}\"", kind.label()));
2637 let back: super::ChannelKind = serde_json::from_str(&json).expect("and parses back");
2638 std::assert_eq!(kind, back);
2639 }
2640 for class in super::ContentClass::ALL {
2641 let json = serde_json::to_string(&class).expect("a content class serializes");
2642 std::assert_eq!(json, std::format!("\"{}\"", class.label()));
2643 let back: super::ContentClass = serde_json::from_str(&json).expect("and parses back");
2644 std::assert_eq!(class, back);
2645 }
2646 }
2647}