From 34a6c9f185d7b177956e5e2c5d79408e52915136 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 29 Jan 2024 19:21:43 +0700 Subject: [PATCH] feat: support short legacy connectionless invitations (#1705) Signed-off-by: Timo Glastra --- ...nedUrl.test.ts => parseInvitation.test.ts} | 84 ++++++++++++++++++- packages/core/src/utils/parseInvitation.ts | 49 ++++++----- 2 files changed, 111 insertions(+), 22 deletions(-) rename packages/core/src/utils/__tests__/{shortenedUrl.test.ts => parseInvitation.test.ts} (69%) diff --git a/packages/core/src/utils/__tests__/shortenedUrl.test.ts b/packages/core/src/utils/__tests__/parseInvitation.test.ts similarity index 69% rename from packages/core/src/utils/__tests__/shortenedUrl.test.ts rename to packages/core/src/utils/__tests__/parseInvitation.test.ts index ffe4a2934a..b9966f52cd 100644 --- a/packages/core/src/utils/__tests__/shortenedUrl.test.ts +++ b/packages/core/src/utils/__tests__/parseInvitation.test.ts @@ -1,9 +1,11 @@ +import { agentDependencies } from '../../../tests' import { ConnectionInvitationMessage } from '../../modules/connections' import { InvitationType, OutOfBandInvitation } from '../../modules/oob' import { convertToNewInvitation } from '../../modules/oob/helpers' +import { JsonEncoder } from '../JsonEncoder' import { JsonTransformer } from '../JsonTransformer' import { MessageValidator } from '../MessageValidator' -import { oobInvitationFromShortUrl } from '../parseInvitation' +import { oobInvitationFromShortUrl, parseInvitationShortUrl } from '../parseInvitation' const mockOobInvite = { '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation', @@ -21,6 +23,16 @@ const mockConnectionInvite = { recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], } +const mockLegacyConnectionless = { + '@id': '035b6404-f496-4cb6-a2b5-8bd09e8c92c1', + '@type': 'https://didcomm.org/some-protocol/1.0/some-message', + '~service': { + recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + routingKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + serviceEndpoint: 'https://example.com/endpoint', + }, +} + const header = new Headers() const dummyHeader = new Headers() @@ -49,6 +61,13 @@ const mockedResponseOobUrl = { dummyHeader.forEach(mockedResponseOobUrl.headers.append) +const mockedLegacyConnectionlessInvitationJson = { + status: 200, + ok: true, + json: async () => mockLegacyConnectionless, + headers: header, +} as Response + const mockedResponseConnectionJson = { status: 200, ok: true, @@ -103,15 +122,78 @@ describe('shortened urls resolving to oob invitations', () => { }) }) +describe('legacy connectionless', () => { + test('parse url containing d_m ', async () => { + const parsed = await parseInvitationShortUrl( + `https://example.com?d_m=${JsonEncoder.toBase64URL(mockLegacyConnectionless)}`, + agentDependencies + ) + expect(parsed.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + label: undefined, + 'requests~attach': [ + { + '@id': expect.any(String), + data: { + base64: + 'eyJAaWQiOiIwMzViNjQwNC1mNDk2LTRjYjYtYTJiNS04YmQwOWU4YzkyYzEiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvc29tZS1wcm90b2NvbC8xLjAvc29tZS1tZXNzYWdlIn0=', + }, + 'mime-type': 'application/json', + }, + ], + services: [ + { + id: expect.any(String), + recipientKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + routingKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + ], + }) + }) + + test('parse short url returning legacy connectionless invitation to out of band invitation', async () => { + const parsed = await oobInvitationFromShortUrl(mockedLegacyConnectionlessInvitationJson) + expect(parsed.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + label: undefined, + 'requests~attach': [ + { + '@id': expect.any(String), + data: { + base64: + 'eyJAaWQiOiIwMzViNjQwNC1mNDk2LTRjYjYtYTJiNS04YmQwOWU4YzkyYzEiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvc29tZS1wcm90b2NvbC8xLjAvc29tZS1tZXNzYWdlIn0=', + }, + 'mime-type': 'application/json', + }, + ], + services: [ + { + id: expect.any(String), + recipientKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + routingKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + ], + }) + }) +}) + describe('shortened urls resolving to connection invitations', () => { test('Resolve a mocked response in the form of a connection invitation as a json object', async () => { const short = await oobInvitationFromShortUrl(mockedResponseConnectionJson) expect(short).toEqual(connectionInvitationToNew) }) + test('Resolve a mocked Response in the form of a connection invitation encoded in an url c_i query parameter', async () => { const short = await oobInvitationFromShortUrl(mockedResponseConnectionUrl) expect(short).toEqual(connectionInvitationToNew) }) + test('Resolve a mocked Response in the form of a connection invitation encoded in an url oob query parameter', async () => { const mockedResponseConnectionInOobUrl = { status: 200, diff --git a/packages/core/src/utils/parseInvitation.ts b/packages/core/src/utils/parseInvitation.ts index 3dd3dcc8dc..4b5b5232a8 100644 --- a/packages/core/src/utils/parseInvitation.ts +++ b/packages/core/src/utils/parseInvitation.ts @@ -59,6 +59,9 @@ export const parseInvitationJson = (invitationJson: Record): Ou const outOfBandInvitation = convertToNewInvitation(invitation) outOfBandInvitation.invitationType = InvitationType.Connection return outOfBandInvitation + } else if (invitationJson['~service']) { + // This is probably a legacy connectionless invitation + return transformLegacyConnectionlessInvitationToOutOfBandInvitation(invitationJson) } else { throw new AriesFrameworkError(`Invitation with '@type' ${parsedMessageType.messageTypeUri} not supported.`) } @@ -105,6 +108,30 @@ export const oobInvitationFromShortUrl = async (response: Response): Promise) { + const agentMessage = JsonTransformer.fromJSON(messageJson, AgentMessage) + + // ~service is required for legacy connectionless invitations + if (!agentMessage.service) { + throw new AriesFrameworkError('Invalid legacy connectionless invitation url. Missing ~service decorator.') + } + + // This destructuring removes the ~service property from the message, and + // we can can use messageWithoutService to create the out of band invitation + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { '~service': service, ...messageWithoutService } = messageJson + + // transform into out of band invitation + const invitation = new OutOfBandInvitation({ + services: [OutOfBandDidCommService.fromResolvedDidCommService(agentMessage.service.resolvedDidCommService)], + }) + + invitation.invitationType = InvitationType.Connectionless + invitation.addRequest(JsonTransformer.fromJSON(messageWithoutService, AgentMessage)) + + return invitation +} + /** * Parses URL containing encoded invitation and returns invitation message. Compatible with * parsing short Urls @@ -126,27 +153,7 @@ export const parseInvitationShortUrl = async ( // Legacy connectionless invitation else if (parsedUrl['d_m']) { const messageJson = JsonEncoder.fromBase64(parsedUrl['d_m'] as string) - const agentMessage = JsonTransformer.fromJSON(messageJson, AgentMessage) - - // ~service is required for legacy connectionless invitations - if (!agentMessage.service) { - throw new AriesFrameworkError('Invalid legacy connectionless invitation url. Missing ~service decorator.') - } - - // This destructuring removes the ~service property from the message, and - // we can can use messageWithoutService to create the out of band invitation - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { '~service': service, ...messageWithoutService } = messageJson - - // transform into out of band invitation - const invitation = new OutOfBandInvitation({ - services: [OutOfBandDidCommService.fromResolvedDidCommService(agentMessage.service.resolvedDidCommService)], - }) - - invitation.invitationType = InvitationType.Connectionless - invitation.addRequest(JsonTransformer.fromJSON(messageWithoutService, AgentMessage)) - - return invitation + return transformLegacyConnectionlessInvitationToOutOfBandInvitation(messageJson) } else { try { const outOfBandInvitation = await oobInvitationFromShortUrl(await fetchShortUrl(invitationUrl, dependencies))