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: allow agent without inbound endpoint to connect when using multi-use invitation #712

1 change: 1 addition & 0 deletions packages/core/src/agent/Dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class Dispatcher {
returnRoute: true,
})
} else if (outboundMessage) {
outboundMessage.sessionId = messageContext.sessionId
await this.messageSender.sendMessage(outboundMessage)
}

Expand Down
18 changes: 10 additions & 8 deletions packages/core/src/agent/MessageReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ export class MessageReceiver {

const message = await this.transformAndValidate(plaintextMessage, connection)

const messageContext = new InboundMessageContext(message, {
// Only make the connection available in message context if the connection is ready
// To prevent unwanted usage of unready connections. Connections can still be retrieved from
// Storage if the specific protocol allows an unready connection to be used.
connection: connection?.isReady ? connection : undefined,
senderVerkey: senderKey,
recipientVerkey: recipientKey,
})

// We want to save a session if there is a chance of returning outbound message via inbound transport.
// That can happen when inbound message has `return_route` set to `all` or `thread`.
// If `return_route` defines just `thread`, we decide later whether to use session according to outbound message `threadId`.
Expand All @@ -111,17 +120,10 @@ export class MessageReceiver {
// use return routing to make connections. This is especially useful for creating connections
// with mediators when you don't have a public endpoint yet.
session.connection = connection ?? undefined
messageContext.sessionId = session.id
this.transportService.saveSession(session)
}

const messageContext = new InboundMessageContext(message, {
// Only make the connection available in message context if the connection is ready
// To prevent unwanted usage of unready connections. Connections can still be retrieved from
// Storage if the specific protocol allows an unready connection to be used.
connection: connection?.isReady ? connection : undefined,
senderVerkey: senderKey,
recipientVerkey: recipientKey,
})
await this.dispatcher.dispatch(messageContext)
}

Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/agent/MessageSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,24 @@ export class MessageSender {
transportPriority?: TransportPriorityOptions
}
) {
const { connection, payload } = outboundMessage
const { connection, payload, sessionId } = outboundMessage
const errors: Error[] = []

this.logger.debug('Send outbound message', {
message: payload,
connectionId: connection.id,
})

let session: TransportSession | undefined

if (sessionId) {
session = this.transportService.findSessionById(sessionId)
}
if (!session) {
session = this.transportService.findSessionByConnectionId(connection.id)
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be done nicely in a ternary expression instead of a let with if/else

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 tried that, but that won't work in this case because I want to find as session by id if the session id is present, and if there is no session form that first step then I want to find the session by connection id.

So it could be that both methods are called, and with a ternerary only one of the two will be called

}

// Try to send to already open session
const session = this.transportService.findSessionByConnectionId(connection.id)
if (session?.inboundMessage?.hasReturnRouting(payload.threadId)) {
this.logger.debug(`Found session with return routing for message '${payload.id}' (connection '${connection.id}'`)
try {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/agent/TransportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class TransportService {
}

public findSessionByConnectionId(connectionId: string) {
return Object.values(this.transportSessionTable).find((session) => session.connection?.id === connectionId)
return Object.values(this.transportSessionTable).find((session) => session?.connection?.id === connectionId)
}

public hasInboundEndpoint(didDoc: DidDoc): boolean {
Expand Down Expand Up @@ -57,7 +57,7 @@ export class TransportService {
}

interface TransportSessionTable {
[sessionId: string]: TransportSession
[sessionId: string]: TransportSession | undefined
}

export interface TransportSession {
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/agent/__tests__/MessageSender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('MessageSender', () => {

const transportService = new TransportService()
const transportServiceFindSessionMock = mockFunction(transportService.findSessionByConnectionId)
const transportServiceFindSessionByIdMock = mockFunction(transportService.findSessionById)
const transportServiceHasInboundEndpoint = mockFunction(transportService.hasInboundEndpoint)

const firstDidCommService = new DidCommService({
Expand Down Expand Up @@ -219,6 +220,21 @@ describe('MessageSender', () => {
expect(sendMessageSpy).toHaveBeenCalledTimes(1)
})

test('call send message on session when outbound message has sessionId attached', async () => {
transportServiceFindSessionByIdMock.mockReturnValue(session)
messageSender.registerOutboundTransport(outboundTransport)
const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage')
const sendMessageToServiceSpy = jest.spyOn(messageSender, 'sendMessageToService')

await messageSender.sendMessage({ ...outboundMessage, sessionId: 'session-123' })

expect(session.send).toHaveBeenCalledTimes(1)
expect(session.send).toHaveBeenNthCalledWith(1, encryptedMessage)
expect(sendMessageSpy).toHaveBeenCalledTimes(0)
expect(sendMessageToServiceSpy).toHaveBeenCalledTimes(0)
expect(transportServiceFindSessionByIdMock).toHaveBeenCalledWith('session-123')
})

test('call send message on session when there is a session for a given connection', async () => {
messageSender.registerOutboundTransport(outboundTransport)
const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage')
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/agent/models/InboundMessageContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ export interface MessageContextParams {
connection?: ConnectionRecord
senderVerkey?: string
recipientVerkey?: string
sessionId?: string
}

export class InboundMessageContext<T extends AgentMessage = AgentMessage> {
public message: T
public connection?: ConnectionRecord
public senderVerkey?: string
public recipientVerkey?: string
public sessionId?: string

public constructor(message: T, context: MessageContextParams = {}) {
this.message = message
this.recipientVerkey = context.recipientVerkey
this.senderVerkey = context.senderVerkey
this.connection = context.connection
this.sessionId = context.sessionId
}

/**
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/modules/routing/__tests__/mediation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('mediator establishment', () => {

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

Expand All @@ -75,7 +75,7 @@ describe('mediator establishment', () => {
},
recipientConfig.agentDependencies
)
recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(recipientMessages, subjectMap))
recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages))
await recipientAgent.initialize()

Expand All @@ -98,7 +98,7 @@ describe('mediator establishment', () => {

// Initialize sender agent
senderAgent = new Agent(senderConfig.config, senderConfig.agentDependencies)
senderAgent.registerOutboundTransport(new SubjectOutboundTransport(senderMessages, subjectMap))
senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages))
await senderAgent.initialize()

Expand Down Expand Up @@ -155,7 +155,7 @@ describe('mediator establishment', () => {

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

Expand All @@ -175,7 +175,7 @@ describe('mediator establishment', () => {
},
recipientConfig.agentDependencies
)
recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(recipientMessages, subjectMap))
recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages))
await recipientAgent.initialize()

Expand Down Expand Up @@ -206,13 +206,13 @@ describe('mediator establishment', () => {
},
recipientConfig.agentDependencies
)
recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(recipientMessages, subjectMap))
recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages))
await recipientAgent.initialize()

// Initialize sender agent
senderAgent = new Agent(senderConfig.config, senderConfig.agentDependencies)
senderAgent.registerOutboundTransport(new SubjectOutboundTransport(senderMessages, subjectMap))
senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages))
await senderAgent.initialize()

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export interface DecryptedMessageContext {
export interface OutboundMessage<T extends AgentMessage = AgentMessage> {
payload: T
connection: ConnectionRecord
sessionId?: string
}

export interface OutboundServiceMessage<T extends AgentMessage = AgentMessage> {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/tests/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ describe('agents', () => {

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

bobAgent = new Agent(bobConfig.config, bobConfig.agentDependencies)
bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages))
bobAgent.registerOutboundTransport(new SubjectOutboundTransport(bobMessages, subjectMap))
bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
await bobAgent.initialize()

const aliceConnectionAtAliceBob = await aliceAgent.connections.createConnection()
Expand Down
4 changes: 2 additions & 2 deletions packages/core/tests/connectionless-credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ describe('credentials', () => {
}
faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies)
faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages))
faberAgent.registerOutboundTransport(new SubjectOutboundTransport(aliceMessages, subjectMap))
faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
await faberAgent.initialize()

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

const { definition } = await prepareForIssuance(faberAgent, ['name', 'age'])
Expand Down
6 changes: 3 additions & 3 deletions packages/core/tests/connectionless-proofs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ describe('Present Proof', () => {

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

Expand All @@ -218,12 +218,12 @@ describe('Present Proof', () => {
})

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

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

Expand Down
89 changes: 72 additions & 17 deletions packages/core/tests/connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ import { Agent } from '../src/agent/Agent'

import { getBaseConfig } from './helpers'

const faberConfig = getBaseConfig('Faber Agent Connections', {
endpoints: ['rxjs:faber'],
})
const aliceConfig = getBaseConfig('Alice Agent Connections', {
endpoints: ['rxjs:alice'],
})

describe('connections', () => {
let faberAgent: Agent
let aliceAgent: Agent

beforeAll(async () => {
afterEach(async () => {
await faberAgent.shutdown()
await faberAgent.wallet.delete()
await aliceAgent.shutdown()
await aliceAgent.wallet.delete()
})

it('should be able to make multiple connections using a multi use invite', async () => {
const faberConfig = getBaseConfig('Faber Agent Connections', {
endpoints: ['rxjs:faber'],
})
const aliceConfig = getBaseConfig('Alice Agent Connections', {
endpoints: ['rxjs:alice'],
})

const faberMessages = new Subject<SubjectMessage>()
const aliceMessages = new Subject<SubjectMessage>()
const subjectMap = {
Expand All @@ -30,23 +37,71 @@ describe('connections', () => {

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

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

afterAll(async () => {
await faberAgent.shutdown()
await faberAgent.wallet.delete()
await aliceAgent.shutdown()
await aliceAgent.wallet.delete()
const {
invitation,
connectionRecord: { id: faberConnectionId },
} = await faberAgent.connections.createConnection({
multiUseInvitation: true,
})

const invitationUrl = invitation.toUrl({ domain: 'https://example.com' })

// Create first connection
let aliceFaberConnection1 = await aliceAgent.connections.receiveInvitationFromUrl(invitationUrl)
aliceFaberConnection1 = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection1.id)
expect(aliceFaberConnection1.state).toBe(ConnectionState.Complete)

// Create second connection
let aliceFaberConnection2 = await aliceAgent.connections.receiveInvitationFromUrl(invitationUrl)
aliceFaberConnection2 = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection2.id)
expect(aliceFaberConnection2.state).toBe(ConnectionState.Complete)

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let faberAliceConnection1 = await faberAgent.connections.getByThreadId(aliceFaberConnection1.threadId!)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let faberAliceConnection2 = await faberAgent.connections.getByThreadId(aliceFaberConnection2.threadId!)

faberAliceConnection1 = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection1.id)
faberAliceConnection2 = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection2.id)

expect(faberAliceConnection1).toBeConnectedWith(aliceFaberConnection1)
expect(faberAliceConnection2).toBeConnectedWith(aliceFaberConnection2)

const faberConnection = await faberAgent.connections.getById(faberConnectionId)
// Expect initial connection to still be in state invited
return expect(faberConnection.state).toBe(ConnectionState.Invited)
})

it('should be able to make multiple connections using a multi use invite', async () => {
it('create multiple connections with multi use invite without inbound transport', async () => {
const faberMessages = new Subject<SubjectMessage>()
const subjectMap = {
'rxjs:faber': faberMessages,
}

const faberConfig = getBaseConfig('Faber Agent Connections 2', {
endpoints: ['rxjs:faber'],
})
const aliceConfig = getBaseConfig('Alice Agent Connections 2')

// Faber defines both inbound and outbound transports
faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies)
faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages))
faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
await faberAgent.initialize()

// Alice only has outbound transport
aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies)
aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
await aliceAgent.initialize()

const {
invitation,
connectionRecord: { id: faberConnectionId },
Expand Down
Loading