diff --git a/README.md b/README.md index 805cde3b0c..41636ca53a 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,10 @@ Some features are not yet supported, but are on our roadmap. Check [the roadmap] - ✅ Present Proof Protocol ([RFC 0037](https://github.com/hyperledger/aries-rfcs/tree/master/features/0037-present-proof/README.md)) - ✅ Connection Protocol ([RFC 0160](https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md)) - ✅ Basic Message Protocol ([RFC 0095](https://github.com/hyperledger/aries-rfcs/blob/master/features/0095-basic-message/README.md)) +- ✅ Mediator Coordination Protocol ([RFC 0211](https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md)) - ✅ Indy Credentials (with `did:sov` support) - ✅ HTTP Transport -- ✅ Mediator Coordination Protocol ([RFC 0211](https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md)) +- ✅ Auto accept proofs - 🚧 Revocation of Indy Credentials - 🚧 Electron - 🚧 WebSocket Transport diff --git a/packages/core/src/agent/AgentConfig.ts b/packages/core/src/agent/AgentConfig.ts index bb232eb480..46b7f887ea 100644 --- a/packages/core/src/agent/AgentConfig.ts +++ b/packages/core/src/agent/AgentConfig.ts @@ -9,6 +9,7 @@ import { DID_COMM_TRANSPORT_QUEUE } from '../constants' import { AriesFrameworkError } from '../error' import { ConsoleLogger, LogLevel } from '../logger' import { AutoAcceptCredential } from '../modules/credentials/CredentialAutoAcceptType' +import { AutoAcceptProof } from '../modules/proofs/ProofAutoAcceptType' import { MediatorPickupStrategy } from '../modules/routing/MediatorPickupStrategy' import { DidCommMimeType } from '../types' @@ -69,6 +70,10 @@ export class AgentConfig { return this.initConfig.autoAcceptConnections ?? false } + public get autoAcceptProofs() { + return this.initConfig.autoAcceptProofs ?? AutoAcceptProof.Never + } + public get autoAcceptCredentials() { return this.initConfig.autoAcceptCredentials ?? AutoAcceptCredential.Never } diff --git a/packages/core/src/modules/proofs/ProofAutoAcceptType.ts b/packages/core/src/modules/proofs/ProofAutoAcceptType.ts new file mode 100644 index 0000000000..53d80c89b9 --- /dev/null +++ b/packages/core/src/modules/proofs/ProofAutoAcceptType.ts @@ -0,0 +1,13 @@ +/** + * Typing of the state for auto acceptance + */ +export enum AutoAcceptProof { + // Always auto accepts the proof no matter if it changed in subsequent steps + Always = 'always', + + // Needs one acceptation and the rest will be automated if nothing changes + ContentApproved = 'contentApproved', + + // DEFAULT: Never auto accept a proof + Never = 'never', +} diff --git a/packages/core/src/modules/proofs/ProofResponseCoordinator.ts b/packages/core/src/modules/proofs/ProofResponseCoordinator.ts new file mode 100644 index 0000000000..eb0574f1cc --- /dev/null +++ b/packages/core/src/modules/proofs/ProofResponseCoordinator.ts @@ -0,0 +1,86 @@ +import type { ProofRecord } from './repository' + +import { scoped, Lifecycle } from 'tsyringe' + +import { AgentConfig } from '../../agent/AgentConfig' + +import { AutoAcceptProof } from './ProofAutoAcceptType' + +/** + * This class handles all the automation with all the messages in the present proof protocol + * Every function returns `true` if it should automate the flow and `false` if not + */ +@scoped(Lifecycle.ContainerScoped) +export class ProofResponseCoordinator { + private agentConfig: AgentConfig + + public constructor(agentConfig: AgentConfig) { + this.agentConfig = agentConfig + } + + /** + * Returns the proof auto accept config based on priority: + * - The record config takes first priority + * - Otherwise the agent config + * - Otherwise {@link AutoAcceptProof.Never} is returned + */ + private static composeAutoAccept( + recordConfig: AutoAcceptProof | undefined, + agentConfig: AutoAcceptProof | undefined + ) { + return recordConfig ?? agentConfig ?? AutoAcceptProof.Never + } + + /** + * Checks whether it should automatically respond to a proposal + */ + public shoudlAutoRespondToProposal(proofRecord: ProofRecord) { + const autoAccept = ProofResponseCoordinator.composeAutoAccept( + proofRecord.autoAcceptProof, + this.agentConfig.autoAcceptProofs + ) + + if (autoAccept === AutoAcceptProof.Always) { + return true + } + return false + } + + /** + * Checks whether it should automatically respond to a request + */ + public shouldAutoRespondToRequest(proofRecord: ProofRecord) { + const autoAccept = ProofResponseCoordinator.composeAutoAccept( + proofRecord.autoAcceptProof, + this.agentConfig.autoAcceptProofs + ) + + if ( + autoAccept === AutoAcceptProof.Always || + (autoAccept === AutoAcceptProof.ContentApproved && proofRecord.proposalMessage) + ) { + return true + } + + return false + } + + /** + * Checks whether it should automatically respond to a presention of proof + */ + public shouldAutoRespondToPresentation(proofRecord: ProofRecord) { + const autoAccept = ProofResponseCoordinator.composeAutoAccept( + proofRecord.autoAcceptProof, + this.agentConfig.autoAcceptProofs + ) + + if ( + autoAccept === AutoAcceptProof.Always || + (autoAccept === AutoAcceptProof.ContentApproved && proofRecord.requestMessage) + ) { + return true + } + + return false + } +} diff --git a/packages/core/src/modules/proofs/ProofsModule.ts b/packages/core/src/modules/proofs/ProofsModule.ts index 8bbdcf5700..ea164cd0cc 100644 --- a/packages/core/src/modules/proofs/ProofsModule.ts +++ b/packages/core/src/modules/proofs/ProofsModule.ts @@ -4,12 +4,14 @@ import type { ProofRecord } from './repository/ProofRecord' import { Lifecycle, scoped } from 'tsyringe' +import { AgentConfig } from '../../agent/AgentConfig' import { Dispatcher } from '../../agent/Dispatcher' import { MessageSender } from '../../agent/MessageSender' import { createOutboundMessage } from '../../agent/helpers' import { AriesFrameworkError } from '../../error' import { ConnectionService } from '../connections/services/ConnectionService' +import { ProofResponseCoordinator } from './ProofResponseCoordinator' import { ProposePresentationHandler, RequestPresentationHandler, @@ -24,16 +26,22 @@ export class ProofsModule { private proofService: ProofService private connectionService: ConnectionService private messageSender: MessageSender + private agentConfig: AgentConfig + private proofResponseCoordinator: ProofResponseCoordinator public constructor( dispatcher: Dispatcher, proofService: ProofService, connectionService: ConnectionService, - messageSender: MessageSender + agentConfig: AgentConfig, + messageSender: MessageSender, + proofResponseCoordinator: ProofResponseCoordinator ) { this.proofService = proofService this.connectionService = connectionService this.messageSender = messageSender + this.agentConfig = agentConfig + this.proofResponseCoordinator = proofResponseCoordinator this.registerHandlers(dispatcher) } @@ -270,9 +278,15 @@ export class ProofsModule { } private registerHandlers(dispatcher: Dispatcher) { - dispatcher.registerHandler(new ProposePresentationHandler(this.proofService)) - dispatcher.registerHandler(new RequestPresentationHandler(this.proofService)) - dispatcher.registerHandler(new PresentationHandler(this.proofService)) + dispatcher.registerHandler( + new ProposePresentationHandler(this.proofService, this.agentConfig, this.proofResponseCoordinator) + ) + dispatcher.registerHandler( + new RequestPresentationHandler(this.proofService, this.agentConfig, this.proofResponseCoordinator) + ) + dispatcher.registerHandler( + new PresentationHandler(this.proofService, this.agentConfig, this.proofResponseCoordinator) + ) dispatcher.registerHandler(new PresentationAckHandler(this.proofService)) } } diff --git a/packages/core/src/modules/proofs/handlers/PresentationHandler.ts b/packages/core/src/modules/proofs/handlers/PresentationHandler.ts index ddd96a7c34..ca4714186b 100644 --- a/packages/core/src/modules/proofs/handlers/PresentationHandler.ts +++ b/packages/core/src/modules/proofs/handlers/PresentationHandler.ts @@ -1,17 +1,48 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { ProofResponseCoordinator } from '../ProofResponseCoordinator' +import type { ProofRecord } from '../repository' import type { ProofService } from '../services' +import { createOutboundMessage } from '../../../agent/helpers' import { PresentationMessage } from '../messages' export class PresentationHandler implements Handler { private proofService: ProofService + private agentConfig: AgentConfig + private proofResponseCoordinator: ProofResponseCoordinator public supportedMessages = [PresentationMessage] - public constructor(proofService: ProofService) { + public constructor( + proofService: ProofService, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator + ) { this.proofService = proofService + this.agentConfig = agentConfig + this.proofResponseCoordinator = proofResponseCoordinator } public async handle(messageContext: HandlerInboundMessage) { - await this.proofService.processPresentation(messageContext) + const proofRecord = await this.proofService.processPresentation(messageContext) + + if (this.proofResponseCoordinator.shouldAutoRespondToPresentation(proofRecord)) { + return await this.createAck(proofRecord, messageContext) + } + } + + private async createAck(proofRecord: ProofRecord, messageContext: HandlerInboundMessage) { + this.agentConfig.logger.info( + `Automatically sending acknowledgement with autoAccept on ${this.agentConfig.autoAcceptProofs}` + ) + + if (!messageContext.connection) { + this.agentConfig.logger.error('No connection on the messageContext') + return + } + + const { message } = await this.proofService.createAck(proofRecord) + + return createOutboundMessage(messageContext.connection, message) } } diff --git a/packages/core/src/modules/proofs/handlers/ProposePresentationHandler.ts b/packages/core/src/modules/proofs/handlers/ProposePresentationHandler.ts index 3fa1b5b6e8..387431bc7f 100644 --- a/packages/core/src/modules/proofs/handlers/ProposePresentationHandler.ts +++ b/packages/core/src/modules/proofs/handlers/ProposePresentationHandler.ts @@ -1,17 +1,62 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { ProofResponseCoordinator } from '../ProofResponseCoordinator' +import type { ProofRecord } from '../repository' import type { ProofService } from '../services' +import { createOutboundMessage } from '../../../agent/helpers' import { ProposePresentationMessage } from '../messages' export class ProposePresentationHandler implements Handler { private proofService: ProofService + private agentConfig: AgentConfig + private proofResponseCoordinator: ProofResponseCoordinator public supportedMessages = [ProposePresentationMessage] - public constructor(proofService: ProofService) { + public constructor( + proofService: ProofService, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator + ) { this.proofService = proofService + this.agentConfig = agentConfig + this.proofResponseCoordinator = proofResponseCoordinator } public async handle(messageContext: HandlerInboundMessage) { - await this.proofService.processProposal(messageContext) + const proofRecord = await this.proofService.processProposal(messageContext) + + if (this.proofResponseCoordinator.shoudlAutoRespondToProposal(proofRecord)) { + return await this.createRequest(proofRecord, messageContext) + } + } + + private async createRequest( + proofRecord: ProofRecord, + messageContext: HandlerInboundMessage + ) { + this.agentConfig.logger.info( + `Automatically sending request with autoAccept on ${this.agentConfig.autoAcceptProofs}` + ) + + if (!messageContext.connection) { + this.agentConfig.logger.error('No connection on the messageContext') + return + } + if (!proofRecord.proposalMessage) { + this.agentConfig.logger.error(`Proof record with id ${proofRecord.id} is missing required credential proposal`) + return + } + const proofRequest = await this.proofService.createProofRequestFromProposal( + proofRecord.proposalMessage.presentationProposal, + { + name: 'proof-request', + version: '1.0', + } + ) + + const { message } = await this.proofService.createRequestAsResponse(proofRecord, proofRequest) + + return createOutboundMessage(messageContext.connection, message) } } diff --git a/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts b/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts index f60cb2d16e..8c14c0ffe9 100644 --- a/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts +++ b/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts @@ -1,17 +1,64 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { ProofResponseCoordinator } from '../ProofResponseCoordinator' +import type { ProofRecord } from '../repository' import type { ProofService } from '../services' +import { createOutboundMessage } from '../../../agent/helpers' import { RequestPresentationMessage } from '../messages' export class RequestPresentationHandler implements Handler { private proofService: ProofService + private agentConfig: AgentConfig + private proofResponseCoordinator: ProofResponseCoordinator public supportedMessages = [RequestPresentationMessage] - public constructor(proofService: ProofService) { + public constructor( + proofService: ProofService, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator + ) { this.proofService = proofService + this.agentConfig = agentConfig + this.proofResponseCoordinator = proofResponseCoordinator } public async handle(messageContext: HandlerInboundMessage) { - await this.proofService.processRequest(messageContext) + const proofRecord = await this.proofService.processRequest(messageContext) + + if (this.proofResponseCoordinator.shouldAutoRespondToRequest(proofRecord)) { + return await this.createPresentation(proofRecord, messageContext) + } + } + + private async createPresentation( + proofRecord: ProofRecord, + messageContext: HandlerInboundMessage + ) { + const indyProofRequest = proofRecord.requestMessage?.indyProofRequest + + this.agentConfig.logger.info( + `Automatically sending presentation with autoAccept on ${this.agentConfig.autoAcceptProofs}` + ) + + if (!messageContext.connection) { + this.agentConfig.logger.error('No connection on the messageContext') + return + } + + if (!indyProofRequest) { + return + } + + const retrievedCredentials = await this.proofService.getRequestedCredentialsForProofRequest( + indyProofRequest, + proofRecord.proposalMessage?.presentationProposal + ) + + const requestedCredentials = this.proofService.autoSelectCredentialsForProofRequest(retrievedCredentials) + + const { message } = await this.proofService.createPresentation(proofRecord, requestedCredentials) + + return createOutboundMessage(messageContext.connection, message) } } diff --git a/packages/core/src/modules/proofs/index.ts b/packages/core/src/modules/proofs/index.ts index b57ee319c3..a4e5d95714 100644 --- a/packages/core/src/modules/proofs/index.ts +++ b/packages/core/src/modules/proofs/index.ts @@ -5,3 +5,4 @@ export * from './ProofState' export * from './repository' export * from './ProofEvents' export * from './ProofsModule' +export * from './ProofAutoAcceptType' diff --git a/packages/core/src/modules/proofs/repository/ProofRecord.ts b/packages/core/src/modules/proofs/repository/ProofRecord.ts index b434c546e7..0312bc3ecf 100644 --- a/packages/core/src/modules/proofs/repository/ProofRecord.ts +++ b/packages/core/src/modules/proofs/repository/ProofRecord.ts @@ -1,4 +1,5 @@ import type { TagsBase } from '../../../storage/BaseRecord' +import type { AutoAcceptProof } from '../ProofAutoAcceptType' import type { ProofState } from '../ProofState' import { Type } from 'class-transformer' @@ -18,6 +19,7 @@ export interface ProofRecordProps { threadId: string presentationId?: string tags?: CustomProofTags + autoAcceptProof?: AutoAcceptProof // message data proposalMessage?: ProposePresentationMessage @@ -38,6 +40,7 @@ export class ProofRecord extends BaseRecord { public isVerified?: boolean public presentationId?: string public state!: ProofState + public autoAcceptProof?: AutoAcceptProof // message data @Type(() => ProposePresentationMessage) @@ -64,6 +67,7 @@ export class ProofRecord extends BaseRecord { this.connectionId = props.connectionId this.threadId = props.threadId this.presentationId = props.presentationId + this.autoAcceptProof = props.autoAcceptProof this._tags = props.tags ?? {} } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ff2950de63..a7921b211c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -3,6 +3,7 @@ import type { TransportSession } from './agent/TransportService' import type { Logger } from './logger' import type { ConnectionRecord } from './modules/connections' import type { AutoAcceptCredential } from './modules/credentials/CredentialAutoAcceptType' +import type { AutoAcceptProof } from './modules/proofs' import type { MediatorPickupStrategy } from './modules/routing' import type { Verkey, WalletConfig, WalletCredentials } from 'indy-sdk' @@ -24,6 +25,7 @@ export interface InitConfig { walletConfig?: WalletConfig walletCredentials?: WalletCredentials autoAcceptConnections?: boolean + autoAcceptProofs?: AutoAcceptProof autoAcceptCredentials?: AutoAcceptCredential poolName?: string logger?: Logger diff --git a/packages/core/tests/credentials-auto-accept.test.ts b/packages/core/tests/credentials-auto-accept.test.ts index 991d69bb87..e5d04ce2d1 100644 --- a/packages/core/tests/credentials-auto-accept.test.ts +++ b/packages/core/tests/credentials-auto-accept.test.ts @@ -62,8 +62,8 @@ describe('auto accept credentials', () => { describe('Auto accept on `always`', () => { beforeAll(async () => { ;({ faberAgent, aliceAgent, credDefId, schemaId, faberConnection, aliceConnection } = await setupCredentialTests( - 'faber agent always', - 'alice agent always', + 'faber agent: always', + 'alice agent: always', AutoAcceptCredential.Always )) }) @@ -170,8 +170,8 @@ describe('auto accept credentials', () => { describe('Auto accept on `contentApproved`', () => { beforeAll(async () => { ;({ faberAgent, aliceAgent, credDefId, schemaId, faberConnection, aliceConnection } = await setupCredentialTests( - 'faber agent contentApproved', - 'alice agent contentApproved', + 'faber agent: contentApproved', + 'alice agent: contentApproved', AutoAcceptCredential.ContentApproved )) }) diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 2c907ca2f5..dfff9b0e3f 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -1,10 +1,20 @@ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import type { BasicMessage, BasicMessageReceivedEvent } from '../src/modules/basic-messages' -import type { ConnectionRecordProps } from '../src/modules/connections' -import type { CredentialRecord, CredentialOfferTemplate, CredentialStateChangedEvent } from '../src/modules/credentials' -import type { SchemaTemplate, CredentialDefinitionTemplate } from '../src/modules/ledger' -import type { ProofAttributeInfo, ProofPredicateInfo, ProofRecord, ProofStateChangedEvent } from '../src/modules/proofs' -import type { InitConfig } from '../src/types' +import type { + InitConfig, + ProofRecord, + ProofStateChangedEvent, + CredentialRecord, + CredentialStateChangedEvent, + BasicMessage, + BasicMessageReceivedEvent, + ConnectionRecordProps, + SchemaTemplate, + CredentialDefinitionTemplate, + CredentialOfferTemplate, + ProofAttributeInfo, + ProofPredicateInfo, + AutoAcceptProof, +} from '../src' import type { Schema, CredDef, Did } from 'indy-sdk' import path from 'path' @@ -13,32 +23,36 @@ import { Subject } from 'rxjs' import { SubjectInboundTransporter } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransporter } from '../../../tests/transport/SubjectOutboundTransport' import { agentDependencies } from '../../node/src' -import { Agent } from '../src/agent/Agent' -import { AgentConfig } from '../src/agent/AgentConfig' -import { LogLevel } from '../src/logger/Logger' -import { BasicMessageEventTypes } from '../src/modules/basic-messages' -import { - ConnectionInvitationMessage, - ConnectionRecord, - ConnectionRole, - ConnectionState, - DidCommService, - DidDoc, -} from '../src/modules/connections' import { + LogLevel, + AgentConfig, + ProofState, + ProofEventTypes, CredentialState, CredentialEventTypes, + BasicMessageEventTypes, + ConnectionState, + ConnectionRole, + DidDoc, + DidCommService, + ConnectionInvitationMessage, + ConnectionRecord, CredentialPreview, CredentialPreviewAttribute, -} from '../src/modules/credentials' -import { AutoAcceptCredential } from '../src/modules/credentials/CredentialAutoAcceptType' -import { ProofEventTypes, ProofState } from '../src/modules/proofs' + AriesFrameworkError, + AutoAcceptCredential, + PresentationPreview, + PresentationPreviewAttribute, + PresentationPreviewPredicate, + PredicateType, + Agent, +} from '../src' +import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachment' +import { LinkedAttachment } from '../src/utils/LinkedAttachment' import { uuid } from '../src/utils/uuid' import testLogger, { TestLogger } from './logger' -import { AriesFrameworkError } from '@aries-framework/core' - export const genesisPath = process.env.GENESIS_TXN_PATH ? path.resolve(process.env.GENESIS_TXN_PATH) : path.join(__dirname, '../../../network/genesis/local-genesis.txn') @@ -434,3 +448,127 @@ export async function setupCredentialTests( return { faberAgent, aliceAgent, credDefId, schemaId, faberConnection, aliceConnection } } + +export async function setupProofsTest(faberName: string, aliceName: string, autoAcceptProofs?: AutoAcceptProof) { + const credentialPreview = new CredentialPreview({ + attributes: [ + new CredentialPreviewAttribute({ + name: 'name', + mimeType: 'text/plain', + value: 'John', + }), + new CredentialPreviewAttribute({ + name: 'age', + mimeType: 'text/plain', + value: '99', + }), + ], + }) + + const faberConfig = getBaseConfig(faberName, { + genesisPath, + autoAcceptProofs, + endpoint: 'rxjs:faber', + }) + + const aliceConfig = getBaseConfig(aliceName, { + genesisPath, + autoAcceptProofs, + endpoint: 'rxjs:alice', + }) + + const faberMessages = new Subject() + const aliceMessages = new Subject() + + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + } + const faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) + faberAgent.setInboundTransporter(new SubjectInboundTransporter(faberMessages)) + faberAgent.setOutboundTransporter(new SubjectOutboundTransporter(aliceMessages, subjectMap)) + await faberAgent.initialize() + + const aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + aliceAgent.setInboundTransporter(new SubjectInboundTransporter(aliceMessages)) + aliceAgent.setOutboundTransporter(new SubjectOutboundTransporter(faberMessages, subjectMap)) + await aliceAgent.initialize() + + const schemaTemplate = { + name: `test-schema-${Date.now()}`, + attributes: ['name', 'age', 'image_0', 'image_1'], + version: '1.0', + } + const schema = await registerSchema(faberAgent, schemaTemplate) + + const definitionTemplate = { + schema, + tag: 'TAG', + signatureType: 'CL' as const, + supportRevocation: false, + } + const credentialDefinition = await registerDefinition(faberAgent, definitionTemplate) + const credDefId = credentialDefinition.id + + const publicDid = faberAgent.publicDid?.did + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await ensurePublicDidIsOnLedger(faberAgent, publicDid!) + const [agentAConnection, agentBConnection] = await makeConnection(faberAgent, aliceAgent) + expect(agentAConnection.isReady).toBe(true) + expect(agentBConnection.isReady).toBe(true) + + const faberConnection = agentAConnection + const aliceConnection = agentBConnection + + const presentationPreview = new PresentationPreview({ + attributes: [ + new PresentationPreviewAttribute({ + name: 'name', + credentialDefinitionId: credDefId, + referent: '0', + value: 'John', + }), + new PresentationPreviewAttribute({ + name: 'image_0', + credentialDefinitionId: credDefId, + }), + ], + predicates: [ + new PresentationPreviewPredicate({ + name: 'age', + credentialDefinitionId: credDefId, + predicate: PredicateType.GreaterThanOrEqualTo, + threshold: 50, + }), + ], + }) + + await issueCredential({ + issuerAgent: faberAgent, + issuerConnectionId: faberConnection.id, + holderAgent: aliceAgent, + credentialTemplate: { + credentialDefinitionId: credDefId, + comment: 'some comment about credential', + preview: credentialPreview, + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + }) + + return { faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } +} diff --git a/packages/core/tests/proofs-auto-accept.test.ts b/packages/core/tests/proofs-auto-accept.test.ts new file mode 100644 index 0000000000..943dd2406c --- /dev/null +++ b/packages/core/tests/proofs-auto-accept.test.ts @@ -0,0 +1,206 @@ +import type { Agent, ConnectionRecord, PresentationPreview } from '../src' +import type { CredDefId } from 'indy-sdk' + +import { + AutoAcceptProof, + ProofState, + ProofAttributeInfo, + AttributeFilter, + ProofPredicateInfo, + PredicateType, +} from '../src' + +import { setupProofsTest, waitForProofRecord } from './helpers' +import testLogger from './logger' + +describe('Auto accept present proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let credDefId: CredDefId + let faberConnection: ConnectionRecord + let aliceConnection: ConnectionRecord + let presentationPreview: PresentationPreview + + describe('Auto accept on `always`', () => { + beforeAll(async () => { + ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = + await setupProofsTest('faber agent always', 'alice agent always', AutoAcceptProof.Always)) + }) + + afterAll(async () => { + await aliceAgent.shutdown({ + deleteWallet: true, + }) + await faberAgent.shutdown({ + deleteWallet: true, + }) + }) + + test('Alice starts with proof proposal to Faber, both with autoAcceptProof on `always`', async () => { + testLogger.test('Alice sends presentation proposal to Faber') + const aliceProofRecord = await aliceAgent.proofs.proposeProof(aliceConnection.id, presentationPreview) + + testLogger.test('Faber waits for presentation from Alice') + await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + testLogger.test('Alice waits till it receives presentation ack') + await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + }) + + test('Faber starts with proof requests to Alice, both with autoAcceptProof on `always`', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const faberProofRecord = await faberAgent.proofs.requestProof(faberConnection.id, { + name: 'test-proof-request', + requestedAttributes: attributes, + requestedPredicates: predicates, + }) + + testLogger.test('Faber waits for presentation from Alice') + await waitForProofRecord(faberAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + + // Alice waits till it receives presentation ack + await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + }) + }) + + describe('Auto accept on `contentApproved`', () => { + beforeAll(async () => { + ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = + await setupProofsTest('faber agent', 'alice agent', AutoAcceptProof.ContentApproved)) + }) + + afterAll(async () => { + await aliceAgent.shutdown({ + deleteWallet: true, + }) + await faberAgent.shutdown({ + deleteWallet: true, + }) + }) + + test('Alice starts with proof proposal to Faber, both with autoacceptproof on `contentApproved`', async () => { + testLogger.test('Alice sends presentation proposal to Faber') + const aliceProofRecord = await aliceAgent.proofs.proposeProof(aliceConnection.id, presentationPreview) + + testLogger.test('Faber waits for presentation proposal from Alice') + const faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.ProposalReceived, + }) + + testLogger.test('Faber accepts presentation proposal from Alice') + await faberAgent.proofs.acceptProposal(faberProofRecord.id) + + testLogger.test('Faber waits for presentation from Alice') + await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + // Alice waits till it receives presentation ack + await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + }) + + test('Faber starts with proof requests to Alice, both with autoacceptproof on `contentApproved`', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const faberProofRecord = await faberAgent.proofs.requestProof(faberConnection.id, { + name: 'test-proof-request', + requestedAttributes: attributes, + requestedPredicates: predicates, + }) + + testLogger.test('Alice waits for presentation request from Faber') + const aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Alice accepts presentation request from Faber') + const indyProofRequest = aliceProofRecord.requestMessage?.indyProofRequest + const retrievedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + indyProofRequest!, + presentationPreview + ) + const requestedCredentials = aliceAgent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) + await aliceAgent.proofs.acceptRequest(aliceProofRecord.id, requestedCredentials) + + testLogger.test('Faber waits for presentation from Alice') + await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + // Alice waits till it receives presentation ack + await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + }) + }) +}) diff --git a/packages/core/tests/proofs.test.ts b/packages/core/tests/proofs.test.ts index 536d323c08..15c84b293d 100644 --- a/packages/core/tests/proofs.test.ts +++ b/packages/core/tests/proofs.test.ts @@ -1,53 +1,11 @@ -import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import type { ConnectionRecord } from '../src/modules/connections' +import type { Agent, ConnectionRecord, PresentationPreview } from '../src' import type { CredDefId } from 'indy-sdk' -import { Subject } from 'rxjs' +import { ProofState, ProofAttributeInfo, AttributeFilter, ProofPredicateInfo, PredicateType } from '../src' -import { SubjectInboundTransporter } from '../../../tests/transport/SubjectInboundTransport' -import { SubjectOutboundTransporter } from '../../../tests/transport/SubjectOutboundTransport' -import { Agent } from '../src/agent/Agent' -import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachment' -import { AutoAcceptCredential, CredentialPreview, CredentialPreviewAttribute } from '../src/modules/credentials' -import { - PredicateType, - PresentationPreview, - PresentationPreviewAttribute, - PresentationPreviewPredicate, - ProofState, - ProofAttributeInfo, - AttributeFilter, - ProofPredicateInfo, -} from '../src/modules/proofs' -import { LinkedAttachment } from '../src/utils/LinkedAttachment' - -import { makeConnection, issueCredential, waitForProofRecord, getBaseConfig, prepareForIssuance } from './helpers' +import { setupProofsTest, waitForProofRecord } from './helpers' import testLogger from './logger' -const faberConfig = getBaseConfig('Faber Proofs', { - endpoint: 'rxjs:faber', - autoAcceptCredentials: AutoAcceptCredential.ContentApproved, -}) -const aliceConfig = getBaseConfig('Alice Proofs', { - endpoint: 'rxjs:alice', - autoAcceptCredentials: AutoAcceptCredential.ContentApproved, -}) - -const credentialPreview = new CredentialPreview({ - attributes: [ - new CredentialPreviewAttribute({ - name: 'name', - mimeType: 'text/plain', - value: 'John', - }), - new CredentialPreviewAttribute({ - name: 'age', - mimeType: 'text/plain', - value: '99', - }), - ], -}) - describe('Present Proof', () => { let faberAgent: Agent let aliceAgent: Agent @@ -57,82 +15,8 @@ describe('Present Proof', () => { let presentationPreview: PresentationPreview beforeAll(async () => { - const faberMessages = new Subject() - const aliceMessages = new Subject() - - const subjectMap = { - 'rxjs:faber': faberMessages, - 'rxjs:alice': aliceMessages, - } - faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) - faberAgent.setInboundTransporter(new SubjectInboundTransporter(faberMessages)) - faberAgent.setOutboundTransporter(new SubjectOutboundTransporter(aliceMessages, subjectMap)) - await faberAgent.initialize() - - aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) - aliceAgent.setInboundTransporter(new SubjectInboundTransporter(aliceMessages)) - aliceAgent.setOutboundTransporter(new SubjectOutboundTransporter(faberMessages, subjectMap)) - await aliceAgent.initialize() - - const { definition } = await prepareForIssuance(faberAgent, ['name', 'age', 'image_0', 'image_1']) - credDefId = definition.id - - const [agentAConnection, agentBConnection] = await makeConnection(faberAgent, aliceAgent) - expect(agentAConnection.isReady).toBe(true) - expect(agentBConnection.isReady).toBe(true) - - faberConnection = agentAConnection - aliceConnection = agentBConnection - - presentationPreview = new PresentationPreview({ - attributes: [ - new PresentationPreviewAttribute({ - name: 'name', - credentialDefinitionId: credDefId, - referent: '0', - value: 'John', - }), - new PresentationPreviewAttribute({ - name: 'image_0', - credentialDefinitionId: credDefId, - }), - ], - predicates: [ - new PresentationPreviewPredicate({ - name: 'age', - credentialDefinitionId: credDefId, - predicate: PredicateType.GreaterThanOrEqualTo, - threshold: 50, - }), - ], - }) - - await issueCredential({ - issuerAgent: faberAgent, - issuerConnectionId: faberConnection.id, - holderAgent: aliceAgent, - credentialTemplate: { - credentialDefinitionId: credDefId, - comment: 'some comment about credential', - preview: credentialPreview, - linkedAttachments: [ - new LinkedAttachment({ - name: 'image_0', - attachment: new Attachment({ - filename: 'picture-of-a-cat.png', - data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), - }), - }), - new LinkedAttachment({ - name: 'image_1', - attachment: new Attachment({ - filename: 'picture-of-a-dog.png', - data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), - }), - }), - ], - }, - }) + ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = + await setupProofsTest('faber agent', 'alice agent')) }) afterAll(async () => {