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: Discover Features V2 over DIDComm V2 #1576

Open
wants to merge 8 commits into
base: feat/didcomm-v2
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion packages/core/src/agent/AgentBaseMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { DidCommMessageVersion, PlaintextMessage } from '../didcomm/types'

export interface AgentBaseMessage {
readonly type: string
readonly didCommVersion: DidCommMessageVersion

get didCommVersion(): DidCommMessageVersion
get id(): string
get threadId(): string | undefined

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/agent/Dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Dispatcher {

public async dispatch(messageContext: InboundMessageContext): Promise<void> {
const { agentContext, connection, senderKey, recipientKey, message } = messageContext
const messageHandler = this.messageHandlerRegistry.getHandlerForMessageType(message.type)
const messageHandler = this.messageHandlerRegistry.getHandlerForMessageType(message.type, message.didCommVersion)

if (!messageHandler) {
throw new AriesFrameworkError(`No handler for message type "${message.type}" found`)
Expand Down
20 changes: 15 additions & 5 deletions packages/core/src/agent/MessageHandlerRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MessageHandler } from './MessageHandler'
import type { ConstructableDidCommMessage } from '../didcomm'
import type { ConstructableDidCommMessage, DidCommMessageVersion } from '../didcomm'

import { injectable } from 'tsyringe'

Expand All @@ -13,22 +13,32 @@ export class MessageHandlerRegistry {
this.messageHandlers.push(messageHandler)
}

public getHandlerForMessageType(messageType: string): MessageHandler | undefined {
public getHandlerForMessageType(
messageType: string,
didcommVersion: DidCommMessageVersion
): MessageHandler | undefined {
const incomingMessageType = parseMessageType(messageType)

for (const handler of this.messageHandlers) {
for (const MessageClass of handler.supportedMessages) {
if (canHandleMessageType(MessageClass, incomingMessageType)) return handler
if (didcommVersion === MessageClass.didCommVersion && canHandleMessageType(MessageClass, incomingMessageType)) {
return handler
}
}
}
}

public getMessageClassForMessageType(messageType: string): ConstructableDidCommMessage | undefined {
public getMessageClassForMessageType(
messageType: string,
didcommVersion: DidCommMessageVersion
): ConstructableDidCommMessage | undefined {
const incomingMessageType = parseMessageType(messageType)

for (const handler of this.messageHandlers) {
for (const MessageClass of handler.supportedMessages) {
if (canHandleMessageType(MessageClass, incomingMessageType)) return MessageClass
if (didcommVersion === MessageClass.didCommVersion && canHandleMessageType(MessageClass, incomingMessageType)) {
return MessageClass
}
}
}
}
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/agent/MessageReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { ConnectionRecord } from '../modules/connections'
import type { InboundTransport } from '../transport'

import { InjectionSymbols } from '../constants'
import { isPlaintextMessageV1, isPlaintextMessageV2 } from '../didcomm'
import { DidCommMessageVersion, isPlaintextMessageV1, isPlaintextMessageV2 } from '../didcomm'
import { getPlaintextMessageType, isEncryptedMessage, isPlaintextMessage } from '../didcomm/helpers'
import { AriesFrameworkError } from '../error'
import { Logger } from '../logger'
Expand Down Expand Up @@ -153,7 +153,9 @@ export class MessageReceiver {
const { plaintextMessage, senderKey, recipientKey } = unpackedMessage

this.logger.info(
`Received message with type '${plaintextMessage['@type']}', recipient key ${recipientKey?.fingerprint} and sender key ${senderKey?.fingerprint}`,
`Received message with type '${plaintextMessage['@type'] ?? plaintextMessage['type']}', recipient key ${
recipientKey?.fingerprint
} and sender key ${senderKey?.fingerprint}`,
plaintextMessage
)

Expand Down Expand Up @@ -300,7 +302,9 @@ export class MessageReceiver {
throw new AriesFrameworkError(`No type found in the message: ${message}`)
}

const MessageClass = this.messageHandlerRegistry.getMessageClassForMessageType(messageType)
const didcommVersion = isPlaintextMessageV1(message) ? DidCommMessageVersion.V1 : DidCommMessageVersion.V2
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make this future proof and have an if / else if / else throw error?


const MessageClass = this.messageHandlerRegistry.getMessageClassForMessageType(messageType, didcommVersion)

if (!MessageClass) {
throw new ProblemReportError(`No message class found for message type "${messageType}"`, {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/agent/MessageSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { PackMessageParams } from './EnvelopeService'
import type { AgentMessageSentEvent } from './Events'
import type { TransportSession } from './TransportService'
import type { AgentContext } from './context'
import type { DidCommV1Message, EncryptedMessage, OutboundPackage } from '../didcomm'
import type { EncryptedMessage, OutboundPackage } from '../didcomm'
import type { ConnectionRecord } from '../modules/connections'
import type { ResolvedDidCommService } from '../modules/didcomm'
import type { DidDocument } from '../modules/dids'
Expand Down Expand Up @@ -217,7 +217,7 @@ export class MessageSender {
}
) {
const { agentContext, connection, outOfBand } = outboundMessageContext
const message = outboundMessageContext.message as DidCommV1Message
const message = outboundMessageContext.message

const errors: Error[] = []

Expand Down Expand Up @@ -321,7 +321,7 @@ export class MessageSender {
const [firstOurAuthenticationKey] = ourAuthenticationKeys
// If the returnRoute is already set we won't override it. This allows to set the returnRoute manually if this is desired.
const shouldAddReturnRoute =
message.transport?.returnRoute === undefined && !this.transportService.hasInboundEndpoint(ourDidDocument)
!message.hasAnyReturnRoute() && !this.transportService.hasInboundEndpoint(ourDidDocument)

// Loop through all available services and try to send the message
for await (const service of services) {
Expand Down
14 changes: 9 additions & 5 deletions packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { MessageHandler } from '../MessageHandler'

import { DidCommV1Message } from '../../didcomm'
import { DidCommMessageVersion, DidCommV1Message } from '../../didcomm'
import { parseMessageType } from '../../utils/messageType'
import { MessageHandlerRegistry } from '../MessageHandlerRegistry'

Expand Down Expand Up @@ -112,28 +112,32 @@ describe('MessageHandlerRegistry', () => {
describe('getMessageClassForMessageType()', () => {
it('should return the correct message class for a registered message type', () => {
const messageClass = messageHandlerRegistry.getMessageClassForMessageType(
'https://didcomm.org/connections/1.0/invitation'
'https://didcomm.org/connections/1.0/invitation',
DidCommMessageVersion.V1
)
expect(messageClass).toBe(ConnectionInvitationTestMessage)
})

it('should return undefined if no message class is registered for the message type', () => {
const messageClass = messageHandlerRegistry.getMessageClassForMessageType(
'https://didcomm.org/non-existing/1.0/invitation'
'https://didcomm.org/non-existing/1.0/invitation',
DidCommMessageVersion.V1
)
expect(messageClass).toBeUndefined()
})

it('should return the message class with a higher minor version for the message type', () => {
const messageClass = messageHandlerRegistry.getMessageClassForMessageType(
'https://didcomm.org/fake-protocol/1.0/message'
'https://didcomm.org/fake-protocol/1.0/message',
DidCommMessageVersion.V1
)
expect(messageClass).toBe(CustomProtocolMessage)
})

it('should not return the message class with a different major version', () => {
const messageClass = messageHandlerRegistry.getMessageClassForMessageType(
'https://didcomm.org/fake-protocol/2.0/message'
'https://didcomm.org/fake-protocol/2.0/message',
DidCommMessageVersion.V1
)
expect(messageClass).toBeUndefined()
})
Expand Down
46 changes: 39 additions & 7 deletions packages/core/src/agent/getOutboundMessageContext.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import type { AgentBaseMessage } from './AgentBaseMessage'
import type { AgentContext } from './context'
import type { DidCommV1Message } from '../didcomm/versions/v1'
import type { ConnectionRecord, Routing } from '../modules/connections'
import type { ResolvedDidCommService } from '../modules/didcomm'
import type { OutOfBandRecord } from '../modules/oob'
import type { BaseRecordAny } from '../storage/BaseRecord'

import { Key } from '../crypto'
import { ServiceDecorator } from '../decorators/service/ServiceDecorator'
import { DidCommMessageVersion, DidCommV1Message, DidCommV2Message } from '../didcomm'
import { AriesFrameworkError } from '../error'
import { OutOfBandService, OutOfBandRole, OutOfBandRepository } from '../modules/oob'
import { OutOfBandRole } from '../modules/oob/domain'
import { OutOfBandService } from '../modules/oob/protocols'
import { OutOfBandRepository } from '../modules/oob/repository'
import { OutOfBandRecordMetadataKeys } from '../modules/oob/repository/outOfBandRecordMetadataTypes'
import { RoutingService } from '../modules/routing'
import { DidCommMessageRepository, DidCommMessageRole } from '../storage'
import { RoutingService } from '../modules/routing/services'
import { DidCommMessageRepository, DidCommMessageRole } from '../storage/didcomm'
import { uuid } from '../utils/uuid'

import { OutboundMessageContext } from './models'
Expand All @@ -37,9 +40,9 @@ export async function getOutboundMessageContext(
}: {
connectionRecord?: ConnectionRecord
associatedRecord?: BaseRecordAny
message: DidCommV1Message
lastReceivedMessage?: DidCommV1Message
lastSentMessage?: DidCommV1Message
message: AgentBaseMessage
lastReceivedMessage?: AgentBaseMessage
lastSentMessage?: AgentBaseMessage
}
) {
// TODO: even if using a connection record, we should check if there's an oob record associated and this
Expand All @@ -48,6 +51,27 @@ export async function getOutboundMessageContext(
agentContext.config.logger.debug(
`Creating outbound message context for message ${message.id} with connection ${connectionRecord.id}`
)

// Check that message DIDComm version matches connection
if (
(connectionRecord.isDidCommV1Connection && message.didCommVersion !== DidCommMessageVersion.V1) ||
(connectionRecord.isDidCommV2Connection && message.didCommVersion !== DidCommMessageVersion.V2)
) {
throw new AriesFrameworkError(
`Message DIDComm version ${message.didCommVersion} does not match connection ${connectionRecord.id}`
)
}
Comment on lines +55 to +63
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. I think we may have to tweak this at some point as the services in a did document dictate which didcomm versions are supported. We only have did:peer connections now, but if we want to make it future proof already, we could also handle this later on, after we've resolved the services for a connection.

Thoughts?


// Attach 'from' and 'to' fields according to connection record (unless they are previously defined)
if (message instanceof DidCommV2Message) {
message.from = message.from ?? connectionRecord.did
const recipients = message.to ?? (connectionRecord.theirDid ? [connectionRecord.theirDid] : undefined)
if (!recipients) {
throw new AriesFrameworkError('Cannot find recipient did for message')
}
message.to = recipients
}
Comment on lines +65 to +73
Copy link
Contributor

Choose a reason for hiding this comment

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

Can there be any danger in having different from/to than the connectionRecord? What would be the use case? I think if you want custom from/to you should probably not pass a connection record to the getOutboundMessageContext message.


return new OutboundMessageContext(message, {
agentContext,
associatedRecord,
Expand All @@ -67,6 +91,14 @@ export async function getOutboundMessageContext(
)
}

if (
!(message instanceof DidCommV1Message) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can invert all these statements?

Suggested change
!(message instanceof DidCommV1Message) ||
message instanceof DidCommV2Message

(lastReceivedMessage !== undefined && !(lastReceivedMessage instanceof DidCommV1Message)) ||
(lastSentMessage !== undefined && !(lastSentMessage instanceof DidCommV1Message))
) {
throw new AriesFrameworkError('No connection record associated with DIDComm V2 messages exchange')
}

// Connectionless
return getConnectionlessOutboundMessageContext(agentContext, {
message,
Expand Down
40 changes: 36 additions & 4 deletions packages/core/src/decorators/attachment/v2/V2Attachment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { JwsDetachedFormat, JwsFlattenedDetachedFormat } from '../../../crypto/JwsTypes'

import { Expose, Type } from 'class-transformer'
import { IsBase64, IsInstance, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator'
import { IsBase64, IsDate, IsInstance, IsInt, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator'

import { Jws } from '../../../crypto/JwsTypes'
import { AriesFrameworkError } from '../../../error'
import { JsonEncoder } from '../../../utils/JsonEncoder'
import { uuid } from '../../../utils/uuid'
Expand All @@ -12,6 +13,8 @@ export interface V2AttachmentOptions {
id?: string
description?: string
filename?: string
format?: string
lastmodTime?: Date
mediaType?: string
byteCount?: number
data: V2AttachmentData
Expand All @@ -21,7 +24,8 @@ export interface V2AttachmentDataOptions {
base64?: string
json?: Record<string, unknown>
links?: string[]
jws?: Jws
jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat
hash?: string
}

/**
Expand Down Expand Up @@ -52,14 +56,22 @@ export class V2AttachmentData {
* A JSON Web Signature over the content of the attachment. Optional.
*/
@IsOptional()
public jws?: Jws
public jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat

/**
* The hash of the content encoded in multi-hash format. Used as an integrity check for the attachment, and MUST be used if the data is referenced via the links data attribute.
*/
@IsOptional()
@IsString()
public hash?: string

public constructor(options: V2AttachmentDataOptions) {
if (options) {
this.base64 = options.base64
this.json = options.json
this.links = options.links
this.jws = options.jws
this.hash = options.hash
}
}
}
Expand All @@ -73,7 +85,10 @@ export class V2Attachment {
if (options) {
this.id = options.id ?? uuid()
this.description = options.description
this.byteCount = options.byteCount
this.filename = options.filename
this.format = options.format
this.lastmodTime = options.lastmodTime
this.mediaType = options.mediaType
this.data = options.data
}
Expand Down Expand Up @@ -110,6 +125,23 @@ export class V2Attachment {
@IsMimeType()
public mediaType?: string

/**
* A hint about when the content in this attachment was last modified.
*/
@Expose({ name: 'lastmod_time' })
@Type(() => Date)
@IsOptional()
@IsDate()
public lastmodTime?: Date

/**
* Optional, and mostly relevant when content is included by reference instead of by value. Lets the receiver guess how expensive it will be, in time, bandwidth, and storage, to fully fetch the attachment.
*/
@Expose({ name: 'byte_count' })
@IsOptional()
@IsInt()
public byteCount?: number

@Type(() => V2AttachmentData)
@ValidateNested()
@IsInstance(V2AttachmentData)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export function ThreadDecorated<T extends DidComV1BaseMessageConstructor>(Base:
return this.thread?.threadId ?? this.id
}

public get parentThreadId(): string | undefined {
return this.thread?.parentThreadId
}

public setThread(options: Partial<ThreadDecorator>) {
this.thread = new ThreadDecorator(options)
}
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/didcomm/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { DidCommMessageVersion } from './types'
import type { DidCommV1Message } from './versions/v1'
import type { DidCommV2Message } from './versions/v2'
import type { ParsedMessageType } from '../utils/messageType'
import type { Constructor } from '../utils/mixins'

export * from './versions/v1'
export * from './versions/v2'
export * from './transformers'
export * from './types'
export * from './helpers'
export * from './JweEnvelope'

export type ConstructableDidCommMessage = Constructor<DidCommV1Message | DidCommV2Message> & { type: ParsedMessageType }
export type ConstructableDidCommMessage = Constructor<DidCommV1Message | DidCommV2Message> & {
type: ParsedMessageType
didCommVersion: DidCommMessageVersion
}
Loading