diff --git a/package-lock.json b/package-lock.json index baf374518..76ff77aee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7717,6 +7717,9 @@ "name": "@tbd54566975/credentials", "version": "0.1.5", "license": "Apache-2.0", + "dependencies": { + "uuid": "^9.0.0" + }, "devDependencies": { "@types/chai": "4.3.0", "@types/eslint": "8.37.0", diff --git a/packages/credentials/package.json b/packages/credentials/package.json index 19a7009e8..8a16b9974 100644 --- a/packages/credentials/package.json +++ b/packages/credentials/package.json @@ -81,7 +81,9 @@ "engines": { "node": ">=18.0.0" }, - "dependencies": {}, + "dependencies": { + "uuid": "^9.0.0" + }, "devDependencies": { "@types/chai": "4.3.0", "@types/eslint": "8.37.0", diff --git a/packages/credentials/src/utils.ts b/packages/credentials/src/utils.ts new file mode 100644 index 000000000..b6485c51c --- /dev/null +++ b/packages/credentials/src/utils.ts @@ -0,0 +1,4 @@ +export function getCurrentXmlSchema112Timestamp() : string { + // Omit the milliseconds part from toISOString() output + return new Date().toISOString().replace(/\.\d+Z$/, 'Z'); +} \ No newline at end of file diff --git a/packages/web5/src/vc-api.ts b/packages/web5/src/vc-api.ts index b741f7076..6837cd83b 100644 --- a/packages/web5/src/vc-api.ts +++ b/packages/web5/src/vc-api.ts @@ -1,16 +1,70 @@ -import type { Web5Agent } from '@tbd54566975/web5-agent'; +import { Web5Agent } from '@tbd54566975/web5-agent'; +import type { VerifiableCredential } from '../../credentials/src/types.js'; + +import { getCurrentXmlSchema112Timestamp } from '../../credentials/src/utils.js'; +import { v4 as uuidv4 } from 'uuid'; +import { dataToBlob } from './utils.js'; +import { Record } from './record.js'; + +import { DwnInterfaceName, DwnMethodName, RecordsWriteMessage, RecordsWriteOptions } from '@tbd54566975/dwn-sdk-js'; +import { RecordsWriteResponse } from './dwn-api.js'; + +export type VcCreateResponse = RecordsWriteResponse & { + vc: VerifiableCredential; +}; export class VcApi { - #agent: Web5Agent; + #web5Agent: Web5Agent; #connectedDid: string; constructor(agent: Web5Agent, connectedDid: string) { - this.#agent = agent; + this.#web5Agent = agent; this.#connectedDid = connectedDid; } - async create() { - // TODO: implement - throw new Error('Not implemented.'); + async create(credentialSubject: any): Promise { + if (!credentialSubject || typeof credentialSubject !== 'object') { + throw new Error('credentialSubject not valid'); + } + + const vc: VerifiableCredential = { + '@context' : ['https://www.w3.org/2018/credentials/v1'], + credentialSubject : credentialSubject, + type : ['VerifiableCredential'], + issuer : { id: this.#connectedDid }, + issuanceDate : getCurrentXmlSchema112Timestamp(), + id : uuidv4(), + }; + + const messageOptions: Partial = { ...{ schema: 'vc/vc', dataFormat: 'application/json' } }; + + const { dataBlob, dataFormat } = dataToBlob(vc, 'application/json'); + messageOptions.dataFormat = dataFormat; + + const agentResponse = await this.#web5Agent.processDwnRequest({ + author : this.#connectedDid, + dataStream : dataBlob, + messageOptions, + messageType : DwnInterfaceName.Records + DwnMethodName.Write, + store : true, + target : this.#connectedDid + }); + + const { message, reply: { status } } = agentResponse; + const responseMessage = message as RecordsWriteMessage; + + let record: Record; + if (200 <= status.code && status.code <= 299) { + const recordOptions = { + author : this.#connectedDid, + encodedData : dataBlob, + target : this.#connectedDid, + ...responseMessage, + }; + + record = new Record(this.#web5Agent, recordOptions); + } + + return { record, status, vc }; } } \ No newline at end of file diff --git a/packages/web5/tests/web5-vc.spec.ts b/packages/web5/tests/web5-vc.spec.ts index 4bee55cb8..269a8b44f 100644 --- a/packages/web5/tests/web5-vc.spec.ts +++ b/packages/web5/tests/web5-vc.spec.ts @@ -29,12 +29,23 @@ describe('web5.vc', () => { }); describe('create', () => { - it('is not implemented', async () => { + it('valid vc', async () => { + const credentialSubject = {firstName: 'alice'}; + const result = await vc.create(credentialSubject); + + expect(result.status.code).to.equal(202); + expect(result.status.detail).to.equal('Accepted'); + expect(result.record).to.exist; + expect(await result.record?.data.json()).to.deep.equal(result.vc); + }); + + it('invalid credential subject', async () => { + const credentialSubject = 'badcredsubject'; try { - await vc.create(); + await vc.create(credentialSubject); expect.fail(); } catch(e) { - expect(e.message).to.include('Not implemented.'); + expect(e.message).to.include('credentialSubject not valid'); } }); });