@digitalbazaar/vc
Version:
Verifiable Credentials JavaScript library.
886 lines (817 loc) • 30.4 kB
JavaScript
/**
* A JavaScript implementation of Verifiable Credentials.
*
* @author Dave Longley
* @author David I. Lehn
*
* @license BSD 3-Clause License
* Copyright (c) 2017-2025 Digital Bazaar, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of the Digital Bazaar, Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import {
assertCredentialContext,
assertDateString,
checkContextVersion,
compareTime,
getContextForVersion
} from './helpers.js';
import {documentLoader as _documentLoader} from './documentLoader.js';
import {CredentialIssuancePurpose} from './CredentialIssuancePurpose.js';
import jsigs from 'jsonld-signatures';
import jsonld from 'jsonld';
const {AssertionProofPurpose, AuthenticationProofPurpose} = jsigs.purposes;
export {dateRegex} from './helpers.js';
export const defaultDocumentLoader = jsigs.extendContextLoader(_documentLoader);
export {CredentialIssuancePurpose};
/**
* @typedef {object} LinkedDataSignature
*/
/**
* @typedef {object} Presentation
*/
/**
* @typedef {object} ProofPurpose
*/
/**
* @typedef {object} VerifiableCredential
*/
/**
* @typedef {object} VerifiablePresentation
*/
/**
* @typedef {object} VerifyPresentationResult
* @property {boolean} verified - True if verified, false if not.
* @property {object} presentationResult
* @property {Array} credentialResults
* @property {object} error
*/
/**
* @typedef {object} VerifyCredentialResult
* @property {boolean} verified - True if verified, false if not.
* @property {object} statusResult
* @property {Array} results
* @property {object} error
*/
/**
* Issues a verifiable credential (by taking a base credential document,
* and adding a digital signature to it).
*
* @param {object} [options={}] - The options to use.
*
* @param {object} options.credential - Base credential document.
* @param {LinkedDataSignature} options.suite - Signature suite (with private
* key material or an API to use it), passed in to sign().
*
* @param {ProofPurpose} [options.purpose] - A ProofPurpose. If not specified,
* a default purpose will be created.
*
* Other optional params passed to `sign()`:
* @param {object} [options.documentLoader] - A document loader.
* @param {string|Date} [options.now] - A string representing date time in
* ISO 8601 format or an instance of Date. Defaults to current date time.
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
* that clocks may be skewed when checking capability expiration date-times
* against `date` and when comparing invocation proof creation time against
* delegation proof creation time.
*
* @throws {Error} If missing required properties.
*
* @returns {Promise<VerifiableCredential>} Resolves on completion.
*/
export async function issue({
credential, suite,
purpose = new CredentialIssuancePurpose(),
documentLoader = defaultDocumentLoader,
now,
maxClockSkew = 300
} = {}) {
// check to make sure the `suite` has required params
// Note: verificationMethod defaults to publicKey.id, in suite constructor
if(!suite) {
throw new TypeError('"suite" parameter is required for issuing.');
}
if(!suite.verificationMethod) {
throw new TypeError('"suite.verificationMethod" property is required.');
}
if(!credential) {
throw new TypeError('"credential" parameter is required for issuing.');
}
if(checkContextVersion({
credential,
version: 1.0
}) && !credential.issuanceDate) {
const now = (new Date()).toJSON();
credential.issuanceDate = `${now.slice(0, now.length - 5)}Z`;
}
// run common credential checks
_checkCredential({credential, now, mode: 'issue', maxClockSkew});
return jsigs.sign(credential, {purpose, documentLoader, suite});
}
/**
* Derives a proof from the given verifiable credential, resulting in a new
* verifiable credential. This method is usually used to generate selective
* disclosure and / or unlinkable proofs.
*
* @param {object} [options={}] - The options to use.
*
* @param {object} options.verifiableCredential - The verifiable credential
* containing a base proof to derive another proof from.
* @param {LinkedDataSignature} options.suite - Derived proof signature suite.
*
* Other optional params passed to `derive()`:
* @param {object} [options.documentLoader] - A document loader.
*
* @throws {Error} If missing required properties.
*
* @returns {Promise<VerifiableCredential>} Resolves on completion.
*/
export async function derive({
verifiableCredential, suite,
documentLoader = defaultDocumentLoader
} = {}) {
if(!verifiableCredential) {
throw new TypeError(
'"verifiableCredential" parameter is required for deriving.');
}
if(!suite) {
throw new TypeError('"suite" parameter is required for deriving.');
}
// run common credential checks
_checkCredential({credential: verifiableCredential, mode: 'issue'});
return jsigs.derive(verifiableCredential, {
purpose: new AssertionProofPurpose(),
documentLoader,
suite
});
}
/**
* Verifies a verifiable presentation:
* - Checks that the presentation is well-formed
* - Checks the proofs (for example, checks digital signatures against the
* provided public keys).
*
* @param {object} [options={}] - The options to use.
*
* @param {VerifiablePresentation} options.presentation - Verifiable
* presentation, signed or unsigned, that may contain within it a
* verifiable credential.
*
* @param {LinkedDataSignature|LinkedDataSignature[]} options.suite - One or
* more signature suites that are supported by the caller's use case. This is
* an explicit design decision -- the calling code must specify which
* signature types (ed25519, RSA, etc) are allowed.
* Although it is expected that the secure resolution/fetching of the public
* key material (to verify against) is to be handled by the documentLoader,
* the suite param can optionally include the key directly.
*
* @param {boolean} [options.unsignedPresentation=false] - By default, this
* function assumes that a presentation is signed (and will return an error if
* a `proof` section is missing). Set this to `true` if you're using an
* unsigned presentation.
*
* Either pass in a proof purpose,
* @param {AuthenticationProofPurpose} [options.presentationPurpose] - Optional
* proof purpose (a default one will be created if not passed in).
*
* or a default purpose will be created with params:
* @param {string} [options.challenge] - Required if purpose is not passed in.
* @param {string} [options.controller] - A controller.
* @param {string} [options.domain] - A domain.
*
* @param {Function} [options.documentLoader] - A document loader.
* @param {Function} [options.checkStatus] - Optional function for checking
* credential status if `credentialStatus` is present on the credential.
* @param {string|Date} [options.now] - A string representing date time in
* ISO 8601 format or an instance of Date. Defaults to current date time.
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
* that clocks may be skewed when checking capability expiration date-times
* against `date` and when comparing invocation proof creation time against
* delegation proof creation time.
* @param {boolean} [options.includeCredentials=false] - Set to `true` to
* include the credentials in the credential results.
*
* @returns {Promise<VerifyPresentationResult>} The verification result.
*/
export async function verify(options = {}) {
const {presentation} = options;
try {
if(!presentation) {
throw new TypeError(
'A "presentation" property is required for verifying.');
}
return _verifyPresentation(options);
} catch(error) {
return {
verified: false,
results: [{presentation, verified: false, error}],
error
};
}
}
/**
* Verifies a verifiable credential:
* - Checks that the credential is well-formed
* - Checks the proofs (for example, checks digital signatures against the
* provided public keys).
*
* @param {object} [options={}] - The options.
*
* @param {object} options.credential - Verifiable credential.
*
* @param {LinkedDataSignature|LinkedDataSignature[]} options.suite - One or
* more signature suites that are supported by the caller's use case. This is
* an explicit design decision -- the calling code must specify which
* signature types (ed25519, RSA, etc) are allowed.
* Although it is expected that the secure resolution/fetching of the public
* key material (to verify against) is to be handled by the documentLoader,
* the suite param can optionally include the key directly.
*
* @param {CredentialIssuancePurpose} [options.purpose] - Optional
* proof purpose (a default one will be created if not passed in).
* @param {Function} [options.documentLoader] - A document loader.
* @param {Function} [options.checkStatus] - Optional function for checking
* credential status if `credentialStatus` is present on the credential.
* @param {string|Date} [options.now] - A string representing date time in
* ISO 8601 format or an instance of Date. Defaults to current date time.
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
* that clocks may be skewed when checking capability expiration date-times
* against `date` and when comparing invocation proof creation time against
* delegation proof creation time.
*
* @returns {Promise<VerifyCredentialResult>} The verification result.
*/
export async function verifyCredential(options = {}) {
const {credential} = options;
try {
if(!credential) {
throw new TypeError(
'A "credential" property is required for verifying.');
}
return await _verifyCredential(options);
} catch(error) {
return {
verified: false,
results: [{credential, verified: false, error}],
error
};
}
}
/**
* Verifies a verifiable credential.
*
* @private
* @param {object} [options={}] - The options.
*
* @param {object} options.credential - Verifiable credential.
* @param {LinkedDataSignature|LinkedDataSignature[]} options.suite - See the
* definition in the `verify()` docstring, for this param.
* @param {string|Date} [options.now] - A string representing date time in
* ISO 8601 format or an instance of Date. Defaults to current date time.
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
* that clocks may be skewed when checking capability expiration date-times
* against `date` and when comparing invocation proof creation time against
* delegation proof creation time.
*
* @throws {Error} If required parameters are missing (in `_checkCredential`).
*
* @param {CredentialIssuancePurpose} [options.purpose] - A purpose.
* @param {Function} [options.documentLoader] - A document loader.
* @param {Function} [options.checkStatus] - Optional function for checking
* credential status if `credentialStatus` is present on the credential.
*
* @returns {Promise<VerifyCredentialResult>} The verification result.
*/
async function _verifyCredential(options = {}) {
const {credential, checkStatus, now, maxClockSkew} = options;
// run common credential checks
_checkCredential({credential, now, maxClockSkew});
// if credential status is provided, a `checkStatus` function must be given
if(credential.credentialStatus && typeof options.checkStatus !== 'function') {
throw new TypeError(
'A "checkStatus" function must be given to verify credentials with ' +
'"credentialStatus".');
}
const documentLoader = options.documentLoader || defaultDocumentLoader;
const {controller} = options;
const purpose = options.purpose || new CredentialIssuancePurpose({
controller
});
const result = await jsigs.verify(
credential, {...options, purpose, documentLoader});
// if verification has already failed, skip status check
if(!result.verified) {
return result;
}
if(credential.credentialStatus) {
result.statusResult = await checkStatus(options);
if(!result.statusResult.verified) {
result.verified = false;
}
}
return result;
}
/**
* Creates an unsigned presentation from a given verifiable credential.
*
* @param {object} options - Options to use.
* @param {object|Array<object>} [options.verifiableCredential] - One or more
* verifiable credential.
* @param {string} [options.id] - Optional VP id.
* @param {string} [options.holder] - Optional presentation holder url.
* @param {string|Date} [options.now] - A string representing date time in
* ISO 8601 format or an instance of Date. Defaults to current date time.
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
* that clocks may be skewed when checking capability expiration date-times
* against `date` and when comparing invocation proof creation time against
* delegation proof creation time.
* @param {number} [options.version = 2.0] - The VC context version to use.
*
* @throws {TypeError} If verifiableCredential param is missing.
* @throws {Error} If the credential (or the presentation params) are missing
* required properties.
*
* @returns {Presentation} The credential wrapped inside of a
* VerifiablePresentation.
*/
export function createPresentation({
verifiableCredential, id, holder, now, version = 2.0, maxClockSkew = 300
} = {}) {
const initialContext = getContextForVersion({version});
const presentation = {
'@context': [initialContext],
type: ['VerifiablePresentation']
};
if(verifiableCredential) {
const credentials = [].concat(verifiableCredential);
// ensure all credentials are valid
for(const credential of credentials) {
_checkCredential({credential, now, maxClockSkew});
}
presentation.verifiableCredential = credentials;
}
if(id) {
presentation.id = id;
}
if(holder) {
presentation.holder = holder;
}
_checkPresentation(presentation);
return presentation;
}
/**
* Signs a given presentation.
*
* @param {object} [options={}] - Options to use.
*
* Required:
* @param {Presentation} options.presentation - A presentation.
* @param {LinkedDataSignature} options.suite - passed in to sign()
*
* Either pass in a ProofPurpose, or a default one will be created with params:
* @param {ProofPurpose} [options.purpose] - A ProofPurpose. If not specified,
* a default purpose will be created with the domain and challenge options.
*
* @param {string} [options.domain] - A domain.
* @param {string} options.challenge - A required challenge.
*
* @param {Function} [options.documentLoader] - A document loader.
*
* @returns {Promise<{VerifiablePresentation}>} A VerifiablePresentation with
* a proof.
*/
export async function signPresentation(options = {}) {
const {presentation, domain, challenge} = options;
const purpose = options.purpose || new AuthenticationProofPurpose({
domain,
challenge
});
const documentLoader = options.documentLoader || defaultDocumentLoader;
return jsigs.sign(presentation, {...options, purpose, documentLoader});
}
/**
* Verifies that the VerifiablePresentation is well formed, and checks the
* proof signature if it's present. Also verifies all the VerifiableCredentials
* that are present in the presentation, if any.
*
* @param {object} [options={}] - The options.
* @param {VerifiablePresentation} options.presentation - A
* VerifiablePresentation.
*
* @param {LinkedDataSignature|LinkedDataSignature[]} options.suite - See the
* definition in the `verify()` docstring, for this param.
*
* @param {boolean} [options.unsignedPresentation=false] - By default, this
* function assumes that a presentation is signed (and will return an error if
* a `proof` section is missing). Set this to `true` if you're using an
* unsigned presentation.
*
* Either pass in a proof purpose,
* @param {AuthenticationProofPurpose} [options.presentationPurpose] - A
* ProofPurpose. If not specified, a default purpose will be created with
* the challenge, controller, and domain options.
*
* @param {string} [options.challenge] - A challenge. Required if purpose is
* not passed in.
* @param {string} [options.controller] - A controller. Required if purpose is
* not passed in.
* @param {string} [options.domain] - A domain. Required if purpose is not
* passed in.
*
* @param {Function} [options.documentLoader] - A document loader.
* @param {Function} [options.checkStatus] - Optional function for checking
* credential status if `credentialStatus` is present on the credential.
* @param {string|Date} [options.now] - A string representing date time in
* ISO 8601 format or an instance of Date. Defaults to current date time.
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
* that clocks may be skewed when checking capability expiration date-times
* against `date` and when comparing invocation proof creation time against
* delegation proof creation time.
*
* @throws {Error} If presentation is missing required params.
*
* @returns {Promise<VerifyPresentationResult>} The verification result.
*/
async function _verifyPresentation(options = {}) {
const {presentation, unsignedPresentation, includeCredentials} = options;
_checkPresentation(presentation);
const documentLoader = options.documentLoader || defaultDocumentLoader;
// FIXME: verify presentation first, then each individual credential
// only if that proof is verified
// if verifiableCredentials are present, verify them, individually
let credentialResults;
let verified = true;
const credentials = jsonld.getValues(presentation, 'verifiableCredential');
if(credentials.length > 0) {
// verify every credential in `verifiableCredential`
credentialResults = await Promise.all(credentials.map(credential => {
return verifyCredential({...options, credential, documentLoader});
}));
for(const [i, credentialResult] of credentialResults.entries()) {
const credential = credentials[i];
credentialResult.credentialId = credential.id;
if(includeCredentials) {
credentialResult.credential = credential;
}
}
const allCredentialsVerified = credentialResults.every(r => r.verified);
if(!allCredentialsVerified) {
verified = false;
}
}
if(unsignedPresentation) {
// No need to verify the proof section of this presentation
return {verified, results: [presentation], credentialResults};
}
const {controller, domain, challenge} = options;
if(!options.presentationPurpose && !challenge) {
throw new Error(
'A "challenge" param is required for AuthenticationProofPurpose.');
}
const purpose = options.presentationPurpose ||
new AuthenticationProofPurpose({controller, domain, challenge});
const presentationResult = await jsigs.verify(
presentation, {...options, purpose, documentLoader});
return {
presentationResult,
verified: verified && presentationResult.verified,
credentialResults,
error: presentationResult.error
};
}
/**
* @param {string|object} obj - Either an object with an id property
* or a string that is an id.
* @returns {string|undefined} Either an id or undefined.
* @private
*/
function _getId(obj) {
if(typeof obj === 'string') {
return obj;
}
if(!('id' in obj)) {
return;
}
return obj.id;
}
// export for testing
/**
* @param {object} presentation - An object that could be a presentation.
*
* @throws {Error}
* @private
*/
export function _checkPresentation(presentation) {
// normalize to an array to allow the common case of context being a string
const context = Array.isArray(presentation['@context']) ?
presentation['@context'] : [presentation['@context']];
assertCredentialContext({context});
const types = jsonld.getValues(presentation, 'type');
// check type presence
if(!types.includes('VerifiablePresentation')) {
throw new Error('"type" must include "VerifiablePresentation".');
}
}
// these props of a VC must be an object with a type
// if present in a VC or VP
const mustHaveType = [
'proof',
'credentialStatus',
'termsOfUse',
'evidence'
];
// export for testing
/**
* @param {object} options - The options.
* @param {object} options.credential - An object that could be a
* VerifiableCredential.
* @param {string|Date} [options.now] - A string representing date time in
* ISO 8601 format or an instance of Date. Defaults to current date time.
* @param {number} [options.maxClockSkew=300] - A maximum number of seconds
* that clocks may be skewed when checking capability expiration date-times
* against `date` and when comparing invocation proof creation time against
* delegation proof creation time.
* @param {string} [options.mode] - The mode of operation for this
* validation function, either `issue` or `verify`.
*
* @throws {Error}
* @private
*/
export function _checkCredential({
credential, now = new Date(), mode = 'verify', maxClockSkew = 300
} = {}) {
if(typeof now === 'string') {
now = new Date(now);
}
assertCredentialContext({context: credential['@context']});
// check type presence and cardinality
if(!credential.type) {
throw new Error('"type" property is required.');
}
if(!jsonld.getValues(credential, 'type').includes('VerifiableCredential')) {
throw new Error('"type" must include `VerifiableCredential`.');
}
_checkCredentialSubjects({credential});
if(!credential.issuer) {
throw new Error('"issuer" property is required.');
}
if(checkContextVersion({credential, version: 1.0})) {
// check issuanceDate exists
if(!credential.issuanceDate) {
throw new Error('"issuanceDate" property is required.');
}
// check issuanceDate format on issue
assertDateString({credential, prop: 'issuanceDate'});
// check issuanceDate cardinality
if(jsonld.getValues(credential, 'issuanceDate').length > 1) {
throw new Error('"issuanceDate" property can only have one value.');
}
// optionally check expirationDate
if('expirationDate' in credential) {
// check if `expirationDate` property is a date
assertDateString({credential, prop: 'expirationDate'});
if(mode === 'verify') {
// check if `now` is after `expirationDate`
const expirationDate = new Date(credential.expirationDate);
if(compareTime({t1: now, t2: expirationDate, maxClockSkew}) > 0) {
throw new Error('Credential has expired.');
}
}
}
// check if `now` is before `issuanceDate` on verification
if(mode === 'verify') {
const issuanceDate = new Date(credential.issuanceDate);
if(compareTime({t1: issuanceDate, t2: now, maxClockSkew}) > 0) {
throw new Error(
`The current date time (${now.toISOString()}) is before the ` +
`"issuanceDate" (${credential.issuanceDate}).`);
}
}
}
if(checkContextVersion({credential, version: 2.0})) {
// check if 'validUntil' and 'validFrom'
let {validUntil, validFrom} = credential;
if(validUntil) {
assertDateString({credential, prop: 'validUntil'});
if(mode === 'verify') {
validUntil = new Date(credential.validUntil);
if(compareTime({t1: now, t2: validUntil, maxClockSkew}) > 0) {
throw new Error(
`The current date time (${now.toISOString()}) is after ` +
`"validUntil" (${credential.validUntil}).`);
}
}
}
if(validFrom) {
assertDateString({credential, prop: 'validFrom'});
if(mode === 'verify') {
// check if `now` is before `validFrom`
validFrom = new Date(credential.validFrom);
if(compareTime({t1: validFrom, t2: now, maxClockSkew}) > 0) {
throw new Error(
`The current date time (${now.toISOString()}) is before ` +
`"validFrom" (${credential.validFrom}).`);
}
}
}
}
// check issuer cardinality
if(jsonld.getValues(credential, 'issuer').length > 1) {
throw new Error('"issuer" property can only have one value.');
}
// check issuer is a URL
if('issuer' in credential) {
const issuer = _getId(credential.issuer);
if(!issuer) {
throw new Error(`"issuer" id is required.`);
}
_validateUriId({id: issuer, propertyName: 'issuer'});
}
// check credentialStatus
jsonld.getValues(credential, 'credentialStatus').forEach(cs => {
// check if optional "id" is a URL
if('id' in cs) {
_validateUriId({id: cs.id, propertyName: 'credentialStatus.id'});
}
// check "type" present
if(!cs.type) {
throw new Error('"credentialStatus" must include a type.');
}
});
// check evidences are URLs
jsonld.getValues(credential, 'evidence').forEach(evidence => {
const evidenceId = _getId(evidence);
if(evidenceId) {
_validateUriId({id: evidenceId, propertyName: 'evidence'});
}
});
// check if properties that require a type are
// defined, objects, and objects with types
for(const prop of mustHaveType) {
if(prop in credential) {
const _value = credential[prop];
if(Array.isArray(_value)) {
_value.forEach(entry => _checkTypedObject(entry, prop));
continue;
}
_checkTypedObject(_value, prop);
}
}
}
/**
* @private
* Checks that a property is non-empty object with
* property type.
*
* @param {object} obj - A potential object.
* @param {string} name - The name of the property.
*
* @throws {Error} if the property is not an object with a type.
*
* @returns {undefined} - Returns on success.
*/
function _checkTypedObject(obj, name) {
if(!isObject(obj)) {
throw new Error(`property "${name}" must be an object.`);
}
if(_emptyObject(obj)) {
throw new Error(`property "${name}" can not be an empty object.`);
}
if(!('type' in obj)) {
throw new Error(`property "${name}" must have property type.`);
}
}
/**
* @private
* Takes in a credential and checks the credentialSubject(s)
*
* @param {object} options - Options.
* @param {object} options.credential - The credential to check.
*
* @throws {Error} error - Throws on errors in the credential subject.
*
* @returns {undefined} - Returns on success.
*/
function _checkCredentialSubjects({credential}) {
if(!credential?.credentialSubject) {
throw new Error('"credentialSubject" property is required.');
}
if(Array.isArray(credential?.credentialSubject)) {
return credential?.credentialSubject.map(
subject => _checkCredentialSubject({subject}));
}
return _checkCredentialSubject({subject: credential?.credentialSubject});
}
/**
* @private
*
* Checks a credential subject is valid.
*
* @param {object} options - Options.
* @param {object} options.subject - A potential credential subject.
*
* @throws {Error} If the credentialSubject is not valid.
*
* @returns {undefined} Returns on success.
*/
function _checkCredentialSubject({subject}) {
if(isObject(subject) === false) {
throw new Error('"credentialSubject" must be a non-null object.');
}
if(_emptyObject(subject)) {
throw new Error('"credentialSubject" must make a claim.');
}
// If credentialSubject.id is present and is not a URI, reject it
if(subject.id) {
_validateUriId({
id: subject.id, propertyName: 'credentialSubject.id'
});
}
}
/**
* @private
* Checks if parameter is an object.
*
* @param {object} obj - A potential object.
*
* @returns {boolean} - Returns false if not an object or null.
*/
function isObject(obj) {
// return false for null even though it has type object
if(obj === null) {
return false;
}
// if something has type object and is not null return true
if((typeof obj) === 'object') {
return true;
}
// return false for strings, symbols, etc.
return false;
}
/**
* @private
* Is it an empty object?
*
* @param {object} obj - A potential object.
*
* @returns {boolean} - Is it empty?
*/
function _emptyObject(obj) {
// if the parameter is not an object return true
// as a non-object is an empty object
if(!isObject(obj)) {
return true;
}
return Object.keys(obj).length === 0;
}
/**
* @private
*
* Validates if an ID is a URL.
*
* @param {object} options - Options.
* @param {string} options.id - the id.
* @param {string} options.propertyName - The property name.
*
* @throws {Error} Throws if an id is not a URL.
*
* @returns {undefined} Returns on success.
*/
function _validateUriId({id, propertyName}) {
let parsed;
try {
parsed = new URL(id);
} catch(e) {
const error = new TypeError(`"${propertyName}" must be a URI: "${id}".`);
error.cause = e;
throw error;
}
if(!parsed.protocol) {
throw new TypeError(`"${propertyName}" must be a URI: "${id}".`);
}
}