1#[allow(deprecated)]
6use crate::{
7 AuthorityGrant, CredentialSubject, CredentialSubjectAuthority, CredentialSubjectBasic,
8 CredentialSubjectEndorsement, CredentialSubjectMembership, CredentialSubjectRCard,
9 CredentialSubjectWitness, DTGCommon, DTGCredential, DTGCredentialError, DTGCredentialType,
10 WitnessContext,
11};
12use chrono::{DateTime, Utc};
13use serde_json::Value;
14
15impl DTGCredential {
16 pub fn new_vmc(
40 issuer: String,
41 subject: String,
42 valid_from: DateTime<Utc>,
43 valid_until: Option<DateTime<Utc>>,
44 personhood: bool,
45 ) -> Self {
46 let mut vmc = DTGCommon {
47 issuer,
48 valid_from,
49 valid_until,
50 credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
51 id: subject,
52 digest: None,
53 }),
54 ..Default::default()
55 };
56
57 vmc.type_.push(DTGCredentialType::Membership.to_string());
58
59 if personhood {
60 vmc.type_.push("PersonhoodCredential".to_string());
61 }
62
63 DTGCredential {
64 credential: vmc,
65 type_: DTGCredentialType::Membership,
66 version: crate::W3CVCVersion::V2_0,
67 }
68 }
69
70 pub fn new_member_vmc(
108 grant: &Value,
109 valid_from: DateTime<Utc>,
110 valid_until: Option<DateTime<Utc>>,
111 ) -> Result<Self, DTGCredentialError> {
112 let object = grant
113 .as_object()
114 .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("not a JSON object".into()))?;
115
116 let is_membership = object
117 .get("type")
118 .and_then(Value::as_array)
119 .is_some_and(|types| {
120 types
121 .iter()
122 .filter_map(Value::as_str)
123 .any(|t| t == "MembershipCredential")
124 });
125 if !is_membership {
126 return Err(DTGCredentialError::NotAMembershipGrant(
127 "`type` does not include `MembershipCredential`".into(),
128 ));
129 }
130
131 let subject = object
132 .get("credentialSubject")
133 .and_then(Value::as_object)
134 .ok_or_else(|| {
135 DTGCredentialError::NotAMembershipGrant("no `credentialSubject`".into())
136 })?;
137
138 if subject.contains_key("digest") {
139 return Err(DTGCredentialError::NotAMembershipGrant(
140 "the credential carries a `digest`, so it is itself a member-issued \
141 acknowledgement rather than a community-issued grant"
142 .into(),
143 ));
144 }
145
146 let member = subject
151 .get("id")
152 .and_then(Value::as_str)
153 .ok_or_else(|| {
154 DTGCredentialError::NotAMembershipGrant("no `credentialSubject.id`".into())
155 })?
156 .to_string();
157
158 let community = object
160 .get("issuer")
161 .and_then(|i| {
162 i.as_str()
163 .map(str::to_string)
164 .or_else(|| i.get("id").and_then(Value::as_str).map(str::to_string))
165 })
166 .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("no `issuer`".into()))?;
167
168 let mut vmc = DTGCommon {
169 issuer: member,
170 valid_from,
171 valid_until,
172 credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
173 id: community,
174 digest: Some(crate::digest_json(grant)?),
175 }),
176 ..Default::default()
177 };
178
179 vmc.type_.push(DTGCredentialType::Membership.to_string());
180
181 Ok(DTGCredential {
182 credential: vmc,
183 type_: DTGCredentialType::Membership,
184 version: crate::W3CVCVersion::V2_0,
185 })
186 }
187
188 pub fn new_vrc(
194 issuer: String,
195 subject: String,
196 valid_from: DateTime<Utc>,
197 valid_until: Option<DateTime<Utc>>,
198 ) -> Self {
199 let mut vrc = DTGCommon {
200 issuer,
201 valid_from,
202 valid_until,
203 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
204 ..Default::default()
205 };
206
207 vrc.type_.push(DTGCredentialType::Relationship.to_string());
208
209 DTGCredential {
210 credential: vrc,
211 type_: DTGCredentialType::Relationship,
212 version: crate::W3CVCVersion::V2_0,
213 }
214 }
215
216 pub fn new_vic(
222 issuer: String,
223 subject: String,
224 valid_from: DateTime<Utc>,
225 valid_until: Option<DateTime<Utc>>,
226 ) -> Self {
227 let mut vic = DTGCommon {
228 issuer,
229 valid_from,
230 valid_until,
231 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
232 ..Default::default()
233 };
234
235 vic.type_.push(DTGCredentialType::Invitation.to_string());
236
237 DTGCredential {
238 credential: vic,
239 type_: DTGCredentialType::Invitation,
240 version: crate::W3CVCVersion::V2_0,
241 }
242 }
243
244 pub fn new_vac(
255 issuer: String,
256 subject: String,
257 scope: String,
258 actions: Vec<String>,
259 valid_from: DateTime<Utc>,
260 valid_until: Option<DateTime<Utc>>,
261 ) -> Result<Self, DTGCredentialError> {
262 if actions.is_empty() {
263 return Err(DTGCredentialError::EmptyAuthorityActions);
264 }
265 let mut vac = DTGCommon {
266 issuer,
267 valid_from,
268 valid_until,
269 credential_subject: CredentialSubject::Authority(CredentialSubjectAuthority {
270 id: subject,
271 authority: AuthorityGrant {
272 scope,
273 actions,
274 parent: None,
275 audience: None,
276 },
277 }),
278 ..Default::default()
279 };
280
281 vac.type_.push(DTGCredentialType::Authority.to_string());
282
283 Ok(DTGCredential {
284 credential: vac,
285 type_: DTGCredentialType::Authority,
286 version: crate::W3CVCVersion::V2_0,
287 })
288 }
289
290 pub fn attenuate(
310 &self,
311 subject: String,
312 actions: Vec<String>,
313 valid_from: DateTime<Utc>,
314 valid_until: Option<DateTime<Utc>>,
315 audience: Option<String>,
316 ) -> Result<Self, DTGCredentialError> {
317 let parent_grant = self
318 .credential()
319 .authority()
320 .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
321
322 let parent_id = self
323 .id()
324 .ok_or(DTGCredentialError::AttenuationParentHasNoId)?
325 .to_string();
326
327 if actions.is_empty() {
328 return Err(DTGCredentialError::EmptyAuthorityActions);
329 }
330 for action in &actions {
331 if !parent_grant.actions.contains(action) {
332 return Err(DTGCredentialError::AttenuationWidens(format!(
333 "action `{action}` is not conferred by the parent"
334 )));
335 }
336 }
337 if let (Some(until), Some(parent_until)) = (valid_until, self.credential().valid_until())
338 && until > parent_until
339 {
340 return Err(DTGCredentialError::AttenuationWidens(format!(
341 "validUntil {until} is beyond the parent's {parent_until}"
342 )));
343 }
344
345 let mut vac = DTGCommon {
346 issuer: self.credential().subject().to_string(),
348 valid_from,
349 valid_until,
350 credential_subject: CredentialSubject::Authority(CredentialSubjectAuthority {
351 id: subject,
352 authority: AuthorityGrant {
353 scope: parent_grant.scope.clone(),
355 actions,
356 parent: Some(parent_id),
357 audience,
358 },
359 }),
360 ..Default::default()
361 };
362
363 vac.type_.push(DTGCredentialType::Authority.to_string());
364
365 Ok(DTGCredential {
366 credential: vac,
367 type_: DTGCredentialType::Authority,
368 version: crate::W3CVCVersion::V2_0,
369 })
370 }
371
372 pub fn new_vdc(
380 issuer: String,
381 subject: String,
382 valid_from: DateTime<Utc>,
383 valid_until: Option<DateTime<Utc>>,
384 ) -> Self {
385 let mut vdc = DTGCommon {
386 issuer,
387 valid_from,
388 valid_until,
389 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
390 ..Default::default()
391 };
392
393 vdc.type_.push(DTGCredentialType::Delegation.to_string());
394
395 DTGCredential {
396 credential: vdc,
397 type_: DTGCredentialType::Delegation,
398 version: crate::W3CVCVersion::V2_0,
399 }
400 }
401
402 pub fn new_vpc(
408 issuer: String,
409 subject: String,
410 valid_from: DateTime<Utc>,
411 valid_until: Option<DateTime<Utc>>,
412 ) -> Self {
413 let mut vpc = DTGCommon {
414 issuer,
415 valid_from,
416 valid_until,
417 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
418 ..Default::default()
419 };
420
421 vpc.type_.push(DTGCredentialType::Persona.to_string());
422
423 DTGCredential {
424 credential: vpc,
425 type_: DTGCredentialType::Persona,
426 version: crate::W3CVCVersion::V2_0,
427 }
428 }
429
430 pub fn new_vec(
437 issuer: String,
438 subject: String,
439 valid_from: DateTime<Utc>,
440 valid_until: Option<DateTime<Utc>>,
441 endorsement: Value,
442 ) -> Self {
443 let mut vec = DTGCommon {
444 issuer,
445 valid_from,
446 valid_until,
447 credential_subject: CredentialSubject::Endorsement(CredentialSubjectEndorsement {
448 id: subject,
449 endorsement,
450 }),
451 ..Default::default()
452 };
453
454 vec.type_.push(DTGCredentialType::Endorsement.to_string());
455
456 DTGCredential {
457 credential: vec,
458 type_: DTGCredentialType::Endorsement,
459 version: crate::W3CVCVersion::V2_0,
460 }
461 }
462
463 pub fn new_vwc(
480 issuer: String,
481 subject: String,
482 valid_from: DateTime<Utc>,
483 valid_until: Option<DateTime<Utc>>,
484 task_context: String,
485 digest: Option<String>,
486 witness_context: Option<WitnessContext>,
487 ) -> Self {
488 let mut vwc = DTGCommon {
489 issuer,
490 valid_from,
491 valid_until,
492 task_context: Some(task_context),
493 credential_subject: CredentialSubject::Witness(CredentialSubjectWitness {
494 id: subject,
495 digest,
496 witness_context,
497 }),
498 ..Default::default()
499 };
500
501 vwc.type_.push(DTGCredentialType::Witness.to_string());
502
503 DTGCredential {
504 credential: vwc,
505 type_: DTGCredentialType::Witness,
506 version: crate::W3CVCVersion::V2_0,
507 }
508 }
509
510 #[deprecated(
517 since = "0.2.0",
518 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
519 It was removed from the DTG Core Credentials specification in Working Draft 01 \
520 and will be defined by the planned DTG Verifiable Data Structures specification. \
521 This constructor will be removed in a future release."
522 )]
523 #[allow(deprecated)]
524 pub fn new_rcard(
525 issuer: String,
526 subject: String,
527 valid_from: DateTime<Utc>,
528 valid_until: Option<DateTime<Utc>>,
529 card: Value,
530 ) -> Self {
531 let mut rcard = DTGCommon {
532 issuer,
533 valid_from,
534 valid_until,
535 credential_subject: CredentialSubject::RCard(CredentialSubjectRCard {
536 id: subject,
537 card,
538 }),
539 ..Default::default()
540 };
541
542 rcard.type_.push(DTGCredentialType::RCard.to_string());
543
544 DTGCredential {
545 credential: rcard,
546 type_: DTGCredentialType::RCard,
547 version: crate::W3CVCVersion::V2_0,
548 }
549 }
550
551 pub fn with_id(mut self, id: impl Into<String>) -> Self {
577 self.credential.id = Some(id.into());
578 self
579 }
580
581 pub fn set_id(&mut self, id: impl Into<String>) {
586 self.credential.id = Some(id.into());
587 }
588}
589
590#[cfg(test)]
591#[allow(deprecated)]
592mod tests {
593 use crate::{DTGCredential, WitnessContext};
594 use chrono::{DateTime, Utc};
595 use serde_json::json;
596
597 #[test]
598 fn test_vmc_serialization() {
599 let vmc = DTGCredential::new_vmc(
600 "did:example:issuer".to_string(),
601 "did:example:subject".to_string(),
602 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
603 .unwrap()
604 .with_timezone(&Utc),
605 None,
606 false,
607 );
608
609 let txt = serde_json::to_string_pretty(&vmc).unwrap();
610 let sample = r#"{
611 "@context": [
612 "https://www.w3.org/ns/credentials/v2",
613 "https://firstperson.network/credentials/dtg/v1"
614 ],
615 "type": [
616 "VerifiableCredential",
617 "DTGCredential",
618 "MembershipCredential"
619 ],
620 "issuer": "did:example:issuer",
621 "validFrom": "2025-12-11T00:00:00Z",
622 "credentialSubject": {
623 "id": "did:example:subject"
624 }
625}"#;
626
627 assert_eq!(txt, sample);
628 }
629
630 #[test]
631 fn test_vmc_phc_serialization() {
632 let vmc = DTGCredential::new_vmc(
633 "did:example:issuer".to_string(),
634 "did:example:subject".to_string(),
635 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
636 .unwrap()
637 .with_timezone(&Utc),
638 None,
639 true,
640 );
641
642 let txt = serde_json::to_string_pretty(&vmc).unwrap();
643 let sample = r#"{
644 "@context": [
645 "https://www.w3.org/ns/credentials/v2",
646 "https://firstperson.network/credentials/dtg/v1"
647 ],
648 "type": [
649 "VerifiableCredential",
650 "DTGCredential",
651 "MembershipCredential",
652 "PersonhoodCredential"
653 ],
654 "issuer": "did:example:issuer",
655 "validFrom": "2025-12-11T00:00:00Z",
656 "credentialSubject": {
657 "id": "did:example:subject"
658 }
659}"#;
660
661 assert_eq!(txt, sample);
662 }
663 #[test]
666 fn test_vmc_without_id_omits_the_property() {
667 let vmc = DTGCredential::new_vmc(
668 "did:example:issuer".to_string(),
669 "did:example:subject".to_string(),
670 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
671 .unwrap()
672 .with_timezone(&Utc),
673 None,
674 false,
675 );
676
677 assert_eq!(vmc.id(), None);
678 let value: serde_json::Value = serde_json::to_value(&vmc).unwrap();
679 assert!(
680 value.get("id").is_none(),
681 "an unset id must not appear on the wire at all: {value}"
682 );
683 }
684
685 #[test]
689 fn test_vmc_with_id_serialization() {
690 let vmc = DTGCredential::new_vmc(
691 "did:example:issuer".to_string(),
692 "did:example:subject".to_string(),
693 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
694 .unwrap()
695 .with_timezone(&Utc),
696 None,
697 false,
698 )
699 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
700
701 let txt = serde_json::to_string_pretty(&vmc).unwrap();
702 let sample = r#"{
703 "@context": [
704 "https://www.w3.org/ns/credentials/v2",
705 "https://firstperson.network/credentials/dtg/v1"
706 ],
707 "type": [
708 "VerifiableCredential",
709 "DTGCredential",
710 "MembershipCredential"
711 ],
712 "id": "urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff",
713 "issuer": "did:example:issuer",
714 "validFrom": "2025-12-11T00:00:00Z",
715 "credentialSubject": {
716 "id": "did:example:subject"
717 }
718}"#;
719
720 assert_eq!(txt, sample);
721 }
722
723 #[test]
727 fn test_id_round_trips_through_deserialization() {
728 let vmc = DTGCredential::new_vmc(
729 "did:example:issuer".to_string(),
730 "did:example:subject".to_string(),
731 Utc::now(),
732 None,
733 false,
734 )
735 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
736
737 let txt = serde_json::to_string(&vmc).unwrap();
738 let parsed: DTGCredential = serde_json::from_str(&txt).unwrap();
739 assert_eq!(
740 parsed.id(),
741 Some("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff")
742 );
743 }
744
745 #[test]
748 fn test_missing_id_deserializes_as_none() {
749 let parsed: DTGCredential = serde_json::from_str(
750 r#"{
751 "@context": ["https://www.w3.org/ns/credentials/v2"],
752 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
753 "issuer": "did:example:issuer",
754 "validFrom": "2025-12-11T00:00:00Z",
755 "credentialSubject": { "id": "did:example:subject" }
756 }"#,
757 )
758 .unwrap();
759 assert_eq!(parsed.id(), None);
760 }
761
762 #[test]
764 fn test_set_id_matches_with_id() {
765 let build = || {
766 DTGCredential::new_vrc(
767 "did:example:issuer".to_string(),
768 "did:example:subject".to_string(),
769 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
770 .unwrap()
771 .with_timezone(&Utc),
772 None,
773 )
774 };
775 let mut in_place = build();
776 in_place.set_id("urn:uuid:abc");
777 assert_eq!(
778 serde_json::to_value(&in_place).unwrap(),
779 serde_json::to_value(build().with_id("urn:uuid:abc")).unwrap()
780 );
781 }
782
783 #[test]
784 fn test_vrc_serialization() {
785 let vrc = DTGCredential::new_vrc(
786 "did:example:issuer".to_string(),
787 "did:example:subject".to_string(),
788 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
789 .unwrap()
790 .with_timezone(&Utc),
791 None,
792 );
793
794 let txt = serde_json::to_string_pretty(&vrc).unwrap();
795 let sample = r#"{
796 "@context": [
797 "https://www.w3.org/ns/credentials/v2",
798 "https://firstperson.network/credentials/dtg/v1"
799 ],
800 "type": [
801 "VerifiableCredential",
802 "DTGCredential",
803 "RelationshipCredential"
804 ],
805 "issuer": "did:example:issuer",
806 "validFrom": "2025-12-11T00:00:00Z",
807 "credentialSubject": {
808 "id": "did:example:subject"
809 }
810}"#;
811
812 assert_eq!(txt, sample);
813 }
814
815 #[test]
816 fn test_vic_serialization() {
817 let vic = DTGCredential::new_vic(
818 "did:example:issuer".to_string(),
819 "did:example:subject".to_string(),
820 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
821 .unwrap()
822 .with_timezone(&Utc),
823 None,
824 );
825
826 let txt = serde_json::to_string_pretty(&vic).unwrap();
827 let sample = r#"{
828 "@context": [
829 "https://www.w3.org/ns/credentials/v2",
830 "https://firstperson.network/credentials/dtg/v1"
831 ],
832 "type": [
833 "VerifiableCredential",
834 "DTGCredential",
835 "InvitationCredential"
836 ],
837 "issuer": "did:example:issuer",
838 "validFrom": "2025-12-11T00:00:00Z",
839 "credentialSubject": {
840 "id": "did:example:subject"
841 }
842}"#;
843
844 assert_eq!(txt, sample);
845 }
846
847 #[test]
848 fn test_vpc_serialization() {
849 let vpc = DTGCredential::new_vpc(
850 "did:example:issuer".to_string(),
851 "did:example:subject".to_string(),
852 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
853 .unwrap()
854 .with_timezone(&Utc),
855 None,
856 );
857
858 let txt = serde_json::to_string_pretty(&vpc).unwrap();
859 let sample = r#"{
860 "@context": [
861 "https://www.w3.org/ns/credentials/v2",
862 "https://firstperson.network/credentials/dtg/v1"
863 ],
864 "type": [
865 "VerifiableCredential",
866 "DTGCredential",
867 "PersonaCredential"
868 ],
869 "issuer": "did:example:issuer",
870 "validFrom": "2025-12-11T00:00:00Z",
871 "credentialSubject": {
872 "id": "did:example:subject"
873 }
874}"#;
875
876 assert_eq!(txt, sample);
877 }
878
879 #[test]
880 fn test_vec_serialization() {
881 let vec = DTGCredential::new_vec(
882 "did:example:issuer".to_string(),
883 "did:example:subject".to_string(),
884 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
885 .unwrap()
886 .with_timezone(&Utc),
887 None,
888 json!({
889 "type": "SkillEndorsement",
890 "name": "Software Development",
891 "competencyLevel": "expert"
892 }),
893 );
894
895 let txt = serde_json::to_string_pretty(&vec).unwrap();
896 let sample = r#"{
897 "@context": [
898 "https://www.w3.org/ns/credentials/v2",
899 "https://firstperson.network/credentials/dtg/v1"
900 ],
901 "type": [
902 "VerifiableCredential",
903 "DTGCredential",
904 "EndorsementCredential"
905 ],
906 "issuer": "did:example:issuer",
907 "validFrom": "2025-12-11T00:00:00Z",
908 "credentialSubject": {
909 "id": "did:example:subject",
910 "endorsement": {
911 "competencyLevel": "expert",
912 "name": "Software Development",
913 "type": "SkillEndorsement"
914 }
915 }
916}"#;
917
918 assert_eq!(txt, sample);
919 }
920
921 #[test]
922 fn test_vwc_serialization() {
923 let vwc = DTGCredential::new_vwc(
924 "did:example:issuer".to_string(),
925 "did:example:subject".to_string(),
926 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
927 .unwrap()
928 .with_timezone(&Utc),
929 None,
930 "thread-abc-123".to_string(),
931 Some("zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw".to_string()),
932 Some(WitnessContext {
933 event: Some("EthDenver 2024".to_string()),
934 session_id: Some("session-8822-nonce".to_string()),
935 method: Some("in-person-proximity".to_string()),
936 }),
937 );
938
939 let txt = serde_json::to_string_pretty(&vwc).unwrap();
940
941 let sample = r#"{
942 "@context": [
943 "https://www.w3.org/ns/credentials/v2",
944 "https://firstperson.network/credentials/dtg/v1"
945 ],
946 "type": [
947 "VerifiableCredential",
948 "DTGCredential",
949 "WitnessCredential"
950 ],
951 "issuer": "did:example:issuer",
952 "validFrom": "2025-12-11T00:00:00Z",
953 "taskContext": "thread-abc-123",
954 "credentialSubject": {
955 "id": "did:example:subject",
956 "digest": "zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw",
957 "witnessContext": {
958 "event": "EthDenver 2024",
959 "sessionId": "session-8822-nonce",
960 "method": "in-person-proximity"
961 }
962 }
963}"#;
964
965 assert_eq!(txt, sample);
966 }
967
968 #[test]
969 fn test_rcard_serialization() {
970 let rcard = DTGCredential::new_rcard(
971 "did:example:issuer".to_string(),
972 "did:example:subject".to_string(),
973 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
974 .unwrap()
975 .with_timezone(&Utc),
976 None,
977 json!([
978 "vcard",
979 [
980 ["fn", {}, "text", "Alice Smith"],
981 ["email", {}, "text", "alice@example.com"]
982 ]
983 ]),
984 );
985
986 let txt = serde_json::to_string_pretty(&rcard).unwrap();
987
988 let sample = r#"{
989 "@context": [
990 "https://www.w3.org/ns/credentials/v2",
991 "https://firstperson.network/credentials/dtg/v1"
992 ],
993 "type": [
994 "VerifiableCredential",
995 "DTGCredential",
996 "RCardCredential"
997 ],
998 "issuer": "did:example:issuer",
999 "validFrom": "2025-12-11T00:00:00Z",
1000 "credentialSubject": {
1001 "id": "did:example:subject",
1002 "card": [
1003 "vcard",
1004 [
1005 [
1006 "fn",
1007 {},
1008 "text",
1009 "Alice Smith"
1010 ],
1011 [
1012 "email",
1013 {},
1014 "text",
1015 "alice@example.com"
1016 ]
1017 ]
1018 ]
1019 }
1020}"#;
1021
1022 assert_eq!(txt, sample);
1023 }
1024}