-
Notifications
You must be signed in to change notification settings - Fork 185
/
parseInvitation.ts
168 lines (149 loc) · 6.78 KB
/
parseInvitation.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import type { AgentDependencies } from '../agent/AgentDependencies'
import { AbortController } from 'abort-controller'
import { parseUrl } from 'query-string'
import { AgentMessage } from '../agent/AgentMessage'
import { CredoError } from '../error'
import { ConnectionInvitationMessage } from '../modules/connections'
import { OutOfBandDidCommService } from '../modules/oob/domain/OutOfBandDidCommService'
import { convertToNewInvitation } from '../modules/oob/helpers'
import { InvitationType, OutOfBandInvitation } from '../modules/oob/messages'
import { JsonEncoder } from './JsonEncoder'
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 CredoError(`Get request failed on provided url: ${error.message}`, { cause: error })
}
clearTimeout(id)
return response
}
/**
* Parses a JSON containing an invitation message and returns an OutOfBandInvitation instance
*
* @param invitationJson JSON object containing message
* @returns OutOfBandInvitation
*/
export const parseInvitationJson = (invitationJson: Record<string, unknown>): OutOfBandInvitation => {
const messageType = invitationJson['@type'] as string
if (!messageType) {
throw new CredoError('Invitation is not a valid DIDComm message')
}
const parsedMessageType = parseMessageType(messageType)
if (supportsIncomingMessageType(parsedMessageType, OutOfBandInvitation.type)) {
const invitation = JsonTransformer.fromJSON(invitationJson, OutOfBandInvitation)
MessageValidator.validateSync(invitation)
invitation.invitationType = InvitationType.OutOfBand
return invitation
} else if (supportsIncomingMessageType(parsedMessageType, ConnectionInvitationMessage.type)) {
const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage)
MessageValidator.validateSync(invitation)
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 CredoError(`Invitation with '@type' ${parsedMessageType.messageTypeUri} not supported.`)
}
}
/**
* Parses URL containing encoded invitation and returns invitation message.
*
* @param invitationUrl URL containing encoded invitation
*
* @returns OutOfBandInvitation
*/
export const parseInvitationUrl = (invitationUrl: string): OutOfBandInvitation => {
const parsedUrl = parseUrl(invitationUrl).query
const encodedInvitation = parsedUrl['oob'] ?? parsedUrl['c_i'] ?? parsedUrl['d_m']
if (typeof encodedInvitation === 'string') {
const invitationJson = JsonEncoder.fromBase64(encodedInvitation) as Record<string, unknown>
return parseInvitationJson(invitationJson)
}
throw new CredoError(
'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<OutOfBandInvitation> => {
if (response) {
if (response.headers.get('Content-Type')?.startsWith('application/json') && response.ok) {
const invitationJson = (await response.json()) as Record<string, unknown>
return parseInvitationJson(invitationJson)
} 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 CredoError('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 CredoError('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
*
* @param invitationUrl URL containing encoded invitation
*
* @param dependencies Agent dependencies containing fetch
*
* @returns OutOfBandInvitation
*/
export const parseInvitationShortUrl = async (
invitationUrl: string,
dependencies: AgentDependencies
): Promise<OutOfBandInvitation> => {
const parsedUrl = parseUrl(invitationUrl).query
if (parsedUrl['oob'] || parsedUrl['c_i']) {
return parseInvitationUrl(invitationUrl)
}
// Legacy connectionless invitation
else if (parsedUrl['d_m']) {
const messageJson = JsonEncoder.fromBase64(parsedUrl['d_m'] as string)
return transformLegacyConnectionlessInvitationToOutOfBandInvitation(messageJson)
} else {
try {
const outOfBandInvitation = await oobInvitationFromShortUrl(await fetchShortUrl(invitationUrl, dependencies))
outOfBandInvitation.invitationType = InvitationType.OutOfBand
return outOfBandInvitation
} catch (error) {
throw new CredoError(
'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'
)
}
}
}