Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support mediation for connectionless exchange #577

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ export class ConnectionRequestHandler implements Handler {
// routing object is required for multi use invitation, because we're creating a
// new keypair that possibly needs to be registered at a mediator
if (connectionRecord.multiUseInvitation) {
const mediationRecord = await this.mediationRecipientService.discoverMediation()
routing = await this.mediationRecipientService.getRouting(mediationRecord)
routing = await this.mediationRecipientService.getRouting()
}

connectionRecord = await this.connectionService.processRequest(messageContext, routing)
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/modules/routing/RecipientModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ export class RecipientModule {
const connection = await this.connectionService.findByInvitationKey(invitation.recipientKeys[0])
if (!connection) {
this.logger.debug('Mediation Connection does not exist, creating connection')
const routing = await this.mediationRecipientService.getRouting()
// null means we don't want to use the current mediator when connecting to another mediator
const routing = await this.mediationRecipientService.getRouting(null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would your fix work if we preserved the previous API? I'm asking because I don't like passing null and having different behavior based on the difference between null ,undefined or value.

Although, I would welcome if we would not need to call getDefaultMediator before every getRouting, I don't think this is a good approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like your goal is not to get routing but just to create new keys. Maybe, we need to have 2 separate methods.

  1. creating did, keys, and endpoint not involving a mediator
  2. creating did, keys, and endpoint involving a mediator

The name getRouting is kind of misleading because it's actually not only about routing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on the null vs undefined point. That's a nasty fix from me.

I disagree however that the naming of getRouting is misleading as IMO mediation is routing. The protocol used for forward messages is called the routing protocol (https://didcomm.org/routing/1.0/forward) which is what is used when you use a mediator that adds extra keys to the routingKeys array.

what if I add an optional parameter to the getRouting method to specify whether the default mediator should be taken into account. It will be true by default, but can be set to false?

const routing = await this.mediationRecipientService.getRouting({
   mediationRecord: xxx,
   useDefaultMediator: true
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be better 👍

As you wrote, mediation is routing. Then I would expect that routing returns things only related to routing/mediation. I was surprised by the following line in createConnection when I was looking at how we're creating a did and verkey for a new connection:

const { endpoints, did, verkey, routingKeys, mediatorId } = options.routing

I thought that endpoints, did, verkey are somehow related just to mediation. But we use it to create a connection even if there is no mediation in use.

Perhaps, it's not a big deal. We don't have to address that. I'm familiar with how it works now and can live with that :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that endpoints, did, verkey are somehow related just to mediation. But we use it to create a connection even if there is no mediation in use.

It's a bit weird it lives in the mediation service, even thought it handled non-mediation stuff. I think the idea was to eventually move it to a routing service that handles everything related to routing (mediators, relays, other possible weird routing stuff). The connection service would then call the routing service whenever it needs keys for a connections.


const invitationConnectionRecord = await this.connectionService.processInvitation(invitation, {
autoAcceptConnection: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,21 +155,29 @@ export class MediationRecipientService {
return keylistUpdateMessage
}

public async getRouting(mediationRecord?: MediationRecord): Promise<Routing> {
public async getRouting(mediationRecord?: MediationRecord | null): Promise<Routing> {
let mediator = mediationRecord

// If mediator is undefined, try to find the default mediator. If the mediator is null we
// don't want to use the default mediator (override)
if (mediator === undefined) {
mediator = await this.findDefaultMediator()
}

let endpoints = this.config.endpoints
let routingKeys: string[] = []

// Create and store new key
const { did, verkey } = await this.wallet.createDid()
if (mediationRecord) {
routingKeys = [...routingKeys, ...mediationRecord.routingKeys]
endpoints = mediationRecord.endpoint ? [mediationRecord.endpoint] : endpoints
if (mediator) {
routingKeys = [...routingKeys, ...mediator.routingKeys]
endpoints = mediator.endpoint ? [mediator.endpoint] : endpoints
// new did has been created and mediator needs to be updated with the public key.
mediationRecord = await this.keylistUpdateAndAwait(mediationRecord, verkey)
mediator = await this.keylistUpdateAndAwait(mediator, verkey)
} else {
// TODO: check that recipient keys are in wallet
}
return { endpoints, routingKeys, did, verkey, mediatorId: mediationRecord?.id }
return { endpoints, routingKeys, did, verkey, mediatorId: mediator?.id }
}

public async saveRoute(recipientKey: string, mediationRecord: MediationRecord) {
Expand Down
183 changes: 182 additions & 1 deletion packages/core/tests/connectionless-proofs.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import type { ProofStateChangedEvent } from '../src/modules/proofs'
import type { SubjectMessage } from 'tests/transport/SubjectInboundTransport'

import { Subject, ReplaySubject } from 'rxjs'

import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport'
import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport'
import { Agent } from '../src/agent/Agent'
import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachment'
import {
PredicateType,
ProofState,
ProofAttributeInfo,
AttributeFilter,
ProofPredicateInfo,
AutoAcceptProof,
ProofEventTypes,
} from '../src/modules/proofs'
import { LinkedAttachment } from '../src/utils/LinkedAttachment'
import { uuid } from '../src/utils/uuid'

import { setupProofsTest, waitForProofRecordSubject } from './helpers'
import {
getBaseConfig,
issueCredential,
makeConnection,
prepareForIssuance,
setupProofsTest,
waitForProofRecordSubject,
} from './helpers'
import testLogger from './logger'

import { CredentialPreview } from '@aries-framework/core'
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved

describe('Present Proof', () => {
test('Faber starts with connection-less proof requests to Alice', async () => {
const { aliceAgent, faberAgent, aliceReplay, credDefId, faberReplay } = await setupProofsTest(
Expand Down Expand Up @@ -141,4 +162,164 @@ describe('Present Proof', () => {
state: ProofState.Done,
})
})

test('Faber starts with connection-less proof requests to Alice with auto-accept enabled and both agents having a mediator', async () => {
testLogger.test('Faber sends presentation request to Alice')

const credentialPreview = CredentialPreview.fromRecord({
name: 'John',
age: '99',
})

const unique = uuid().substring(0, 4)

const mediatorConfig = getBaseConfig(`Connectionless proofs with mediator Mediator-${unique}`, {
autoAcceptMediationRequests: true,
endpoints: ['rxjs:mediator'],
})

const faberMessages = new Subject<SubjectMessage>()
const aliceMessages = new Subject<SubjectMessage>()
const mediatorMessages = new Subject<SubjectMessage>()

const subjectMap = {
'rxjs:mediator': mediatorMessages,
}

// Initialize mediator
const mediatorAgent = new Agent(mediatorConfig.config, mediatorConfig.agentDependencies)
mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(mediatorMessages, subjectMap))
mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages))
await mediatorAgent.initialize()

const faberMediationInvitation = await mediatorAgent.connections.createConnection()
const aliceMediationInvitation = await mediatorAgent.connections.createConnection()

const faberConfig = getBaseConfig(`Connectionless proofs with mediator Faber-${unique}`, {
autoAcceptProofs: AutoAcceptProof.Always,
mediatorConnectionsInvite: faberMediationInvitation.invitation.toUrl({ domain: 'https://example.com' }),
})

const aliceConfig = getBaseConfig(`Connectionless proofs with mediator Alice-${unique}`, {
autoAcceptProofs: AutoAcceptProof.Always,
// logger: new TestLogger(LogLevel.test),
mediatorConnectionsInvite: aliceMediationInvitation.invitation.toUrl({ domain: 'https://example.com' }),
})

const faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies)
faberAgent.registerOutboundTransport(new SubjectOutboundTransport(faberMessages, subjectMap))
faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages))
await faberAgent.initialize()

const aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies)
aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(aliceMessages, subjectMap))
aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages))
await aliceAgent.initialize()

const { definition } = await prepareForIssuance(faberAgent, ['name', 'age', 'image_0', 'image_1'])

const [faberConnection, aliceConnection] = await makeConnection(faberAgent, aliceAgent)
expect(faberConnection.isReady).toBe(true)
expect(aliceConnection.isReady).toBe(true)

await issueCredential({
issuerAgent: faberAgent,
issuerConnectionId: faberConnection.id,
holderAgent: aliceAgent,
credentialTemplate: {
credentialDefinitionId: definition.id,
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==' }),
}),
}),
],
},
})
const faberReplay = new ReplaySubject<ProofStateChangedEvent>()
const aliceReplay = new ReplaySubject<ProofStateChangedEvent>()

faberAgent.events.observable<ProofStateChangedEvent>(ProofEventTypes.ProofStateChanged).subscribe(faberReplay)
aliceAgent.events.observable<ProofStateChangedEvent>(ProofEventTypes.ProofStateChanged).subscribe(aliceReplay)

const attributes = {
name: new ProofAttributeInfo({
name: 'name',
restrictions: [
new AttributeFilter({
credentialDefinitionId: definition.id,
}),
],
}),
}

const predicates = {
age: new ProofPredicateInfo({
name: 'age',
predicateType: PredicateType.GreaterThanOrEqualTo,
predicateValue: 50,
restrictions: [
new AttributeFilter({
credentialDefinitionId: definition.id,
}),
],
}),
}

// eslint-disable-next-line prefer-const
let { proofRecord: faberProofRecord, requestMessage } = await faberAgent.proofs.createOutOfBandRequest(
{
name: 'test-proof-request',
requestedAttributes: attributes,
requestedPredicates: predicates,
},
{
autoAcceptProof: AutoAcceptProof.ContentApproved,
}
)

const mediationRecord = await faberAgent.mediationRecipient.findDefaultMediator()
if (!mediationRecord) {
throw new Error('Faber agent has no default mediator')
}

expect(requestMessage).toMatchObject({
service: {
recipientKeys: [expect.any(String)],
routingKeys: mediationRecord.routingKeys,
serviceEndpoint: mediationRecord.endpoint,
},
})

await aliceAgent.receiveMessage(requestMessage.toJSON())

await waitForProofRecordSubject(aliceReplay, {
threadId: faberProofRecord.threadId,
state: ProofState.Done,
})

await waitForProofRecordSubject(faberReplay, {
threadId: faberProofRecord.threadId,
state: ProofState.Done,
})

await faberAgent.shutdown()
await faberAgent.wallet.delete()
await aliceAgent.shutdown()
await aliceAgent.wallet.delete()
await mediatorAgent.shutdown()
await mediatorAgent.wallet.delete()
})
})