import type { AcceptCredentialOptions, AcceptCredentialOfferOptions, AcceptCredentialProposalOptions, AcceptCredentialRequestOptions, CreateCredentialOfferOptions, FindCredentialMessageReturn, FindCredentialOfferMessageReturn, FindCredentialProposalMessageReturn, FindCredentialRequestMessageReturn, GetCredentialFormatDataReturn, NegotiateCredentialOfferOptions, NegotiateCredentialProposalOptions, OfferCredentialOptions, ProposeCredentialOptions, SendCredentialProblemReportOptions, DeleteCredentialOptions, SendRevocationNotificationOptions, } from './CredentialsApiOptions' import type { CredentialProtocol } from './protocol/CredentialProtocol' import type { CredentialFormatsFromProtocols } from './protocol/CredentialProtocolOptions' import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord' import type { AgentMessage } from '../../agent/AgentMessage' import type { Query } from '../../storage/StorageService' import { AgentContext } from '../../agent' import { MessageSender } from '../../agent/MessageSender' import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' import { InjectionSymbols } from '../../constants' import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' import { DidCommMessageRepository } from '../../storage/didcomm/DidCommMessageRepository' import { ConnectionService } from '../connections/services' import { RoutingService } from '../routing/services/RoutingService' import { CredentialsModuleConfig } from './CredentialsModuleConfig' import { CredentialState } from './models/CredentialState' import { RevocationNotificationService } from './protocol/revocation-notification/services' import { CredentialRepository } from './repository/CredentialRepository' export interface CredentialsApi { // Propose Credential methods proposeCredential(options: ProposeCredentialOptions): Promise acceptProposal(options: AcceptCredentialProposalOptions): Promise negotiateProposal(options: NegotiateCredentialProposalOptions): Promise // Offer Credential Methods offerCredential(options: OfferCredentialOptions): Promise acceptOffer(options: AcceptCredentialOfferOptions): Promise declineOffer(credentialRecordId: string): Promise negotiateOffer(options: NegotiateCredentialOfferOptions): Promise // Request Credential Methods // This is for beginning the exchange with a request (no proposal or offer). Only possible // (currently) with W3C. We will not implement this in phase I // when the issuer accepts the request he issues the credential to the holder acceptRequest(options: AcceptCredentialRequestOptions): Promise // Issue Credential Methods acceptCredential(options: AcceptCredentialOptions): Promise // Revoke Credential Methods sendRevocationNotification(options: SendRevocationNotificationOptions): Promise // out of band createOffer(options: CreateCredentialOfferOptions): Promise<{ message: AgentMessage credentialRecord: CredentialExchangeRecord }> sendProblemReport(options: SendCredentialProblemReportOptions): Promise // Record Methods getAll(): Promise findAllByQuery(query: Query): Promise getById(credentialRecordId: string): Promise findById(credentialRecordId: string): Promise deleteById(credentialRecordId: string, options?: DeleteCredentialOptions): Promise update(credentialRecord: CredentialExchangeRecord): Promise getFormatData(credentialRecordId: string): Promise>> // DidComm Message Records findProposalMessage(credentialExchangeId: string): Promise> findOfferMessage(credentialExchangeId: string): Promise> findRequestMessage(credentialExchangeId: string): Promise> findCredentialMessage(credentialExchangeId: string): Promise> } @injectable() export class CredentialsApi implements CredentialsApi { /** * Configuration for the credentials module */ public readonly config: CredentialsModuleConfig private connectionService: ConnectionService private messageSender: MessageSender private credentialRepository: CredentialRepository private agentContext: AgentContext private didCommMessageRepository: DidCommMessageRepository private revocationNotificationService: RevocationNotificationService private routingService: RoutingService private logger: Logger public constructor( messageSender: MessageSender, connectionService: ConnectionService, agentContext: AgentContext, @inject(InjectionSymbols.Logger) logger: Logger, credentialRepository: CredentialRepository, mediationRecipientService: RoutingService, didCommMessageRepository: DidCommMessageRepository, revocationNotificationService: RevocationNotificationService, config: CredentialsModuleConfig ) { this.messageSender = messageSender this.connectionService = connectionService this.credentialRepository = credentialRepository this.routingService = mediationRecipientService this.agentContext = agentContext this.didCommMessageRepository = didCommMessageRepository this.revocationNotificationService = revocationNotificationService this.logger = logger this.config = config } private getProtocol(protocolVersion: PVT): CredentialProtocol { const credentialProtocol = this.config.credentialProtocols.find((protocol) => protocol.version === protocolVersion) if (!credentialProtocol) { throw new AriesFrameworkError(`No credential protocol registered for protocol version ${protocolVersion}`) } return credentialProtocol } /** * Initiate a new credential exchange as holder by sending a credential proposal message * to the connection with the specified connection id. * * @param options configuration to use for the proposal * @returns Credential exchange record associated with the sent proposal message */ public async proposeCredential(options: ProposeCredentialOptions): Promise { const protocol = this.getProtocol(options.protocolVersion) const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) // Assert connectionRecord.assertReady() // will get back a credential record -> map to Credential Exchange Record const { credentialRecord, message } = await protocol.createProposal(this.agentContext, { connectionRecord, credentialFormats: options.credentialFormats, comment: options.comment, autoAcceptCredential: options.autoAcceptCredential, }) const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, associatedRecord: credentialRecord, connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } /** * Accept a credential proposal as issuer (by sending a credential offer message) to the connection * associated with the credential record. * * @param options config object for accepting the proposal * @returns Credential exchange record associated with the credential offer * */ public async acceptProposal(options: AcceptCredentialProposalOptions): Promise { const credentialRecord = await this.getById(options.credentialRecordId) if (!credentialRecord.connectionId) { throw new AriesFrameworkError( `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support credential proposal or negotiation.` ) } // with version we can get the protocol const protocol = this.getProtocol(credentialRecord.protocolVersion) const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) // Assert connectionRecord.assertReady() // will get back a credential record -> map to Credential Exchange Record const { message } = await protocol.acceptProposal(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, comment: options.comment, autoAcceptCredential: options.autoAcceptCredential, }) // send the message const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, associatedRecord: credentialRecord, connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } /** * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection * associated with the credential record. * * @param options configuration for the offer see {@link NegotiateCredentialProposalOptions} * @returns Credential exchange record associated with the credential offer * */ public async negotiateProposal(options: NegotiateCredentialProposalOptions): Promise { const credentialRecord = await this.getById(options.credentialRecordId) if (!credentialRecord.connectionId) { throw new AriesFrameworkError( `No connection id for credential record ${credentialRecord.id} not found. Connection-less issuance does not support negotiation` ) } // with version we can get the Service const protocol = this.getProtocol(credentialRecord.protocolVersion) const { message } = await protocol.negotiateProposal(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, comment: options.comment, autoAcceptCredential: options.autoAcceptCredential, }) const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, associatedRecord: credentialRecord, connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } /** * Initiate a new credential exchange as issuer by sending a credential offer message * to the connection with the specified connection id. * * @param options config options for the credential offer * @returns Credential exchange record associated with the sent credential offer message */ public async offerCredential(options: OfferCredentialOptions): Promise { const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) const protocol = this.getProtocol(options.protocolVersion) this.logger.debug(`Got a credentialProtocol object for version ${options.protocolVersion}`) const { message, credentialRecord } = await protocol.createOffer(this.agentContext, { credentialFormats: options.credentialFormats, autoAcceptCredential: options.autoAcceptCredential, comment: options.comment, connectionRecord, }) this.logger.debug('Offer Message successfully created; message= ', message) const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, associatedRecord: credentialRecord, connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } /** * Accept a credential offer as holder (by sending a credential request message) to the connection * associated with the credential record. * * @param options The object containing config options of the offer to be accepted * @returns Object containing offer associated credential record */ public async acceptOffer(options: AcceptCredentialOfferOptions): Promise { const credentialRecord = await this.getById(options.credentialRecordId) const protocol = this.getProtocol(credentialRecord.protocolVersion) this.logger.debug(`Got a credentialProtocol object for this version; version = ${protocol.version}`) const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) if (!offerMessage) { throw new AriesFrameworkError(`No offer message found for credential record with id '${credentialRecord.id}'`) } // Use connection if present const connectionRecord = credentialRecord.connectionId ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) : undefined connectionRecord?.assertReady() const { message } = await protocol.acceptOffer(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, comment: options.comment, autoAcceptCredential: options.autoAcceptCredential, }) const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, connectionRecord, associatedRecord: credentialRecord, lastReceivedMessage: offerMessage, }) await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } public async declineOffer(credentialRecordId: string): Promise { const credentialRecord = await this.getById(credentialRecordId) credentialRecord.assertState(CredentialState.OfferReceived) // with version we can get the Service const protocol = this.getProtocol(credentialRecord.protocolVersion) await protocol.updateState(this.agentContext, credentialRecord, CredentialState.Declined) return credentialRecord } public async negotiateOffer(options: NegotiateCredentialOfferOptions): Promise { const credentialRecord = await this.getById(options.credentialRecordId) if (!credentialRecord.connectionId) { throw new AriesFrameworkError( `No connection id for credential record ${credentialRecord.id} not found. Connection-less issuance does not support negotiation` ) } const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) // Assert connectionRecord.assertReady() const protocol = this.getProtocol(credentialRecord.protocolVersion) const { message } = await protocol.negotiateOffer(this.agentContext, { credentialFormats: options.credentialFormats, credentialRecord, comment: options.comment, autoAcceptCredential: options.autoAcceptCredential, }) const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, associatedRecord: credentialRecord, connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } /** * Initiate a new credential exchange as issuer by creating a credential offer * not bound to any connection. The offer must be delivered out-of-band to the holder * @param options The credential options to use for the offer * @returns The credential record and credential offer message */ public async createOffer(options: CreateCredentialOfferOptions): Promise<{ message: AgentMessage credentialRecord: CredentialExchangeRecord }> { const protocol = this.getProtocol(options.protocolVersion) this.logger.debug(`Got a credentialProtocol object for version ${options.protocolVersion}`) const { message, credentialRecord } = await protocol.createOffer(this.agentContext, { credentialFormats: options.credentialFormats, comment: options.comment, autoAcceptCredential: options.autoAcceptCredential, }) this.logger.debug('Offer Message successfully created', { message }) return { message, credentialRecord } } /** * Accept a credential request as holder (by sending a credential request message) to the connection * associated with the credential record. * * @param options The object containing config options of the request * @returns CredentialExchangeRecord updated with information pertaining to this request */ public async acceptRequest(options: AcceptCredentialRequestOptions): Promise { const credentialRecord = await this.getById(options.credentialRecordId) // with version we can get the Service const protocol = this.getProtocol(credentialRecord.protocolVersion) this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) // Use connection if present const connectionRecord = credentialRecord.connectionId ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) : undefined connectionRecord?.assertReady() const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) if (!requestMessage) { throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) } const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) if (!offerMessage) { throw new AriesFrameworkError(`No offer message found for credential record with id '${credentialRecord.id}'`) } const { message } = await protocol.acceptRequest(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, comment: options.comment, autoAcceptCredential: options.autoAcceptCredential, }) this.logger.debug('We have a credential message (sending outbound): ', message) const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, connectionRecord, associatedRecord: credentialRecord, lastReceivedMessage: requestMessage, lastSentMessage: offerMessage, }) await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } /** * Accept a credential as holder (by sending a credential acknowledgement message) to the connection * associated with the credential record. * * @param credentialRecordId The id of the credential record for which to accept the credential * @returns credential exchange record associated with the sent credential acknowledgement message * */ public async acceptCredential(options: AcceptCredentialOptions): Promise { const credentialRecord = await this.getById(options.credentialRecordId) // with version we can get the Service const protocol = this.getProtocol(credentialRecord.protocolVersion) this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) // Use connection if present const connectionRecord = credentialRecord.connectionId ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) : undefined connectionRecord?.assertReady() const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) if (!requestMessage) { throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) } const credentialMessage = await protocol.findCredentialMessage(this.agentContext, credentialRecord.id) if (!credentialMessage) { throw new AriesFrameworkError( `No credential message found for credential record with id '${credentialRecord.id}'` ) } const { message } = await protocol.acceptCredential(this.agentContext, { credentialRecord, }) const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, connectionRecord, associatedRecord: credentialRecord, lastReceivedMessage: credentialMessage, }) await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } /** * Send a revocation notification for a credential exchange record. Currently Revocation Notification V2 protocol is supported * * @param credentialRecordId The id of the credential record for which to send revocation notification */ public async sendRevocationNotification(options: SendRevocationNotificationOptions): Promise { const { credentialRecordId, revocationId, revocationFormat, comment, requestAck } = options const credentialRecord = await this.getById(credentialRecordId) const { message } = await this.revocationNotificationService.v2CreateRevocationNotification({ credentialId: revocationId, revocationFormat, comment, requestAck, }) const protocol = this.getProtocol(credentialRecord.protocolVersion) const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) if (!requestMessage) { throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) } const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) if (!offerMessage) { throw new AriesFrameworkError(`No offer message found for credential record with id '${credentialRecord.id}'`) } // Use connection if present const connectionRecord = credentialRecord.connectionId ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) : undefined connectionRecord?.assertReady() const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, connectionRecord, associatedRecord: credentialRecord, }) await this.messageSender.sendMessage(outboundMessageContext) } /** * Send problem report message for a credential record * @param credentialRecordId The id of the credential record for which to send problem report * @param message message to send * @returns credential record associated with the credential problem report message */ public async sendProblemReport(options: SendCredentialProblemReportOptions) { const credentialRecord = await this.getById(options.credentialRecordId) if (!credentialRecord.connectionId) { throw new AriesFrameworkError(`No connectionId found for credential record '${credentialRecord.id}'.`) } const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) const protocol = this.getProtocol(credentialRecord.protocolVersion) const { message } = await protocol.createProblemReport(this.agentContext, { description: options.description, credentialRecord, }) message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId, }) const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, associatedRecord: credentialRecord, connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } public async getFormatData( credentialRecordId: string ): Promise>> { const credentialRecord = await this.getById(credentialRecordId) const protocol = this.getProtocol(credentialRecord.protocolVersion) return protocol.getFormatData(this.agentContext, credentialRecordId) } /** * Retrieve a credential record by id * * @param credentialRecordId The credential record id * @throws {RecordNotFoundError} If no record is found * @return The credential record * */ public getById(credentialRecordId: string): Promise { return this.credentialRepository.getById(this.agentContext, credentialRecordId) } /** * Retrieve all credential records * * @returns List containing all credential records */ public getAll(): Promise { return this.credentialRepository.getAll(this.agentContext) } /** * Retrieve all credential records by specified query params * * @returns List containing all credential records matching specified query paramaters */ public findAllByQuery(query: Query) { return this.credentialRepository.findByQuery(this.agentContext, query) } /** * Find a credential record by id * * @param credentialRecordId the credential record id * @returns The credential record or null if not found */ public findById(credentialRecordId: string): Promise { return this.credentialRepository.findById(this.agentContext, credentialRecordId) } /** * Delete a credential record by id, also calls service to delete from wallet * * @param credentialId the credential record id * @param options the delete credential options for the delete operation */ public async deleteById(credentialId: string, options?: DeleteCredentialOptions) { const credentialRecord = await this.getById(credentialId) const protocol = this.getProtocol(credentialRecord.protocolVersion) return protocol.delete(this.agentContext, credentialRecord, options) } /** * Update a credential exchange record * * @param credentialRecord the credential exchange record */ public async update(credentialRecord: CredentialExchangeRecord): Promise { await this.credentialRepository.update(this.agentContext, credentialRecord) } public async findProposalMessage(credentialExchangeId: string): Promise> { const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) return protocol.findProposalMessage( this.agentContext, credentialExchangeId ) as FindCredentialProposalMessageReturn } public async findOfferMessage(credentialExchangeId: string): Promise> { const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) return protocol.findOfferMessage(this.agentContext, credentialExchangeId) as FindCredentialOfferMessageReturn } public async findRequestMessage(credentialExchangeId: string): Promise> { const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) return protocol.findRequestMessage( this.agentContext, credentialExchangeId ) as FindCredentialRequestMessageReturn } public async findCredentialMessage(credentialExchangeId: string): Promise> { const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) return protocol.findCredentialMessage(this.agentContext, credentialExchangeId) as FindCredentialMessageReturn } private async getServiceForCredentialExchangeId(credentialExchangeId: string) { const credentialExchangeRecord = await this.getById(credentialExchangeId) return this.getProtocol(credentialExchangeRecord.protocolVersion) } }