diff --git a/packages/core/src/agent/MessageHandlerRegistry.ts b/packages/core/src/agent/MessageHandlerRegistry.ts index 574a9331f3..71a27fa024 100644 --- a/packages/core/src/agent/MessageHandlerRegistry.ts +++ b/packages/core/src/agent/MessageHandlerRegistry.ts @@ -1,9 +1,10 @@ import type { AgentMessage } from './AgentMessage' import type { MessageHandler } from './MessageHandler' +import type { ParsedDidCommProtocolUri } from '../utils/messageType' import { injectable } from 'tsyringe' -import { canHandleMessageType, parseMessageType } from '../utils/messageType' +import { supportsIncomingDidCommProtocolUri, canHandleMessageType, parseMessageType } from '../utils/messageType' @injectable() export class MessageHandlerRegistry { @@ -47,13 +48,24 @@ export class MessageHandlerRegistry { * Returns array of protocol IDs that dispatcher is able to handle. * Protocol ID format is PIURI specified at https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0003-protocols/README.md#piuri. */ - public get supportedProtocols() { - return Array.from(new Set(this.supportedMessageTypes.map((m) => m.protocolUri))) + public get supportedProtocolUris() { + const seenProtocolUris = new Set() + + const protocolUris: ParsedDidCommProtocolUri[] = this.supportedMessageTypes + .filter((m) => { + const has = seenProtocolUris.has(m.protocolUri) + seenProtocolUris.add(m.protocolUri) + return !has + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(({ messageName, messageTypeUri, ...parsedProtocolUri }) => parsedProtocolUri) + + return protocolUris } - public filterSupportedProtocolsByMessageFamilies(messageFamilies: string[]) { - return this.supportedProtocols.filter((protocolId) => - messageFamilies.find((messageFamily) => protocolId.startsWith(messageFamily)) + public filterSupportedProtocolsByProtocolUris(parsedProtocolUris: ParsedDidCommProtocolUri[]) { + return this.supportedProtocolUris.filter((supportedProtocol) => + parsedProtocolUris.some((p) => supportsIncomingDidCommProtocolUri(supportedProtocol, p)) ) } } diff --git a/packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts b/packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts index 64b3946b62..4c20ac1fa2 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 { parseMessageType } from '../../utils/messageType' +import { parseDidCommProtocolUri, parseMessageType } from '../../utils/messageType' import { AgentMessage } from '../AgentMessage' import { MessageHandlerRegistry } from '../MessageHandlerRegistry' @@ -74,36 +74,36 @@ describe('MessageHandlerRegistry', () => { describe('supportedProtocols', () => { test('return all supported message protocols URIs', async () => { - const messageTypes = messageHandlerRegistry.supportedProtocols + const messageTypes = messageHandlerRegistry.supportedProtocolUris expect(messageTypes).toEqual([ - 'https://didcomm.org/connections/1.0', - 'https://didcomm.org/notification/1.0', - 'https://didcomm.org/issue-credential/1.0', - 'https://didcomm.org/fake-protocol/1.5', + parseDidCommProtocolUri('https://didcomm.org/connections/1.0'), + parseDidCommProtocolUri('https://didcomm.org/notification/1.0'), + parseDidCommProtocolUri('https://didcomm.org/issue-credential/1.0'), + parseDidCommProtocolUri('https://didcomm.org/fake-protocol/1.5'), ]) }) }) - describe('filterSupportedProtocolsByMessageFamilies', () => { + describe('filterSupportedProtocolsByProtocolUris', () => { it('should return empty array when input is empty array', async () => { - const supportedProtocols = messageHandlerRegistry.filterSupportedProtocolsByMessageFamilies([]) + const supportedProtocols = messageHandlerRegistry.filterSupportedProtocolsByProtocolUris([]) expect(supportedProtocols).toEqual([]) }) it('should return empty array when input contains only unsupported protocol', async () => { - const supportedProtocols = messageHandlerRegistry.filterSupportedProtocolsByMessageFamilies([ - 'https://didcomm.org/unsupported-protocol/1.0', + const supportedProtocols = messageHandlerRegistry.filterSupportedProtocolsByProtocolUris([ + parseDidCommProtocolUri('https://didcomm.org/unsupported-protocol/1.0'), ]) expect(supportedProtocols).toEqual([]) }) it('should return array with only supported protocol when input contains supported and unsupported protocol', async () => { - const supportedProtocols = messageHandlerRegistry.filterSupportedProtocolsByMessageFamilies([ - 'https://didcomm.org/connections', - 'https://didcomm.org/didexchange', + const supportedProtocols = messageHandlerRegistry.filterSupportedProtocolsByProtocolUris([ + parseDidCommProtocolUri('https://didcomm.org/connections/1.0'), + parseDidCommProtocolUri('https://didcomm.org/didexchange/1.0'), ]) - expect(supportedProtocols).toEqual(['https://didcomm.org/connections/1.0']) + expect(supportedProtocols).toEqual([parseDidCommProtocolUri('https://didcomm.org/connections/1.0')]) }) }) diff --git a/packages/core/src/modules/connections/models/HandshakeProtocol.ts b/packages/core/src/modules/connections/models/HandshakeProtocol.ts index cee69c7fcd..8a084b05bb 100644 --- a/packages/core/src/modules/connections/models/HandshakeProtocol.ts +++ b/packages/core/src/modules/connections/models/HandshakeProtocol.ts @@ -1,4 +1,8 @@ +/** + * Enum values should be sorted based on order of preference. Values will be + * included in this order when creating out of band invitations. + */ export enum HandshakeProtocol { - Connections = 'https://didcomm.org/connections/1.0', - DidExchange = 'https://didcomm.org/didexchange/1.1', + DidExchange = 'https://didcomm.org/didexchange/1.x', + Connections = 'https://didcomm.org/connections/1.x', } diff --git a/packages/core/src/modules/connections/repository/ConnectionRecord.ts b/packages/core/src/modules/connections/repository/ConnectionRecord.ts index 2f0fa23059..f25fa5ee1c 100644 --- a/packages/core/src/modules/connections/repository/ConnectionRecord.ts +++ b/packages/core/src/modules/connections/repository/ConnectionRecord.ts @@ -1,12 +1,13 @@ import type { ConnectionMetadata } from './ConnectionMetadataTypes' import type { TagsBase } from '../../../storage/BaseRecord' -import type { HandshakeProtocol } from '../models' import type { ConnectionType } from '../models/ConnectionType' +import { Transform } from 'class-transformer' + import { AriesFrameworkError } from '../../../error' import { BaseRecord } from '../../../storage/BaseRecord' import { uuid } from '../../../utils/uuid' -import { rfc0160StateFromDidExchangeState, DidExchangeRole, DidExchangeState } from '../models' +import { rfc0160StateFromDidExchangeState, DidExchangeRole, DidExchangeState, HandshakeProtocol } from '../models' export interface ConnectionRecordProps { id?: string @@ -46,10 +47,7 @@ export type DefaultConnectionTags = { previousTheirDids?: Array } -export class ConnectionRecord - extends BaseRecord - implements ConnectionRecordProps -{ +export class ConnectionRecord extends BaseRecord { public state!: DidExchangeState public role!: DidExchangeRole @@ -65,6 +63,18 @@ export class ConnectionRecord public threadId?: string public mediatorId?: string public errorMessage?: string + + // We used to store connection record using major.minor version, but we now + // only store the major version, storing .x for the minor version. We have this + // transformation so we don't have to migrate the data in the database. + @Transform( + ({ value }) => { + if (!value || typeof value !== 'string' || value.endsWith('.x')) return value + return value.split('.').slice(0, -1).join('.') + '.x' + }, + + { toClassOnly: true } + ) public protocol?: HandshakeProtocol public outOfBandId?: string public invitationDid?: string diff --git a/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts b/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts index e052bfc594..a2be2ebf53 100644 --- a/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts +++ b/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts @@ -1,4 +1,5 @@ -import { DidExchangeRole, DidExchangeState } from '../../models' +import { JsonTransformer } from '../../../../utils' +import { DidExchangeRole, DidExchangeState, HandshakeProtocol } from '../../models' import { ConnectionRecord } from '../ConnectionRecord' describe('ConnectionRecord', () => { @@ -30,4 +31,26 @@ describe('ConnectionRecord', () => { }) }) }) + + it('should transform handshake protocol with minor version to .x', () => { + const connectionRecord = JsonTransformer.fromJSON( + { + protocol: 'https://didcomm.org/didexchange/1.0', + }, + ConnectionRecord + ) + + expect(connectionRecord.protocol).toEqual(HandshakeProtocol.DidExchange) + }) + + it('should not transform handshake protocol when minor version is .x', () => { + const connectionRecord = JsonTransformer.fromJSON( + { + protocol: 'https://didcomm.org/didexchange/1.x', + }, + ConnectionRecord + ) + + expect(connectionRecord.protocol).toEqual(HandshakeProtocol.DidExchange) + }) }) diff --git a/packages/core/src/modules/oob/OutOfBandApi.ts b/packages/core/src/modules/oob/OutOfBandApi.ts index 1a0ae3cf4a..38bfeade4c 100644 --- a/packages/core/src/modules/oob/OutOfBandApi.ts +++ b/packages/core/src/modules/oob/OutOfBandApi.ts @@ -21,7 +21,12 @@ import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' import { JsonEncoder, JsonTransformer } from '../../utils' -import { parseMessageType, supportsIncomingMessageType } from '../../utils/messageType' +import { + parseDidCommProtocolUri, + parseMessageType, + supportsIncomingDidCommProtocolUri, + supportsIncomingMessageType, +} from '../../utils/messageType' import { parseInvitationShortUrl } from '../../utils/parseInvitation' import { ConnectionsApi, DidExchangeState, HandshakeProtocol } from '../connections' import { DidCommDocumentService } from '../didcomm' @@ -166,16 +171,17 @@ export class OutOfBandApi { throw new AriesFrameworkError("Attribute 'multiUseInvitation' can not be 'true' when 'messages' is defined.") } - let handshakeProtocols + let handshakeProtocols: string[] | undefined if (handshake) { - // Find supported handshake protocol preserving the order of handshake protocols defined - // by agent + // Assert ALL custom handshake protocols are supported if (customHandshakeProtocols) { - this.assertHandshakeProtocols(customHandshakeProtocols) - handshakeProtocols = customHandshakeProtocols - } else { - handshakeProtocols = this.getSupportedHandshakeProtocols() + this.assertHandshakeProtocolsSupported(customHandshakeProtocols) } + + // Find supported handshake protocol preserving the order of handshake protocols defined by agent or in config + handshakeProtocols = this.getSupportedHandshakeProtocols(customHandshakeProtocols).map( + (p) => p.parsedProtocolUri.protocolUri + ) } const routing = config.routing ?? (await this.routingService.getRouting(this.agentContext, {})) @@ -365,11 +371,15 @@ export class OutOfBandApi { * @returns out-of-band record and connection record if one has been created. */ public async receiveImplicitInvitation(config: ReceiveOutOfBandImplicitInvitationConfig) { + const handshakeProtocols = this.getSupportedHandshakeProtocols( + config.handshakeProtocols ?? [HandshakeProtocol.DidExchange] + ).map((p) => p.parsedProtocolUri.protocolUri) + const invitation = new OutOfBandInvitation({ id: config.did, label: config.label ?? '', services: [config.did], - handshakeProtocols: config.handshakeProtocols ?? [HandshakeProtocol.DidExchange], + handshakeProtocols, }) return this._receiveInvitation(invitation, { ...config, isImplicit: true }) @@ -580,13 +590,13 @@ export class OutOfBandApi { this.logger.debug('Connection does not exist or reuse is disabled. Creating a new connection.') // Find first supported handshake protocol preserving the order of handshake protocols // defined by `handshake_protocols` attribute in the invitation message - const handshakeProtocol = this.getFirstSupportedProtocol(handshakeProtocols) + const firstSupportedProtocol = this.getFirstSupportedProtocol(handshakeProtocols) connectionRecord = await this.connectionsApi.acceptOutOfBandInvitation(outOfBandRecord, { label, alias, imageUrl, autoAcceptConnection, - protocol: handshakeProtocol, + protocol: firstSupportedProtocol.handshakeProtocol, routing, ourDid, }) @@ -699,9 +709,9 @@ export class OutOfBandApi { return this.outOfBandService.deleteById(this.agentContext, outOfBandId) } - private assertHandshakeProtocols(handshakeProtocols: HandshakeProtocol[]) { + private assertHandshakeProtocolsSupported(handshakeProtocols: HandshakeProtocol[]) { if (!this.areHandshakeProtocolsSupported(handshakeProtocols)) { - const supportedProtocols = this.getSupportedHandshakeProtocols() + const supportedProtocols = this.getSupportedHandshakeProtocols().map((p) => p.handshakeProtocol) throw new AriesFrameworkError( `Handshake protocols [${handshakeProtocols}] are not supported. Supported protocols are [${supportedProtocols}]` ) @@ -709,37 +719,71 @@ export class OutOfBandApi { } private areHandshakeProtocolsSupported(handshakeProtocols: HandshakeProtocol[]) { - const supportedProtocols = this.getSupportedHandshakeProtocols() - return handshakeProtocols.every((p) => supportedProtocols.includes(p)) + const supportedProtocols = this.getSupportedHandshakeProtocols(handshakeProtocols) + return supportedProtocols.length === handshakeProtocols.length } - private getSupportedHandshakeProtocols(): HandshakeProtocol[] { - // TODO: update to featureRegistry - const handshakeMessageFamilies = ['https://didcomm.org/didexchange', 'https://didcomm.org/connections'] - const handshakeProtocols = - this.messageHandlerRegistry.filterSupportedProtocolsByMessageFamilies(handshakeMessageFamilies) + private getSupportedHandshakeProtocols(limitToHandshakeProtocols?: HandshakeProtocol[]) { + const allHandshakeProtocols = limitToHandshakeProtocols ?? Object.values(HandshakeProtocol) + + // Replace .x in the handshake protocol with .0 to allow it to be parsed + const parsedHandshakeProtocolUris = allHandshakeProtocols.map((h) => ({ + handshakeProtocol: h, + parsedProtocolUri: parseDidCommProtocolUri(h.replace('.x', '.0')), + })) - if (handshakeProtocols.length === 0) { + // Now find all handshake protocols that start with the protocol uri without minor version '//.' + const supportedHandshakeProtocols = this.messageHandlerRegistry.filterSupportedProtocolsByProtocolUris( + parsedHandshakeProtocolUris.map((p) => p.parsedProtocolUri) + ) + + if (supportedHandshakeProtocols.length === 0) { throw new AriesFrameworkError('There is no handshake protocol supported. Agent can not create a connection.') } - // Order protocols according to `handshakeMessageFamilies` array - const orderedProtocols = handshakeMessageFamilies - .map((messageFamily) => handshakeProtocols.find((p) => p.startsWith(messageFamily))) - .filter((item): item is string => !!item) + // Order protocols according to `parsedHandshakeProtocolUris` array (order of preference) + const orderedProtocols = parsedHandshakeProtocolUris + .map((p) => { + const found = supportedHandshakeProtocols.find((s) => + supportsIncomingDidCommProtocolUri(s, p.parsedProtocolUri) + ) + // We need to override the parsedProtocolUri with the one from the supported protocols, as we used `.0` as the minor + // version before. But when we return it, we want to return the correct minor version that we actually support + return found ? { ...p, parsedProtocolUri: found } : null + }) + .filter((p): p is NonNullable => p !== null) - return orderedProtocols as HandshakeProtocol[] + return orderedProtocols } - private getFirstSupportedProtocol(handshakeProtocols: HandshakeProtocol[]) { + /** + * Get the first supported protocol based on the handshake protocols provided in the out of band + * invitation. + * + * Returns an enum value from {@link HandshakeProtocol} or throw an error if no protocol is supported. + * Minor versions are ignored when selecting a supported protocols, so if the `outOfBandInvitationSupportedProtocolsWithMinorVersion` + * value is `https://didcomm.org/didexchange/1.0` and the agent supports `https://didcomm.org/didexchange/1.1` + * this will be fine, and the returned value will be {@link HandshakeProtocol.DidExchange}. + */ + private getFirstSupportedProtocol(protocolUris: string[]) { const supportedProtocols = this.getSupportedHandshakeProtocols() - const handshakeProtocol = handshakeProtocols.find((p) => supportedProtocols.includes(p)) - if (!handshakeProtocol) { + const parsedProtocolUris = protocolUris.map(parseDidCommProtocolUri) + + const firstSupportedProtocol = supportedProtocols.find((supportedProtocol) => + parsedProtocolUris.find((parsedProtocol) => + supportsIncomingDidCommProtocolUri(supportedProtocol.parsedProtocolUri, parsedProtocol) + ) + ) + + if (!firstSupportedProtocol) { throw new AriesFrameworkError( - `Handshake protocols [${handshakeProtocols}] are not supported. Supported protocols are [${supportedProtocols}]` + `Handshake protocols [${protocolUris}] are not supported. Supported protocols are [${supportedProtocols.map( + (p) => p.handshakeProtocol + )}]` ) } - return handshakeProtocol + + return firstSupportedProtocol } private async findExistingConnection(outOfBandInvitation: OutOfBandInvitation) { diff --git a/packages/core/src/modules/oob/__tests__/connect-to-self.e2e.test.ts b/packages/core/src/modules/oob/__tests__/connect-to-self.e2e.test.ts index 9fca79dff8..6f80f36419 100644 --- a/packages/core/src/modules/oob/__tests__/connect-to-self.e2e.test.ts +++ b/packages/core/src/modules/oob/__tests__/connect-to-self.e2e.test.ts @@ -64,6 +64,30 @@ describe('out of band', () => { expect(senderReceiverConnection).toBeConnectedWith(receiverSenderConnection) }) + test(`make a connection with self using https://didcomm.org/didexchange/1.1 protocol, but invitation using https://didcomm.org/didexchange/1.0`, async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation() + + const { outOfBandInvitation } = outOfBandRecord + outOfBandInvitation.handshakeProtocols = ['https://didcomm.org/didexchange/1.0'] + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + // eslint-disable-next-line prefer-const + let { outOfBandRecord: receivedOutOfBandRecord, connectionRecord: receiverSenderConnection } = + await faberAgent.oob.receiveInvitationFromUrl(urlMessage) + expect(receivedOutOfBandRecord.state).toBe(OutOfBandState.PrepareResponse) + + receiverSenderConnection = await faberAgent.connections.returnWhenIsConnected(receiverSenderConnection!.id) + expect(receiverSenderConnection.state).toBe(DidExchangeState.Completed) + + let [senderReceiverConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + senderReceiverConnection = await faberAgent.connections.returnWhenIsConnected(senderReceiverConnection.id) + expect(senderReceiverConnection.state).toBe(DidExchangeState.Completed) + expect(senderReceiverConnection.protocol).toBe(HandshakeProtocol.DidExchange) + + expect(receiverSenderConnection).toBeConnectedWith(senderReceiverConnection!) + expect(senderReceiverConnection).toBeConnectedWith(receiverSenderConnection) + }) + test(`make a connection with self using ${HandshakeProtocol.Connections} protocol`, async () => { const outOfBandRecord = await faberAgent.oob.createInvitation({ handshakeProtocols: [HandshakeProtocol.Connections], diff --git a/packages/core/src/modules/oob/helpers.ts b/packages/core/src/modules/oob/helpers.ts index be2fe3f0b4..110bbd904c 100644 --- a/packages/core/src/modules/oob/helpers.ts +++ b/packages/core/src/modules/oob/helpers.ts @@ -1,6 +1,6 @@ import type { OutOfBandInvitationOptions } from './messages' -import { ConnectionInvitationMessage, HandshakeProtocol } from '../connections' +import { ConnectionInvitationMessage } from '../connections' import { didKeyToVerkey, verkeyToDidKey } from '../dids/helpers' import { OutOfBandDidCommService } from './domain/OutOfBandDidCommService' @@ -29,7 +29,9 @@ export function convertToNewInvitation(oldInvitation: ConnectionInvitationMessag appendedAttachments: oldInvitation.appendedAttachments, accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], services: [service], - handshakeProtocols: [HandshakeProtocol.Connections], + // NOTE: we hardcode it to 1.0, we won't see support for newer versions of the protocol + // and we also can process 1.0 if we support newer versions + handshakeProtocols: ['https://didcomm.org/connections/1.0'], } const outOfBandInvitation = new OutOfBandInvitation(options) diff --git a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts index d900284329..83d3bdf03f 100644 --- a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts +++ b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts @@ -1,5 +1,4 @@ import type { PlaintextMessage } from '../../../types' -import type { HandshakeProtocol } from '../../connections' import { Exclude, Expose, Transform, TransformationType, Type } from 'class-transformer' import { ArrayNotEmpty, IsArray, IsInstance, IsOptional, IsUrl, ValidateNested } from 'class-validator' @@ -21,7 +20,7 @@ export interface OutOfBandInvitationOptions { goalCode?: string goal?: string accept?: string[] - handshakeProtocols?: HandshakeProtocol[] + handshakeProtocols?: string[] services: Array imageUrl?: string appendedAttachments?: Attachment[] @@ -134,7 +133,7 @@ export class OutOfBandInvitation extends AgentMessage { public readonly accept?: string[] @Transform(({ value }) => value?.map(replaceLegacyDidSovPrefix), { toClassOnly: true }) @Expose({ name: 'handshake_protocols' }) - public handshakeProtocols?: HandshakeProtocol[] + public handshakeProtocols?: string[] @Expose({ name: 'requests~attach' }) @Type(() => Attachment) diff --git a/packages/core/src/utils/__tests__/messageType.test.ts b/packages/core/src/utils/__tests__/messageType.test.ts index 13c818c1c2..904e035eb7 100644 --- a/packages/core/src/utils/__tests__/messageType.test.ts +++ b/packages/core/src/utils/__tests__/messageType.test.ts @@ -1,11 +1,13 @@ import { AgentMessage } from '../../agent/AgentMessage' import { canHandleMessageType, + parseDidCommProtocolUri, parseMessageType, replaceLegacyDidSovPrefix, replaceLegacyDidSovPrefixOnMessage, replaceNewDidCommPrefixWithLegacyDidSov, replaceNewDidCommPrefixWithLegacyDidSovOnMessage, + supportsIncomingDidCommProtocolUri, supportsIncomingMessageType, } from '../messageType' @@ -121,6 +123,103 @@ describe('messageType', () => { messageTypeUri: 'https://didcomm.org/issue-credential/4.5/propose-credential', }) }) + + test('throws error when invalid message type is passed', () => { + expect(() => parseMessageType('https://didcomm.org/connections/1.0/message-type/and-else')).toThrow() + }) + }) + + describe('parseDidCommProtocolUri()', () => { + test('correctly parses the protocol uri', () => { + expect(parseDidCommProtocolUri('https://didcomm.org/connections/1.0')).toEqual({ + documentUri: 'https://didcomm.org', + protocolName: 'connections', + protocolVersion: '1.0', + protocolMajorVersion: 1, + protocolMinorVersion: 0, + protocolUri: 'https://didcomm.org/connections/1.0', + }) + + expect(parseDidCommProtocolUri('https://didcomm.org/issue-credential/4.5')).toEqual({ + documentUri: 'https://didcomm.org', + protocolName: 'issue-credential', + protocolVersion: '4.5', + protocolMajorVersion: 4, + protocolMinorVersion: 5, + protocolUri: `https://didcomm.org/issue-credential/4.5`, + }) + }) + + test('throws error when message type is passed', () => { + expect(() => parseDidCommProtocolUri('https://didcomm.org/connections/1.0/message-type')).toThrow() + }) + }) + + describe('supportsIncomingDidCommProtocolUri()', () => { + test('returns true when the document uri, protocol name, major version all match and the minor version is lower than the expected minor version', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.0') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version all match and the minor version is higher than the expected minor version', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.8') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version and minor version all match', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(true) + }) + + test('returns true when the protocol name, major version and minor version all match and the incoming protocol uri is using the legacy did sov prefix', () => { + const incomingProtocolUri = parseDidCommProtocolUri('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(true) + }) + + test('returns false when the protocol name, major version and minor version all match and the incoming protocol uri is using the legacy did sov prefix but allowLegacyDidSovPrefixMismatch is set to false', () => { + const incomingProtocolUri = parseDidCommProtocolUri('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect( + supportsIncomingDidCommProtocolUri(expectedProtocolUri, incomingProtocolUri, { + allowLegacyDidSovPrefixMismatch: false, + }) + ).toBe(false) + }) + + test('returns false when the major version does not match', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/2.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(false) + + const incomingProtocolUri2 = parseDidCommProtocolUri('https://didcomm.org/connections/2.0') + const expectedProtocolUri2 = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri2, expectedProtocolUri2)).toBe(false) + }) + + test('returns false when the protocol name does not match', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/issue-credential/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(false) + }) + + test('returns false when the document uri does not match', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://my-protocol.org/connections/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(false) + }) }) describe('supportsIncomingMessageType()', () => { diff --git a/packages/core/src/utils/__tests__/string.test.ts b/packages/core/src/utils/__tests__/string.test.ts deleted file mode 100644 index 7bb4121d1a..0000000000 --- a/packages/core/src/utils/__tests__/string.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { rightSplit } from '../string' - -describe('string', () => { - describe('rightSplit', () => { - it('correctly splits a string starting from the right', () => { - const messageType = 'https://didcomm.org/connections/1.0/invitation' - - expect(rightSplit(messageType, '/', 3)).toEqual(['https://didcomm.org', 'connections', '1.0', 'invitation']) - }) - }) -}) diff --git a/packages/core/src/utils/messageType.ts b/packages/core/src/utils/messageType.ts index 7d7232d330..a76903d504 100644 --- a/packages/core/src/utils/messageType.ts +++ b/packages/core/src/utils/messageType.ts @@ -1,20 +1,12 @@ -import type { VersionString } from './version' import type { PlaintextMessage } from '../types' import type { ValidationOptions, ValidationArguments } from 'class-validator' import { ValidateBy, buildMessage } from 'class-validator' -import { rightSplit } from './string' -import { parseVersionString } from './version' - -export interface ParsedMessageType { - /** - * Message name - * - * @example request - */ - messageName: string +const PROTOCOL_URI_REGEX = /^(.+)\/([^/\\]+)\/(\d+).(\d+)$/ +const MESSAGE_TYPE_REGEX = /^(.+)\/([^/\\]+)\/(\d+).(\d+)\/([^/\\]+)$/ +export interface ParsedDidCommProtocolUri { /** * Version of the protocol * @@ -58,6 +50,15 @@ export interface ParsedMessageType { * @example https://didcomm.org/connections/1.0 */ protocolUri: string +} + +export interface ParsedMessageType extends ParsedDidCommProtocolUri { + /** + * Message name + * + * @example request + */ + messageName: string /** * Uri identifier of the message. Includes all parts @@ -68,22 +69,95 @@ export interface ParsedMessageType { messageTypeUri: string } +// TODO: rename to `parseDidCommMessageType` and `DidCommParsedProtocolUri` +// in the future export function parseMessageType(messageType: string): ParsedMessageType { - const [documentUri, protocolName, protocolVersion, messageName] = rightSplit(messageType, '/', 3) - const [protocolMajorVersion, protocolMinorVersion] = parseVersionString(protocolVersion as VersionString) + const match = MESSAGE_TYPE_REGEX.exec(messageType) + + if (!match) { + throw new Error(`Invalid message type: ${messageType}`) + } + + const [, documentUri, protocolName, protocolVersionMajor, protocolVersionMinor, messageName] = match return { documentUri, protocolName, - protocolVersion, - protocolMajorVersion, - protocolMinorVersion, + protocolVersion: `${protocolVersionMajor}.${protocolVersionMinor}`, + protocolMajorVersion: parseInt(protocolVersionMajor), + protocolMinorVersion: parseInt(protocolVersionMinor), messageName, - protocolUri: `${documentUri}/${protocolName}/${protocolVersion}`, + protocolUri: `${documentUri}/${protocolName}/${protocolVersionMajor}.${protocolVersionMinor}`, messageTypeUri: messageType, } } +export function parseDidCommProtocolUri(didCommProtocolUri: string): ParsedDidCommProtocolUri { + const match = PROTOCOL_URI_REGEX.exec(didCommProtocolUri) + + if (!match) { + throw new Error(`Invalid protocol uri: ${didCommProtocolUri}`) + } + + const [, documentUri, protocolName, protocolVersionMajor, protocolVersionMinor] = match + + return { + documentUri, + protocolName, + protocolVersion: `${protocolVersionMajor}.${protocolVersionMinor}`, + protocolMajorVersion: parseInt(protocolVersionMajor), + protocolMinorVersion: parseInt(protocolVersionMinor), + protocolUri: `${documentUri}/${protocolName}/${protocolVersionMajor}.${protocolVersionMinor}`, + } +} + +/** + * Check whether the incoming didcomm protocol uri is a protocol uri that can be handled by comparing it to the expected didcomm protocol uri. + * In this case the expected protocol uri is e.g. the handshake protocol supported (https://didcomm.org/connections/1.0), and the incoming protocol uri + * is the uri that is parsed from the incoming out of band invitation handshake_protocols. + * + * The method will make sure the following fields are equal: + * - documentUri + * - protocolName + * - majorVersion + * + * If allowLegacyDidSovPrefixMismatch is true (default) it will allow for the case where the incoming protocol uri still has the legacy + * did:sov:BzCbsNYhMrjHiqZDTUASHg;spec did prefix, but the expected message type does not. This only works for incoming messages with a prefix + * of did:sov:BzCbsNYhMrjHiqZDTUASHg;spec and the expected message type having a prefix value of https:/didcomm.org + * + * @example + * const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.0') + * const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + * + * // Returns true because the incoming protocol uri is equal to the expected protocol uri, except for + * // the minor version, which is lower + * const isIncomingProtocolUriSupported = supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri) + * + * @example + * const incomingProtocolUri = parseDidCommProtocolUri('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0') + * const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.0') + * + * // Returns true because the incoming protocol uri is equal to the expected protocol uri, except for + * // the legacy did sov prefix. + * const isIncomingProtocolUriSupported = supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri) + */ +export function supportsIncomingDidCommProtocolUri( + incomingProtocolUri: ParsedDidCommProtocolUri, + expectedProtocolUri: ParsedDidCommProtocolUri, + { allowLegacyDidSovPrefixMismatch = true }: { allowLegacyDidSovPrefixMismatch?: boolean } = {} +) { + const incomingDocumentUri = allowLegacyDidSovPrefixMismatch + ? replaceLegacyDidSovPrefix(incomingProtocolUri.documentUri) + : incomingProtocolUri.documentUri + + const documentUriMatches = expectedProtocolUri.documentUri === incomingDocumentUri + const protocolNameMatches = expectedProtocolUri.protocolName === incomingProtocolUri.protocolName + const majorVersionMatches = expectedProtocolUri.protocolMajorVersion === incomingProtocolUri.protocolMajorVersion + + // Everything besides the minor version must match + return documentUriMatches && protocolNameMatches && majorVersionMatches +} + /** * Check whether the incoming message type is a message type that can be handled by comparing it to the expected message type. * In this case the expected message type is e.g. the type declared on an agent message class, and the incoming message type is the type diff --git a/packages/core/src/utils/string.ts b/packages/core/src/utils/string.ts deleted file mode 100644 index bddc1689e1..0000000000 --- a/packages/core/src/utils/string.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function rightSplit(string: string, sep: string, limit: number) { - const split = string.split(sep) - return limit ? [split.slice(0, -limit).join(sep)].concat(split.slice(-limit)) : split -} diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index 15b784e033..c573c73215 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -195,7 +195,7 @@ describe('out of band', () => { const { outOfBandInvitation } = await faberAgent.oob.createInvitation(makeConnectionConfig) // expect supported handshake protocols - expect(outOfBandInvitation.handshakeProtocols).toContain(HandshakeProtocol.DidExchange) + expect(outOfBandInvitation.handshakeProtocols).toContain('https://didcomm.org/didexchange/1.1') expect(outOfBandInvitation.getRequests()).toBeUndefined() // expect contains services @@ -243,7 +243,7 @@ describe('out of band', () => { }) // expect supported handshake protocols - expect(outOfBandInvitation.handshakeProtocols).toContain(HandshakeProtocol.Connections) + expect(outOfBandInvitation.handshakeProtocols).toContain('https://didcomm.org/connections/1.0') expect(outOfBandInvitation.getRequests()).toHaveLength(1) // expect contains services @@ -692,7 +692,7 @@ describe('out of band', () => { await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( new AriesFrameworkError( - `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.1,https://didcomm.org/connections/1.0]` + `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.x,https://didcomm.org/connections/1.x]` ) ) })