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

feat(core): add support for multi use invitations #460

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 5 additions & 1 deletion packages/core/src/modules/connections/ConnectionsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class ConnectionsModule {
autoAcceptConnection?: boolean
alias?: string
mediatorId?: string
multiUseInvitation?: boolean
}): Promise<{
invitation: ConnectionInvitationMessage
connectionRecord: ConnectionRecord
Expand All @@ -58,6 +59,7 @@ export class ConnectionsModule {
autoAcceptConnection: config?.autoAcceptConnection,
alias: config?.alias,
routing: myRouting,
multiUseInvitation: config?.multiUseInvitation,
})

return { connectionRecord, invitation }
Expand Down Expand Up @@ -254,7 +256,9 @@ export class ConnectionsModule {
}

private registerHandlers(dispatcher: Dispatcher) {
dispatcher.registerHandler(new ConnectionRequestHandler(this.connectionService, this.agentConfig))
dispatcher.registerHandler(
new ConnectionRequestHandler(this.connectionService, this.agentConfig, this.mediationRecipientService)
)
dispatcher.registerHandler(new ConnectionResponseHandler(this.connectionService, this.agentConfig))
dispatcher.registerHandler(new AckMessageHandler(this.connectionService))
dispatcher.registerHandler(new TrustPingMessageHandler(this.trustPingService, this.connectionService))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('ConnectionService', () => {
myRouting = { did: 'fakeDid', verkey: 'fakeVerkey', endpoints: config.endpoints ?? [], routingKeys: [] }
})

describe('createConnectionWithInvitation', () => {
describe('createInvitation', () => {
it('returns a connection record with values set', async () => {
expect.assertions(7)
const { connectionRecord: connectionRecord } = await connectionService.createInvitation({ routing: myRouting })
Expand Down Expand Up @@ -126,6 +126,20 @@ describe('ConnectionService', () => {
expect(aliasDefined.alias).toBe('test-alias')
expect(aliasUndefined.alias).toBeUndefined()
})

it('returns a connection record with the multiUseInvitation parameter from the config', async () => {
expect.assertions(2)

const { connectionRecord: multiUseDefined } = await connectionService.createInvitation({
multiUseInvitation: true,
routing: myRouting,
})
const { connectionRecord: multiUseUndefined } = await connectionService.createInvitation({ routing: myRouting })

expect(multiUseDefined.multiUseInvitation).toBe(true)
// Defaults to false
expect(multiUseUndefined.multiUseInvitation).toBe(false)
})
})

describe('processInvitation', () => {
Expand Down Expand Up @@ -291,6 +305,59 @@ describe('ConnectionService', () => {
expect(processedConnection.threadId).toBe(connectionRequest.id)
})

it('returns a new connection record containing the information from the connection request when multiUseInvitation is enabled on the connection', async () => {
expect.assertions(10)

const connectionRecord = getMockConnection({
id: 'test',
state: ConnectionState.Invited,
verkey: 'my-key',
role: ConnectionRole.Inviter,
multiUseInvitation: true,
})

const theirDid = 'their-did'
const theirVerkey = 'their-verkey'
const theirDidDoc = new DidDoc({
id: theirDid,
publicKey: [],
authentication: [],
service: [
new DidCommService({
id: `${theirDid};indy`,
serviceEndpoint: 'https://endpoint.com',
recipientKeys: [theirVerkey],
}),
],
})

const connectionRequest = new ConnectionRequestMessage({
did: theirDid,
didDoc: theirDidDoc,
label: 'test-label',
})

const messageContext = new InboundMessageContext(connectionRequest, {
connection: connectionRecord,
senderVerkey: theirVerkey,
recipientVerkey: 'my-key',
})

const processedConnection = await connectionService.processRequest(messageContext, myRouting)

expect(processedConnection.state).toBe(ConnectionState.Requested)
expect(processedConnection.theirDid).toBe(theirDid)
expect(processedConnection.theirDidDoc).toEqual(theirDidDoc)
expect(processedConnection.theirKey).toBe(theirVerkey)
expect(processedConnection.theirLabel).toBe('test-label')
expect(processedConnection.threadId).toBe(connectionRequest.id)

expect(connectionRepository.save).toHaveBeenCalledTimes(1)
expect(processedConnection.id).not.toBe(connectionRecord.id)
expect(connectionRecord.id).toBe('test')
expect(connectionRecord.state).toBe(ConnectionState.Invited)
})

it('throws an error when the message context does not have a connection', async () => {
expect.assertions(1)

Expand Down Expand Up @@ -365,6 +432,38 @@ describe('ConnectionService', () => {
`Connection with id ${connection.id} has no recipient keys.`
)
})

it('throws an error when a request for a multi use invitation is processed without routing provided', async () => {
const connectionRecord = getMockConnection({
state: ConnectionState.Invited,
verkey: 'my-key',
role: ConnectionRole.Inviter,
multiUseInvitation: true,
})

const theirDidDoc = new DidDoc({
id: 'their-did',
publicKey: [],
authentication: [],
service: [],
})

const connectionRequest = new ConnectionRequestMessage({
did: 'their-did',
didDoc: theirDidDoc,
label: 'test-label',
})

const messageContext = new InboundMessageContext(connectionRequest, {
connection: connectionRecord,
senderVerkey: 'their-verkey',
recipientVerkey: 'my-key',
})

expect(connectionService.processRequest(messageContext)).rejects.toThrowError(
'Cannot process request for multi-use invitation without routing object. Make sure to call processRequest with the routing parameter provided.'
)
})
})

describe('createResponse', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AgentConfig } from '../../../agent/AgentConfig'
import type { Handler, HandlerInboundMessage } from '../../../agent/Handler'
import type { ConnectionService } from '../services/ConnectionService'
import type { MediationRecipientService } from '../../routing/services/MediationRecipientService'
import type { ConnectionService, Routing } from '../services/ConnectionService'

import { createOutboundMessage } from '../../../agent/helpers'
import { AriesFrameworkError } from '../../../error'
Expand All @@ -9,23 +10,38 @@ import { ConnectionRequestMessage } from '../messages'
export class ConnectionRequestHandler implements Handler {
private connectionService: ConnectionService
private agentConfig: AgentConfig
private mediationRecipientService: MediationRecipientService
public supportedMessages = [ConnectionRequestMessage]

public constructor(connectionService: ConnectionService, agentConfig: AgentConfig) {
public constructor(
connectionService: ConnectionService,
agentConfig: AgentConfig,
mediationRecipientService: MediationRecipientService
) {
this.connectionService = connectionService
this.agentConfig = agentConfig
this.mediationRecipientService = mediationRecipientService
}

public async handle(messageContext: HandlerInboundMessage<ConnectionRequestHandler>) {
if (!messageContext.connection) {
throw new AriesFrameworkError(`Connection for verkey ${messageContext.recipientVerkey} not found!`)
}

await this.connectionService.processRequest(messageContext)
let routing: Routing | undefined

if (messageContext.connection?.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) {
const { message } = await this.connectionService.createResponse(messageContext.connection.id)
return createOutboundMessage(messageContext.connection, message)
// 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 (messageContext.connection.multiUseInvitation) {
const mediationRecord = await this.mediationRecipientService.discoverMediation()
routing = await this.mediationRecipientService.getRouting(mediationRecord)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is routing really necessary for multi-use invitations in general? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe not. But I would rather take all edge cases into account for these things. Otherwise we need good documentation on what features do and don't work together :)


const connection = await this.connectionService.processRequest(messageContext, routing)

if (connection?.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) {
const { message } = await this.connectionService.createResponse(connection.id)
return createOutboundMessage(connection, message)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface ConnectionRecordProps {
autoAcceptConnection?: boolean
threadId?: string
tags?: CustomConnectionTags
multiUseInvitation: boolean
}

export type CustomConnectionTags = TagsBase
Expand Down Expand Up @@ -59,6 +60,7 @@ export class ConnectionRecord
public invitation?: ConnectionInvitationMessage
public alias?: string
public autoAcceptConnection?: boolean
public multiUseInvitation!: boolean

public threadId?: string

Expand All @@ -84,6 +86,7 @@ export class ConnectionRecord
this._tags = props.tags ?? {}
this.invitation = props.invitation
this.threadId = props.threadId
this.multiUseInvitation = props.multiUseInvitation
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,16 @@ export class ConnectionService {
routing: Routing
autoAcceptConnection?: boolean
alias?: string
multiUseInvitation?: boolean
}): Promise<ConnectionProtocolMsgReturnType<ConnectionInvitationMessage>> {
// TODO: public did, multi use
// TODO: public did
const connectionRecord = await this.createConnection({
role: ConnectionRole.Inviter,
state: ConnectionState.Invited,
alias: config?.alias,
routing: config.routing,
autoAcceptConnection: config?.autoAcceptConnection,
multiUseInvitation: config.multiUseInvitation ?? false,
})

const { didDoc } = connectionRecord
Expand Down Expand Up @@ -130,6 +132,7 @@ export class ConnectionService {
tags: {
invitationKey: invitation.recipientKeys && invitation.recipientKeys[0],
},
multiUseInvitation: false,
})
await this.connectionRepository.update(connectionRecord)
this.eventEmitter.emit<ConnectionStateChangedEvent>({
Expand Down Expand Up @@ -179,9 +182,11 @@ export class ConnectionService {
* @returns updated connection record
*/
public async processRequest(
messageContext: InboundMessageContext<ConnectionRequestMessage>
messageContext: InboundMessageContext<ConnectionRequestMessage>,
routing?: Routing
): Promise<ConnectionRecord> {
const { message, connection: connectionRecord, recipientVerkey } = messageContext
const { message, recipientVerkey } = messageContext
let connectionRecord = messageContext.connection

if (!connectionRecord) {
throw new AriesFrameworkError(`Connection for verkey ${recipientVerkey} not found!`)
Expand All @@ -194,6 +199,25 @@ export class ConnectionService {
throw new AriesFrameworkError('Invalid message')
}

// Create new connection if using a multi use invitation
if (connectionRecord.multiUseInvitation) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be combined with the if statement below.

if (!routing) {
throw new AriesFrameworkError(
'Cannot process request for multi-use invitation without routing object. Make sure to call processRequest with the routing parameter provided.'
)
}

connectionRecord = await this.createConnection({
role: connectionRecord.role,
state: connectionRecord.state,
multiUseInvitation: false,
routing,
autoAcceptConnection: connectionRecord.autoAcceptConnection,
invitation: connectionRecord.invitation,
tags: connectionRecord.getTags(),
})
}

connectionRecord.theirDidDoc = message.connection.didDoc
connectionRecord.theirLabel = message.label
connectionRecord.threadId = message.id
Expand Down Expand Up @@ -233,9 +257,12 @@ export class ConnectionService {
throw new AriesFrameworkError(`Connection record with id ${connectionId} does not have a thread id`)
}

// Use invitationKey by default, fall back to verkey
const signingKey = (connectionRecord.getTag('invitationKey') as string) ?? connectionRecord.verkey

const connectionResponse = new ConnectionResponseMessage({
threadId: connectionRecord.threadId,
connectionSig: await signData(connectionJson, this.wallet, connectionRecord.verkey),
connectionSig: await signData(connectionJson, this.wallet, signingKey),
})

await this.updateState(connectionRecord, ConnectionState.Responded)
Expand Down Expand Up @@ -533,6 +560,7 @@ export class ConnectionService {
routing: Routing
theirLabel?: string
autoAcceptConnection?: boolean
multiUseInvitation: boolean
tags?: CustomConnectionTags
}): Promise<ConnectionRecord> {
const { endpoints, did, verkey, routingKeys } = options.routing
Expand Down Expand Up @@ -579,6 +607,7 @@ export class ConnectionService {
alias: options.alias,
theirLabel: options.theirLabel,
autoAcceptConnection: options.autoAcceptConnection,
multiUseInvitation: options.multiUseInvitation,
})

await this.connectionRepository.save(connectionRecord)
Expand Down
Loading