Skip to content

Commit

Permalink
feat: support short legacy connectionless invitations (#1705)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed Jan 29, 2024
1 parent c66b37e commit 34a6c9f
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
49 changes: 28 additions & 21 deletions packages/core/src/utils/parseInvitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export const parseInvitationJson = (invitationJson: Record<string, unknown>): 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.`)
}
Expand Down Expand Up @@ -105,6 +108,30 @@ export const oobInvitationFromShortUrl = async (response: Response): Promise<Out
throw new AriesFrameworkError('HTTP request time out or did not receive valid response')
}

export function transformLegacyConnectionlessInvitationToOutOfBandInvitation(messageJson: Record<string, unknown>) {
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
Expand All @@ -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))
Expand Down

0 comments on commit 34a6c9f

Please sign in to comment.