Skip to content

Commit

Permalink
feat(core): add support for multi use inviations
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Sep 19, 2021
1 parent 52c7897 commit 592d94e
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 12 deletions.
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)
}

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) {
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

0 comments on commit 592d94e

Please sign in to comment.