Skip to main content

dtg_credentials/
create.rs

1/*!
2*   Builder methods for creating new entities.
3*/
4
5#[allow(deprecated)]
6use crate::{
7    CredentialSubject, CredentialSubjectBasic, CredentialSubjectEndorsement,
8    CredentialSubjectRCard, CredentialSubjectWitness, DTGCommon, DTGCredential, DTGCredentialType,
9    WitnessContext,
10};
11use chrono::{DateTime, Utc};
12use serde_json::Value;
13
14impl DTGCredential {
15    /// Creates a new Verified Memebrship Credential (VMC)
16    /// issuer: The issuer DID of the credential
17    /// subject: The DID of the subject of this credential
18    /// valid_from: The datetime from which this credential is valid
19    /// valid_until: Optional: The datetime this credential is valid until
20    /// personhood: Whether this VMC can be used as a form of Personhood Credential
21    ///             - Adds PersonhoodCredential to the type array if true
22    pub fn new_vmc(
23        issuer: String,
24        subject: String,
25        valid_from: DateTime<Utc>,
26        valid_until: Option<DateTime<Utc>>,
27        personhood: bool,
28    ) -> Self {
29        let mut vmc = DTGCommon {
30            issuer,
31            valid_from,
32            valid_until,
33            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
34            ..Default::default()
35        };
36
37        vmc.type_.push(DTGCredentialType::Membership.to_string());
38
39        if personhood {
40            vmc.type_.push("PersonhoodCredential".to_string());
41        }
42
43        DTGCredential {
44            credential: vmc,
45            type_: DTGCredentialType::Membership,
46            version: crate::W3CVCVersion::V2_0,
47        }
48    }
49
50    /// Creates a new Verified Relationship Credential (VRC)
51    /// issuer: The issuer DID of the credential
52    /// subject: The DID of the subject of this credential
53    /// valid_from: The datetime from which this credential is valid
54    /// valid_until: Optional: The datetime this credential is valid until
55    pub fn new_vrc(
56        issuer: String,
57        subject: String,
58        valid_from: DateTime<Utc>,
59        valid_until: Option<DateTime<Utc>>,
60    ) -> Self {
61        let mut vrc = DTGCommon {
62            issuer,
63            valid_from,
64            valid_until,
65            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
66            ..Default::default()
67        };
68
69        vrc.type_.push(DTGCredentialType::Relationship.to_string());
70
71        DTGCredential {
72            credential: vrc,
73            type_: DTGCredentialType::Relationship,
74            version: crate::W3CVCVersion::V2_0,
75        }
76    }
77
78    /// Creates a new Verified Invitation Credential (VIC)
79    /// issuer: The issuer DID of the credential
80    /// subject: The DID of the subject of this credential
81    /// valid_from: The datetime from which this credential is valid
82    /// valid_until: Optional: The datetime this credential is valid until
83    pub fn new_vic(
84        issuer: String,
85        subject: String,
86        valid_from: DateTime<Utc>,
87        valid_until: Option<DateTime<Utc>>,
88    ) -> Self {
89        let mut vic = DTGCommon {
90            issuer,
91            valid_from,
92            valid_until,
93            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
94            ..Default::default()
95        };
96
97        vic.type_.push(DTGCredentialType::Invitation.to_string());
98
99        DTGCredential {
100            credential: vic,
101            type_: DTGCredentialType::Invitation,
102            version: crate::W3CVCVersion::V2_0,
103        }
104    }
105
106    /// Creates a new Verified Persona Credential (VPC)
107    /// issuer: The issuer DID of the credential
108    /// subject: The DID of the subject of this credential
109    /// valid_from: The datetime from which this credential is valid
110    /// valid_until: Optional: The datetime this credential is valid until
111    pub fn new_vpc(
112        issuer: String,
113        subject: String,
114        valid_from: DateTime<Utc>,
115        valid_until: Option<DateTime<Utc>>,
116    ) -> Self {
117        let mut vpc = DTGCommon {
118            issuer,
119            valid_from,
120            valid_until,
121            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
122            ..Default::default()
123        };
124
125        vpc.type_.push(DTGCredentialType::Persona.to_string());
126
127        DTGCredential {
128            credential: vpc,
129            type_: DTGCredentialType::Persona,
130            version: crate::W3CVCVersion::V2_0,
131        }
132    }
133
134    /// Creates a new Verified Endorsement Credential (VEC)
135    /// issuer: The issuer DID of the credential
136    /// subject: The DID of the subject of this credential
137    /// valid_from: The datetime from which this credential is valid
138    /// valid_until: Optional: The datetime this credential is valid until
139    /// endorsement: The endorsement details for this credential
140    pub fn new_vec(
141        issuer: String,
142        subject: String,
143        valid_from: DateTime<Utc>,
144        valid_until: Option<DateTime<Utc>>,
145        endorsement: Value,
146    ) -> Self {
147        let mut vec = DTGCommon {
148            issuer,
149            valid_from,
150            valid_until,
151            credential_subject: CredentialSubject::Endorsement(CredentialSubjectEndorsement {
152                id: subject,
153                endorsement,
154            }),
155            ..Default::default()
156        };
157
158        vec.type_.push(DTGCredentialType::Endorsement.to_string());
159
160        DTGCredential {
161            credential: vec,
162            type_: DTGCredentialType::Endorsement,
163            version: crate::W3CVCVersion::V2_0,
164        }
165    }
166
167    /// Creates a new Verified Witness Credential (VWC)
168    /// issuer: The issuer DID of the credential - an M-DID, or the DID of a VTA acting
169    ///         according to VTC policy
170    /// subject: The DID of the observed party. For a witnessed bi-directional exchange this
171    ///          MUST be the issuer of the VRC that this VWC attests (the VRC referenced by
172    ///          `digest`), so that the two VWCs of an exchange are unambiguously bound to
173    ///          their respective directions. The witness should issue one VWC per direction.
174    /// valid_from: The datetime from which this credential is valid
175    /// valid_until: Optional: The datetime this credential is valid until
176    /// task_context: Required `threadId` of the trust task exchange the witnessing occurred in
177    /// digest: Optional Witness cryptographic hash of the witnessed VRC (prevents misuse).
178    ///         Produce this with [DTGCredential::digest_multibase] on the witnessed VRC.
179    /// witness_context: Optional Semantic context for the witness
180    pub fn new_vwc(
181        issuer: String,
182        subject: String,
183        valid_from: DateTime<Utc>,
184        valid_until: Option<DateTime<Utc>>,
185        task_context: String,
186        digest: Option<String>,
187        witness_context: Option<WitnessContext>,
188    ) -> Self {
189        let mut vwc = DTGCommon {
190            issuer,
191            valid_from,
192            valid_until,
193            task_context: Some(task_context),
194            credential_subject: CredentialSubject::Witness(CredentialSubjectWitness {
195                id: subject,
196                digest,
197                witness_context,
198            }),
199            ..Default::default()
200        };
201
202        vwc.type_.push(DTGCredentialType::Witness.to_string());
203
204        DTGCredential {
205            credential: vwc,
206            type_: DTGCredentialType::Witness,
207            version: crate::W3CVCVersion::V2_0,
208        }
209    }
210
211    /// Creates a new Verified RCard Credential (VWC)
212    /// issuer: The issuer DID of the credential
213    /// subject: The DID of the subject of this credential
214    /// valid_from: The datetime from which this credential is valid
215    /// valid_until: Optional: The datetime this credential is valid until
216    /// card: JSON Value representing a Jcard (RFC 7095) format
217    #[deprecated(
218        since = "0.2.0",
219        note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
220                It was removed from the DTG Core Credentials specification in Working Draft 01 \
221                and will be defined by the planned DTG Verifiable Data Structures specification. \
222                This constructor will be removed in a future release."
223    )]
224    #[allow(deprecated)]
225    pub fn new_rcard(
226        issuer: String,
227        subject: String,
228        valid_from: DateTime<Utc>,
229        valid_until: Option<DateTime<Utc>>,
230        card: Value,
231    ) -> Self {
232        let mut rcard = DTGCommon {
233            issuer,
234            valid_from,
235            valid_until,
236            credential_subject: CredentialSubject::RCard(CredentialSubjectRCard {
237                id: subject,
238                card,
239            }),
240            ..Default::default()
241        };
242
243        rcard.type_.push(DTGCredentialType::RCard.to_string());
244
245        DTGCredential {
246            credential: rcard,
247            type_: DTGCredentialType::RCard,
248            version: crate::W3CVCVersion::V2_0,
249        }
250    }
251
252    /// Sets this credential's own identifier, consuming and returning it so it chains onto
253    /// any of the `new_*` constructors above.
254    ///
255    /// `id` MUST be a single URL per the W3C VC Data Model; `urn:uuid:<uuid>` is the usual
256    /// choice for a credential with no dereferenceable home. This crate does not validate it.
257    ///
258    /// ```
259    /// # use chrono::Utc;
260    /// # use dtg_credentials::DTGCredential;
261    /// let vmc = DTGCredential::new_vmc(
262    ///     "did:example:member".to_string(),
263    ///     "did:example:community".to_string(),
264    ///     Utc::now(),
265    ///     None,
266    ///     false,
267    /// )
268    /// .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
269    /// assert_eq!(vmc.id(), Some("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52"));
270    /// ```
271    ///
272    /// # Set it before signing
273    ///
274    /// A Data Integrity proof covers the credential minus its `proof`, so `id` is part of what
275    /// is signed. Chain this onto the constructor, before [DTGCredential::sign] — adding an id
276    /// to an already-signed credential leaves a document whose proof no longer verifies.
277    pub fn with_id(mut self, id: impl Into<String>) -> Self {
278        self.credential.id = Some(id.into());
279        self
280    }
281
282    /// Sets this credential's own identifier in place.
283    ///
284    /// The non-consuming form of [DTGCredential::with_id]; the same "before signing" caveat
285    /// applies.
286    pub fn set_id(&mut self, id: impl Into<String>) {
287        self.credential.id = Some(id.into());
288    }
289}
290
291#[cfg(test)]
292#[allow(deprecated)]
293mod tests {
294    use crate::{DTGCredential, WitnessContext};
295    use chrono::{DateTime, Utc};
296    use serde_json::json;
297
298    #[test]
299    fn test_vmc_serialization() {
300        let vmc = DTGCredential::new_vmc(
301            "did:example:issuer".to_string(),
302            "did:example:subject".to_string(),
303            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
304                .unwrap()
305                .with_timezone(&Utc),
306            None,
307            false,
308        );
309
310        let txt = serde_json::to_string_pretty(&vmc).unwrap();
311        let sample = r#"{
312  "@context": [
313    "https://www.w3.org/ns/credentials/v2",
314    "https://firstperson.network/credentials/dtg/v1"
315  ],
316  "type": [
317    "VerifiableCredential",
318    "DTGCredential",
319    "MembershipCredential"
320  ],
321  "issuer": "did:example:issuer",
322  "validFrom": "2025-12-11T00:00:00Z",
323  "credentialSubject": {
324    "id": "did:example:subject"
325  }
326}"#;
327
328        assert_eq!(txt, sample);
329    }
330
331    #[test]
332    fn test_vmc_phc_serialization() {
333        let vmc = DTGCredential::new_vmc(
334            "did:example:issuer".to_string(),
335            "did:example:subject".to_string(),
336            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
337                .unwrap()
338                .with_timezone(&Utc),
339            None,
340            true,
341        );
342
343        let txt = serde_json::to_string_pretty(&vmc).unwrap();
344        let sample = r#"{
345  "@context": [
346    "https://www.w3.org/ns/credentials/v2",
347    "https://firstperson.network/credentials/dtg/v1"
348  ],
349  "type": [
350    "VerifiableCredential",
351    "DTGCredential",
352    "MembershipCredential",
353    "PersonhoodCredential"
354  ],
355  "issuer": "did:example:issuer",
356  "validFrom": "2025-12-11T00:00:00Z",
357  "credentialSubject": {
358    "id": "did:example:subject"
359  }
360}"#;
361
362        assert_eq!(txt, sample);
363    }
364    /// `id` is OPTIONAL, and a credential that was never given one must keep serializing the
365    /// shape it always did — no `"id": null`, no empty string.
366    #[test]
367    fn test_vmc_without_id_omits_the_property() {
368        let vmc = DTGCredential::new_vmc(
369            "did:example:issuer".to_string(),
370            "did:example:subject".to_string(),
371            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
372                .unwrap()
373                .with_timezone(&Utc),
374            None,
375            false,
376        );
377
378        assert_eq!(vmc.id(), None);
379        let value: serde_json::Value = serde_json::to_value(&vmc).unwrap();
380        assert!(
381            value.get("id").is_none(),
382            "an unset id must not appear on the wire at all: {value}"
383        );
384    }
385
386    /// `with_id` puts the identifier at the top level of the credential — a sibling of
387    /// `issuer`, not something nested under `credentialSubject` (which carries the *subject's*
388    /// id, a different thing entirely).
389    #[test]
390    fn test_vmc_with_id_serialization() {
391        let vmc = DTGCredential::new_vmc(
392            "did:example:issuer".to_string(),
393            "did:example:subject".to_string(),
394            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
395                .unwrap()
396                .with_timezone(&Utc),
397            None,
398            false,
399        )
400        .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
401
402        let txt = serde_json::to_string_pretty(&vmc).unwrap();
403        let sample = r#"{
404  "@context": [
405    "https://www.w3.org/ns/credentials/v2",
406    "https://firstperson.network/credentials/dtg/v1"
407  ],
408  "type": [
409    "VerifiableCredential",
410    "DTGCredential",
411    "MembershipCredential"
412  ],
413  "id": "urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff",
414  "issuer": "did:example:issuer",
415  "validFrom": "2025-12-11T00:00:00Z",
416  "credentialSubject": {
417    "id": "did:example:subject"
418  }
419}"#;
420
421        assert_eq!(txt, sample);
422    }
423
424    /// The identifier has to survive a round trip. It arrives on the wire and is read back
425    /// through `TryFrom<DTGCommon>`, which is where `taskContext` was previously being dropped
426    /// — a field that deserializes into nothing breaks signing and verification silently.
427    #[test]
428    fn test_id_round_trips_through_deserialization() {
429        let vmc = DTGCredential::new_vmc(
430            "did:example:issuer".to_string(),
431            "did:example:subject".to_string(),
432            Utc::now(),
433            None,
434            false,
435        )
436        .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
437
438        let txt = serde_json::to_string(&vmc).unwrap();
439        let parsed: DTGCredential = serde_json::from_str(&txt).unwrap();
440        assert_eq!(
441            parsed.id(),
442            Some("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff")
443        );
444    }
445
446    /// A credential with no `id` still deserializes — the property is OPTIONAL, and every
447    /// credential issued before this field existed has none.
448    #[test]
449    fn test_missing_id_deserializes_as_none() {
450        let parsed: DTGCredential = serde_json::from_str(
451            r#"{
452              "@context": ["https://www.w3.org/ns/credentials/v2"],
453              "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
454              "issuer": "did:example:issuer",
455              "validFrom": "2025-12-11T00:00:00Z",
456              "credentialSubject": { "id": "did:example:subject" }
457            }"#,
458        )
459        .unwrap();
460        assert_eq!(parsed.id(), None);
461    }
462
463    /// `set_id` is the in-place form of `with_id`; both write the same property.
464    #[test]
465    fn test_set_id_matches_with_id() {
466        let build = || {
467            DTGCredential::new_vrc(
468                "did:example:issuer".to_string(),
469                "did:example:subject".to_string(),
470                DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
471                    .unwrap()
472                    .with_timezone(&Utc),
473                None,
474            )
475        };
476        let mut in_place = build();
477        in_place.set_id("urn:uuid:abc");
478        assert_eq!(
479            serde_json::to_value(&in_place).unwrap(),
480            serde_json::to_value(build().with_id("urn:uuid:abc")).unwrap()
481        );
482    }
483
484    #[test]
485    fn test_vrc_serialization() {
486        let vrc = DTGCredential::new_vrc(
487            "did:example:issuer".to_string(),
488            "did:example:subject".to_string(),
489            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
490                .unwrap()
491                .with_timezone(&Utc),
492            None,
493        );
494
495        let txt = serde_json::to_string_pretty(&vrc).unwrap();
496        let sample = r#"{
497  "@context": [
498    "https://www.w3.org/ns/credentials/v2",
499    "https://firstperson.network/credentials/dtg/v1"
500  ],
501  "type": [
502    "VerifiableCredential",
503    "DTGCredential",
504    "RelationshipCredential"
505  ],
506  "issuer": "did:example:issuer",
507  "validFrom": "2025-12-11T00:00:00Z",
508  "credentialSubject": {
509    "id": "did:example:subject"
510  }
511}"#;
512
513        assert_eq!(txt, sample);
514    }
515
516    #[test]
517    fn test_vic_serialization() {
518        let vic = DTGCredential::new_vic(
519            "did:example:issuer".to_string(),
520            "did:example:subject".to_string(),
521            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
522                .unwrap()
523                .with_timezone(&Utc),
524            None,
525        );
526
527        let txt = serde_json::to_string_pretty(&vic).unwrap();
528        let sample = r#"{
529  "@context": [
530    "https://www.w3.org/ns/credentials/v2",
531    "https://firstperson.network/credentials/dtg/v1"
532  ],
533  "type": [
534    "VerifiableCredential",
535    "DTGCredential",
536    "InvitationCredential"
537  ],
538  "issuer": "did:example:issuer",
539  "validFrom": "2025-12-11T00:00:00Z",
540  "credentialSubject": {
541    "id": "did:example:subject"
542  }
543}"#;
544
545        assert_eq!(txt, sample);
546    }
547
548    #[test]
549    fn test_vpc_serialization() {
550        let vpc = DTGCredential::new_vpc(
551            "did:example:issuer".to_string(),
552            "did:example:subject".to_string(),
553            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
554                .unwrap()
555                .with_timezone(&Utc),
556            None,
557        );
558
559        let txt = serde_json::to_string_pretty(&vpc).unwrap();
560        let sample = r#"{
561  "@context": [
562    "https://www.w3.org/ns/credentials/v2",
563    "https://firstperson.network/credentials/dtg/v1"
564  ],
565  "type": [
566    "VerifiableCredential",
567    "DTGCredential",
568    "PersonaCredential"
569  ],
570  "issuer": "did:example:issuer",
571  "validFrom": "2025-12-11T00:00:00Z",
572  "credentialSubject": {
573    "id": "did:example:subject"
574  }
575}"#;
576
577        assert_eq!(txt, sample);
578    }
579
580    #[test]
581    fn test_vec_serialization() {
582        let vec = DTGCredential::new_vec(
583            "did:example:issuer".to_string(),
584            "did:example:subject".to_string(),
585            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
586                .unwrap()
587                .with_timezone(&Utc),
588            None,
589            json!({
590              "type": "SkillEndorsement",
591              "name": "Software Development",
592              "competencyLevel": "expert"
593            }),
594        );
595
596        let txt = serde_json::to_string_pretty(&vec).unwrap();
597        let sample = r#"{
598  "@context": [
599    "https://www.w3.org/ns/credentials/v2",
600    "https://firstperson.network/credentials/dtg/v1"
601  ],
602  "type": [
603    "VerifiableCredential",
604    "DTGCredential",
605    "EndorsementCredential"
606  ],
607  "issuer": "did:example:issuer",
608  "validFrom": "2025-12-11T00:00:00Z",
609  "credentialSubject": {
610    "id": "did:example:subject",
611    "endorsement": {
612      "competencyLevel": "expert",
613      "name": "Software Development",
614      "type": "SkillEndorsement"
615    }
616  }
617}"#;
618
619        assert_eq!(txt, sample);
620    }
621
622    #[test]
623    fn test_vwc_serialization() {
624        let vwc = DTGCredential::new_vwc(
625            "did:example:issuer".to_string(),
626            "did:example:subject".to_string(),
627            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
628                .unwrap()
629                .with_timezone(&Utc),
630            None,
631            "thread-abc-123".to_string(),
632            Some("zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw".to_string()),
633            Some(WitnessContext {
634                event: Some("EthDenver 2024".to_string()),
635                session_id: Some("session-8822-nonce".to_string()),
636                method: Some("in-person-proximity".to_string()),
637            }),
638        );
639
640        let txt = serde_json::to_string_pretty(&vwc).unwrap();
641
642        let sample = r#"{
643  "@context": [
644    "https://www.w3.org/ns/credentials/v2",
645    "https://firstperson.network/credentials/dtg/v1"
646  ],
647  "type": [
648    "VerifiableCredential",
649    "DTGCredential",
650    "WitnessCredential"
651  ],
652  "issuer": "did:example:issuer",
653  "validFrom": "2025-12-11T00:00:00Z",
654  "taskContext": "thread-abc-123",
655  "credentialSubject": {
656    "id": "did:example:subject",
657    "digest": "zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw",
658    "witnessContext": {
659      "event": "EthDenver 2024",
660      "sessionId": "session-8822-nonce",
661      "method": "in-person-proximity"
662    }
663  }
664}"#;
665
666        assert_eq!(txt, sample);
667    }
668
669    #[test]
670    fn test_rcard_serialization() {
671        let rcard = DTGCredential::new_rcard(
672            "did:example:issuer".to_string(),
673            "did:example:subject".to_string(),
674            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
675                .unwrap()
676                .with_timezone(&Utc),
677            None,
678            json!([
679                "vcard",
680                [
681                    ["fn", {}, "text", "Alice Smith"],
682                    ["email", {}, "text", "alice@example.com"]
683                ]
684            ]),
685        );
686
687        let txt = serde_json::to_string_pretty(&rcard).unwrap();
688
689        let sample = r#"{
690  "@context": [
691    "https://www.w3.org/ns/credentials/v2",
692    "https://firstperson.network/credentials/dtg/v1"
693  ],
694  "type": [
695    "VerifiableCredential",
696    "DTGCredential",
697    "RCardCredential"
698  ],
699  "issuer": "did:example:issuer",
700  "validFrom": "2025-12-11T00:00:00Z",
701  "credentialSubject": {
702    "id": "did:example:subject",
703    "card": [
704      "vcard",
705      [
706        [
707          "fn",
708          {},
709          "text",
710          "Alice Smith"
711        ],
712        [
713          "email",
714          {},
715          "text",
716          "alice@example.com"
717        ]
718      ]
719    ]
720  }
721}"#;
722
723        assert_eq!(txt, sample);
724    }
725}