diff --git a/packages/core/src/agent/AgentBaseMessage.ts b/packages/core/src/agent/AgentBaseMessage.ts index ba92cc782d..e809f449d2 100644 --- a/packages/core/src/agent/AgentBaseMessage.ts +++ b/packages/core/src/agent/AgentBaseMessage.ts @@ -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 diff --git a/packages/core/src/agent/Dispatcher.ts b/packages/core/src/agent/Dispatcher.ts index eb0530bf18..537da1578b 100644 --- a/packages/core/src/agent/Dispatcher.ts +++ b/packages/core/src/agent/Dispatcher.ts @@ -36,7 +36,7 @@ class Dispatcher { public async dispatch(messageContext: InboundMessageContext): Promise { 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`) diff --git a/packages/core/src/agent/MessageHandlerRegistry.ts b/packages/core/src/agent/MessageHandlerRegistry.ts index f050d4ea9e..89a8b2bad5 100644 --- a/packages/core/src/agent/MessageHandlerRegistry.ts +++ b/packages/core/src/agent/MessageHandlerRegistry.ts @@ -1,5 +1,5 @@ import type { MessageHandler } from './MessageHandler' -import type { ConstructableDidCommMessage } from '../didcomm' +import type { ConstructableDidCommMessage, DidCommMessageVersion } from '../didcomm' import { injectable } from 'tsyringe' @@ -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 + } } } } diff --git a/packages/core/src/agent/MessageReceiver.ts b/packages/core/src/agent/MessageReceiver.ts index d177ddee16..68732713d5 100644 --- a/packages/core/src/agent/MessageReceiver.ts +++ b/packages/core/src/agent/MessageReceiver.ts @@ -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' @@ -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 ) @@ -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 + + const MessageClass = this.messageHandlerRegistry.getMessageClassForMessageType(messageType, didcommVersion) if (!MessageClass) { throw new ProblemReportError(`No message class found for message type "${messageType}"`, { diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index 13433e1c0a..2f2365fc66 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -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' @@ -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[] = [] @@ -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) { diff --git a/packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts b/packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts index 6af8ff4292..4db1a5171a 100644 --- a/packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts +++ b/packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts @@ -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' @@ -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() }) diff --git a/packages/core/src/agent/getOutboundMessageContext.ts b/packages/core/src/agent/getOutboundMessageContext.ts index ea57257da8..40e4118451 100644 --- a/packages/core/src/agent/getOutboundMessageContext.ts +++ b/packages/core/src/agent/getOutboundMessageContext.ts @@ -1,5 +1,5 @@ +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' @@ -7,11 +7,14 @@ 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' @@ -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 @@ -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}` + ) + } + + // 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 + } + return new OutboundMessageContext(message, { agentContext, associatedRecord, @@ -67,6 +91,14 @@ export async function getOutboundMessageContext( ) } + if ( + !(message instanceof DidCommV1Message) || + (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, diff --git a/packages/core/src/decorators/attachment/v2/V2Attachment.ts b/packages/core/src/decorators/attachment/v2/V2Attachment.ts index e46aad845c..6b4e4aa89f 100644 --- a/packages/core/src/decorators/attachment/v2/V2Attachment.ts +++ b/packages/core/src/decorators/attachment/v2/V2Attachment.ts @@ -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' @@ -12,6 +13,8 @@ export interface V2AttachmentOptions { id?: string description?: string filename?: string + format?: string + lastmodTime?: Date mediaType?: string byteCount?: number data: V2AttachmentData @@ -21,7 +24,8 @@ export interface V2AttachmentDataOptions { base64?: string json?: Record links?: string[] - jws?: Jws + jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat + hash?: string } /** @@ -52,7 +56,14 @@ 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) { @@ -60,6 +71,7 @@ export class V2AttachmentData { this.json = options.json this.links = options.links this.jws = options.jws + this.hash = options.hash } } } @@ -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 } @@ -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) diff --git a/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts b/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts index 5141e5a051..a7cad5f7a0 100644 --- a/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts +++ b/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts @@ -21,6 +21,10 @@ export function ThreadDecorated(Base: return this.thread?.threadId ?? this.id } + public get parentThreadId(): string | undefined { + return this.thread?.parentThreadId + } + public setThread(options: Partial) { this.thread = new ThreadDecorator(options) } diff --git a/packages/core/src/didcomm/index.ts b/packages/core/src/didcomm/index.ts index e74379142a..87485fe332 100644 --- a/packages/core/src/didcomm/index.ts +++ b/packages/core/src/didcomm/index.ts @@ -1,3 +1,4 @@ +import type { DidCommMessageVersion } from './types' import type { DidCommV1Message } from './versions/v1' import type { DidCommV2Message } from './versions/v2' import type { ParsedMessageType } from '../utils/messageType' @@ -5,8 +6,12 @@ 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 & { type: ParsedMessageType } +export type ConstructableDidCommMessage = Constructor & { + type: ParsedMessageType + didCommVersion: DidCommMessageVersion +} diff --git a/packages/core/src/didcomm/transformers.ts b/packages/core/src/didcomm/transformers.ts new file mode 100644 index 0000000000..7e29a728af --- /dev/null +++ b/packages/core/src/didcomm/transformers.ts @@ -0,0 +1,39 @@ +import { Attachment, AttachmentData, V2Attachment, V2AttachmentData } from '../decorators/attachment' + +export function toV2Attachment(v1Attachment: Attachment): V2Attachment { + const { id, description, byteCount, filename, lastmodTime, mimeType, data } = v1Attachment + return new V2Attachment({ + id, + description, + byteCount, + filename, + lastmodTime, + mediaType: mimeType, + data: new V2AttachmentData({ + base64: data.base64, + json: data.json, + jws: data.jws, + links: data.links, + hash: data.sha256, + }), + }) +} + +export function toV1Attachment(v2Attachment: V2Attachment): Attachment { + const { id, description, byteCount, filename, lastmodTime, mediaType, data } = v2Attachment + return new Attachment({ + id, + description, + byteCount, + filename, + lastmodTime, + mimeType: mediaType, + data: new AttachmentData({ + base64: data.base64, + json: data.json, + jws: data.jws, + links: data.links, + sha256: data.hash, + }), + }) +} diff --git a/packages/core/src/didcomm/versions/v1/DidCommV1Message.ts b/packages/core/src/didcomm/versions/v1/DidCommV1Message.ts index e5e9ae3651..4361748a9c 100644 --- a/packages/core/src/didcomm/versions/v1/DidCommV1Message.ts +++ b/packages/core/src/didcomm/versions/v1/DidCommV1Message.ts @@ -33,9 +33,9 @@ export class DidCommV1Message extends Decorated implements AgentBaseMessage { @Exclude() public readonly allowDidSovPrefix: boolean = false - public get didCommVersion(): DidCommMessageVersion { - return DidCommMessageVersion.V1 - } + @Exclude() + public readonly didCommVersion = DidCommMessageVersion.V1 + public static readonly didCommVersion = DidCommMessageVersion.V1 public serviceDecorator(): ServiceDecorator | undefined { return this.service diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts index 4e17749e91..a49a97f9e2 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts @@ -17,6 +17,8 @@ export type DidCommV2MessageParams = { to?: string | string[] thid?: string parentThreadId?: string + senderOrder?: number + receivedOrders?: { [key: string]: number } // TODO: Update to DIDComm V2 format createdTime?: number expiresTime?: number fromPrior?: string @@ -24,6 +26,12 @@ export type DidCommV2MessageParams = { body?: unknown } +type DidCommV2ReceiverOrder = { + id: string + last: number + gaps: number[] +} + export class DidCommV2BaseMessage { @Matches(MessageIdRegExp) public id!: string @@ -63,6 +71,16 @@ export class DidCommV2BaseMessage { @IsOptional() public parentThreadId?: string + @Expose({ name: 'sender_order' }) + @IsNumber() + @IsOptional() + public senderOrder?: number + + @Expose({ name: 'received_orders' }) + @IsOptional() + @IsArray() + public receivedOrders?: DidCommV2ReceiverOrder[] + @Expose({ name: 'from_prior' }) @IsString() @IsOptional() @@ -86,6 +104,12 @@ export class DidCommV2BaseMessage { this.to = typeof options.to === 'string' ? [options.to] : options.to this.thid = options.thid this.parentThreadId = options.parentThreadId + this.senderOrder = options.senderOrder + this.receivedOrders = Object.entries(options.receivedOrders ?? {}).map(([id, last]) => ({ + id, + last, + gaps: [], + })) this.createdTime = options.createdTime this.expiresTime = options.expiresTime this.fromPrior = options.fromPrior diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts index a45f45da6f..1b0959a0f7 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts @@ -1,8 +1,11 @@ import type { PlaintextDidCommV2Message } from './types' import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' import type { ServiceDecorator } from '../../../decorators/service/ServiceDecorator' +import type { ThreadDecorator } from '../../../decorators/thread/ThreadDecorator' import type { PlaintextMessage } from '../../types' +import { Exclude } from 'class-transformer' + import { AriesFrameworkError } from '../../../error' import { JsonTransformer } from '../../../utils/JsonTransformer' import { DidCommMessageVersion } from '../../types' @@ -10,9 +13,9 @@ import { DidCommMessageVersion } from '../../types' import { DidCommV2BaseMessage } from './DidCommV2BaseMessage' export class DidCommV2Message extends DidCommV2BaseMessage implements AgentBaseMessage { - public get didCommVersion(): DidCommMessageVersion { - return DidCommMessageVersion.V2 - } + @Exclude() + public readonly didCommVersion = DidCommMessageVersion.V2 + public static readonly didCommVersion = DidCommMessageVersion.V2 public toJSON(): PlaintextMessage { return JsonTransformer.toJSON(this) as PlaintextDidCommV2Message @@ -22,8 +25,19 @@ export class DidCommV2Message extends DidCommV2BaseMessage implements AgentBaseM return undefined } - public get threadId(): string | undefined { - return this.thid + public get threadId(): string { + return this.thid ?? this.id + } + + public setThread(options: Partial) { + this.thid = options.threadId + this.parentThreadId = options.parentThreadId + this.senderOrder = options.senderOrder + this.receivedOrders = Object.entries(options.receivedOrders ?? {}).map(([id, last]) => ({ + id, + last, + gaps: [], + })) } public hasAnyReturnRoute() { diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts index 3d074f9d18..9ba149b7a4 100644 --- a/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts @@ -13,7 +13,7 @@ import { catchError, filter, map, takeUntil, timeout } from 'rxjs/operators' import { AgentContext } from '../../agent' import { EventEmitter } from '../../agent/EventEmitter' import { MessageSender } from '../../agent/MessageSender' -import { OutboundMessageContext } from '../../agent/models' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' import { InjectionSymbols } from '../../constants' import { AriesFrameworkError } from '../../error' import { inject, injectable } from '../../plugins' @@ -96,17 +96,15 @@ export class DiscoverFeaturesApi< public async queryFeatures(options: QueryFeaturesOptions) { const service = this.getService(options.protocolVersion) - const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) - const { message: queryMessage } = await service.createQuery({ + const { message } = await service.createQuery({ + connectionRecord, queries: options.queries, comment: options.comment, }) - const outboundMessageContext = new OutboundMessageContext(queryMessage, { - agentContext: this.agentContext, - connection, - }) + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, connectionRecord }) const replaySubject = new ReplaySubject(1) if (options.awaitDisclosures) { @@ -117,7 +115,7 @@ export class DiscoverFeaturesApi< // Stop when the agent shuts down takeUntil(this.stop$), // filter by connection id - filter((e) => e.payload.connection?.id === connection.id), + filter((e) => e.payload.connection?.id === connectionRecord.id), // Return disclosures map((e) => e.payload.disclosures), // If we don't have an answer in timeoutMs miliseconds (no response, not supported, etc...) error @@ -148,16 +146,14 @@ export class DiscoverFeaturesApi< public async discloseFeatures(options: DiscloseFeaturesOptions) { const service = this.getService(options.protocolVersion) - const connection = await this.connectionService.getById(this.agentContext, options.connectionId) - const { message: disclosuresMessage } = await service.createDisclosure({ + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + const { message } = await service.createDisclosure({ + connectionRecord, disclosureQueries: options.disclosureQueries, threadId: options.threadId, }) - const outboundMessageContext = new OutboundMessageContext(disclosuresMessage, { - agentContext: this.agentContext, - connection, - }) + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { message, connectionRecord }) await this.messageSender.sendMessage(outboundMessageContext) } } diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesEvents.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesEvents.ts index 36cea8a6a2..a80f6f0a04 100644 --- a/packages/core/src/modules/discover-features/DiscoverFeaturesEvents.ts +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesEvents.ts @@ -1,6 +1,6 @@ import type { BaseEvent } from '../../agent/Events' import type { Feature, FeatureQueryOptions } from '../../agent/models' -import type { DidCommV1Message } from '../../didcomm' +import type { DidCommV1Message, DidCommV2Message } from '../../didcomm' import type { ConnectionRecord } from '../connections' export enum DiscoverFeaturesEventTypes { @@ -11,7 +11,7 @@ export enum DiscoverFeaturesEventTypes { export interface DiscoverFeaturesQueryReceivedEvent extends BaseEvent { type: typeof DiscoverFeaturesEventTypes.QueryReceived payload: { - message: DidCommV1Message + message: DidCommV1Message | DidCommV2Message queries: FeatureQueryOptions[] protocolVersion: string connection: ConnectionRecord @@ -22,7 +22,7 @@ export interface DiscoverFeaturesQueryReceivedEvent extends BaseEvent { export interface DiscoverFeaturesDisclosureReceivedEvent extends BaseEvent { type: typeof DiscoverFeaturesEventTypes.DisclosureReceived payload: { - message: DidCommV1Message + message: DidCommV1Message | DidCommV2Message disclosures: Feature[] protocolVersion: string connection: ConnectionRecord diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesServiceOptions.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesServiceOptions.ts index 04c9e651d8..8d0f477982 100644 --- a/packages/core/src/modules/discover-features/DiscoverFeaturesServiceOptions.ts +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesServiceOptions.ts @@ -1,16 +1,19 @@ +import type { AgentBaseMessage } from '../../agent/AgentBaseMessage' import type { FeatureQueryOptions } from '../../agent/models' -import type { DidCommV1Message } from '../../didcomm' +import type { ConnectionRecord } from '../connections' export interface CreateQueryOptions { + connectionRecord?: ConnectionRecord queries: FeatureQueryOptions[] comment?: string } export interface CreateDisclosureOptions { + connectionRecord?: ConnectionRecord disclosureQueries: FeatureQueryOptions[] threadId?: string } -export interface DiscoverFeaturesProtocolMsgReturnType { +export interface DiscoverFeaturesProtocolMsgReturnType { message: MessageType } diff --git a/packages/core/src/modules/discover-features/__tests__/v2-discover-features.e2e.test.ts b/packages/core/src/modules/discover-features/__tests__/v2-discover-features.e2e.test.ts index f5a4b9f782..231af67e90 100644 --- a/packages/core/src/modules/discover-features/__tests__/v2-discover-features.e2e.test.ts +++ b/packages/core/src/modules/discover-features/__tests__/v2-discover-features.e2e.test.ts @@ -6,11 +6,13 @@ import type { import { ReplaySubject } from 'rxjs' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { getAskarAnonCredsIndyModules } from '../../../../../anoncreds/tests/legacyAnonCredsSetup' import { setupSubjectTransports } from '../../../../tests' import { getAgentOptions, makeConnection } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { GoalCode, Feature } from '../../../agent/models' +import { DidCommMessageVersion } from '../../../didcomm' +import { OutOfBandVersion } from '../../oob' import { DiscoverFeaturesEventTypes } from '../DiscoverFeaturesEvents' import { waitForDisclosureSubject, waitForQuerySubject } from './helpers' @@ -20,7 +22,7 @@ const faberAgentOptions = getAgentOptions( { endpoints: ['rxjs:faber'], }, - getIndySdkModules() + getAskarAnonCredsIndyModules() ) const aliceAgentOptions = getAgentOptions( @@ -28,206 +30,213 @@ const aliceAgentOptions = getAgentOptions( { endpoints: ['rxjs:alice'], }, - getIndySdkModules() + getAskarAnonCredsIndyModules() ) -describe('v2 discover features', () => { - let faberAgent: Agent - let aliceAgent: Agent - let aliceConnection: ConnectionRecord - let faberConnection: ConnectionRecord - - beforeAll(async () => { - faberAgent = new Agent(faberAgentOptions) - aliceAgent = new Agent(aliceAgentOptions) - setupSubjectTransports([faberAgent, aliceAgent]) - - await faberAgent.initialize() - await aliceAgent.initialize() - ;[faberConnection, aliceConnection] = await makeConnection(faberAgent, aliceAgent) - }) - - afterAll(async () => { - await faberAgent.shutdown() - await faberAgent.wallet.delete() - await aliceAgent.shutdown() - await aliceAgent.wallet.delete() - }) - - test('Faber asks Alice for issue credential protocol support', async () => { - const faberReplay = new ReplaySubject() - const aliceReplay = new ReplaySubject() - - faberAgent.discovery.config.autoAcceptQueries - faberAgent.events - .observable(DiscoverFeaturesEventTypes.DisclosureReceived) - .subscribe(faberReplay) - aliceAgent.events - .observable(DiscoverFeaturesEventTypes.QueryReceived) - .subscribe(aliceReplay) - - await faberAgent.discovery.queryFeatures({ - connectionId: faberConnection.id, - protocolVersion: 'v2', - queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], +describe.each([[DidCommMessageVersion.V1], [DidCommMessageVersion.V2]])( + `v2 discover features - %s`, + (didcommVersion) => { + let faberAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + let faberConnection: ConnectionRecord + + beforeAll(async () => { + faberAgent = new Agent(faberAgentOptions) + aliceAgent = new Agent(aliceAgentOptions) + setupSubjectTransports([faberAgent, aliceAgent]) + + await faberAgent.initialize() + await aliceAgent.initialize() + ;[faberConnection, aliceConnection] = await makeConnection( + faberAgent, + aliceAgent, + didcommVersion === DidCommMessageVersion.V2 ? OutOfBandVersion.V2 : undefined + ) }) - const query = await waitForQuerySubject(aliceReplay, { timeoutMs: 10000 }) - - expect(query).toMatchObject({ - protocolVersion: 'v2', - queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() }) - const disclosure = await waitForDisclosureSubject(faberReplay, { timeoutMs: 10000 }) - - expect(disclosure).toMatchObject({ - protocolVersion: 'v2', - disclosures: [ - { type: 'protocol', id: 'https://didcomm.org/revocation_notification/1.0', roles: ['holder'] }, - { type: 'protocol', id: 'https://didcomm.org/revocation_notification/2.0', roles: ['holder'] }, - ], + test('Faber asks Alice for issue credential protocol support', async () => { + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + faberAgent.discovery.config.autoAcceptQueries + faberAgent.events + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .subscribe(faberReplay) + aliceAgent.events + .observable(DiscoverFeaturesEventTypes.QueryReceived) + .subscribe(aliceReplay) + + await faberAgent.discovery.queryFeatures({ + connectionId: faberConnection.id, + protocolVersion: 'v2', + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + }) + + const query = await waitForQuerySubject(aliceReplay, { timeoutMs: 10000 }) + + expect(query).toMatchObject({ + protocolVersion: 'v2', + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + }) + + const disclosure = await waitForDisclosureSubject(faberReplay, { timeoutMs: 10000 }) + + expect(disclosure).toMatchObject({ + protocolVersion: 'v2', + disclosures: [ + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/1.0', roles: ['holder'] }, + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/2.0', roles: ['holder'] }, + ], + }) }) - }) - - test('Faber defines a supported goal code and Alice queries', async () => { - const faberReplay = new ReplaySubject() - const aliceReplay = new ReplaySubject() - - aliceAgent.events - .observable(DiscoverFeaturesEventTypes.DisclosureReceived) - .subscribe(aliceReplay) - faberAgent.events - .observable(DiscoverFeaturesEventTypes.QueryReceived) - .subscribe(faberReplay) - - // Register some goal codes - faberAgent.features.register(new GoalCode({ id: 'faber.vc.issuance' }), new GoalCode({ id: 'faber.vc.query' })) - - await aliceAgent.discovery.queryFeatures({ - connectionId: aliceConnection.id, - protocolVersion: 'v2', - queries: [{ featureType: 'goal-code', match: '*' }], - }) - - const query = await waitForQuerySubject(faberReplay, { timeoutMs: 10000 }) - expect(query).toMatchObject({ - protocolVersion: 'v2', - queries: [{ featureType: 'goal-code', match: '*' }], + test('Faber defines a supported goal code and Alice queries', async () => { + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + aliceAgent.events + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .subscribe(aliceReplay) + faberAgent.events + .observable(DiscoverFeaturesEventTypes.QueryReceived) + .subscribe(faberReplay) + + // Register some goal codes + faberAgent.features.register(new GoalCode({ id: 'faber.vc.issuance' }), new GoalCode({ id: 'faber.vc.query' })) + + await aliceAgent.discovery.queryFeatures({ + connectionId: aliceConnection.id, + protocolVersion: 'v2', + queries: [{ featureType: 'goal-code', match: '*' }], + }) + + const query = await waitForQuerySubject(faberReplay, { timeoutMs: 10000 }) + + expect(query).toMatchObject({ + protocolVersion: 'v2', + queries: [{ featureType: 'goal-code', match: '*' }], + }) + + const disclosure = await waitForDisclosureSubject(aliceReplay, { timeoutMs: 10000 }) + + expect(disclosure).toMatchObject({ + protocolVersion: 'v2', + disclosures: [ + { type: 'goal-code', id: 'faber.vc.issuance' }, + { type: 'goal-code', id: 'faber.vc.query' }, + ], + }) }) - const disclosure = await waitForDisclosureSubject(aliceReplay, { timeoutMs: 10000 }) - - expect(disclosure).toMatchObject({ - protocolVersion: 'v2', - disclosures: [ - { type: 'goal-code', id: 'faber.vc.issuance' }, - { type: 'goal-code', id: 'faber.vc.query' }, - ], - }) - }) - - test('Faber defines a custom feature and Alice queries', async () => { - const faberReplay = new ReplaySubject() - const aliceReplay = new ReplaySubject() - - aliceAgent.events - .observable(DiscoverFeaturesEventTypes.DisclosureReceived) - .subscribe(aliceReplay) - faberAgent.events - .observable(DiscoverFeaturesEventTypes.QueryReceived) - .subscribe(faberReplay) - - // Define a custom feature type - class GenericFeature extends Feature { - public 'generic-field'!: string - - public constructor(options: { id: string; genericField: string }) { - super({ id: options.id, type: 'generic' }) - this['generic-field'] = options.genericField + test('Faber defines a custom feature and Alice queries', async () => { + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + aliceAgent.events + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .subscribe(aliceReplay) + faberAgent.events + .observable(DiscoverFeaturesEventTypes.QueryReceived) + .subscribe(faberReplay) + + // Define a custom feature type + class GenericFeature extends Feature { + public 'generic-field'!: string + + public constructor(options: { id: string; genericField: string }) { + super({ id: options.id, type: 'generic' }) + this['generic-field'] = options.genericField + } } - } - - // Register a custom feature - faberAgent.features.register(new GenericFeature({ id: 'custom-feature', genericField: 'custom-field' })) - - await aliceAgent.discovery.queryFeatures({ - connectionId: aliceConnection.id, - protocolVersion: 'v2', - queries: [{ featureType: 'generic', match: 'custom-feature' }], - }) - - const query = await waitForQuerySubject(faberReplay, { timeoutMs: 10000 }) - - expect(query).toMatchObject({ - protocolVersion: 'v2', - queries: [{ featureType: 'generic', match: 'custom-feature' }], - }) - const disclosure = await waitForDisclosureSubject(aliceReplay, { timeoutMs: 10000 }) - - expect(disclosure).toMatchObject({ - protocolVersion: 'v2', - disclosures: [ - { - type: 'generic', - id: 'custom-feature', - 'generic-field': 'custom-field', - }, - ], + // Register a custom feature + faberAgent.features.register(new GenericFeature({ id: 'custom-feature', genericField: 'custom-field' })) + + await aliceAgent.discovery.queryFeatures({ + connectionId: aliceConnection.id, + protocolVersion: 'v2', + queries: [{ featureType: 'generic', match: 'custom-feature' }], + }) + + const query = await waitForQuerySubject(faberReplay, { timeoutMs: 10000 }) + + expect(query).toMatchObject({ + protocolVersion: 'v2', + queries: [{ featureType: 'generic', match: 'custom-feature' }], + }) + + const disclosure = await waitForDisclosureSubject(aliceReplay, { timeoutMs: 10000 }) + + expect(disclosure).toMatchObject({ + protocolVersion: 'v2', + disclosures: [ + { + type: 'generic', + id: 'custom-feature', + 'generic-field': 'custom-field', + }, + ], + }) }) - }) - - test('Faber proactively sends a set of features to Alice', async () => { - const faberReplay = new ReplaySubject() - const aliceReplay = new ReplaySubject() - - aliceAgent.events - .observable(DiscoverFeaturesEventTypes.DisclosureReceived) - .subscribe(aliceReplay) - faberAgent.events - .observable(DiscoverFeaturesEventTypes.QueryReceived) - .subscribe(faberReplay) - - // Register a custom feature - faberAgent.features.register( - new Feature({ id: 'AIP2.0', type: 'aip' }), - new Feature({ id: 'AIP2.0/INDYCRED', type: 'aip' }), - new Feature({ id: 'AIP2.0/MEDIATE', type: 'aip' }) - ) - - await faberAgent.discovery.discloseFeatures({ - connectionId: faberConnection.id, - protocolVersion: 'v2', - disclosureQueries: [{ featureType: 'aip', match: '*' }], - }) - - const disclosure = await waitForDisclosureSubject(aliceReplay, { timeoutMs: 10000 }) - expect(disclosure).toMatchObject({ - protocolVersion: 'v2', - disclosures: [ - { type: 'aip', id: 'AIP2.0' }, - { type: 'aip', id: 'AIP2.0/INDYCRED' }, - { type: 'aip', id: 'AIP2.0/MEDIATE' }, - ], - }) - }) - - test('Faber asks Alice for issue credential protocol support synchronously', async () => { - const matchingFeatures = await faberAgent.discovery.queryFeatures({ - connectionId: faberConnection.id, - protocolVersion: 'v2', - queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], - awaitDisclosures: true, + test('Faber proactively sends a set of features to Alice', async () => { + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + aliceAgent.events + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .subscribe(aliceReplay) + faberAgent.events + .observable(DiscoverFeaturesEventTypes.QueryReceived) + .subscribe(faberReplay) + + // Register a custom feature + faberAgent.features.register( + new Feature({ id: 'AIP2.0', type: 'aip' }), + new Feature({ id: 'AIP2.0/INDYCRED', type: 'aip' }), + new Feature({ id: 'AIP2.0/MEDIATE', type: 'aip' }) + ) + + await faberAgent.discovery.discloseFeatures({ + connectionId: faberConnection.id, + protocolVersion: 'v2', + disclosureQueries: [{ featureType: 'aip', match: '*' }], + }) + + const disclosure = await waitForDisclosureSubject(aliceReplay, { timeoutMs: 10000 }) + + expect(disclosure).toMatchObject({ + protocolVersion: 'v2', + disclosures: [ + { type: 'aip', id: 'AIP2.0' }, + { type: 'aip', id: 'AIP2.0/INDYCRED' }, + { type: 'aip', id: 'AIP2.0/MEDIATE' }, + ], + }) }) - expect(matchingFeatures).toMatchObject({ - features: [ - { type: 'protocol', id: 'https://didcomm.org/revocation_notification/1.0', roles: ['holder'] }, - { type: 'protocol', id: 'https://didcomm.org/revocation_notification/2.0', roles: ['holder'] }, - ], + test('Faber asks Alice for issue credential protocol support synchronously', async () => { + const matchingFeatures = await faberAgent.discovery.queryFeatures({ + connectionId: faberConnection.id, + protocolVersion: 'v2', + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + awaitDisclosures: true, + }) + + expect(matchingFeatures).toMatchObject({ + features: [ + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/1.0', roles: ['holder'] }, + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/2.0', roles: ['holder'] }, + ], + }) }) - }) -}) + } +) diff --git a/packages/core/src/modules/discover-features/protocol/v2/V2DiscoverFeaturesService.ts b/packages/core/src/modules/discover-features/protocol/v2/V2DiscoverFeaturesService.ts index 0196a351c4..4755252443 100644 --- a/packages/core/src/modules/discover-features/protocol/v2/V2DiscoverFeaturesService.ts +++ b/packages/core/src/modules/discover-features/protocol/v2/V2DiscoverFeaturesService.ts @@ -21,6 +21,8 @@ import { DiscoverFeaturesService } from '../../services' import { V2DisclosuresMessageHandler, V2QueriesMessageHandler } from './handlers' import { V2QueriesMessage, V2DisclosuresMessage } from './messages' +import { V2DisclosuresDidCommV2Message } from './messages/V2DisclosuresDidCommV2Message' +import { V2QueriesDidCommV2Message } from './messages/V2QueriesDidCommV2Message' @injectable() export class V2DiscoverFeaturesService extends DiscoverFeaturesService { @@ -47,15 +49,17 @@ export class V2DiscoverFeaturesService extends DiscoverFeaturesService { public async createQuery( options: CreateQueryOptions - ): Promise> { - const queryMessage = new V2QueriesMessage({ queries: options.queries }) + ): Promise> { + const queryMessage = options.connectionRecord?.isDidCommV2Connection + ? new V2QueriesDidCommV2Message({ queries: options.queries }) + : new V2QueriesMessage({ queries: options.queries }) return { message: queryMessage } } public async processQuery( - messageContext: InboundMessageContext - ): Promise | void> { + messageContext: InboundMessageContext + ): Promise | void> { const { queries, threadId } = messageContext.message const connection = messageContext.assertReadyConnection() @@ -71,10 +75,11 @@ export class V2DiscoverFeaturesService extends DiscoverFeaturesService { }, }) - // Process query and send responde automatically if configured to do so, otherwise + // Process query and send respose automatically if configured to do so, otherwise // just send the event and let controller decide if (this.discoverFeaturesModuleConfig.autoAcceptQueries) { return await this.createDisclosure({ + connectionRecord: connection, threadId, disclosureQueries: queries, }) @@ -83,18 +88,25 @@ export class V2DiscoverFeaturesService extends DiscoverFeaturesService { public async createDisclosure( options: CreateDisclosureOptions - ): Promise> { + ): Promise> { const matches = this.featureRegistry.query(...options.disclosureQueries) - const discloseMessage = new V2DisclosuresMessage({ - threadId: options.threadId, - features: matches, - }) + const discloseMessage = options.connectionRecord?.isDidCommV2Connection + ? new V2DisclosuresDidCommV2Message({ + threadId: options.threadId, + features: matches, + }) + : new V2DisclosuresMessage({ + threadId: options.threadId, + features: matches, + }) return { message: discloseMessage } } - public async processDisclosure(messageContext: InboundMessageContext): Promise { + public async processDisclosure( + messageContext: InboundMessageContext + ): Promise { const { disclosures, threadId } = messageContext.message const connection = messageContext.assertReadyConnection() diff --git a/packages/core/src/modules/discover-features/protocol/v2/handlers/V2DisclosuresMessageHandler.ts b/packages/core/src/modules/discover-features/protocol/v2/handlers/V2DisclosuresMessageHandler.ts index 1691e7a5a8..4524ffaa1d 100644 --- a/packages/core/src/modules/discover-features/protocol/v2/handlers/V2DisclosuresMessageHandler.ts +++ b/packages/core/src/modules/discover-features/protocol/v2/handlers/V2DisclosuresMessageHandler.ts @@ -2,10 +2,11 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../. import type { V2DiscoverFeaturesService } from '../V2DiscoverFeaturesService' import { V2DisclosuresMessage } from '../messages' +import { V2DisclosuresDidCommV2Message } from '../messages/V2DisclosuresDidCommV2Message' export class V2DisclosuresMessageHandler implements MessageHandler { private discoverFeaturesService: V2DiscoverFeaturesService - public supportedMessages = [V2DisclosuresMessage] + public supportedMessages = [V2DisclosuresMessage, V2DisclosuresDidCommV2Message] public constructor(discoverFeaturesService: V2DiscoverFeaturesService) { this.discoverFeaturesService = discoverFeaturesService diff --git a/packages/core/src/modules/discover-features/protocol/v2/handlers/V2QueriesMessageHandler.ts b/packages/core/src/modules/discover-features/protocol/v2/handlers/V2QueriesMessageHandler.ts index 45798397be..7269cd9f48 100644 --- a/packages/core/src/modules/discover-features/protocol/v2/handlers/V2QueriesMessageHandler.ts +++ b/packages/core/src/modules/discover-features/protocol/v2/handlers/V2QueriesMessageHandler.ts @@ -1,26 +1,27 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' import type { V2DiscoverFeaturesService } from '../V2DiscoverFeaturesService' -import { OutboundMessageContext } from '../../../../../agent/models' +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' import { V2QueriesMessage } from '../messages' +import { V2QueriesDidCommV2Message } from '../messages/V2QueriesDidCommV2Message' export class V2QueriesMessageHandler implements MessageHandler { private discoverFeaturesService: V2DiscoverFeaturesService - public supportedMessages = [V2QueriesMessage] + public supportedMessages = [V2QueriesMessage, V2QueriesDidCommV2Message] public constructor(discoverFeaturesService: V2DiscoverFeaturesService) { this.discoverFeaturesService = discoverFeaturesService } public async handle(inboundMessage: MessageHandlerInboundMessage) { - const connection = inboundMessage.assertReadyConnection() + const connectionRecord = inboundMessage.assertReadyConnection() const discloseMessage = await this.discoverFeaturesService.processQuery(inboundMessage) if (discloseMessage) { - return new OutboundMessageContext(discloseMessage.message, { - agentContext: inboundMessage.agentContext, - connection, + return getOutboundMessageContext(inboundMessage.agentContext, { + message: discloseMessage.message, + connectionRecord, }) } } diff --git a/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresDidCommV2Message.ts b/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresDidCommV2Message.ts new file mode 100644 index 0000000000..5c2069d342 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresDidCommV2Message.ts @@ -0,0 +1,51 @@ +import type { V2DisclosuresMessageOptions } from './V2DisclosuresMessageOptions' + +import { Type } from 'class-transformer' +import { IsInstance, IsObject, ValidateNested } from 'class-validator' + +import { Feature } from '../../../../../agent/models' +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +class V2DisclosuresMessageBody { + public constructor(options: { disclosures: Feature[] }) { + if (options) { + this.disclosures = options.disclosures + } + } + @IsInstance(Feature, { each: true }) + @Type(() => Feature) + public disclosures!: Feature[] +} + +export class V2DisclosuresDidCommV2Message extends DidCommV2Message { + public readonly allowDidSovPrefix = false + + public constructor(options: V2DisclosuresMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.body = new V2DisclosuresMessageBody({ disclosures: options.features ?? [] }) + + if (options.threadId) { + this.setThread({ + threadId: options.threadId, + }) + } + } + } + + @IsValidMessageType(V2DisclosuresDidCommV2Message.type) + public readonly type = V2DisclosuresDidCommV2Message.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/discover-features/2.0/disclosures') + + @IsObject() + @ValidateNested() + @Type(() => V2DisclosuresMessageBody) + public body!: V2DisclosuresMessageBody + + public get disclosures() { + return this.body.disclosures + } +} diff --git a/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessage.ts b/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessage.ts index 4d3c1fa87f..438e930817 100644 --- a/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessage.ts +++ b/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessage.ts @@ -1,3 +1,5 @@ +import type { V2DisclosuresMessageOptions } from './V2DisclosuresMessageOptions' + import { Type } from 'class-transformer' import { IsInstance } from 'class-validator' @@ -5,12 +7,6 @@ import { Feature } from '../../../../../agent/models' import { DidCommV1Message } from '../../../../../didcomm' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -export interface V2DisclosuresMessageOptions { - id?: string - threadId?: string - features?: Feature[] -} - export class V2DisclosuresMessage extends DidCommV1Message { public constructor(options: V2DisclosuresMessageOptions) { super() diff --git a/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessageOptions.ts b/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessageOptions.ts new file mode 100644 index 0000000000..7098f1024b --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessageOptions.ts @@ -0,0 +1,7 @@ +import type { Feature } from '../../../../../agent/models' + +export interface V2DisclosuresMessageOptions { + id?: string + threadId?: string + features?: Feature[] +} diff --git a/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesDidCommV2Message.ts b/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesDidCommV2Message.ts new file mode 100644 index 0000000000..4ea9c05575 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesDidCommV2Message.ts @@ -0,0 +1,46 @@ +import type { V2QueriesMessageOptions } from './V2QueriesMessageOptions' + +import { Type } from 'class-transformer' +import { ArrayNotEmpty, IsInstance, IsObject, ValidateNested } from 'class-validator' + +import { FeatureQuery } from '../../../../../agent/models' +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +class V2QueriesMessageBody { + public constructor(options: { queries: FeatureQuery[] }) { + if (options) { + this.queries = options.queries + } + } + @IsInstance(FeatureQuery, { each: true }) + @Type(() => FeatureQuery) + @ArrayNotEmpty() + public queries!: FeatureQuery[] +} + +export class V2QueriesDidCommV2Message extends DidCommV2Message { + public readonly allowDidSovPrefix = false + + public constructor(options: V2QueriesMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.body = new V2QueriesMessageBody({ queries: options.queries.map((q) => new FeatureQuery(q)) }) + } + } + + @IsValidMessageType(V2QueriesDidCommV2Message.type) + public readonly type = V2QueriesDidCommV2Message.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/discover-features/2.0/queries') + + @IsObject() + @ValidateNested() + @Type(() => V2QueriesMessageBody) + public body!: V2QueriesMessageBody + + public get queries() { + return this.body.queries + } +} diff --git a/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessage.ts b/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessage.ts index 10faaa51f2..d9bcc84022 100644 --- a/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessage.ts +++ b/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessage.ts @@ -1,4 +1,4 @@ -import type { FeatureQueryOptions } from '../../../../../agent/models' +import type { V2QueriesMessageOptions } from './V2QueriesMessageOptions' import { Type } from 'class-transformer' import { ArrayNotEmpty, IsInstance } from 'class-validator' @@ -7,14 +7,8 @@ import { FeatureQuery } from '../../../../../agent/models' import { DidCommV1Message } from '../../../../../didcomm' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -export interface V2DiscoverFeaturesQueriesMessageOptions { - id?: string - queries: FeatureQueryOptions[] - comment?: string -} - export class V2QueriesMessage extends DidCommV1Message { - public constructor(options: V2DiscoverFeaturesQueriesMessageOptions) { + public constructor(options: V2QueriesMessageOptions) { super() if (options) { diff --git a/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessageOptions.ts b/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessageOptions.ts new file mode 100644 index 0000000000..a534ce4870 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessageOptions.ts @@ -0,0 +1,6 @@ +import type { FeatureQueryOptions } from '../../../../../agent/models' + +export interface V2QueriesMessageOptions { + id?: string + queries: FeatureQueryOptions[] +} diff --git a/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts b/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts index dfe4bd0f32..d00cf1816d 100644 --- a/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts +++ b/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts @@ -1,7 +1,7 @@ import type { EventEmitter } from '../../../agent/EventEmitter' import type { FeatureRegistry } from '../../../agent/FeatureRegistry' import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import type { DidCommV1Message } from '../../../didcomm' +import type { DidCommV1Message, DidCommV2Message } from '../../../didcomm' import type { Logger } from '../../../logger' import type { DiscoverFeaturesModuleConfig } from '../DiscoverFeaturesModuleConfig' import type { @@ -32,13 +32,15 @@ export abstract class DiscoverFeaturesService { public abstract createQuery( options: CreateQueryOptions - ): Promise> + ): Promise> public abstract processQuery( - messageContext: InboundMessageContext - ): Promise | void> + messageContext: InboundMessageContext + ): Promise | void> public abstract createDisclosure( options: CreateDisclosureOptions - ): Promise> - public abstract processDisclosure(messageContext: InboundMessageContext): Promise + ): Promise> + public abstract processDisclosure( + messageContext: InboundMessageContext + ): Promise } diff --git a/packages/core/src/modules/problem-reports/versions/v2/helpers.ts b/packages/core/src/modules/problem-reports/versions/v2/helpers.ts index 1440efd8fb..72d0a3ee6a 100644 --- a/packages/core/src/modules/problem-reports/versions/v2/helpers.ts +++ b/packages/core/src/modules/problem-reports/versions/v2/helpers.ts @@ -2,7 +2,7 @@ import type { PlaintextDidCommV2Message } from '../../../../didcomm' import { ProblemReportReason } from '../../models/ProblemReportReason' -import { ProblemReportMessage } from './messages' +import { V2ProblemReportMessage } from './messages' /** * Build the v2 problem report message to the recipient. @@ -12,11 +12,11 @@ import { ProblemReportMessage } from './messages' export const buildProblemReportV2Message = ( plaintextMessage: PlaintextDidCommV2Message, errorMessage: string -): ProblemReportMessage | undefined => { +): V2ProblemReportMessage | undefined => { // Cannot send problem report for message with unknown sender or recipient if (!plaintextMessage.from || !plaintextMessage.to?.length) return - return new ProblemReportMessage({ + return new V2ProblemReportMessage({ parentThreadId: plaintextMessage.id, from: plaintextMessage.to.length ? plaintextMessage.to[0] : undefined, to: plaintextMessage.from, diff --git a/packages/core/src/modules/problem-reports/versions/v2/index.ts b/packages/core/src/modules/problem-reports/versions/v2/index.ts index f9d434a2f5..efbb9da5dd 100644 --- a/packages/core/src/modules/problem-reports/versions/v2/index.ts +++ b/packages/core/src/modules/problem-reports/versions/v2/index.ts @@ -1,2 +1,2 @@ -export { ProblemReportMessage as V2ProblemReportMessage } from './messages' +export { V2ProblemReportMessage } from './messages' export { buildProblemReportV2Message } from './helpers' diff --git a/packages/core/src/modules/problem-reports/versions/v2/messages/ProblemReportMessage.ts b/packages/core/src/modules/problem-reports/versions/v2/messages/V2ProblemReportMessage.ts similarity index 86% rename from packages/core/src/modules/problem-reports/versions/v2/messages/ProblemReportMessage.ts rename to packages/core/src/modules/problem-reports/versions/v2/messages/V2ProblemReportMessage.ts index 299066f047..3469ff4b3f 100644 --- a/packages/core/src/modules/problem-reports/versions/v2/messages/ProblemReportMessage.ts +++ b/packages/core/src/modules/problem-reports/versions/v2/messages/V2ProblemReportMessage.ts @@ -27,7 +27,7 @@ export class ProblemReportBody { public args?: string[] } -export class ProblemReportMessage extends DidCommV2Message { +export class V2ProblemReportMessage extends DidCommV2Message { public constructor(options?: V2ProblemReportMessageOptions) { super(options) if (options) { @@ -36,8 +36,8 @@ export class ProblemReportMessage extends DidCommV2Message { } } - @IsValidMessageType(ProblemReportMessage.type) - public readonly type = ProblemReportMessage.type.messageTypeUri + @IsValidMessageType(V2ProblemReportMessage.type) + public readonly type = V2ProblemReportMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/report-problem/2.0/problem-report') @Expose({ name: 'body' }) diff --git a/packages/core/src/modules/problem-reports/versions/v2/messages/index.ts b/packages/core/src/modules/problem-reports/versions/v2/messages/index.ts index 57670e5421..0b4cfdcb5d 100644 --- a/packages/core/src/modules/problem-reports/versions/v2/messages/index.ts +++ b/packages/core/src/modules/problem-reports/versions/v2/messages/index.ts @@ -1 +1 @@ -export * from './ProblemReportMessage' +export * from './V2ProblemReportMessage' diff --git a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts index 6e5a60b3b0..7d0a88c555 100644 --- a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts +++ b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts @@ -58,13 +58,13 @@ export class DidCommMessageRecord extends BaseRecord } public getTags() { - const messageId = this.message['@id'] as string - const messageType = this.message['@type'] as string + const messageId = (this.message['@id'] ?? this.message['id']) as string + const messageType = (this.message['@type'] ?? this.message['type']) as string const { protocolName, protocolMajorVersion, protocolMinorVersion, messageName } = parseMessageType(messageType) const thread = this.message['~thread'] - let threadId = messageId + let threadId = (this.message['thid'] ?? messageId) as string if (isJsonObject(thread) && typeof thread.thid === 'string') { threadId = thread.thid @@ -89,7 +89,7 @@ export class DidCommMessageRecord extends BaseRecord public getMessageInstance( messageClass: MessageClass ): InstanceType { - const messageType = parseMessageType(this.message['@type'] as string) + const messageType = parseMessageType((this.message['@type'] ?? this.message['type']) as string) if (!canHandleMessageType(messageClass, messageType)) { throw new AriesFrameworkError('Provided message class type does not match type of stored message') diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 4fe9732007..2ccd901218 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -31,6 +31,7 @@ import { catchError, filter, map, take, timeout } from 'rxjs/operators' import { agentDependencies, IndySdkPostgresWalletScheme } from '../../node/src' import { + OutOfBandVersion, OutOfBandDidCommService, ConnectionsModule, ConnectionEventTypes, @@ -217,7 +218,7 @@ const isCredentialStateChangedEvent = (e: BaseEvent): e is CredentialStateChange const isConnectionStateChangedEvent = (e: BaseEvent): e is ConnectionStateChangedEvent => e.type === ConnectionEventTypes.ConnectionStateChanged const isTrustPingReceivedEvent = (e: BaseEvent): e is TrustPingReceivedEvent => - e.type === TrustPingEventTypes.TrustPingReceivedEvent + e.type === TrustPingEventTypes.TrustPingReceivedEvent || e.type === TrustPingEventTypes.V2TrustPingReceivedEvent const isTrustPingResponseReceivedEvent = (e: BaseEvent): e is TrustPingResponseReceivedEvent => e.type === TrustPingEventTypes.TrustPingResponseReceivedEvent @@ -267,11 +268,16 @@ export function waitForProofExchangeRecordSubject( export async function waitForTrustPingReceivedEvent( agent: Agent, options: { + protocolVersion?: 'v1' | 'v2' threadId?: string timeoutMs?: number } ) { - const observable = agent.events.observable(TrustPingEventTypes.TrustPingReceivedEvent) + const observable = agent.events.observable( + options.protocolVersion === 'v2' + ? TrustPingEventTypes.V2TrustPingReceivedEvent + : TrustPingEventTypes.TrustPingReceivedEvent + ) return waitForTrustPingReceivedEventSubject(observable, options) } @@ -532,20 +538,36 @@ export function getMockOutOfBand({ return outOfBandRecord } -export async function makeConnection(agentA: Agent, agentB: Agent) { - const agentAOutOfBand = await agentA.oob.createInvitation({ - handshakeProtocols: [HandshakeProtocol.Connections], - }) +export async function makeConnection(agentA: Agent, agentB: Agent, version?: OutOfBandVersion) { + if (version === OutOfBandVersion.V2) { + const agentAOutOfBand = await agentA.oob.createInvitation({ + version, + }) - let { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation( - agentAOutOfBand.getOutOfBandInvitation() - ) + const { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation( + agentAOutOfBand.v2OutOfBandInvitation! + ) + if (!agentBConnection) throw new Error('No connection for receiver') + await agentB.connections.sendPing(agentBConnection.id, {}) + await waitForTrustPingReceivedEvent(agentA, { protocolVersion: 'v2', timeoutMs: 4000 }) + const [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) + if (!agentAConnection) throw new Error('No connection for inviter') + return [agentAConnection, agentBConnection] + } else { + const agentAOutOfBand = await agentA.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + let { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation( + agentAOutOfBand.getOutOfBandInvitation() + ) - agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection!.id) - let [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) - agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection!.id) + agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection!.id) + let [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) + agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection!.id) - return [agentAConnection, agentBConnection] + return [agentAConnection, agentBConnection] + } } /**