Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support short legacy connectionless invitations #1705

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: '',
'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: '',
'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
55 changes: 31 additions & 24 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,33 @@ 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({
// The label is currently required by the OutOfBandInvitation class, but not according to the specification.
// FIXME: In 0.5.0 we will make this optional: https://github.com/hyperledger/aries-framework-javascript/issues/1524
label: '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can merge the optional label PR first, we can simply avoid using this empty label. See #1680 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update once #1680 is merged

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,30 +156,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({
// The label is currently required by the OutOfBandInvitation class, but not according to the specification.
// FIXME: In 0.5.0 we will make this optional: https://github.com/hyperledger/aries-framework-javascript/issues/1524
label: '',
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
Loading