diff --git a/packages/core/package.json b/packages/core/package.json index 583988d9a3..e2dcaae496 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -49,6 +49,7 @@ "web-did-resolver": "^2.0.8" }, "devDependencies": { + "node-fetch": "^2.0", "@types/bn.js": "^5.1.0", "@types/events": "^3.0.0", "@types/luxon": "^1.27.0", diff --git a/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts b/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts index 95e77a2e64..ffbdf744d5 100644 --- a/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts +++ b/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts @@ -121,22 +121,18 @@ export class ConnectionInvitationMessage extends AgentMessage { * * @param invitationUrl invitation url containing c_i or d_m parameter * - * @throws Error when url can not be decoded to JSON, or decoded message is not a valid `ConnectionInvitationMessage` - * @throws Error when the url does not contain c_i or d_m as parameter + * @throws Error when the url can not be decoded to JSON, or decoded message is not a valid 'ConnectionInvitationMessage' */ public static fromUrl(invitationUrl: string) { const parsedUrl = parseUrl(invitationUrl).query const encodedInvitation = parsedUrl['c_i'] ?? parsedUrl['d_m'] - if (typeof encodedInvitation === 'string') { const invitationJson = JsonEncoder.fromBase64(encodedInvitation) const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage) return invitation } else { - throw new AriesFrameworkError( - 'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters; `c_i` or `d_m`' - ) + throw new AriesFrameworkError('InvitationUrl is invalid. Needs to be encoded with either c_i, d_m, or oob') } } } diff --git a/packages/core/src/modules/oob/OutOfBandModule.ts b/packages/core/src/modules/oob/OutOfBandModule.ts index b38f77c7b1..333af57332 100644 --- a/packages/core/src/modules/oob/OutOfBandModule.ts +++ b/packages/core/src/modules/oob/OutOfBandModule.ts @@ -23,7 +23,7 @@ import { injectable, module } from '../../plugins' import { DidCommMessageRepository, DidCommMessageRole } from '../../storage' import { JsonEncoder, JsonTransformer } from '../../utils' import { parseMessageType, supportsIncomingMessageType } from '../../utils/messageType' -import { parseInvitationUrl } from '../../utils/parseInvitation' +import { parseInvitationUrl, parseInvitationShortUrl } from '../../utils/parseInvitation' import { DidKey } from '../dids' import { didKeyToVerkey } from '../dids/helpers' import { outOfBandServiceToNumAlgo2Did } from '../dids/methods/peer/peerDidNumAlgo2' @@ -271,8 +271,8 @@ export class OutOfBandModule { * @param config configuration of how out-of-band invitation should be processed * @returns out-of-band record and connection record if one has been created */ - public receiveInvitationFromUrl(invitationUrl: string, config: ReceiveOutOfBandInvitationConfig = {}) { - const message = this.parseInvitation(invitationUrl) + public async receiveInvitationFromUrl(invitationUrl: string, config: ReceiveOutOfBandInvitationConfig = {}) { + const message = await this.parseInvitationShortUrl(invitationUrl) return this.receiveInvitation(message, config) } @@ -287,6 +287,18 @@ export class OutOfBandModule { return parseInvitationUrl(invitationUrl) } + /** + * Parses URL containing encoded invitation and returns invitation message. Compatible with + * parsing shortened URLs + * + * @param invitationUrl URL containing encoded invitation + * + * @returns OutOfBandInvitation + */ + public async parseInvitationShortUrl(invitation: string): Promise { + return await parseInvitationShortUrl(invitation, this.agentConfig.agentDependencies) + } + /** * Creates inbound out-of-band record and assigns out-of-band invitation message to it if the * message is valid. It automatically passes out-of-band invitation for further processing to diff --git a/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts b/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts index d6fbb79137..c663265a9a 100644 --- a/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts +++ b/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts @@ -5,260 +5,258 @@ import { JsonEncoder } from '../../../utils/JsonEncoder' import { JsonTransformer } from '../../../utils/JsonTransformer' import { OutOfBandInvitation } from '../messages/OutOfBandInvitation' -describe('OutOfBandInvitation', () => { - describe('toUrl', () => { - test('encode the message into the URL containing the base64 encoded invitation as the oob query parameter', async () => { - const domain = 'https://example.com/ssi' - const json = { - '@type': 'https://didcomm.org/out-of-band/1.1/invitation', - services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], - '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', - label: 'Faber College', - goal_code: 'issue-vc', - goal: 'To issue a Faber College Graduate credential', - handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], - } - const invitation = JsonTransformer.fromJSON(json, OutOfBandInvitation) - const invitationUrl = invitation.toUrl({ - domain, - }) - - expect(invitationUrl).toBe(`${domain}?oob=${JsonEncoder.toBase64URL(json)}`) +describe('toUrl', () => { + test('encode the message into the URL containing the base64 encoded invitation as the oob query parameter', async () => { + const domain = 'https://example.com/ssi' + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + } + const invitation = JsonTransformer.fromJSON(json, OutOfBandInvitation) + const invitationUrl = invitation.toUrl({ + domain, }) + + expect(invitationUrl).toBe(`${domain}?oob=${JsonEncoder.toBase64URL(json)}`) }) +}) - describe('fromUrl', () => { - test('decode the URL containing the base64 encoded invitation as the oob parameter into an `OutOfBandInvitation`', () => { - const invitationUrl = - 'http://example.com/ssi?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0K' +describe('fromUrl', () => { + test('decode the URL containing the base64 encoded invitation as the oob parameter into an `OutOfBandInvitation`', () => { + const invitationUrl = + 'http://example.com/ssi?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0K' - const invitation = OutOfBandInvitation.fromUrl(invitationUrl) - const json = JsonTransformer.toJSON(invitation) - expect(json).toEqual({ - '@type': 'https://didcomm.org/out-of-band/1.1/invitation', - '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', - label: 'Faber College', - goal_code: 'issue-vc', - goal: 'To issue a Faber College Graduate credential', - handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], - services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], - }) + const invitation = OutOfBandInvitation.fromUrl(invitationUrl) + const json = JsonTransformer.toJSON(invitation) + expect(json).toEqual({ + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], }) }) +}) - describe('fromJson', () => { - test('create an instance of `OutOfBandInvitation` from JSON object', () => { - const json = { - '@type': 'https://didcomm.org/out-of-band/1.1/invitation', - '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', - label: 'Faber College', - goal_code: 'issue-vc', - goal: 'To issue a Faber College Graduate credential', - handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], - services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], - } - - const invitation = OutOfBandInvitation.fromJson(json) +describe('fromJson', () => { + test('create an instance of `OutOfBandInvitation` from JSON object', () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + } - expect(invitation).toBeDefined() - expect(invitation).toBeInstanceOf(OutOfBandInvitation) - }) + const invitation = OutOfBandInvitation.fromJson(json) - test('create an instance of `OutOfBandInvitation` from JSON object with inline service', () => { - const json = { - '@type': 'https://didcomm.org/out-of-band/1.1/invitation', - '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', - label: 'Faber College', - goal_code: 'issue-vc', - goal: 'To issue a Faber College Graduate credential', - handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], - services: [ - { - id: '#inline', - recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], - routingKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], - serviceEndpoint: 'https://example.com/ssi', - }, - ], - } + expect(invitation).toBeDefined() + expect(invitation).toBeInstanceOf(OutOfBandInvitation) + }) - const invitation = OutOfBandInvitation.fromJson(json) - expect(invitation).toBeDefined() - expect(invitation).toBeInstanceOf(OutOfBandInvitation) - }) + test('create an instance of `OutOfBandInvitation` from JSON object with inline service', () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [ + { + id: '#inline', + recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + routingKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + serviceEndpoint: 'https://example.com/ssi', + }, + ], + } - test('create an instance of `OutOfBandInvitation` from JSON object with appended attachments', () => { - const json = { - '@type': 'https://didcomm.org/out-of-band/1.1/invitation', - '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', - label: 'Faber College', - goal_code: 'issue-vc', - goal: 'To issue a Faber College Graduate credential', - handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], - services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], - '~attach': [ - { - '@id': 'view-1', - 'mime-type': 'image/png', - filename: 'IMG1092348.png', - lastmod_time: '2018-12-24 18:24:07Z', - description: 'view from doorway, facing east, with lights off', - data: { - base64: 'dmlldyBmcm9tIGRvb3J3YXksIGZhY2luZyBlYXN0LCB3aXRoIGxpZ2h0cyBvZmY=', - }, - }, - { - '@id': 'view-2', - 'mime-type': 'image/png', - filename: 'IMG1092349.png', - lastmod_time: '2018-12-24 18:25:49Z', - description: 'view with lamp in the background', - data: { - base64: 'dmlldyB3aXRoIGxhbXAgaW4gdGhlIGJhY2tncm91bmQ=', - }, - }, - ], - } + const invitation = OutOfBandInvitation.fromJson(json) + expect(invitation).toBeDefined() + expect(invitation).toBeInstanceOf(OutOfBandInvitation) + }) - const invitation = OutOfBandInvitation.fromJson(json) - expect(invitation).toBeDefined() - expect(invitation).toBeInstanceOf(OutOfBandInvitation) - expect(invitation.appendedAttachments).toBeDefined() - expect(invitation.appendedAttachments?.length).toEqual(2) - expect(invitation.getAppendedAttachmentById('view-1')).toEqual( - new Attachment({ - id: 'view-1', - mimeType: 'image/png', + test('create an instance of `OutOfBandInvitation` from JSON object with appended attachments', () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + '~attach': [ + { + '@id': 'view-1', + 'mime-type': 'image/png', filename: 'IMG1092348.png', - lastmodTime: new Date('2018-12-24 18:24:07Z'), + lastmod_time: '2018-12-24 18:24:07Z', description: 'view from doorway, facing east, with lights off', data: { base64: 'dmlldyBmcm9tIGRvb3J3YXksIGZhY2luZyBlYXN0LCB3aXRoIGxpZ2h0cyBvZmY=', }, - }) - ) - expect(invitation.getAppendedAttachmentById('view-2')).toEqual( - new Attachment({ - id: 'view-2', - mimeType: 'image/png', + }, + { + '@id': 'view-2', + 'mime-type': 'image/png', filename: 'IMG1092349.png', - lastmodTime: new Date('2018-12-24 18:25:49Z'), + lastmod_time: '2018-12-24 18:25:49Z', description: 'view with lamp in the background', data: { base64: 'dmlldyB3aXRoIGxhbXAgaW4gdGhlIGJhY2tncm91bmQ=', }, - }) - ) - }) + }, + ], + } + + const invitation = OutOfBandInvitation.fromJson(json) + expect(invitation).toBeDefined() + expect(invitation).toBeInstanceOf(OutOfBandInvitation) + expect(invitation.appendedAttachments).toBeDefined() + expect(invitation.appendedAttachments?.length).toEqual(2) + expect(invitation.getAppendedAttachmentById('view-1')).toEqual( + new Attachment({ + id: 'view-1', + mimeType: 'image/png', + filename: 'IMG1092348.png', + lastmodTime: new Date('2018-12-24 18:24:07Z'), + description: 'view from doorway, facing east, with lights off', + data: { + base64: 'dmlldyBmcm9tIGRvb3J3YXksIGZhY2luZyBlYXN0LCB3aXRoIGxpZ2h0cyBvZmY=', + }, + }) + ) + expect(invitation.getAppendedAttachmentById('view-2')).toEqual( + new Attachment({ + id: 'view-2', + mimeType: 'image/png', + filename: 'IMG1092349.png', + lastmodTime: new Date('2018-12-24 18:25:49Z'), + description: 'view with lamp in the background', + data: { + base64: 'dmlldyB3aXRoIGxhbXAgaW4gdGhlIGJhY2tncm91bmQ=', + }, + }) + ) + }) - test('throw validation error when services attribute is empty', () => { - const json = { - '@type': 'https://didcomm.org/out-of-band/1.1/invitation', - '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', - label: 'Faber College', - goal_code: 'issue-vc', - goal: 'To issue a Faber College Graduate credential', - handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], - services: [], - } + test('throw validation error when services attribute is empty', () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [], + } - expect.assertions(1) - try { - OutOfBandInvitation.fromJson(json) - } catch (error) { - const firstError = error as ClassValidationError - expect(firstError.validationErrors[0]).toMatchObject({ - children: [], - constraints: { arrayNotEmpty: 'services should not be empty' }, - property: 'services', - target: { - goal: 'To issue a Faber College Graduate credential', - label: 'Faber College', - services: [], - }, - value: [], - }) - } - }) + expect.assertions(1) + try { + OutOfBandInvitation.fromJson(json) + } catch (error) { + const firstError = error as ClassValidationError + expect(firstError.validationErrors[0]).toMatchObject({ + children: [], + constraints: { arrayNotEmpty: 'services should not be empty' }, + property: 'services', + target: { + goal: 'To issue a Faber College Graduate credential', + label: 'Faber College', + services: [], + }, + value: [], + }) + } + }) - test('transforms legacy prefix message @type and handshake_protocols to https://didcomm.org prefix', () => { - const json = { - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation', - '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', - label: 'Faber College', - goal_code: 'issue-vc', - goal: 'To issue a Faber College Graduate credential', - handshake_protocols: [ - 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0', - 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0', - ], - services: ['did:sov:123'], - } + test('transforms legacy prefix message @type and handshake_protocols to https://didcomm.org prefix', () => { + const json = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: [ + 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0', + 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0', + ], + services: ['did:sov:123'], + } - const invitation = OutOfBandInvitation.fromJson(json) + const invitation = OutOfBandInvitation.fromJson(json) - expect(invitation.type).toBe('https://didcomm.org/out-of-band/1.1/invitation') - expect(invitation.handshakeProtocols).toEqual([ - 'https://didcomm.org/didexchange/1.0', - 'https://didcomm.org/connections/1.0', - ]) - }) + expect(invitation.type).toBe('https://didcomm.org/out-of-band/1.1/invitation') + expect(invitation.handshakeProtocols).toEqual([ + 'https://didcomm.org/didexchange/1.0', + 'https://didcomm.org/connections/1.0', + ]) + }) - // Check if options @Transform for legacy did:sov prefix doesn't fail if handshake_protocols is not present - test('should successfully transform if no handshake_protocols is present', () => { - const json = { - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation', - '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', - label: 'Faber College', - goal_code: 'issue-vc', - goal: 'To issue a Faber College Graduate credential', - services: ['did:sov:123'], - } + // Check if options @Transform for legacy did:sov prefix doesn't fail if handshake_protocols is not present + test('should successfully transform if no handshake_protocols is present', () => { + const json = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + services: ['did:sov:123'], + } - const invitation = OutOfBandInvitation.fromJson(json) + const invitation = OutOfBandInvitation.fromJson(json) - expect(invitation.type).toBe('https://didcomm.org/out-of-band/1.1/invitation') - expect(invitation.handshakeProtocols).toBeUndefined() - }) + expect(invitation.type).toBe('https://didcomm.org/out-of-band/1.1/invitation') + expect(invitation.handshakeProtocols).toBeUndefined() + }) - test('throw validation error when incorrect service object present in services attribute', async () => { - const json = { - '@type': 'https://didcomm.org/out-of-band/1.1/invitation', - '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', - label: 'Faber College', - goal_code: 'issue-vc', - goal: 'To issue a Faber College Graduate credential', - handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], - services: [ - { - id: '#inline', - routingKeys: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], - serviceEndpoint: 'https://example.com/ssi', - }, - ], - } + test('throw validation error when incorrect service object present in services attribute', async () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [ + { + id: '#inline', + routingKeys: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + serviceEndpoint: 'https://example.com/ssi', + }, + ], + } - expect.assertions(1) - try { - OutOfBandInvitation.fromJson(json) - } catch (error) { - const firstError = error as ClassValidationError - expect(firstError.validationErrors[0]).toMatchObject({ - children: [], - constraints: { - arrayNotEmpty: 'recipientKeys should not be empty', - isDidKeyString: 'each value in recipientKeys must be a did:key string', - }, - property: 'recipientKeys', - target: { - id: '#inline', - routingKeys: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], - serviceEndpoint: 'https://example.com/ssi', - type: 'did-communication', - }, - value: undefined, - }) - } - }) + expect.assertions(1) + try { + OutOfBandInvitation.fromJson(json) + } catch (error) { + const firstError = error as ClassValidationError + expect(firstError.validationErrors[0]).toMatchObject({ + children: [], + constraints: { + arrayNotEmpty: 'recipientKeys should not be empty', + isDidKeyString: 'each value in recipientKeys must be a did:key string', + }, + property: 'recipientKeys', + target: { + id: '#inline', + routingKeys: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + serviceEndpoint: 'https://example.com/ssi', + type: 'did-communication', + }, + value: undefined, + }) + } }) }) diff --git a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts index f07af89292..5b6b776499 100644 --- a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts +++ b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts @@ -72,7 +72,6 @@ export class OutOfBandInvitation extends AgentMessage { public static fromUrl(invitationUrl: string) { const parsedUrl = parseUrl(invitationUrl).query const encodedInvitation = parsedUrl['oob'] - if (typeof encodedInvitation === 'string') { const invitationJson = JsonEncoder.fromBase64(encodedInvitation) const invitation = this.fromJson(invitationJson) @@ -123,7 +122,6 @@ export class OutOfBandInvitation extends AgentMessage { public readonly goal?: string public readonly accept?: string[] - @Transform(({ value }) => value?.map(replaceLegacyDidSovPrefix), { toClassOnly: true }) @Expose({ name: 'handshake_protocols' }) public handshakeProtocols?: HandshakeProtocol[] diff --git a/packages/core/src/utils/__tests__/shortenedUrl.test.ts b/packages/core/src/utils/__tests__/shortenedUrl.test.ts new file mode 100644 index 0000000000..a6e2364f97 --- /dev/null +++ b/packages/core/src/utils/__tests__/shortenedUrl.test.ts @@ -0,0 +1,109 @@ +import type { Response } from 'node-fetch' + +import { Headers } from 'node-fetch' + +import { ConnectionInvitationMessage } from '../../modules/connections' +import { OutOfBandInvitation } from '../../modules/oob' +import { convertToNewInvitation } from '../../modules/oob/helpers' +import { JsonTransformer } from '../JsonTransformer' +import { MessageValidator } from '../MessageValidator' +import { oobInvitationfromShortUrl } from '../parseInvitation' + +const mockOobInvite = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation', + '@id': '764af259-8bb4-4546-b91a-924c912d0bb8', + label: 'Alice', + handshake_protocols: ['did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0'], + services: ['did:sov:MvTqVXCEmJ87usL9uQTo7v'], +} + +const mockConnectionInvite = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation', + '@id': '20971ef0-1029-46db-a25b-af4c465dd16b', + label: 'test', + serviceEndpoint: 'http://sour-cow-15.tun1.indiciotech.io', + recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], +} + +const header = new Headers() + +const dummyHeader = new Headers() + +header.append('Content-Type', 'application/json') + +const mockedResponseOobJson = { + status: 200, + ok: true, + headers: header, + json: async () => ({ + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation', + '@id': '764af259-8bb4-4546-b91a-924c912d0bb8', + label: 'Alice', + handshake_protocols: ['did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0'], + services: ['did:sov:MvTqVXCEmJ87usL9uQTo7v'], + }), +} as Response + +const mockedResponseOobUrl = { + status: 200, + ok: true, + headers: dummyHeader, + url: 'https://wonderful-rabbit-5.tun2.indiciotech.io?oob=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9vdXQtb2YtYmFuZC8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiNzY0YWYyNTktOGJiNC00NTQ2LWI5MWEtOTI0YzkxMmQwYmI4IiwgImxhYmVsIjogIkFsaWNlIiwgImhhbmRzaGFrZV9wcm90b2NvbHMiOiBbImRpZDpzb3Y6QnpDYnNOWWhNcmpIaXFaRFRVQVNIZztzcGVjL2Nvbm5lY3Rpb25zLzEuMCJdLCAic2VydmljZXMiOiBbImRpZDpzb3Y6TXZUcVZYQ0VtSjg3dXNMOXVRVG83diJdfQ====', +} as Response + +mockedResponseOobUrl.headers = dummyHeader + +const mockedResponseConnectionJson = { + status: 200, + ok: true, + json: async () => ({ + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation', + '@id': '20971ef0-1029-46db-a25b-af4c465dd16b', + label: 'test', + serviceEndpoint: 'http://sour-cow-15.tun1.indiciotech.io', + recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + }), +} as Response + +mockedResponseConnectionJson['headers'] = header + +const mockedResponseConnectionUrl = { + status: 200, + ok: true, + url: 'http://sour-cow-15.tun1.indiciotech.io?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiMjA5NzFlZjAtMTAyOS00NmRiLWEyNWItYWY0YzQ2NWRkMTZiIiwgImxhYmVsIjogInRlc3QiLCAic2VydmljZUVuZHBvaW50IjogImh0dHA6Ly9zb3VyLWNvdy0xNS50dW4xLmluZGljaW90ZWNoLmlvIiwgInJlY2lwaWVudEtleXMiOiBbIjVHdnBmOU00ajd2V3BIeWVUeXZCS2JqWWU3cVdjNzJrR282cVphTEhrTHJkIl19', +} as Response + +mockedResponseConnectionUrl['headers'] = dummyHeader + +let outOfBandInvitationMock: OutOfBandInvitation +let connectionInvitationMock: ConnectionInvitationMessage +let connectionInvitationToNew: OutOfBandInvitation + +beforeAll(async () => { + outOfBandInvitationMock = await JsonTransformer.fromJSON(mockOobInvite, OutOfBandInvitation) + await MessageValidator.validateSync(outOfBandInvitationMock) + connectionInvitationMock = await JsonTransformer.fromJSON(mockConnectionInvite, ConnectionInvitationMessage) + await MessageValidator.validateSync(connectionInvitationMock) + connectionInvitationToNew = convertToNewInvitation(connectionInvitationMock) +}) + +describe('shortened urls resolving to oob invitations', () => { + test('Resolve a mocked response in the form of a oob invitation as a json object', async () => { + const short = await oobInvitationfromShortUrl(mockedResponseOobJson) + expect(short).toEqual(outOfBandInvitationMock) + }) + test('Resolve a mocked response in the form of a oob invitation encoded in an url', async () => { + const short = await oobInvitationfromShortUrl(mockedResponseOobUrl) + expect(short).toEqual(outOfBandInvitationMock) + }) +}) +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', async () => { + const short = await oobInvitationfromShortUrl(mockedResponseConnectionUrl) + expect(short).toEqual(connectionInvitationToNew) + }) +}) diff --git a/packages/core/src/utils/parseInvitation.ts b/packages/core/src/utils/parseInvitation.ts index 0421eb6b67..713360512e 100644 --- a/packages/core/src/utils/parseInvitation.ts +++ b/packages/core/src/utils/parseInvitation.ts @@ -1,3 +1,7 @@ +import type { AgentDependencies } from '../agent/AgentDependencies' +import type { Response } from 'node-fetch' + +import { AbortController } from 'abort-controller' import { parseUrl } from 'query-string' import { AriesFrameworkError } from '../error' @@ -5,6 +9,29 @@ import { ConnectionInvitationMessage } from '../modules/connections' import { convertToNewInvitation } from '../modules/oob/helpers' import { OutOfBandInvitation } from '../modules/oob/messages' +import { JsonTransformer } from './JsonTransformer' +import { MessageValidator } from './MessageValidator' +import { parseMessageType, supportsIncomingMessageType } from './messageType' + +const fetchShortUrl = async (invitationUrl: string, dependencies: AgentDependencies) => { + const abortController = new AbortController() + const id = setTimeout(() => abortController.abort(), 15000) + let response + try { + response = await dependencies.fetch(invitationUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + throw new AriesFrameworkError(`Get request failed on provided url: ${error.message}`, { cause: error }) + } + clearTimeout(id) + return response +} + /** * Parses URL containing encoded invitation and returns invitation message. * @@ -25,3 +52,66 @@ export const parseInvitationUrl = (invitationUrl: string): OutOfBandInvitation = 'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters: `oob`, `c_i` or `d_m`.' ) } + +//This currently does not follow the RFC because of issues with fetch, currently uses a janky work around +export const oobInvitationfromShortUrl = async (response: Response): Promise => { + if (response) { + if (response.headers.get('Content-Type') === 'application/json' && response.ok) { + const invitationJson = await response.json() + const parsedMessageType = parseMessageType(invitationJson['@type']) + if (supportsIncomingMessageType(parsedMessageType, OutOfBandInvitation.type)) { + const invitation = JsonTransformer.fromJSON(invitationJson, OutOfBandInvitation) + await MessageValidator.validateSync(invitation) + return invitation + } else if (supportsIncomingMessageType(parsedMessageType, ConnectionInvitationMessage.type)) { + const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage) + await MessageValidator.validateSync(invitation) + return convertToNewInvitation(invitation) + } else { + throw new AriesFrameworkError(`Invitation with '@type' ${parsedMessageType.messageTypeUri} not supported.`) + } + } else if (response['url']) { + // The following if else is for here for trinsic shorten urls + // Because the redirect targets a deep link the automatic redirect does not occur + let responseUrl + const location = response.headers.get('Location') + if ((response.status === 302 || response.status === 301) && location) responseUrl = location + else responseUrl = response['url'] + + return parseInvitationUrl(responseUrl) + } + } + throw new AriesFrameworkError('HTTP request time out or did not receive valid response') +} + +/** + * Parses URL containing encoded invitation and returns invitation message. Compatible with + * parsing short Urls + * + * @param invitationUrl URL containing encoded invitation + * + * @param dependencies Agent dependicies containing fetch + * + * @returns OutOfBandInvitation + */ +export const parseInvitationShortUrl = async ( + invitationUrl: string, + dependencies: AgentDependencies +): Promise => { + const parsedUrl = parseUrl(invitationUrl).query + if (parsedUrl['oob']) { + const outOfBandInvitation = OutOfBandInvitation.fromUrl(invitationUrl) + return outOfBandInvitation + } else if (parsedUrl['c_i'] || parsedUrl['d_m']) { + const invitation = ConnectionInvitationMessage.fromUrl(invitationUrl) + return convertToNewInvitation(invitation) + } else { + try { + return oobInvitationfromShortUrl(await fetchShortUrl(invitationUrl, dependencies)) + } catch (error) { + throw new AriesFrameworkError( + 'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters: `oob`, `c_i` or `d_m`, or be valid shortened URL' + ) + } + } +}