diff --git a/packages/core/src/modules/vc/W3cCredentialService.ts b/packages/core/src/modules/vc/W3cCredentialService.ts index 7273fff9eb..de578625d1 100644 --- a/packages/core/src/modules/vc/W3cCredentialService.ts +++ b/packages/core/src/modules/vc/W3cCredentialService.ts @@ -1,14 +1,14 @@ import type { JwsLinkedDataSignature } from '../../crypto/JwsLinkedDataSignature' -import type { DidInfo } from '../../wallet' import type { VerifyCredentialResult, W3cCredential, W3cVerifyCredentialResult } from './models' import type { VerifyPresentationResult } from './models/presentation/VerifyPresentationResult' import type { W3cPresentation } from './models/presentation/W3Presentation' import type { RemoteDocument, Url } from 'jsonld/jsonld-spec' -// @ts-ignore -import jsonld from '@digitalcredentials/jsonld' +import jsonld, { expand } from '@digitalcredentials/jsonld' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import documentLoaderNode from '@digitalcredentials/jsonld/lib/documentLoaders/node' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import documentLoaderXhr from '@digitalcredentials/jsonld/lib/documentLoaders/xhr' import vc from '@digitalcredentials/vc' @@ -20,12 +20,13 @@ import { Ed25519Signature2018 } from '../../crypto/Ed25519Signature2018' import { createWalletKeyPairClass } from '../../crypto/WalletKeyPair' import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' -import { JsonTransformer } from '../../utils' +import { JsonTransformer, orArrayToArray } from '../../utils' import { Wallet } from '../../wallet' import { DidKey, DidResolverService } from '../dids' import { W3cVerifiableCredential } from './models' import { W3cCredentialRecord } from './models/credential/W3cCredentialRecord' +import { W3cCredentialRepository } from './models/credential/W3cCredentialRepository' import { W3cVerifiablePresentation } from './models/presentation/W3cVerifiablePresentation' interface LdProofDetailOptions { @@ -68,6 +69,7 @@ class SignatureSuiteRegistry { @scoped(Lifecycle.ContainerScoped) export class W3cCredentialService { private wallet: Wallet + private w3cCredentialRepository: W3cCredentialRepository private didResolver: DidResolverService private agentConfig: AgentConfig private logger: Logger @@ -79,11 +81,13 @@ export class W3cCredentialService { public constructor( @inject('Wallet') wallet: Wallet, + w3cCredentialRepository: W3cCredentialRepository, didResolver: DidResolverService, agentConfig: AgentConfig, logger: Logger ) { this.wallet = wallet + this.w3cCredentialRepository = w3cCredentialRepository this.didResolver = didResolver this.agentConfig = agentConfig this.logger = logger @@ -196,10 +200,21 @@ export class W3cCredentialService { * @returns the credential record that was written to storage */ public async storeCredential(record: W3cVerifiableCredential): Promise { - // MOCK - return new W3cCredentialRecord({ + // Get the expanded types + const expandedTypes = (await expand(JsonTransformer.toJSON(record), { documentLoader: this.documentLoader }))[0][ + '@type' + ] + + // Create an instance of the w3cCredentialRecord + const w3cCredentialRecord = new W3cCredentialRecord({ + tags: { expandedTypes: orArrayToArray(expandedTypes) }, credential: record, }) + + // Store the w3c credential record + await this.w3cCredentialRepository.save(w3cCredentialRecord) + + return w3cCredentialRecord } /** diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts index 0f49392b93..63bf151fbe 100644 --- a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts @@ -9,7 +9,8 @@ import { DidResolverService } from '../../dids' import { DidRepository } from '../../dids/repository' import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' import { W3cCredentialService } from '../W3cCredentialService' -import { W3cCredential } from '../models' +import { W3cCredential, W3cVerifiableCredential } from '../models' +import { W3cCredentialRepository } from '../models/credential/W3cCredentialRepository' const TEST_DID_KEY = 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL' @@ -18,12 +19,16 @@ jest.mock('../../ledger/services/IndyLedgerService') const IndyLedgerServiceMock = IndyLedgerService as jest.Mock const DidRepositoryMock = DidRepository as unknown as jest.Mock +jest.mock('../models/credential/W3cCredentialRepository') +const W3cCredentialRepositoryMock = W3cCredentialRepository as jest.Mock + describe('W3cCredentialService', () => { let wallet: IndyWallet let agentConfig: AgentConfig let didResolverService: DidResolverService let logger: TestLogger let w3cCredentialService: W3cCredentialService + let w3cCredentialRepository: W3cCredentialRepository beforeAll(async () => { agentConfig = getAgentConfig('W3cCredentialServiceTest') @@ -33,14 +38,65 @@ describe('W3cCredentialService', () => { await wallet.createAndOpen(agentConfig.walletConfig!) await wallet.initPublicDid({}) didResolverService = new DidResolverService(agentConfig, new IndyLedgerServiceMock(), new DidRepositoryMock()) - w3cCredentialService = new W3cCredentialService(wallet, didResolverService, agentConfig, logger) + w3cCredentialRepository = new W3cCredentialRepositoryMock() + w3cCredentialService = new W3cCredentialService( + wallet, + w3cCredentialRepository, + didResolverService, + agentConfig, + logger + ) }) afterAll(async () => { await wallet.delete() }) - describe('sign', () => { + describe('store', () => { + test('Store a credential', async () => { + const credential = JsonTransformer.fromJSON( + { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + type: 'Ed25519Signature2018', + created: '2022-03-28T15:54:59Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..b0MD_c-8EyGATDuCda1A72qbjD3o8MfiipicmhnYmcdqoIyZzE9MlZ9FZn5sxsIJ3LPqPQj7y1jLlINwCwNSDg', + }, + }, + W3cVerifiableCredential + ) + + const w3cCredentialRecord = await w3cCredentialService.storeCredential(credential) + + expect(w3cCredentialRecord).toMatchObject({ + type: 'W3cCredentialRecord', + id: expect.any(String), + createdAt: expect.any(Date), + credential: expect.any(W3cVerifiableCredential), + }) + + expect(w3cCredentialRecord.getTags()).toMatchObject({ + expandedTypes: [ + 'https://www.w3.org/2018/credentials#VerifiableCredential', + 'https://example.org/examples#UniversityDegreeCredential', + ], + }) + }) + }) + + xdescribe('sign', () => { it('returns a signed credential', async () => { const credential = JsonTransformer.fromJSON( { diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialRecord.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialRecord.ts index d72ea2a5fe..e1a00c7120 100644 --- a/packages/core/src/modules/vc/models/credential/W3cCredentialRecord.ts +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialRecord.ts @@ -1,35 +1,42 @@ import type { TagsBase } from '../../../../storage/BaseRecord' import { Type } from 'class-transformer' +import jsonld from 'jsonld' import { BaseRecord } from '../../../../storage/BaseRecord' +import { uuid } from '../../../../utils/uuid' import { W3cVerifiableCredential } from './W3cVerifiableCredential' export interface W3cCredentialRecordOptions { + id?: string + createdAt?: Date credential: W3cVerifiableCredential + tags: CustomW3cCredentialTags } -/** - * K-TODO: Set the appropriate tags - * @see https://github.com/hyperledger/aries-cloudagent-python/blob/e77d087bdd5f1f803616730e33d4e3f0801b5f8d/aries_cloudagent/storage/vc_holder/xform.py - * - * NOTE: Credential.type entries need to be expanded before storing them as tags - */ -export class W3cCredentialRecord extends BaseRecord { - public constructor(options: W3cCredentialRecordOptions) { - super() - if (options) { - this.credential = options.credential - } - } +export type CustomW3cCredentialTags = TagsBase & { + expandedTypes?: Array +} + +export class W3cCredentialRecord extends BaseRecord { + public static readonly type = 'W3cCredentialRecord' + public readonly type = W3cCredentialRecord.type @Type(() => W3cVerifiableCredential) public credential!: W3cVerifiableCredential - public getTags(): TagsBase { - return { - ...this._tags, + public constructor(props: W3cCredentialRecordOptions) { + super() + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags + this.credential = props.credential } } + + public getTags() { + return this._tags + } } diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialRepository.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialRepository.ts new file mode 100644 index 0000000000..baf11e4dba --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialRepository.ts @@ -0,0 +1,14 @@ +import { inject, scoped, Lifecycle } from 'tsyringe' + +import { InjectionSymbols } from '../../../../constants' +import { Repository } from '../../../../storage/Repository' +import { StorageService } from '../../../../storage/StorageService' + +import { W3cCredentialRecord } from './W3cCredentialRecord' + +@scoped(Lifecycle.ContainerScoped) +export class W3cCredentialRepository extends Repository { + public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { + super(W3cCredentialRecord, storageService) + } +} diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialState.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialState.ts new file mode 100644 index 0000000000..5bf4d586b4 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialState.ts @@ -0,0 +1,16 @@ +/** + * Issue Credential states as defined in RFC 0453 + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md#states + */ +export enum W3cCredentialState { + ProposalSent = 'proposal-sent', + ProposalReceived = 'proposal-received', + OfferSent = 'offer-sent', + OfferReceived = 'offer-received', + RequestSent = 'request-sent', + RequestReceived = 'request-received', + CredentialIssued = 'credential-issued', + CredentialReceived = 'credential-received', + Done = 'done', +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index ef3e921a00..fad6257b32 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -8,3 +8,4 @@ export * from './JWE' export * from './indyProofRequest' export * from './VarintEncoder' export * from './Hasher' +export * from './jsonld' diff --git a/packages/core/src/utils/jsonld.ts b/packages/core/src/utils/jsonld.ts index dcf9791035..5c21a234eb 100644 --- a/packages/core/src/utils/jsonld.ts +++ b/packages/core/src/utils/jsonld.ts @@ -39,3 +39,9 @@ export type Keyword = { '@version': '1.1' '@vocab': string | null } + +export const orArrayToArray = (val?: SingleOrArray): Array | undefined => { + if (!val) return undefined + if (Array.isArray(val)) return val + return [val] +} diff --git a/yarn.lock b/yarn.lock index 6f645c27f0..30fa35805e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2348,6 +2348,11 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== +"@types/digitalcredentials__jsonld@npm:@types/jsonld@^1.5.6", "@types/jsonld@^1.5.6": + version "1.5.6" + resolved "https://registry.yarnpkg.com/@types/jsonld/-/jsonld-1.5.6.tgz#4396c0b17128abf5773bb68b5453b88fc565b0d4" + integrity sha512-OUcfMjRie5IOrJulUQwVNvV57SOdKcTfBj3pjXNxzXqeOIrY2aGDNGW/Tlp83EQPkz4tCE6YWVrGuc/ZeaAQGg== + "@types/eslint@^7.2.13": version "7.29.0" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" @@ -2449,11 +2454,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/jsonld@^1.5.6": - version "1.5.6" - resolved "https://registry.yarnpkg.com/@types/jsonld/-/jsonld-1.5.6.tgz#4396c0b17128abf5773bb68b5453b88fc565b0d4" - integrity sha512-OUcfMjRie5IOrJulUQwVNvV57SOdKcTfBj3pjXNxzXqeOIrY2aGDNGW/Tlp83EQPkz4tCE6YWVrGuc/ZeaAQGg== - "@types/luxon@^1.27.0": version "1.27.1" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.27.1.tgz#aceeb2d5be8fccf541237e184e37ecff5faa9096"