Skip to main content

dtg_credentials/
create.rs

1/*!
2*   Builder methods for creating new entities.
3*/
4
5#[allow(deprecated)]
6use crate::{
7    AuthorityGrant, CredentialSubject, CredentialSubjectAuthority, CredentialSubjectBasic,
8    CredentialSubjectDelegation, CredentialSubjectEndorsement, CredentialSubjectMembership,
9    CredentialSubjectRCard, CredentialSubjectWitness, DTGCommon, DTGCredential, DTGCredentialError,
10    DTGCredentialType, DelegationGrant, WitnessContext,
11};
12use chrono::{DateTime, Utc};
13use serde_json::Value;
14
15impl DTGCredential {
16    /// Creates a new community-issued Verifiable Membership Credential (VMC) — the
17    /// membership **grant**, the community → member half of a membership edge.
18    ///
19    /// A membership edge is a *pair* of VMCs, and this is only one of them. The member
20    /// answers with [DTGCredential::new_member_vmc], and the edge is not complete until
21    /// they have: a community can always issue a credential naming somebody as a member,
22    /// but it cannot produce the acknowledgement without that party's signature. The pair
23    /// is what makes an unconsented membership claim unprovable.
24    ///
25    /// The grant MUST NOT carry a `digestMultibase` — that property is what marks the
26    /// other direction — and this constructor does not set one.
27    ///
28    /// issuer: The identifier of the VTC or VTN granting membership
29    /// subject: The member's identifier, or the member VTC's own for VTN membership
30    /// valid_from: The datetime from which this credential is valid
31    /// valid_until: Optional: The datetime this credential is valid until
32    /// personhood: Whether this VMC can be used as a form of Personhood Credential
33    ///             - Adds PersonhoodCredential to the type array if true
34    ///
35    /// # Give it an `id`
36    ///
37    /// Chain [DTGCredential::with_id] on: the member stores the grant under its `id`, and
38    /// re-issuing is only recognisable as a renewal rather than a duplicate if there is one.
39    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_multibase: 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    /// Creates a new member-issued Verifiable Membership Credential (VMC) — the membership
71    /// **acknowledgement**, the member → community half of a membership edge.
72    ///
73    /// The roles of [DTGCredential::new_vmc] are reversed (the member issues, the community
74    /// is the subject) and the subject carries a `digestMultibase` of the grant being
75    /// acknowledged.
76    /// That digest is what binds the two halves into one edge: an acknowledgement whose
77    /// digest matches no valid grant does not complete anything, and the binding forces an
78    /// order — the grant must exist before this can reference it.
79    ///
80    /// This is the member's consent artifact. Because the member is its issuer, withdrawing
81    /// consent needs no cooperation from the community.
82    ///
83    /// # Takes the grant in its wire form, deliberately
84    ///
85    /// `grant` is the JSON the community sent, not a parsed [DTGCredential]. The digest has
86    /// to cover the document the community will recompute it over, and this library does
87    /// not model every member a credential may carry — `credentialStatus`, which every VMC
88    /// issued against a status list carries, is dropped by a parse-then-re-serialise round
89    /// trip. Building the acknowledgement from a parsed grant would produce a digest that
90    /// verifies nowhere, and would do it silently.
91    ///
92    /// So: keep the bytes you were given, and pass them here.
93    ///
94    /// valid_from: The datetime from which this credential is valid
95    /// valid_until: Optional: The datetime this credential is valid until
96    ///
97    /// # Errors
98    ///
99    /// [DTGCredentialError::NotAMembershipGrant] if `grant` is not a JSON object, does not
100    /// carry `MembershipCredential` in its `type`, has no `issuer` or
101    /// `credentialSubject.id`, or already carries a `digest` — that last is an
102    /// acknowledgement, and acknowledging one does not form an edge.
103    ///
104    /// # Give it an `id`
105    ///
106    /// Chain [DTGCredential::with_id] on before signing. A community keys a member's VMC by
107    /// `id` to tell a re-send from a renewal.
108    pub fn new_member_vmc(
109        grant: &Value,
110        valid_from: DateTime<Utc>,
111        valid_until: Option<DateTime<Utc>>,
112    ) -> Result<Self, DTGCredentialError> {
113        let object = grant
114            .as_object()
115            .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("not a JSON object".into()))?;
116
117        let is_membership = object
118            .get("type")
119            .and_then(Value::as_array)
120            .is_some_and(|types| {
121                types
122                    .iter()
123                    .filter_map(Value::as_str)
124                    .any(|t| t == "MembershipCredential")
125            });
126        if !is_membership {
127            return Err(DTGCredentialError::NotAMembershipGrant(
128                "`type` does not include `MembershipCredential`".into(),
129            ));
130        }
131
132        let subject = object
133            .get("credentialSubject")
134            .and_then(Value::as_object)
135            .ok_or_else(|| {
136                DTGCredentialError::NotAMembershipGrant("no `credentialSubject`".into())
137            })?;
138
139        // Both spellings: `digestMultibase` is the Working Draft 02 name, `digest` the
140        // Working Draft 01 one this library also accepts on the wire. Probing only the
141        // current name would let an acknowledgement issued against the older draft be
142        // acknowledged in turn, which forms no edge.
143        if subject.contains_key("digestMultibase") || subject.contains_key("digest") {
144            return Err(DTGCredentialError::NotAMembershipGrant(
145                "the credential carries a digest of another credential, so it is itself a \
146                 member-issued acknowledgement rather than a community-issued grant"
147                    .into(),
148            ));
149        }
150
151        // The member is the grant's subject and the community its issuer: reading both off
152        // the grant is what keeps the two halves naming the same pair. Taking them as
153        // parameters would let a caller acknowledge one grant while naming the parties of
154        // another, which verifies as a digest match and means nothing.
155        let member = subject
156            .get("id")
157            .and_then(Value::as_str)
158            .ok_or_else(|| {
159                DTGCredentialError::NotAMembershipGrant("no `credentialSubject.id`".into())
160            })?
161            .to_string();
162
163        // `issuer` is a string or an object with an `id`, per the W3C data model.
164        let community = object
165            .get("issuer")
166            .and_then(|i| {
167                i.as_str()
168                    .map(str::to_string)
169                    .or_else(|| i.get("id").and_then(Value::as_str).map(str::to_string))
170            })
171            .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("no `issuer`".into()))?;
172
173        let mut vmc = DTGCommon {
174            issuer: member,
175            valid_from,
176            valid_until,
177            credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
178                id: community,
179                digest_multibase: Some(crate::digest_multibase_json(grant)?),
180            }),
181            ..Default::default()
182        };
183
184        vmc.type_.push(DTGCredentialType::Membership.to_string());
185
186        Ok(DTGCredential {
187            credential: vmc,
188            type_: DTGCredentialType::Membership,
189            version: crate::W3CVCVersion::V2_0,
190        })
191    }
192
193    /// Creates a new Verified Relationship Credential (VRC)
194    /// issuer: The issuer DID of the credential
195    /// subject: The DID of the subject of this credential
196    /// valid_from: The datetime from which this credential is valid
197    /// valid_until: Optional: The datetime this credential is valid until
198    pub fn new_vrc(
199        issuer: String,
200        subject: String,
201        valid_from: DateTime<Utc>,
202        valid_until: Option<DateTime<Utc>>,
203    ) -> Self {
204        let mut vrc = DTGCommon {
205            issuer,
206            valid_from,
207            valid_until,
208            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
209            ..Default::default()
210        };
211
212        vrc.type_.push(DTGCredentialType::Relationship.to_string());
213
214        DTGCredential {
215            credential: vrc,
216            type_: DTGCredentialType::Relationship,
217            version: crate::W3CVCVersion::V2_0,
218        }
219    }
220
221    /// Creates a new Verified Invitation Credential (VIC)
222    /// issuer: The issuer DID of the credential
223    /// subject: The DID of the subject of this credential
224    /// valid_from: The datetime from which this credential is valid
225    /// valid_until: Optional: The datetime this credential is valid until
226    pub fn new_vic(
227        issuer: String,
228        subject: String,
229        valid_from: DateTime<Utc>,
230        valid_until: Option<DateTime<Utc>>,
231    ) -> Self {
232        let mut vic = DTGCommon {
233            issuer,
234            valid_from,
235            valid_until,
236            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
237            ..Default::default()
238        };
239
240        vic.type_.push(DTGCredentialType::Invitation.to_string());
241
242        DTGCredential {
243            credential: vic,
244            type_: DTGCredentialType::Invitation,
245            version: crate::W3CVCVersion::V2_0,
246        }
247    }
248
249    /// Creates a new Verifiable Authority Credential (VAC) — a chain root.
250    ///
251    /// The issuer is the party governing `scope`. To derive a narrower VAC from one you
252    /// already hold, use [DTGCredential::attenuate] instead: a chain root is a grant made
253    /// by the governing party, and minting one directly is how a self-issued grant of
254    /// arbitrary authority gets in.
255    ///
256    /// `actions` MUST NOT be empty — an empty list confers nothing rather than everything.
257    ///
258    /// # `valid_until` is required
259    ///
260    /// Not optional, unlike the base structure and unlike every other `new_*` constructor
261    /// here. Nothing about the subject's current standing is consulted when a VAC is
262    /// verified, so authority that does not expire is authority nobody can withdraw by
263    /// waiting.
264    pub fn new_vac(
265        issuer: String,
266        subject: String,
267        scope: String,
268        actions: Vec<String>,
269        valid_from: DateTime<Utc>,
270        valid_until: DateTime<Utc>,
271    ) -> Result<Self, DTGCredentialError> {
272        if actions.is_empty() {
273            return Err(DTGCredentialError::EmptyAuthorityActions);
274        }
275        let mut vac = DTGCommon {
276            issuer,
277            valid_from,
278            valid_until: Some(valid_until),
279            credential_subject: CredentialSubject::Authority(CredentialSubjectAuthority {
280                id: subject,
281                authority: AuthorityGrant {
282                    scope,
283                    actions,
284                    parent: None,
285                },
286            }),
287            ..Default::default()
288        };
289
290        vac.type_.push(DTGCredentialType::Authority.to_string());
291
292        Ok(DTGCredential {
293            credential: vac,
294            type_: DTGCredentialType::Authority,
295            version: crate::W3CVCVersion::V2_0,
296        })
297    }
298
299    /// Derive a narrower VAC from one this holder already holds.
300    ///
301    /// This is what lets a member equip an agent, a device, or a short-lived session with
302    /// only the authority that task needs, rather than lending it their own. The derived
303    /// credential is issued by the *holder*, not by the party governing the scope, and
304    /// carries `parent` — the **digest** of the credential it narrows — so a verifier can
305    /// walk back to a root.
306    ///
307    /// Refuses anything that would widen. The checks here mirror
308    /// [crate::authority::verify_chain] on purpose: a holder should be unable to *build* a
309    /// chain a verifier would reject, so the failure surfaces at issue time rather than at
310    /// use — but the verifier's checks remain authoritative, because nothing stops a
311    /// different implementation constructing the JSON by hand.
312    ///
313    /// - `self` must be a VAC.
314    /// - `actions` must be a subset of what `self` confers.
315    /// - `valid_until` must not exceed `self`'s.
316    ///
317    /// # Binding the derivative to the agent is `subject`, not a separate field
318    ///
319    /// A VAC is not a bearer credential: [crate::authority::verify_chain] requires the
320    /// party presenting the leaf to be its subject. So equipping an agent means naming the
321    /// agent in `subject`, and there is nothing further to bind. An earlier version of this
322    /// method took an `audience` for that job; it was removed with the property.
323    ///
324    /// # Digests the model
325    ///
326    /// The `parent` digest is computed with [DTGCredential::digest_multibase], which hashes
327    /// this in-memory credential. That is right for a VAC this process built and signed.
328    /// For one that **arrived from a counterparty**, use
329    /// [DTGCredential::attenuate_from_json] and give it the bytes you received — the same
330    /// distinction [DTGCredential::new_member_vmc] draws, and for the same reason.
331    pub fn attenuate(
332        &self,
333        subject: String,
334        actions: Vec<String>,
335        valid_from: DateTime<Utc>,
336        valid_until: DateTime<Utc>,
337    ) -> Result<Self, DTGCredentialError> {
338        let parent_grant = self
339            .credential()
340            .authority()
341            .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
342
343        Self::attenuate_inner(
344            parent_grant.clone(),
345            self.credential().subject().to_string(),
346            self.credential().valid_until(),
347            self.digest_multibase()?,
348            subject,
349            actions,
350            valid_from,
351            valid_until,
352        )
353    }
354
355    /// Derive a narrower VAC from a parent in its **wire form**.
356    ///
357    /// Identical to [DTGCredential::attenuate] except that the parent is the JSON a
358    /// counterparty sent rather than a parsed credential, so the `parent` digest covers
359    /// the document the verifier will recompute it over. Use this whenever the VAC being
360    /// narrowed came from somewhere else.
361    ///
362    /// # Errors
363    ///
364    /// [DTGCredentialError::NotAnAuthorityCredential] if `parent` is not a JSON object
365    /// carrying `AuthorityCredential` in its `type` and a well-formed
366    /// `credentialSubject.authority`, and the same widening errors as
367    /// [DTGCredential::attenuate].
368    pub fn attenuate_from_json(
369        parent: &Value,
370        subject: String,
371        actions: Vec<String>,
372        valid_from: DateTime<Utc>,
373        valid_until: DateTime<Utc>,
374    ) -> Result<Self, DTGCredentialError> {
375        let object = parent
376            .as_object()
377            .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
378
379        let is_authority = object
380            .get("type")
381            .and_then(Value::as_array)
382            .is_some_and(|types| {
383                types
384                    .iter()
385                    .filter_map(Value::as_str)
386                    .any(|t| t == "AuthorityCredential")
387            });
388        if !is_authority {
389            return Err(DTGCredentialError::NotAnAuthorityCredential);
390        }
391
392        let parent_subject = object
393            .get("credentialSubject")
394            .and_then(Value::as_object)
395            .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
396
397        // The holder attenuating is the parent's subject; reading it off the parent is what
398        // keeps a derived VAC from citing a chain its issuer never held.
399        let holder = parent_subject
400            .get("id")
401            .and_then(Value::as_str)
402            .ok_or(DTGCredentialError::NotAnAuthorityCredential)?
403            .to_string();
404
405        let parent_grant: AuthorityGrant = parent_subject
406            .get("authority")
407            .ok_or(DTGCredentialError::NotAnAuthorityCredential)
408            .and_then(|a| {
409                serde_json::from_value(a.clone())
410                    .map_err(|_| DTGCredentialError::NotAnAuthorityCredential)
411            })?;
412
413        let parent_until = object
414            .get("validUntil")
415            .or_else(|| object.get("expirationDate"))
416            .and_then(Value::as_str)
417            .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
418            .map(|t| t.with_timezone(&Utc));
419
420        Self::attenuate_inner(
421            parent_grant,
422            holder,
423            parent_until,
424            crate::digest_multibase_json(parent)?,
425            subject,
426            actions,
427            valid_from,
428            valid_until,
429        )
430    }
431
432    /// The narrowing checks and the assembly, shared by both attenuation entry points.
433    #[allow(clippy::too_many_arguments)]
434    fn attenuate_inner(
435        parent_grant: AuthorityGrant,
436        holder: String,
437        parent_until: Option<DateTime<Utc>>,
438        parent_digest: String,
439        subject: String,
440        actions: Vec<String>,
441        valid_from: DateTime<Utc>,
442        valid_until: DateTime<Utc>,
443    ) -> Result<Self, DTGCredentialError> {
444        if actions.is_empty() {
445            return Err(DTGCredentialError::EmptyAuthorityActions);
446        }
447        for action in &actions {
448            if !parent_grant.actions.contains(action) {
449                return Err(DTGCredentialError::AttenuationWidens(format!(
450                    "action `{action}` is not conferred by the parent"
451                )));
452            }
453        }
454        if let Some(parent_until) = parent_until
455            && valid_until > parent_until
456        {
457            return Err(DTGCredentialError::AttenuationWidens(format!(
458                "validUntil {valid_until} is beyond the parent's {parent_until}"
459            )));
460        }
461
462        let mut vac = DTGCommon {
463            // The holder issues: they are the subject of the parent grant.
464            issuer: holder,
465            valid_from,
466            valid_until: Some(valid_until),
467            credential_subject: CredentialSubject::Authority(CredentialSubjectAuthority {
468                id: subject,
469                authority: AuthorityGrant {
470                    // Scope never changes down a chain.
471                    scope: parent_grant.scope.clone(),
472                    actions,
473                    parent: Some(parent_digest),
474                },
475            }),
476            ..Default::default()
477        };
478
479        vac.type_.push(DTGCredentialType::Authority.to_string());
480
481        Ok(DTGCredential {
482            credential: vac,
483            type_: DTGCredentialType::Authority,
484            version: crate::W3CVCVersion::V2_0,
485        })
486    }
487
488    /// Creates a new Verifiable Delegation Credential (VDC) — the delegation **grant**,
489    /// the delegator → delegate half of a delegation edge.
490    ///
491    /// Establishes that `subject` may act **in the issuer's name**, for the acts named in
492    /// `scope`, until `valid_until`. Within that scope what the delegate does is
493    /// attributable to the delegator.
494    ///
495    /// # This is not authority
496    ///
497    /// A VDC never supplies permission the delegator did not itself hold. A verifier
498    /// substitutes the delegator for the delegate and then asks the permission question it
499    /// would have asked of the delegator directly — so withdrawing the delegator's own
500    /// permission ends the delegate's ability to act immediately, without revoking
501    /// anything. See [DTGCredential::new_vac] for the credential that answers that
502    /// question.
503    ///
504    /// # The edge is not complete without the acceptance
505    ///
506    /// This is one half. The delegate answers with [DTGCredential::new_delegate_vdc], and
507    /// a verifier MUST obtain and verify that half before accepting any party as acting
508    /// under the delegation: a grant alone establishes what the delegator appointed, not
509    /// what the delegate agreed to. Same consent rule as a membership edge, and for the
510    /// same reason — a delegator can always name someone as its delegate, but cannot
511    /// produce the countersignature.
512    ///
513    /// `scope` MUST NOT be empty: a VDC cannot express an unbounded appointment by
514    /// omitting it.
515    ///
516    /// `max_depth` is the number of further re-delegations permitted below this one.
517    /// `None` and `Some(0)` both prohibit re-delegation — the default is a single hop, and
518    /// setting it above zero is the delegator's explicit authorisation, of which there is
519    /// no other kind.
520    ///
521    /// # `valid_until` is required
522    ///
523    /// An appointment with no expiry cannot be reasoned about by a verifier that cannot
524    /// reach the delegator.
525    ///
526    /// # Errors
527    ///
528    /// [DTGCredentialError::MalformedDelegation] if `scope` is empty.
529    pub fn new_vdc(
530        issuer: String,
531        subject: String,
532        valid_from: DateTime<Utc>,
533        valid_until: DateTime<Utc>,
534        scope: Vec<String>,
535        max_depth: Option<u32>,
536    ) -> Result<Self, DTGCredentialError> {
537        if scope.is_empty() {
538            return Err(DTGCredentialError::MalformedDelegation(
539                "a grant MUST carry at least one `scope` entry — a VDC cannot express an \
540                 unbounded appointment by emptying it"
541                    .into(),
542            ));
543        }
544
545        let mut vdc = DTGCommon {
546            issuer,
547            valid_from,
548            valid_until: Some(valid_until),
549            credential_subject: CredentialSubject::Delegation(CredentialSubjectDelegation {
550                id: subject,
551                delegation: DelegationGrant {
552                    scope: Some(scope),
553                    parent: None,
554                    max_depth,
555                    accepts: None,
556                },
557            }),
558            ..Default::default()
559        };
560
561        vdc.type_.push(DTGCredentialType::Delegation.to_string());
562
563        Ok(DTGCredential {
564            credential: vdc,
565            type_: DTGCredentialType::Delegation,
566            version: crate::W3CVCVersion::V2_0,
567        })
568    }
569
570    /// Derive a further VDC from one this delegate already holds — a **re-delegation**.
571    ///
572    /// Only permitted where the held VDC sets `maxDepth` above zero, and only for a subset
573    /// of the acts it was itself appointed for. The default is a single hop: a delegate
574    /// that needs a further delegate and is not authorised to re-delegate asks the
575    /// principal, who issues a fresh root delegation directly — so that the principal
576    /// always holds the complete register of who may speak in its name.
577    ///
578    /// The derived VDC carries `parent`, the digest of the VDC it derives from, and a
579    /// `maxDepth` one less than its parent's.
580    ///
581    /// Like [DTGCredential::attenuate], this digests the in-memory model; for a grant that
582    /// arrived from a counterparty, use [DTGCredential::redelegate_from_json].
583    ///
584    /// # Errors
585    ///
586    /// [DTGCredentialError::MalformedDelegation] if `self` is not a delegation grant, if
587    /// it does not permit re-delegation, if `scope` is empty or not a subset of the
588    /// parent's, or if `valid_until` is later than the parent's.
589    pub fn redelegate(
590        &self,
591        subject: String,
592        scope: Vec<String>,
593        valid_from: DateTime<Utc>,
594        valid_until: DateTime<Utc>,
595    ) -> Result<Self, DTGCredentialError> {
596        let parent = self.credential().delegation().ok_or_else(|| {
597            DTGCredentialError::MalformedDelegation("not a DelegationCredential".into())
598        })?;
599
600        Self::redelegate_inner(
601            parent.clone(),
602            self.credential().subject().to_string(),
603            self.credential().valid_until(),
604            self.digest_multibase()?,
605            subject,
606            scope,
607            valid_from,
608            valid_until,
609        )
610    }
611
612    /// Derive a further VDC from a parent grant in its **wire form**.
613    ///
614    /// Identical to [DTGCredential::redelegate] except that the parent is the JSON the
615    /// delegator sent, so the `parent` digest covers the document a verifier will
616    /// recompute it over.
617    pub fn redelegate_from_json(
618        parent: &Value,
619        subject: String,
620        scope: Vec<String>,
621        valid_from: DateTime<Utc>,
622        valid_until: DateTime<Utc>,
623    ) -> Result<Self, DTGCredentialError> {
624        let (delegate, grant, parent_until) = Self::read_delegation_json(parent)?;
625
626        Self::redelegate_inner(
627            grant,
628            delegate,
629            parent_until,
630            crate::digest_multibase_json(parent)?,
631            subject,
632            scope,
633            valid_from,
634            valid_until,
635        )
636    }
637
638    /// The narrowing checks and the assembly, shared by both re-delegation entry points.
639    #[allow(clippy::too_many_arguments)]
640    fn redelegate_inner(
641        parent_grant: DelegationGrant,
642        holder: String,
643        parent_until: Option<DateTime<Utc>>,
644        parent_digest: String,
645        subject: String,
646        scope: Vec<String>,
647        valid_from: DateTime<Utc>,
648        valid_until: DateTime<Utc>,
649    ) -> Result<Self, DTGCredentialError> {
650        if parent_grant.accepts.is_some() {
651            return Err(DTGCredentialError::MalformedDelegation(
652                "the parent is an acceptance, not a grant — an acceptance appoints nobody \
653                 and cannot be re-delegated from"
654                    .into(),
655            ));
656        }
657
658        // Absence prohibits re-delegation just as `0` does. This is the opposite default
659        // from a VAC, deliberately: a delegate speaks in the principal's name, so the
660        // principal keeps the register of who may do so.
661        let parent_depth = parent_grant.max_depth.unwrap_or(0);
662        if parent_depth == 0 {
663            return Err(DTGCredentialError::MalformedDelegation(
664                "the parent does not permit re-delegation — `maxDepth` is absent or zero, \
665                 and setting it above zero is the delegator's only way to authorise one"
666                    .into(),
667            ));
668        }
669
670        if scope.is_empty() {
671            return Err(DTGCredentialError::MalformedDelegation(
672                "a grant MUST carry at least one `scope` entry".into(),
673            ));
674        }
675        let parent_scope = parent_grant.scope.as_deref().unwrap_or(&[]);
676        for act in &scope {
677            if !parent_scope.contains(act) {
678                return Err(DTGCredentialError::MalformedDelegation(format!(
679                    "`{act}` is not in the scope this delegation derives from"
680                )));
681            }
682        }
683        if let Some(parent_until) = parent_until
684            && valid_until > parent_until
685        {
686            return Err(DTGCredentialError::MalformedDelegation(format!(
687                "validUntil {valid_until} is beyond the parent's {parent_until}"
688            )));
689        }
690
691        let mut vdc = DTGCommon {
692            issuer: holder,
693            valid_from,
694            valid_until: Some(valid_until),
695            credential_subject: CredentialSubject::Delegation(CredentialSubjectDelegation {
696                id: subject,
697                delegation: DelegationGrant {
698                    scope: Some(scope),
699                    parent: Some(parent_digest),
700                    max_depth: Some(parent_depth - 1),
701                    accepts: None,
702                },
703            }),
704            ..Default::default()
705        };
706
707        vdc.type_.push(DTGCredentialType::Delegation.to_string());
708
709        Ok(DTGCredential {
710            credential: vdc,
711            type_: DTGCredentialType::Delegation,
712            version: crate::W3CVCVersion::V2_0,
713        })
714    }
715
716    /// Creates the delegate-issued half of a delegation edge — the **acceptance**.
717    ///
718    /// The roles of [DTGCredential::new_vdc] are reversed (the delegate issues, the
719    /// delegator is the subject) and the subject carries `accepts`, the digest of the
720    /// grant being taken on. That digest is what binds the two halves into one edge.
721    ///
722    /// An acceptance carries no `scope` of its own. What the delegate consented to is the
723    /// scope of the grant it names, which a verifier holds in any case; restating it would
724    /// require an equality check across the two credentials that cannot be satisfied under
725    /// selective disclosure of either.
726    ///
727    /// This is the delegate's consent artifact, and its accountability for acting in
728    /// another's name. Because a delegator cannot produce it, a party holding only the
729    /// delegate's key cannot manufacture appointments either.
730    ///
731    /// # Takes the grant in its wire form, deliberately
732    ///
733    /// Same reasoning as [DTGCredential::new_member_vmc]: the digest has to cover the
734    /// document the delegator will recompute it over. Keep the bytes you were given and
735    /// pass them here.
736    ///
737    /// # Errors
738    ///
739    /// [DTGCredentialError::NotADelegationGrant] if `grant` is not a JSON object carrying
740    /// `DelegationCredential` in its `type`, has no `issuer` or `credentialSubject.id`, or
741    /// already carries `accepts` — that last is itself an acceptance, and accepting one
742    /// forms no edge.
743    pub fn new_delegate_vdc(
744        grant: &Value,
745        valid_from: DateTime<Utc>,
746        valid_until: DateTime<Utc>,
747    ) -> Result<Self, DTGCredentialError> {
748        let object = grant
749            .as_object()
750            .ok_or_else(|| DTGCredentialError::NotADelegationGrant("not a JSON object".into()))?;
751
752        let is_delegation = object
753            .get("type")
754            .and_then(Value::as_array)
755            .is_some_and(|types| {
756                types
757                    .iter()
758                    .filter_map(Value::as_str)
759                    .any(|t| t == "DelegationCredential")
760            });
761        if !is_delegation {
762            return Err(DTGCredentialError::NotADelegationGrant(
763                "`type` does not include `DelegationCredential`".into(),
764            ));
765        }
766
767        let subject = object
768            .get("credentialSubject")
769            .and_then(Value::as_object)
770            .ok_or_else(|| {
771                DTGCredentialError::NotADelegationGrant("no `credentialSubject`".into())
772            })?;
773
774        let delegation = subject
775            .get("delegation")
776            .and_then(Value::as_object)
777            .ok_or_else(|| {
778                DTGCredentialError::NotADelegationGrant("no `credentialSubject.delegation`".into())
779            })?;
780
781        if delegation.contains_key("accepts") {
782            return Err(DTGCredentialError::NotADelegationGrant(
783                "the credential carries `accepts`, so it is itself an acceptance rather \
784                 than a grant"
785                    .into(),
786            ));
787        }
788        if !delegation.contains_key("scope") {
789            return Err(DTGCredentialError::NotADelegationGrant(
790                "the grant carries no `scope`, so there is no appointment to accept".into(),
791            ));
792        }
793
794        // The delegate is the grant's subject and the delegator its issuer. Reading both
795        // off the grant is what keeps the two halves naming the same pair — taking them as
796        // parameters would let a caller accept one grant while naming the parties of
797        // another, which verifies as a digest match and means nothing.
798        let delegate = subject
799            .get("id")
800            .and_then(Value::as_str)
801            .ok_or_else(|| {
802                DTGCredentialError::NotADelegationGrant("no `credentialSubject.id`".into())
803            })?
804            .to_string();
805
806        // `issuer` is a string or an object with an `id`, per the W3C data model.
807        let delegator = object
808            .get("issuer")
809            .and_then(|i| {
810                i.as_str()
811                    .map(str::to_string)
812                    .or_else(|| i.get("id").and_then(Value::as_str).map(str::to_string))
813            })
814            .ok_or_else(|| DTGCredentialError::NotADelegationGrant("no `issuer`".into()))?;
815
816        let mut vdc = DTGCommon {
817            issuer: delegate,
818            valid_from,
819            valid_until: Some(valid_until),
820            credential_subject: CredentialSubject::Delegation(CredentialSubjectDelegation {
821                id: delegator,
822                delegation: DelegationGrant {
823                    scope: None,
824                    parent: None,
825                    max_depth: None,
826                    accepts: Some(crate::digest_multibase_json(grant)?),
827                },
828            }),
829            ..Default::default()
830        };
831
832        vdc.type_.push(DTGCredentialType::Delegation.to_string());
833
834        Ok(DTGCredential {
835            credential: vdc,
836            type_: DTGCredentialType::Delegation,
837            version: crate::W3CVCVersion::V2_0,
838        })
839    }
840
841    /// Reads the delegate, the grant, and the parent's expiry off a VDC in its wire form.
842    fn read_delegation_json(
843        doc: &Value,
844    ) -> Result<(String, DelegationGrant, Option<DateTime<Utc>>), DTGCredentialError> {
845        let object = doc
846            .as_object()
847            .ok_or_else(|| DTGCredentialError::MalformedDelegation("not a JSON object".into()))?;
848
849        let is_delegation = object
850            .get("type")
851            .and_then(Value::as_array)
852            .is_some_and(|types| {
853                types
854                    .iter()
855                    .filter_map(Value::as_str)
856                    .any(|t| t == "DelegationCredential")
857            });
858        if !is_delegation {
859            return Err(DTGCredentialError::MalformedDelegation(
860                "`type` does not include `DelegationCredential`".into(),
861            ));
862        }
863
864        let subject = object
865            .get("credentialSubject")
866            .and_then(Value::as_object)
867            .ok_or_else(|| {
868                DTGCredentialError::MalformedDelegation("no `credentialSubject`".into())
869            })?;
870
871        let delegate = subject
872            .get("id")
873            .and_then(Value::as_str)
874            .ok_or_else(|| {
875                DTGCredentialError::MalformedDelegation("no `credentialSubject.id`".into())
876            })?
877            .to_string();
878
879        let grant: DelegationGrant = subject
880            .get("delegation")
881            .ok_or_else(|| {
882                DTGCredentialError::MalformedDelegation("no `credentialSubject.delegation`".into())
883            })
884            .and_then(|d| {
885                serde_json::from_value(d.clone()).map_err(|e| {
886                    DTGCredentialError::MalformedDelegation(format!("malformed `delegation`: {e}"))
887                })
888            })?;
889
890        let until = object
891            .get("validUntil")
892            .or_else(|| object.get("expirationDate"))
893            .and_then(Value::as_str)
894            .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
895            .map(|t| t.with_timezone(&Utc));
896
897        Ok((delegate, grant, until))
898    }
899
900    /// Creates a new Verified Persona Credential (VPC)
901    /// issuer: The issuer DID of the credential
902    /// subject: The DID of the subject of this credential
903    /// valid_from: The datetime from which this credential is valid
904    /// valid_until: Optional: The datetime this credential is valid until
905    pub fn new_vpc(
906        issuer: String,
907        subject: String,
908        valid_from: DateTime<Utc>,
909        valid_until: Option<DateTime<Utc>>,
910    ) -> Self {
911        let mut vpc = DTGCommon {
912            issuer,
913            valid_from,
914            valid_until,
915            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
916            ..Default::default()
917        };
918
919        vpc.type_.push(DTGCredentialType::Persona.to_string());
920
921        DTGCredential {
922            credential: vpc,
923            type_: DTGCredentialType::Persona,
924            version: crate::W3CVCVersion::V2_0,
925        }
926    }
927
928    /// Creates a new Verified Endorsement Credential (VEC)
929    /// issuer: The issuer DID of the credential
930    /// subject: The DID of the subject of this credential
931    /// valid_from: The datetime from which this credential is valid
932    /// valid_until: Optional: The datetime this credential is valid until
933    /// endorsement: The endorsement details for this credential
934    pub fn new_vec(
935        issuer: String,
936        subject: String,
937        valid_from: DateTime<Utc>,
938        valid_until: Option<DateTime<Utc>>,
939        endorsement: Value,
940    ) -> Self {
941        let mut vec = DTGCommon {
942            issuer,
943            valid_from,
944            valid_until,
945            credential_subject: CredentialSubject::Endorsement(CredentialSubjectEndorsement {
946                id: subject,
947                endorsement,
948            }),
949            ..Default::default()
950        };
951
952        vec.type_.push(DTGCredentialType::Endorsement.to_string());
953
954        DTGCredential {
955            credential: vec,
956            type_: DTGCredentialType::Endorsement,
957            version: crate::W3CVCVersion::V2_0,
958        }
959    }
960
961    /// Creates a new Verified Witness Credential (VWC)
962    /// issuer: The issuer DID of the credential - a member's identifier, or the DID of a
963    ///         VTA acting according to VTC policy
964    /// subject: The DID of the observed party. For a witnessed bi-directional exchange this
965    ///          MUST be the issuer of the VRC that this VWC attests (the VRC referenced by
966    ///          `digestMultibase`), so that the two VWCs of an exchange are unambiguously bound to
967    ///          their respective directions. The witness should issue one VWC per direction.
968    /// valid_from: The datetime from which this credential is valid
969    /// valid_until: Optional: The datetime this credential is valid until
970    /// task_context: Required `threadId` of the trust task exchange the witnessing occurred in
971    /// digest: Cryptographic hash of the witnessed edge credential, binding this VWC to the
972    ///         specific edge. Produce it with [DTGCredential::digest_multibase] on that
973    ///         credential, or [crate::digest_multibase_json] on the bytes you received.
974    ///         REQUIRED by the specification; `Option` here because a VWC that predates the
975    ///         requirement still has to deserialize. A VWC without one identifies the
976    ///         observed party and the exchange, but not which edge was witnessed.
977    /// witness_context: Optional Semantic context for the witness
978    pub fn new_vwc(
979        issuer: String,
980        subject: String,
981        valid_from: DateTime<Utc>,
982        valid_until: Option<DateTime<Utc>>,
983        task_context: String,
984        digest: Option<String>,
985        witness_context: Option<WitnessContext>,
986    ) -> Self {
987        let mut vwc = DTGCommon {
988            issuer,
989            valid_from,
990            valid_until,
991            task_context: Some(task_context),
992            credential_subject: CredentialSubject::Witness(CredentialSubjectWitness {
993                id: subject,
994                digest_multibase: digest,
995                witness_context,
996            }),
997            ..Default::default()
998        };
999
1000        vwc.type_.push(DTGCredentialType::Witness.to_string());
1001
1002        DTGCredential {
1003            credential: vwc,
1004            type_: DTGCredentialType::Witness,
1005            version: crate::W3CVCVersion::V2_0,
1006        }
1007    }
1008
1009    /// Creates a new Verified RCard Credential (VWC)
1010    /// issuer: The issuer DID of the credential
1011    /// subject: The DID of the subject of this credential
1012    /// valid_from: The datetime from which this credential is valid
1013    /// valid_until: Optional: The datetime this credential is valid until
1014    /// card: JSON Value representing a Jcard (RFC 7095) format
1015    #[deprecated(
1016        since = "0.2.0",
1017        note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1018                It was removed from the DTG Core Credentials specification in Working Draft 01 \
1019                and will be defined by the planned DTG Verifiable Data Structures specification. \
1020                This constructor will be removed in a future release."
1021    )]
1022    #[allow(deprecated)]
1023    pub fn new_rcard(
1024        issuer: String,
1025        subject: String,
1026        valid_from: DateTime<Utc>,
1027        valid_until: Option<DateTime<Utc>>,
1028        card: Value,
1029    ) -> Self {
1030        let mut rcard = DTGCommon {
1031            issuer,
1032            valid_from,
1033            valid_until,
1034            credential_subject: CredentialSubject::RCard(CredentialSubjectRCard {
1035                id: subject,
1036                card,
1037            }),
1038            ..Default::default()
1039        };
1040
1041        rcard.type_.push(DTGCredentialType::RCard.to_string());
1042
1043        DTGCredential {
1044            credential: rcard,
1045            type_: DTGCredentialType::RCard,
1046            version: crate::W3CVCVersion::V2_0,
1047        }
1048    }
1049
1050    /// Sets this credential's own identifier, consuming and returning it so it chains onto
1051    /// any of the `new_*` constructors above.
1052    ///
1053    /// `id` MUST be a single URL per the W3C VC Data Model; `urn:uuid:<uuid>` is the usual
1054    /// choice for a credential with no dereferenceable home. This crate does not validate it.
1055    ///
1056    /// ```
1057    /// # use chrono::Utc;
1058    /// # use dtg_credentials::DTGCredential;
1059    /// let vmc = DTGCredential::new_vmc(
1060    ///     "did:example:member".to_string(),
1061    ///     "did:example:community".to_string(),
1062    ///     Utc::now(),
1063    ///     None,
1064    ///     false,
1065    /// )
1066    /// .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
1067    /// assert_eq!(vmc.id(), Some("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52"));
1068    /// ```
1069    ///
1070    /// # Set it before signing
1071    ///
1072    /// A Data Integrity proof covers the credential minus its `proof`, so `id` is part of what
1073    /// is signed. Chain this onto the constructor, before [DTGCredential::sign] — adding an id
1074    /// to an already-signed credential leaves a document whose proof no longer verifies.
1075    pub fn with_id(mut self, id: impl Into<String>) -> Self {
1076        self.credential.id = Some(id.into());
1077        self
1078    }
1079
1080    /// Sets this credential's own identifier in place.
1081    ///
1082    /// The non-consuming form of [DTGCredential::with_id]; the same "before signing" caveat
1083    /// applies.
1084    pub fn set_id(&mut self, id: impl Into<String>) {
1085        self.credential.id = Some(id.into());
1086    }
1087}
1088
1089#[cfg(test)]
1090#[allow(deprecated)]
1091mod tests {
1092    use crate::{DTGCredential, WitnessContext};
1093    use chrono::{DateTime, Utc};
1094    use serde_json::json;
1095
1096    #[test]
1097    fn test_vmc_serialization() {
1098        let vmc = DTGCredential::new_vmc(
1099            "did:example:issuer".to_string(),
1100            "did:example:subject".to_string(),
1101            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1102                .unwrap()
1103                .with_timezone(&Utc),
1104            None,
1105            false,
1106        );
1107
1108        let txt = serde_json::to_string_pretty(&vmc).unwrap();
1109        let sample = r#"{
1110  "@context": [
1111    "https://www.w3.org/ns/credentials/v2",
1112    "https://firstperson.network/credentials/dtg/v1"
1113  ],
1114  "type": [
1115    "VerifiableCredential",
1116    "DTGCredential",
1117    "MembershipCredential"
1118  ],
1119  "issuer": "did:example:issuer",
1120  "validFrom": "2025-12-11T00:00:00Z",
1121  "credentialSubject": {
1122    "id": "did:example:subject"
1123  }
1124}"#;
1125
1126        assert_eq!(txt, sample);
1127    }
1128
1129    #[test]
1130    fn test_vmc_phc_serialization() {
1131        let vmc = DTGCredential::new_vmc(
1132            "did:example:issuer".to_string(),
1133            "did:example:subject".to_string(),
1134            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1135                .unwrap()
1136                .with_timezone(&Utc),
1137            None,
1138            true,
1139        );
1140
1141        let txt = serde_json::to_string_pretty(&vmc).unwrap();
1142        let sample = r#"{
1143  "@context": [
1144    "https://www.w3.org/ns/credentials/v2",
1145    "https://firstperson.network/credentials/dtg/v1"
1146  ],
1147  "type": [
1148    "VerifiableCredential",
1149    "DTGCredential",
1150    "MembershipCredential",
1151    "PersonhoodCredential"
1152  ],
1153  "issuer": "did:example:issuer",
1154  "validFrom": "2025-12-11T00:00:00Z",
1155  "credentialSubject": {
1156    "id": "did:example:subject"
1157  }
1158}"#;
1159
1160        assert_eq!(txt, sample);
1161    }
1162    /// `id` is OPTIONAL, and a credential that was never given one must keep serializing the
1163    /// shape it always did — no `"id": null`, no empty string.
1164    #[test]
1165    fn test_vmc_without_id_omits_the_property() {
1166        let vmc = DTGCredential::new_vmc(
1167            "did:example:issuer".to_string(),
1168            "did:example:subject".to_string(),
1169            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1170                .unwrap()
1171                .with_timezone(&Utc),
1172            None,
1173            false,
1174        );
1175
1176        assert_eq!(vmc.id(), None);
1177        let value: serde_json::Value = serde_json::to_value(&vmc).unwrap();
1178        assert!(
1179            value.get("id").is_none(),
1180            "an unset id must not appear on the wire at all: {value}"
1181        );
1182    }
1183
1184    /// `with_id` puts the identifier at the top level of the credential — a sibling of
1185    /// `issuer`, not something nested under `credentialSubject` (which carries the *subject's*
1186    /// id, a different thing entirely).
1187    #[test]
1188    fn test_vmc_with_id_serialization() {
1189        let vmc = DTGCredential::new_vmc(
1190            "did:example:issuer".to_string(),
1191            "did:example:subject".to_string(),
1192            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1193                .unwrap()
1194                .with_timezone(&Utc),
1195            None,
1196            false,
1197        )
1198        .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
1199
1200        let txt = serde_json::to_string_pretty(&vmc).unwrap();
1201        let sample = r#"{
1202  "@context": [
1203    "https://www.w3.org/ns/credentials/v2",
1204    "https://firstperson.network/credentials/dtg/v1"
1205  ],
1206  "type": [
1207    "VerifiableCredential",
1208    "DTGCredential",
1209    "MembershipCredential"
1210  ],
1211  "id": "urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff",
1212  "issuer": "did:example:issuer",
1213  "validFrom": "2025-12-11T00:00:00Z",
1214  "credentialSubject": {
1215    "id": "did:example:subject"
1216  }
1217}"#;
1218
1219        assert_eq!(txt, sample);
1220    }
1221
1222    /// The identifier has to survive a round trip. It arrives on the wire and is read back
1223    /// through `TryFrom<DTGCommon>`, which is where `taskContext` was previously being dropped
1224    /// — a field that deserializes into nothing breaks signing and verification silently.
1225    #[test]
1226    fn test_id_round_trips_through_deserialization() {
1227        let vmc = DTGCredential::new_vmc(
1228            "did:example:issuer".to_string(),
1229            "did:example:subject".to_string(),
1230            Utc::now(),
1231            None,
1232            false,
1233        )
1234        .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
1235
1236        let txt = serde_json::to_string(&vmc).unwrap();
1237        let parsed: DTGCredential = serde_json::from_str(&txt).unwrap();
1238        assert_eq!(
1239            parsed.id(),
1240            Some("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff")
1241        );
1242    }
1243
1244    /// A credential with no `id` still deserializes — the property is OPTIONAL, and every
1245    /// credential issued before this field existed has none.
1246    #[test]
1247    fn test_missing_id_deserializes_as_none() {
1248        let parsed: DTGCredential = serde_json::from_str(
1249            r#"{
1250              "@context": ["https://www.w3.org/ns/credentials/v2"],
1251              "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1252              "issuer": "did:example:issuer",
1253              "validFrom": "2025-12-11T00:00:00Z",
1254              "credentialSubject": { "id": "did:example:subject" }
1255            }"#,
1256        )
1257        .unwrap();
1258        assert_eq!(parsed.id(), None);
1259    }
1260
1261    /// `set_id` is the in-place form of `with_id`; both write the same property.
1262    #[test]
1263    fn test_set_id_matches_with_id() {
1264        let build = || {
1265            DTGCredential::new_vrc(
1266                "did:example:issuer".to_string(),
1267                "did:example:subject".to_string(),
1268                DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1269                    .unwrap()
1270                    .with_timezone(&Utc),
1271                None,
1272            )
1273        };
1274        let mut in_place = build();
1275        in_place.set_id("urn:uuid:abc");
1276        assert_eq!(
1277            serde_json::to_value(&in_place).unwrap(),
1278            serde_json::to_value(build().with_id("urn:uuid:abc")).unwrap()
1279        );
1280    }
1281
1282    #[test]
1283    fn test_vrc_serialization() {
1284        let vrc = DTGCredential::new_vrc(
1285            "did:example:issuer".to_string(),
1286            "did:example:subject".to_string(),
1287            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1288                .unwrap()
1289                .with_timezone(&Utc),
1290            None,
1291        );
1292
1293        let txt = serde_json::to_string_pretty(&vrc).unwrap();
1294        let sample = r#"{
1295  "@context": [
1296    "https://www.w3.org/ns/credentials/v2",
1297    "https://firstperson.network/credentials/dtg/v1"
1298  ],
1299  "type": [
1300    "VerifiableCredential",
1301    "DTGCredential",
1302    "RelationshipCredential"
1303  ],
1304  "issuer": "did:example:issuer",
1305  "validFrom": "2025-12-11T00:00:00Z",
1306  "credentialSubject": {
1307    "id": "did:example:subject"
1308  }
1309}"#;
1310
1311        assert_eq!(txt, sample);
1312    }
1313
1314    #[test]
1315    fn test_vic_serialization() {
1316        let vic = DTGCredential::new_vic(
1317            "did:example:issuer".to_string(),
1318            "did:example:subject".to_string(),
1319            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1320                .unwrap()
1321                .with_timezone(&Utc),
1322            None,
1323        );
1324
1325        let txt = serde_json::to_string_pretty(&vic).unwrap();
1326        let sample = r#"{
1327  "@context": [
1328    "https://www.w3.org/ns/credentials/v2",
1329    "https://firstperson.network/credentials/dtg/v1"
1330  ],
1331  "type": [
1332    "VerifiableCredential",
1333    "DTGCredential",
1334    "InvitationCredential"
1335  ],
1336  "issuer": "did:example:issuer",
1337  "validFrom": "2025-12-11T00:00:00Z",
1338  "credentialSubject": {
1339    "id": "did:example:subject"
1340  }
1341}"#;
1342
1343        assert_eq!(txt, sample);
1344    }
1345
1346    #[test]
1347    fn test_vpc_serialization() {
1348        let vpc = DTGCredential::new_vpc(
1349            "did:example:issuer".to_string(),
1350            "did:example:subject".to_string(),
1351            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1352                .unwrap()
1353                .with_timezone(&Utc),
1354            None,
1355        );
1356
1357        let txt = serde_json::to_string_pretty(&vpc).unwrap();
1358        let sample = r#"{
1359  "@context": [
1360    "https://www.w3.org/ns/credentials/v2",
1361    "https://firstperson.network/credentials/dtg/v1"
1362  ],
1363  "type": [
1364    "VerifiableCredential",
1365    "DTGCredential",
1366    "PersonaCredential"
1367  ],
1368  "issuer": "did:example:issuer",
1369  "validFrom": "2025-12-11T00:00:00Z",
1370  "credentialSubject": {
1371    "id": "did:example:subject"
1372  }
1373}"#;
1374
1375        assert_eq!(txt, sample);
1376    }
1377
1378    #[test]
1379    fn test_vec_serialization() {
1380        let vec = DTGCredential::new_vec(
1381            "did:example:issuer".to_string(),
1382            "did:example:subject".to_string(),
1383            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1384                .unwrap()
1385                .with_timezone(&Utc),
1386            None,
1387            json!({
1388              "type": "SkillEndorsement",
1389              "name": "Software Development",
1390              "competencyLevel": "expert"
1391            }),
1392        );
1393
1394        let txt = serde_json::to_string_pretty(&vec).unwrap();
1395        let sample = r#"{
1396  "@context": [
1397    "https://www.w3.org/ns/credentials/v2",
1398    "https://firstperson.network/credentials/dtg/v1"
1399  ],
1400  "type": [
1401    "VerifiableCredential",
1402    "DTGCredential",
1403    "EndorsementCredential"
1404  ],
1405  "issuer": "did:example:issuer",
1406  "validFrom": "2025-12-11T00:00:00Z",
1407  "credentialSubject": {
1408    "id": "did:example:subject",
1409    "endorsement": {
1410      "competencyLevel": "expert",
1411      "name": "Software Development",
1412      "type": "SkillEndorsement"
1413    }
1414  }
1415}"#;
1416
1417        assert_eq!(txt, sample);
1418    }
1419
1420    #[test]
1421    fn test_vwc_serialization() {
1422        let vwc = DTGCredential::new_vwc(
1423            "did:example:issuer".to_string(),
1424            "did:example:subject".to_string(),
1425            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1426                .unwrap()
1427                .with_timezone(&Utc),
1428            None,
1429            "thread-abc-123".to_string(),
1430            Some("zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw".to_string()),
1431            Some(WitnessContext {
1432                event: Some("EthDenver 2024".to_string()),
1433                session_id: Some("session-8822-nonce".to_string()),
1434                method: Some("in-person-proximity".to_string()),
1435            }),
1436        );
1437
1438        let txt = serde_json::to_string_pretty(&vwc).unwrap();
1439
1440        let sample = r#"{
1441  "@context": [
1442    "https://www.w3.org/ns/credentials/v2",
1443    "https://firstperson.network/credentials/dtg/v1"
1444  ],
1445  "type": [
1446    "VerifiableCredential",
1447    "DTGCredential",
1448    "WitnessCredential"
1449  ],
1450  "issuer": "did:example:issuer",
1451  "validFrom": "2025-12-11T00:00:00Z",
1452  "taskContext": "thread-abc-123",
1453  "credentialSubject": {
1454    "id": "did:example:subject",
1455    "digestMultibase": "zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw",
1456    "witnessContext": {
1457      "event": "EthDenver 2024",
1458      "sessionId": "session-8822-nonce",
1459      "method": "in-person-proximity"
1460    }
1461  }
1462}"#;
1463
1464        assert_eq!(txt, sample);
1465    }
1466
1467    #[test]
1468    fn test_rcard_serialization() {
1469        let rcard = DTGCredential::new_rcard(
1470            "did:example:issuer".to_string(),
1471            "did:example:subject".to_string(),
1472            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1473                .unwrap()
1474                .with_timezone(&Utc),
1475            None,
1476            json!([
1477                "vcard",
1478                [
1479                    ["fn", {}, "text", "Alice Smith"],
1480                    ["email", {}, "text", "alice@example.com"]
1481                ]
1482            ]),
1483        );
1484
1485        let txt = serde_json::to_string_pretty(&rcard).unwrap();
1486
1487        let sample = r#"{
1488  "@context": [
1489    "https://www.w3.org/ns/credentials/v2",
1490    "https://firstperson.network/credentials/dtg/v1"
1491  ],
1492  "type": [
1493    "VerifiableCredential",
1494    "DTGCredential",
1495    "RCardCredential"
1496  ],
1497  "issuer": "did:example:issuer",
1498  "validFrom": "2025-12-11T00:00:00Z",
1499  "credentialSubject": {
1500    "id": "did:example:subject",
1501    "card": [
1502      "vcard",
1503      [
1504        [
1505          "fn",
1506          {},
1507          "text",
1508          "Alice Smith"
1509        ],
1510        [
1511          "email",
1512          {},
1513          "text",
1514          "alice@example.com"
1515        ]
1516      ]
1517    ]
1518  }
1519}"#;
1520
1521        assert_eq!(txt, sample);
1522    }
1523}