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