import {
expandJSONLD,
issueCredential,
verifyCredential,
DEFAULT_CONTEXT,
DEFAULT_TYPE,
} from './utils/vc/index';
import { validateCredentialSchema } from './utils/vc/schema';
import {
ensureObjectWithId,
isObject,
ensureString,
ensureURI,
ensureValidDatetime,
} from './utils/type-helpers';
import { getUniqueElementsFromArray } from './utils/misc';
/**
* @typedef {object} VerifiableCredentialVerificationResult The credential verification result
* @property {Boolean} verified Is this credential verified or not
* @property {array} results Verification results
* @property {any} [error] Optional error
*/
/**
* Representation of a Verifiable Credential.
*/
class VerifiableCredential {
/**
* Create a new Verifiable Credential instance.
* @param {string} id - id of the credential
*/
constructor(id) {
if (id) {
this.setId(id);
}
this.context = [DEFAULT_CONTEXT];
this.type = [DEFAULT_TYPE];
this.credentialSubject = [];
this.setIssuanceDate(new Date().toISOString());
}
/**
* Sets the credential's ID
* @param {string} id - Signed credential's ID
* @returns {VerifiableCredential}
*/
setId(id) {
this.constructor.verifyID(id);
this.id = id;
return this;
}
/**
* Fail if the given verifiable credential id isn't a valid URI.
* @param {*} id
*/
static verifyID(id) {
ensureURI(id);
}
/**
* Sets the credential's issuer DID
* @param {string} issuer - the issuer's did
* @returns {VerifiableCredential}
*/
setIssuer(issuer) {
this.issuer = issuer;
return this;
}
/**
* Sets the credential's proof
* @param {object} proof - Signed credential proof
* @returns {VerifiableCredential}
*/
setProof(proof) {
this.proof = proof;
return this;
}
/**
* Sets the `credentialSchema` field of the credential with the given id and type as specified in the RFC.
* @param {string} id - schema ID URI
* @param {string} type - type of the credential schema
*/
setSchema(id, type) {
this.constructor.verifyID(id);
this.credentialSchema = {
id,
type,
};
}
/**
* Check that the credential is compliant with given JSON schema, meaning `credentialSubject` has the
* structure specified by the given JSON schema. Use `validateCredentialSchema` but exclude subject's id.
* Allows issuer to validate schema before adding it.
* @param {object} schema - The schema to validate with
* @returns {Promise<Boolean>}
*/
async validateSchema(schema) {
if (!this.credentialSubject) {
throw new Error('No credential subject defined');
}
const expanded = await expandJSONLD(this.toJSON());
return validateCredentialSchema(expanded, schema, this.context);
}
/**
* Sets the context to the given value, overrding all others
* @param {string|object} context - Context to assign
* @returns {VerifiableCredential}
*/
setContext(context) {
if (!isObject(context) && !Array.isArray(context)) {
this.constructor.verifyID(context);
}
this.context = context;
return this;
}
/**
* Add a context to this Credential's context array. Duplicates are omitted.
* @param {string|object} context - Context to add to the credential context array
* @returns {VerifiableCredential}
*/
addContext(context) {
if (!isObject(context)) {
this.constructor.verifyID(context);
}
this.context = getUniqueElementsFromArray(
[...this.context, context],
JSON.stringify,
);
return this;
}
/**
* Add a type to this Credential's type array. Duplicates are omitted.
* @param {string} type - Type to add to the credential type array
* @returns {VerifiableCredential}
*/
addType(type) {
ensureString(type);
this.type = [...new Set([...this.type, type])];
return this;
}
/**
* Add a subject to this Credential. Duplicates are omitted.
* @param {object} subject - Subject of the credential
* @returns {VerifiableCredential}
*/
addSubject(subject) {
if (!this.credentialSubject || this.credentialSubject.length === 0) {
this.credentialSubject = [subject];
}
const subjects = this.credentialSubject.length
? this.credentialSubject
: [this.credentialSubject];
this.credentialSubject = getUniqueElementsFromArray(
[...subjects, subject],
JSON.stringify,
);
return this;
}
/**
* Set the subject for this Credential
* @param {object|array} subject - Subject of the credential as object or array
* @returns {VerifiableCredential}
*/
setSubject(subject) {
if (!isObject(subject) && !Array.isArray(subject)) {
throw new Error('credentialSubject must be either an object or array');
}
this.credentialSubject = subject;
return this;
}
/**
* Set a status for this Credential
* @param {object} status - Status of the credential
* @returns {VerifiableCredential}
*/
setStatus(status) {
ensureObjectWithId(status, 'credentialStatus');
if (!status.type) {
throw new Error('"credentialStatus" must include a type.');
}
this.status = status;
return this;
}
/**
* Set a issuance date for this Credential
* @param {string} issuanceDate - issuanceDate of the credential
* @returns {VerifiableCredential}
*/
setIssuanceDate(issuanceDate) {
ensureValidDatetime(issuanceDate);
this.issuanceDate = issuanceDate;
return this;
}
/**
* Set a expiration date for this Credential
* @param {object} expirationDate - expirationDate of the credential
* @returns {VerifiableCredential}
*/
setExpirationDate(expirationDate) {
ensureValidDatetime(expirationDate);
this.expirationDate = expirationDate;
return this;
}
/**
* Define the JSON representation of a Verifiable Credential.
* @returns {object}
*/
toJSON() {
const { context, status, ...rest } = this;
const credJson = {
'@context': context,
};
if (status) {
credJson.credentialStatus = status;
}
return {
...credJson,
...rest,
};
}
/**
* Sign a Verifiable Credential using the provided keyDoc
* @param {object} keyDoc - key document containing `id`, `controller`, `type`, `privateKeyBase58` and `publicKeyBase58`
* @param {Boolean} [compactProof] - Whether to compact the JSON-LD or not.
* @param {object} [issuerObject] - Optional issuer object to assign
* @param {Boolean} [addSuiteContext] - Toggles the default
* behavior of each signature suite enforcing the presence of its own
* `@context` (if it is not present, it's added to the context list).
* @param {(jsonld|jwt|proofValue)} [type] - Optional format/type of the credential (JSON-LD, JWT, proofValue)
* @returns {Promise<VerifiableCredential>}
*/
async sign(
keyDoc,
compactProof = true,
issuerObject = null,
addSuiteContext = true,
documentLoader = null,
type = null,
) {
const signedVC = await issueCredential(
keyDoc,
this.toJSON(),
compactProof,
documentLoader,
null,
void 0,
issuerObject,
addSuiteContext,
type,
);
this.setProof(signedVC.proof);
this.issuer = signedVC.issuer;
this.context = signedVC['@context'];
return this;
}
/**
* Verify a Verifiable Credential
* @param {object} [params] Verify parameters (TODO: add type info for this object)
* @returns {Promise<VerifiableCredentialVerificationResult>}
*/
async verify({
resolver = null,
compactProof = true,
skipRevocationCheck = false,
skipSchemaCheck = false,
suite = [],
} = {}) {
if (!this.proof) {
throw new Error('The current Verifiable Credential has no proof.');
}
return verifyCredential(this.toJSON(), {
resolver,
compactProof,
skipRevocationCheck,
skipSchemaCheck,
suite,
});
}
/**
* Sets this credential's properties based on a JSON object
* @param {object} json - VC JSON
* @returns {VerifiableCredential}
*/
setFromJSON(json) {
const subject = json.credentialSubject || json.subject;
if (subject) {
const subjects = subject.length ? subject : [subject];
subjects.forEach((value) => {
this.addSubject(value);
});
}
if (json.proof) {
this.setProof(json.proof);
}
if (json.issuer) {
this.setIssuer(json.issuer);
}
const status = json.credentialStatus || json.status;
if (status) {
this.setStatus(status);
}
if (json.issuanceDate) {
this.setIssuanceDate(json.issuanceDate);
}
if (json.expirationDate) {
this.setExpirationDate(json.expirationDate);
}
Object.assign(this, json);
return this;
}
/**
* Creates a new VerifiableCredential instance from a JSON object
* @param {object} json - VC JSON
* @returns {VerifiableCredential}
*/
static fromJSON(json) {
const cert = new this(json.id);
const contexts = json['@context'];
if (contexts) {
cert.setContext(contexts);
} else {
throw new Error(
'No context found in JSON object, verifiable credentials must have a @context field.',
);
}
const types = json.type;
if (types) {
cert.type = [];
if (types.length !== undefined) {
types.forEach((typeVal) => {
cert.addType(typeVal);
});
} else {
cert.addType(types);
}
} else {
throw new Error(
'No type found in JSON object, verifiable credentials must have a type field.',
);
}
return cert.setFromJSON(json);
}
}
export default VerifiableCredential;