@interop/vc 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,736 @@
1
+ /**
2
+ * A TypeScript implementation of Verifiable Credentials.
3
+ *
4
+ * @author Dave Longley
5
+ * @author David I. Lehn
6
+ * @author Dmitri Zagidulin
7
+ *
8
+ * @license BSD 3-Clause License
9
+ * Copyright (c) 2017-2025 Digital Bazaar, Inc.
10
+ * Copyright (c) 2022-2025 DCC (Digital Credentials Consortium).
11
+ * Copyright (c) 2026 TypeScript conversion by Interop Alliance.
12
+ * All rights reserved.
13
+ *
14
+ * Redistribution and use in source and binary forms, with or without
15
+ * modification, are permitted provided that the following conditions are met:
16
+ *
17
+ * Redistributions of source code must retain the above copyright notice,
18
+ * this list of conditions and the following disclaimer.
19
+ *
20
+ * Redistributions in binary form must reproduce the above copyright
21
+ * notice, this list of conditions and the following disclaimer in the
22
+ * documentation and/or other materials provided with the distribution.
23
+ *
24
+ * Neither the name of the Digital Bazaar, Inc. nor the names of its
25
+ * contributors may be used to endorse or promote products derived from
26
+ * this software without specific prior written permission.
27
+ *
28
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
29
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
30
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
31
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
32
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
34
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
35
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
36
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
37
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
38
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
+ */
40
+ import jsigs from '@interop/jsonld-signatures';
41
+ import jsonld from '@interop/jsonld';
42
+ import { assertCredentialContext, assertDateString, checkContextVersion, compareTime, CREDENTIALS_CONTEXT_V1_URL, CREDENTIALS_CONTEXT_V2_URL } from './helpers.js';
43
+ import { documentLoader as _documentLoader } from './documentLoader.js';
44
+ import { CredentialIssuancePurpose } from './CredentialIssuancePurpose.js';
45
+ const { AssertionProofPurpose, AuthenticationProofPurpose } = jsigs.purposes;
46
+ export { dateRegex } from './helpers.js';
47
+ export const defaultDocumentLoader = jsigs.extendContextLoader(_documentLoader);
48
+ export { CredentialIssuancePurpose };
49
+ /**
50
+ * Issues a verifiable credential (by taking a base credential document,
51
+ * and adding a digital signature to it).
52
+ *
53
+ * @param options - The options to use.
54
+ * @param options.credential - Base credential document.
55
+ * @param options.suite - Signature suite (with private key material or an API
56
+ * to use it), passed in to `sign()`.
57
+ * @param options.purpose - A ProofPurpose. If not specified, a default purpose
58
+ * will be created.
59
+ * @param options.documentLoader - A document loader.
60
+ * @param options.now - A string representing date time in ISO 8601 format or an
61
+ * instance of Date. Defaults to current date time.
62
+ * @param options.maxClockSkew - A maximum number of seconds that clocks may be
63
+ * skewed when checking date-times against `now`.
64
+ *
65
+ * @throws {Error} If missing required properties.
66
+ *
67
+ * @returns Resolves on completion.
68
+ */
69
+ export async function issue({ credential, suite, purpose = new CredentialIssuancePurpose(), documentLoader = defaultDocumentLoader, now, maxClockSkew = 300 } = {}) {
70
+ // check to make sure the `suite` has required params
71
+ // Note: verificationMethod defaults to publicKey.id, in suite constructor
72
+ if (!suite) {
73
+ throw new TypeError('"suite" parameter is required for issuing.');
74
+ }
75
+ if (!suite.verificationMethod) {
76
+ throw new TypeError('"suite.verificationMethod" property is required.');
77
+ }
78
+ if (!credential) {
79
+ throw new TypeError('"credential" parameter is required for issuing.');
80
+ }
81
+ if (checkContextVersion({ credential, version: 1.0 }) &&
82
+ !credential.issuanceDate) {
83
+ const now = new Date().toJSON();
84
+ credential.issuanceDate = `${now.slice(0, now.length - 5)}Z`;
85
+ }
86
+ // run common credential checks
87
+ _checkCredential({ credential, now, mode: 'issue', maxClockSkew });
88
+ return (await jsigs.sign(credential, {
89
+ purpose,
90
+ documentLoader,
91
+ suite
92
+ }));
93
+ }
94
+ /**
95
+ * Derives a proof from the given verifiable credential, resulting in a new
96
+ * verifiable credential. This method is usually used to generate selective
97
+ * disclosure and / or unlinkable proofs.
98
+ *
99
+ * @param options - The options to use.
100
+ * @param options.verifiableCredential - The verifiable credential containing a
101
+ * base proof to derive another proof from.
102
+ * @param options.suite - Derived proof signature suite.
103
+ * @param options.documentLoader - A document loader.
104
+ *
105
+ * @throws {Error} If missing required properties.
106
+ *
107
+ * @returns Resolves on completion.
108
+ */
109
+ export async function derive({ verifiableCredential, suite, documentLoader = defaultDocumentLoader } = {}) {
110
+ if (!verifiableCredential) {
111
+ throw new TypeError('"credential" parameter is required for deriving.');
112
+ }
113
+ if (!suite) {
114
+ throw new TypeError('"suite" parameter is required for deriving.');
115
+ }
116
+ // run common credential checks
117
+ _checkCredential({ credential: verifiableCredential, mode: 'issue' });
118
+ return (await jsigs.derive(verifiableCredential, {
119
+ purpose: new AssertionProofPurpose(),
120
+ documentLoader,
121
+ suite
122
+ }));
123
+ }
124
+ /**
125
+ * Verifies a verifiable presentation:
126
+ * - Checks that the presentation is well-formed
127
+ * - Checks the proofs (for example, checks digital signatures against the
128
+ * provided public keys).
129
+ *
130
+ * @param options - The options to use.
131
+ * @param options.presentation - Verifiable presentation, signed or unsigned,
132
+ * that may contain within it a verifiable credential.
133
+ * @param options.suite - One or more signature suites that are supported by the
134
+ * caller's use case. This is an explicit design decision -- the calling code
135
+ * must specify which signature types (ed25519, RSA, etc) are allowed.
136
+ * Although it is expected that the secure resolution/fetching of the public
137
+ * key material (to verify against) is to be handled by the documentLoader,
138
+ * the suite param can optionally include the key directly.
139
+ * @param options.unsignedPresentation - By default, this function assumes that
140
+ * a presentation is signed (and will return an error if a `proof` section is
141
+ * missing). Set this to `true` if you're using an unsigned presentation.
142
+ * @param options.presentationPurpose - Optional proof purpose (a default one
143
+ * will be created if not passed in).
144
+ * @param options.challenge - Required if purpose is not passed in.
145
+ * @param options.controller - A controller.
146
+ * @param options.domain - A domain.
147
+ * @param options.documentLoader - A document loader.
148
+ * @param options.checkStatus - Optional function for checking credential status
149
+ * if `credentialStatus` is present on the credential.
150
+ * @param options.now - A string representing date time in ISO 8601 format or an
151
+ * instance of Date. Defaults to current date time.
152
+ * @param options.maxClockSkew - A maximum number of seconds that clocks may be
153
+ * skewed when checking date-times against `now`.
154
+ * @param options.includeCredentials - Set to `true` to include each verified
155
+ * `credential` in its entry in `credentialResults`. Defaults to `true` to
156
+ * preserve backwards compatibility; set to `false` to omit them.
157
+ *
158
+ * @returns The verification result.
159
+ */
160
+ export async function verify(options = {}) {
161
+ const { presentation } = options;
162
+ try {
163
+ if (!presentation) {
164
+ throw new TypeError('A "presentation" property is required for verifying.');
165
+ }
166
+ return await _verifyPresentation(options);
167
+ }
168
+ catch (error) {
169
+ return {
170
+ verified: false,
171
+ results: [{ presentation, verified: false, error }],
172
+ error: error
173
+ };
174
+ }
175
+ }
176
+ /**
177
+ * Verifies a verifiable credential:
178
+ * - Checks that the credential is well-formed
179
+ * - Checks the proofs (for example, checks digital signatures against the
180
+ * provided public keys).
181
+ *
182
+ * @param options - The options.
183
+ * @param options.credential - Verifiable credential.
184
+ * @param options.suite - One or more signature suites that are supported by the
185
+ * caller's use case. This is an explicit design decision -- the calling code
186
+ * must specify which signature types (ed25519, RSA, etc) are allowed.
187
+ * Although it is expected that the secure resolution/fetching of the public
188
+ * key material (to verify against) is to be handled by the documentLoader,
189
+ * the suite param can optionally include the key directly.
190
+ * @param options.purpose - Optional proof purpose (a default one will be
191
+ * created if not passed in).
192
+ * @param options.documentLoader - A document loader.
193
+ * @param options.checkStatus - Optional function for checking credential status
194
+ * if `credentialStatus` is present on the credential.
195
+ * @param options.now - A string representing date time in ISO 8601 format or an
196
+ * instance of Date. Defaults to current date time.
197
+ * @param options.maxClockSkew - A maximum number of seconds that clocks may be
198
+ * skewed when checking date-times against `now`.
199
+ *
200
+ * @returns The verification result.
201
+ */
202
+ export async function verifyCredential(options = {}) {
203
+ const { credential } = options;
204
+ try {
205
+ if (!credential) {
206
+ throw new TypeError('A "credential" property is required for verifying.');
207
+ }
208
+ return await _verifyCredential(options);
209
+ }
210
+ catch (error) {
211
+ return {
212
+ verified: false,
213
+ results: [{ credential, verified: false, error }],
214
+ error: error
215
+ };
216
+ }
217
+ }
218
+ /**
219
+ * Verifies a verifiable credential.
220
+ *
221
+ * @param options - The options.
222
+ *
223
+ * @throws {Error} If required parameters are missing (in `_checkCredential`).
224
+ *
225
+ * @returns The verification result.
226
+ */
227
+ async function _verifyCredential(options = {}) {
228
+ const credential = options.credential;
229
+ const { checkStatus, now, maxClockSkew = 300 } = options;
230
+ // Fine-grained result log containing checks performed. Example:
231
+ // [
232
+ // {id: 'valid_signature', valid: true},
233
+ // {id: 'issuer_did_resolves', valid: true},
234
+ // {id: 'expiration', valid: true},
235
+ // {id: 'revocation_status', valid: true},
236
+ // {id: 'suspension_status', valid: true}
237
+ // ]
238
+ const log = [];
239
+ const documentLoader = options.documentLoader || defaultDocumentLoader;
240
+ const { controller } = options;
241
+ const purpose = options.purpose || new CredentialIssuancePurpose({ controller });
242
+ let result;
243
+ try {
244
+ result = await jsigs.verify(credential, {
245
+ purpose,
246
+ documentLoader,
247
+ ...options
248
+ });
249
+ }
250
+ catch (error) {
251
+ log.push({ id: 'valid_signature', valid: false });
252
+ error.log = log;
253
+ throw error;
254
+ }
255
+ // if verification has failed, skip status check
256
+ if (!result.verified) {
257
+ return result;
258
+ }
259
+ log.push({ id: 'valid_signature', valid: true });
260
+ log.push({ id: 'issuer_did_resolves', valid: true });
261
+ // if credential status is provided, a `checkStatus` function must be given
262
+ if (credential.credentialStatus && typeof checkStatus !== 'function') {
263
+ throw new TypeError('A "checkStatus" function must be given to verify credentials with ' +
264
+ '"credentialStatus".');
265
+ }
266
+ if (credential.credentialStatus) {
267
+ await addStatusInfoToLog({ options, result, log });
268
+ }
269
+ // run common credential checks (add check results to log)
270
+ _checkCredential({ credential, log, now, maxClockSkew });
271
+ result.log = log;
272
+ if (result.results) {
273
+ result.results[0].log = log;
274
+ }
275
+ return result;
276
+ }
277
+ async function addStatusInfoToLog({ options, result, log }) {
278
+ const { checkStatus } = options;
279
+ result.statusResult = await checkStatus(options);
280
+ if (!result.statusResult.verified) {
281
+ result.verified = false;
282
+ }
283
+ const statusResults = result.statusResult?.results ?? [];
284
+ for (const entry of statusResults) {
285
+ log.push({
286
+ id: `${entry.credentialStatus.statusPurpose}_status`,
287
+ valid: entry.verified
288
+ });
289
+ }
290
+ }
291
+ /**
292
+ * Creates an unsigned presentation from a given verifiable credential.
293
+ *
294
+ * @param options - Options to use.
295
+ * @param options.verifiableCredential - One or more verifiable credential.
296
+ * @param options.id - Optional VP id.
297
+ * @param options.holder - Optional presentation holder url.
298
+ * @param options.now - A string representing date time in ISO 8601 format or an
299
+ * instance of Date. Defaults to current date time.
300
+ * @param options.version - The VC context version to use.
301
+ * @param options.verify - If set to true, throw verification errors for
302
+ * individual VCs (such as when the VC is expired, etc).
303
+ * @param options.maxClockSkew - A maximum number of seconds that clocks may be
304
+ * skewed when checking date-times against `now`.
305
+ *
306
+ * @throws {TypeError} If verifiableCredential param is missing.
307
+ * @throws {Error} If the credential (or the presentation params) are missing
308
+ * required properties.
309
+ *
310
+ * @returns The credential wrapped inside of a VerifiablePresentation.
311
+ */
312
+ export function createPresentation({ verifiableCredential, id, holder, now, version = 2.0, verify = true, maxClockSkew = 300 } = {}) {
313
+ const initialContext = version === 2.0 ? CREDENTIALS_CONTEXT_V2_URL : CREDENTIALS_CONTEXT_V1_URL;
314
+ const presentation = {
315
+ '@context': [initialContext],
316
+ type: ['VerifiablePresentation']
317
+ };
318
+ if (verifiableCredential) {
319
+ const credentials = Array.isArray(verifiableCredential)
320
+ ? verifiableCredential
321
+ : [verifiableCredential];
322
+ if (verify) {
323
+ // ensure all credentials are valid and verified
324
+ for (const credential of credentials) {
325
+ _checkCredential({
326
+ credential,
327
+ now,
328
+ maxClockSkew,
329
+ mode: verify ? 'verify' : 'do not force verify'
330
+ });
331
+ }
332
+ }
333
+ presentation.verifiableCredential = credentials;
334
+ }
335
+ if (id) {
336
+ presentation.id = id;
337
+ }
338
+ if (holder) {
339
+ presentation.holder = holder;
340
+ }
341
+ _checkPresentation(presentation);
342
+ return presentation;
343
+ }
344
+ /**
345
+ * Signs a given presentation.
346
+ *
347
+ * @param options - Options to use.
348
+ * @param options.presentation - A presentation.
349
+ * @param options.suite - Passed in to `sign()`.
350
+ * @param options.purpose - A ProofPurpose. If not specified, a default purpose
351
+ * will be created with the domain and challenge options.
352
+ * @param options.domain - A domain.
353
+ * @param options.challenge - A required challenge.
354
+ * @param options.documentLoader - A document loader.
355
+ *
356
+ * @returns A VerifiablePresentation with a proof.
357
+ */
358
+ export async function signPresentation(options = {}) {
359
+ const { presentation, domain, challenge } = options;
360
+ const purpose = options.purpose ||
361
+ new AuthenticationProofPurpose({
362
+ domain,
363
+ challenge: challenge
364
+ });
365
+ const documentLoader = options.documentLoader || defaultDocumentLoader;
366
+ return (await jsigs.sign(presentation, {
367
+ ...options,
368
+ purpose,
369
+ documentLoader
370
+ }));
371
+ }
372
+ /**
373
+ * Verifies that the VerifiablePresentation is well formed, and checks the
374
+ * proof signature if it's present. Also verifies all the VerifiableCredentials
375
+ * that are present in the presentation, if any.
376
+ *
377
+ * @param options - The options.
378
+ *
379
+ * @throws {Error} If presentation is missing required params.
380
+ *
381
+ * @returns The verification result.
382
+ */
383
+ async function _verifyPresentation(options = {}) {
384
+ const { presentation, unsignedPresentation, includeCredentials = true } = options;
385
+ _checkPresentation(presentation);
386
+ const documentLoader = options.documentLoader || defaultDocumentLoader;
387
+ // FIXME: verify presentation first, then each individual credential
388
+ // only if that proof is verified
389
+ // if verifiableCredentials are present, verify them, individually
390
+ let credentialResults;
391
+ let verified = true;
392
+ const credentials = jsonld.getValues(presentation, 'verifiableCredential');
393
+ if (credentials.length > 0) {
394
+ // verify every credential in `verifiableCredential`
395
+ credentialResults = await Promise.all(credentials.map((credential) => {
396
+ return verifyCredential({ ...options, credential, documentLoader });
397
+ }));
398
+ for (const [i, credentialResult] of credentialResults.entries()) {
399
+ credentialResult.credentialId = credentials[i].id;
400
+ if (includeCredentials) {
401
+ credentialResult.credential = credentials[i];
402
+ }
403
+ }
404
+ const allCredentialsVerified = credentialResults.every(result => result.verified);
405
+ if (!allCredentialsVerified) {
406
+ verified = false;
407
+ }
408
+ }
409
+ if (unsignedPresentation) {
410
+ // No need to verify the proof section of this presentation
411
+ return { verified, results: [presentation], credentialResults };
412
+ }
413
+ const { controller, domain, challenge } = options;
414
+ if (!options.presentationPurpose && !challenge) {
415
+ throw new Error('A "challenge" param is required for AuthenticationProofPurpose.');
416
+ }
417
+ const purpose = options.presentationPurpose ||
418
+ new AuthenticationProofPurpose({
419
+ controller,
420
+ domain,
421
+ challenge: challenge
422
+ });
423
+ const presentationResult = await jsigs.verify(presentation, {
424
+ ...options,
425
+ purpose,
426
+ documentLoader
427
+ });
428
+ return {
429
+ presentationResult,
430
+ verified: verified && presentationResult.verified,
431
+ credentialResults,
432
+ error: presentationResult.error
433
+ };
434
+ }
435
+ /**
436
+ * @param obj - Either an object with an id property or a string that is an id.
437
+ * @returns Either an id or undefined.
438
+ */
439
+ function _getId(obj) {
440
+ if (typeof obj === 'string') {
441
+ return obj;
442
+ }
443
+ if (!('id' in obj)) {
444
+ return;
445
+ }
446
+ return obj.id;
447
+ }
448
+ // export for testing
449
+ /**
450
+ * @param presentation - An object that could be a presentation.
451
+ *
452
+ * @throws {Error}
453
+ */
454
+ export function _checkPresentation(presentation) {
455
+ // normalize to an array to allow the common case of context being a string
456
+ const context = Array.isArray(presentation['@context'])
457
+ ? presentation['@context']
458
+ : [presentation['@context']];
459
+ assertCredentialContext({ context });
460
+ const types = jsonld.getValues(presentation, 'type');
461
+ // check type presence
462
+ if (!types.includes('VerifiablePresentation')) {
463
+ throw new Error('"type" must include "VerifiablePresentation".');
464
+ }
465
+ }
466
+ // these props of a VC must be an object with a type
467
+ // if present in a VC or VP
468
+ const mustHaveType = ['proof', 'credentialStatus', 'termsOfUse', 'evidence'];
469
+ // export for testing
470
+ /**
471
+ * @param options - The options.
472
+ * @param options.credential - An object that could be a VerifiableCredential.
473
+ * @param options.log - Optional events log, for fine-grained verification
474
+ * result reporting.
475
+ * @param options.now - A string representing date time in ISO 8601 format or an
476
+ * instance of Date. Defaults to current date time.
477
+ * @param options.mode - The mode of operation for this validation function,
478
+ * either `issue` or `verify`.
479
+ * @param options.maxClockSkew - A maximum number of seconds that clocks may be
480
+ * skewed when checking date-times against `now`.
481
+ *
482
+ * @throws {Error}
483
+ */
484
+ export function _checkCredential({ credential, log = [], now = new Date(), mode = 'verify', maxClockSkew = 300 }) {
485
+ const nowDate = typeof now === 'string' ? new Date(now) : now;
486
+ assertCredentialContext({ context: credential['@context'] });
487
+ // check type presence and cardinality
488
+ if (!credential.type) {
489
+ throw new Error('"type" property is required.');
490
+ }
491
+ if (!jsonld.getValues(credential, 'type').includes('VerifiableCredential')) {
492
+ throw new Error('"type" must include `VerifiableCredential`.');
493
+ }
494
+ _checkCredentialSubjects({ credential });
495
+ if (!credential.issuer) {
496
+ throw new Error('"issuer" property is required.');
497
+ }
498
+ if (checkContextVersion({ credential, version: 1.0 })) {
499
+ // check issuanceDate exists
500
+ if (!credential.issuanceDate) {
501
+ throw new Error('"issuanceDate" property is required.');
502
+ }
503
+ // check issuanceDate format on issue
504
+ assertDateString({ credential, prop: 'issuanceDate' });
505
+ // check issuanceDate cardinality
506
+ if (jsonld.getValues(credential, 'issuanceDate').length > 1) {
507
+ throw new Error('"issuanceDate" property can only have one value.');
508
+ }
509
+ // optionally check expirationDate
510
+ if ('expirationDate' in credential) {
511
+ // check if `expirationDate` property is a date
512
+ try {
513
+ assertDateString({ credential, prop: 'expirationDate' });
514
+ }
515
+ catch (error) {
516
+ log.push({ id: 'expiration', valid: false });
517
+ error.log = log;
518
+ throw error;
519
+ }
520
+ if (mode === 'verify') {
521
+ // check if `now` is after `expirationDate`
522
+ const expirationDate = new Date(credential.expirationDate);
523
+ if (compareTime({ t1: nowDate, t2: expirationDate, maxClockSkew }) > 0) {
524
+ log.push({ id: 'expiration', valid: false });
525
+ const error = new Error('Credential has expired.');
526
+ error.log = log;
527
+ throw error;
528
+ }
529
+ }
530
+ }
531
+ log.push({ id: 'expiration', valid: true });
532
+ // check if `now` is before `issuanceDate` on verification
533
+ if (mode === 'verify') {
534
+ const issuanceDate = new Date(credential.issuanceDate);
535
+ if (compareTime({ t1: issuanceDate, t2: nowDate, maxClockSkew }) > 0) {
536
+ throw new Error(`The current date time (${nowDate.toISOString()}) is before the ` +
537
+ `"issuanceDate" (${credential.issuanceDate}).`);
538
+ }
539
+ }
540
+ }
541
+ if (checkContextVersion({ credential, version: 2.0 })) {
542
+ // check if 'validUntil' and 'validFrom'
543
+ const { validUntil, validFrom } = credential;
544
+ if (validUntil) {
545
+ try {
546
+ assertDateString({ credential, prop: 'validUntil' });
547
+ }
548
+ catch (error) {
549
+ log.push({ id: 'expiration', valid: false });
550
+ error.log = log;
551
+ throw error;
552
+ }
553
+ if (mode === 'verify') {
554
+ const validUntilDate = new Date(credential.validUntil);
555
+ if (compareTime({ t1: nowDate, t2: validUntilDate, maxClockSkew }) > 0) {
556
+ log.push({ id: 'expiration', valid: false });
557
+ const error = new Error(`The current date time (${nowDate.toISOString()}) is after ` +
558
+ `"validUntil" (${credential.validUntil}).`);
559
+ error.log = log;
560
+ throw error;
561
+ }
562
+ }
563
+ }
564
+ log.push({ id: 'expiration', valid: true });
565
+ if (validFrom) {
566
+ assertDateString({ credential, prop: 'validFrom' });
567
+ if (mode === 'verify') {
568
+ // check if `now` is before `validFrom`
569
+ const validFromDate = new Date(credential.validFrom);
570
+ if (compareTime({ t1: validFromDate, t2: nowDate, maxClockSkew }) > 0) {
571
+ throw new Error(`The current date time (${nowDate.toISOString()}) is before ` +
572
+ `"validFrom" (${credential.validFrom}).`);
573
+ }
574
+ }
575
+ }
576
+ }
577
+ // check issuer cardinality
578
+ if (jsonld.getValues(credential, 'issuer').length > 1) {
579
+ throw new Error('"issuer" property can only have one value.');
580
+ }
581
+ // check issuer is a URL
582
+ if ('issuer' in credential) {
583
+ const issuer = _getId(credential.issuer);
584
+ if (!issuer) {
585
+ throw new Error(`"issuer" id is required.`);
586
+ }
587
+ _validateUriId({ id: issuer, propertyName: 'issuer' });
588
+ }
589
+ // check credentialStatus
590
+ jsonld.getValues(credential, 'credentialStatus').forEach((cs) => {
591
+ // check if optional "id" is a URL
592
+ if ('id' in cs) {
593
+ _validateUriId({ id: cs.id, propertyName: 'credentialStatus.id' });
594
+ }
595
+ // check "type" present
596
+ if (!cs.type) {
597
+ throw new Error('"credentialStatus" must include a type.');
598
+ }
599
+ });
600
+ // check evidences are URLs
601
+ jsonld.getValues(credential, 'evidence').forEach((evidence) => {
602
+ const evidenceId = _getId(evidence);
603
+ if (evidenceId) {
604
+ _validateUriId({ id: evidenceId, propertyName: 'evidence' });
605
+ }
606
+ });
607
+ // check if properties that require a type are
608
+ // defined, objects, and objects with types
609
+ for (const prop of mustHaveType) {
610
+ if (prop in credential) {
611
+ const value = credential[prop];
612
+ if (Array.isArray(value)) {
613
+ value.forEach(entry => _checkTypedObject(entry, prop));
614
+ continue;
615
+ }
616
+ _checkTypedObject(value, prop);
617
+ }
618
+ }
619
+ }
620
+ /**
621
+ * Checks that a property is a non-empty object with property type.
622
+ *
623
+ * @param obj - A potential object.
624
+ * @param name - The name of the property.
625
+ *
626
+ * @throws {Error} If the property is not an object with a type.
627
+ */
628
+ function _checkTypedObject(obj, name) {
629
+ if (!isObject(obj)) {
630
+ throw new Error(`property "${name}" must be an object.`);
631
+ }
632
+ if (_emptyObject(obj)) {
633
+ throw new Error(`property "${name}" can not be an empty object.`);
634
+ }
635
+ if (!('type' in obj)) {
636
+ throw new Error(`property "${name}" must have property type.`);
637
+ }
638
+ }
639
+ /**
640
+ * Takes in a credential and checks the credentialSubject(s).
641
+ *
642
+ * @param options - Options.
643
+ * @param options.credential - The credential to check.
644
+ *
645
+ * @throws {Error} Throws on errors in the credential subject.
646
+ */
647
+ function _checkCredentialSubjects({ credential }) {
648
+ if (!credential?.credentialSubject) {
649
+ throw new Error('"credentialSubject" property is required.');
650
+ }
651
+ if (Array.isArray(credential.credentialSubject)) {
652
+ for (const subject of credential.credentialSubject) {
653
+ _checkCredentialSubject({ subject });
654
+ }
655
+ return;
656
+ }
657
+ _checkCredentialSubject({ subject: credential.credentialSubject });
658
+ }
659
+ /**
660
+ * Checks a credential subject is valid.
661
+ *
662
+ * @param options - Options.
663
+ * @param options.subject - A potential credential subject.
664
+ *
665
+ * @throws {Error} If the credentialSubject is not valid.
666
+ */
667
+ function _checkCredentialSubject({ subject }) {
668
+ if (isObject(subject) === false) {
669
+ throw new Error('"credentialSubject" must be a non-null object.');
670
+ }
671
+ if (_emptyObject(subject)) {
672
+ throw new Error('"credentialSubject" must make a claim.');
673
+ }
674
+ // If credentialSubject.id is present and is not a URI, reject it
675
+ if (subject.id) {
676
+ _validateUriId({ id: subject.id, propertyName: 'credentialSubject.id' });
677
+ }
678
+ }
679
+ /**
680
+ * Checks if parameter is an object.
681
+ *
682
+ * @param obj - A potential object.
683
+ *
684
+ * @returns False if not an object or null.
685
+ */
686
+ function isObject(obj) {
687
+ // return false for null even though it has type object
688
+ if (obj === null) {
689
+ return false;
690
+ }
691
+ // if something has type object and is not null return true
692
+ if (typeof obj === 'object') {
693
+ return true;
694
+ }
695
+ // return false for strings, symbols, etc.
696
+ return false;
697
+ }
698
+ /**
699
+ * Is it an empty object?
700
+ *
701
+ * @param obj - A potential object.
702
+ *
703
+ * @returns Is it empty?
704
+ */
705
+ function _emptyObject(obj) {
706
+ // if the parameter is not an object return true
707
+ // as a non-object is an empty object
708
+ if (!isObject(obj)) {
709
+ return true;
710
+ }
711
+ return Object.keys(obj).length === 0;
712
+ }
713
+ /**
714
+ * Validates if an ID is a URL.
715
+ *
716
+ * @param options - Options.
717
+ * @param options.id - The id.
718
+ * @param options.propertyName - The property name.
719
+ *
720
+ * @throws {Error} Throws if an id is not a URL.
721
+ */
722
+ function _validateUriId({ id, propertyName }) {
723
+ let parsed;
724
+ try {
725
+ parsed = new URL(id);
726
+ }
727
+ catch (cause) {
728
+ const error = new TypeError(`"${propertyName}" must be a URI: "${id}".`);
729
+ error.cause = cause;
730
+ throw error;
731
+ }
732
+ if (!parsed.protocol) {
733
+ throw new TypeError(`"${propertyName}" must be a URI: "${id}".`);
734
+ }
735
+ }
736
+ //# sourceMappingURL=index.js.map