@sphereon/ssi-sdk.ebsi-support 0.26.1-unstable.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +13 -0
  3. package/dist/agent/EbsiSupport.d.ts +12 -0
  4. package/dist/agent/EbsiSupport.d.ts.map +1 -0
  5. package/dist/agent/EbsiSupport.js +202 -0
  6. package/dist/agent/EbsiSupport.js.map +1 -0
  7. package/dist/did/EbsiDidProvider.d.ts +47 -0
  8. package/dist/did/EbsiDidProvider.d.ts.map +1 -0
  9. package/dist/did/EbsiDidProvider.js +172 -0
  10. package/dist/did/EbsiDidProvider.js.map +1 -0
  11. package/dist/did/EbsiDidResolver.d.ts +5 -0
  12. package/dist/did/EbsiDidResolver.d.ts.map +1 -0
  13. package/dist/did/EbsiDidResolver.js +10 -0
  14. package/dist/did/EbsiDidResolver.js.map +1 -0
  15. package/dist/did/functions.d.ts +66 -0
  16. package/dist/did/functions.d.ts.map +1 -0
  17. package/dist/did/functions.js +416 -0
  18. package/dist/did/functions.js.map +1 -0
  19. package/dist/did/index.d.ts +6 -0
  20. package/dist/did/index.d.ts.map +1 -0
  21. package/dist/did/index.js +6 -0
  22. package/dist/did/index.js.map +1 -0
  23. package/dist/did/services/EbsiRPCService.d.ts +13 -0
  24. package/dist/did/services/EbsiRPCService.d.ts.map +1 -0
  25. package/dist/did/services/EbsiRPCService.js +64 -0
  26. package/dist/did/services/EbsiRPCService.js.map +1 -0
  27. package/dist/did/services/EbsiRestService.d.ts +37 -0
  28. package/dist/did/services/EbsiRestService.d.ts.map +1 -0
  29. package/dist/did/services/EbsiRestService.js +90 -0
  30. package/dist/did/services/EbsiRestService.js.map +1 -0
  31. package/dist/did/types.d.ts +386 -0
  32. package/dist/did/types.d.ts.map +1 -0
  33. package/dist/did/types.js +47 -0
  34. package/dist/did/types.js.map +1 -0
  35. package/dist/functions/Attestation.d.ts +32 -0
  36. package/dist/functions/Attestation.d.ts.map +1 -0
  37. package/dist/functions/Attestation.js +182 -0
  38. package/dist/functions/Attestation.js.map +1 -0
  39. package/dist/functions/AttestationHeadlessCallbacks.d.ts +17 -0
  40. package/dist/functions/AttestationHeadlessCallbacks.d.ts.map +1 -0
  41. package/dist/functions/AttestationHeadlessCallbacks.js +194 -0
  42. package/dist/functions/AttestationHeadlessCallbacks.js.map +1 -0
  43. package/dist/functions/index.d.ts +7 -0
  44. package/dist/functions/index.d.ts.map +1 -0
  45. package/dist/functions/index.js +8 -0
  46. package/dist/functions/index.js.map +1 -0
  47. package/dist/index.d.ts +7 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +8 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/types/IEbsiSupport.d.ts +211 -0
  52. package/dist/types/IEbsiSupport.d.ts.map +1 -0
  53. package/dist/types/IEbsiSupport.js +5 -0
  54. package/dist/types/IEbsiSupport.js.map +1 -0
  55. package/package.json +86 -0
  56. package/src/agent/EbsiSupport.ts +250 -0
  57. package/src/did/EbsiDidProvider.ts +269 -0
  58. package/src/did/EbsiDidResolver.ts +16 -0
  59. package/src/did/functions.ts +528 -0
  60. package/src/did/index.ts +5 -0
  61. package/src/did/services/EbsiRPCService.ts +68 -0
  62. package/src/did/services/EbsiRestService.ts +117 -0
  63. package/src/did/types.ts +449 -0
  64. package/src/functions/Attestation.ts +262 -0
  65. package/src/functions/AttestationHeadlessCallbacks.ts +242 -0
  66. package/src/functions/index.ts +15 -0
  67. package/src/index.ts +8 -0
  68. package/src/types/IEbsiSupport.ts +241 -0
@@ -0,0 +1,262 @@
1
+ import { OpenID4VCIClient } from '@sphereon/oid4vci-client'
2
+ import {
3
+ Alg,
4
+ AuthorizationDetails,
5
+ AuthorizationRequestOpts,
6
+ AuthzFlowType,
7
+ CredentialConfigurationSupported,
8
+ getJson,
9
+ getTypesFromCredentialSupported,
10
+ ProofOfPossessionCallbacks,
11
+ } from '@sphereon/oid4vci-common'
12
+ import { getAuthenticationKey, getIdentifier, SupportedDidMethodEnum } from '@sphereon/ssi-sdk-ext.did-utils'
13
+ import { calculateJwkThumbprintForKey } from '@sphereon/ssi-sdk-ext.key-utils'
14
+ import {
15
+ IssuanceOpts,
16
+ OID4VCICallbackStateListener,
17
+ OID4VCIMachineInterpreter,
18
+ OID4VCIMachineState,
19
+ OID4VCIMachineStates,
20
+ PrepareStartArgs,
21
+ signatureAlgorithmFromKey,
22
+ signCallback,
23
+ } from '@sphereon/ssi-sdk.oid4vci-holder'
24
+ import {
25
+ OID4VPCallbackStateListener,
26
+ Siopv2MachineInterpreter,
27
+ Siopv2MachineState,
28
+ Siopv2MachineStates,
29
+ } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
30
+ import { Siopv2OID4VPLinkHandler } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth/dist/link-handler'
31
+ import { IIdentifier } from '@veramo/core'
32
+ import { _ExtendedIKey } from '@veramo/utils'
33
+ import { waitFor } from 'xstate/lib/waitFor'
34
+ import { logger } from '../index'
35
+ import { AttestationResult, CreateAttestationAuthRequestURLArgs, EbsiEnvironment, GetAttestationArgs, IRequiredContext } from '../types/IEbsiSupport'
36
+ import {
37
+ addContactCallback,
38
+ authorizationCodeUrlCallback,
39
+ handleErrorCallback,
40
+ reviewCredentialsCallback,
41
+ selectCredentialsCallback,
42
+ siopDoneCallback,
43
+ } from './AttestationHeadlessCallbacks'
44
+ import { getEbsiApiBaseUrl } from './index'
45
+
46
+ export interface AttestationAuthRequestUrlResult extends Omit<Required<PrepareStartArgs>, 'issuanceOpt'> {
47
+ issuanceOpt?: IssuanceOpts
48
+ authorizationCodeURL: string
49
+ identifier: IIdentifier
50
+ authKey: _ExtendedIKey
51
+ }
52
+
53
+ /**
54
+ * Method to generate an authz url for getting attestation credentials from a (R)TAO on EBSI using a cloud/service wallet
55
+ *
56
+ * This method can be used standalone. But it can also be used as input for the `oid4vciHolderStart` agent method,
57
+ * to start a OID4VCI holder flow.
58
+ *
59
+ * @param opts
60
+ * @param context
61
+ */
62
+ export const ebsiCreateAttestationAuthRequestURL = async (
63
+ {
64
+ clientId: clientIdArg,
65
+ credentialIssuer,
66
+ credentialType,
67
+ idOpts,
68
+ redirectUri,
69
+ requestObjectOpts,
70
+ formats = ['jwt_vc', 'jwt_vc_json'],
71
+ }: CreateAttestationAuthRequestURLArgs,
72
+ context: IRequiredContext,
73
+ ): Promise<AttestationAuthRequestUrlResult> => {
74
+ const identifier = await getIdentifier(idOpts, context)
75
+ if (identifier.provider !== 'did:ebsi' && identifier.provider !== 'did:key') {
76
+ throw Error(
77
+ `EBSI only supports did:key for natural persons and did:ebsi for legal persons. Provider: ${identifier.provider}, did: ${identifier.did}`,
78
+ )
79
+ }
80
+ // This only works if the DID is actually registered, otherwise use our internal KMS;
81
+ // that is why the offline argument is passed in when type is Verifiable Auth to Onboard, as no DID is present at that point yet
82
+ const authKey = await getAuthenticationKey(identifier, context, credentialType === 'VerifiableAuthorisationToOnboard', true)
83
+ const kid = authKey.meta?.jwkThumbprint ?? calculateJwkThumbprintForKey({ key: authKey })
84
+ const clientId = clientIdArg ?? identifier.did
85
+
86
+ const vciClient = await OpenID4VCIClient.fromCredentialIssuer({
87
+ credentialIssuer,
88
+ kid,
89
+ clientId,
90
+ createAuthorizationRequestURL: false, // We will do that down below
91
+ retrieveServerMetadata: true,
92
+ })
93
+
94
+ const allMatches = vciClient.getCredentialsSupported(false)
95
+ let arrayMatches: Array<CredentialConfigurationSupported>
96
+ if (Array.isArray(allMatches)) {
97
+ arrayMatches = allMatches
98
+ } else {
99
+ arrayMatches = Object.entries(allMatches).map(([id, supported]) => {
100
+ supported.id = id
101
+ return supported
102
+ })
103
+ }
104
+ const supportedConfigurations = arrayMatches
105
+ .filter((supported) => getTypesFromCredentialSupported(supported, { filterVerifiableCredential: false }).includes(credentialType))
106
+ .filter((supported) => (supported.format === 'jwt_vc' || supported.format === 'jwt_vc_json') && formats.includes(supported.format))
107
+ if (supportedConfigurations.length === 0) {
108
+ throw Error(`Could not find '${credentialType}' with format(s) '${formats.join(',')}' in list of supported types for issuer: ${credentialIssuer}`)
109
+ }
110
+ const authorizationDetails = supportedConfigurations.map((supported) => {
111
+ return {
112
+ type: 'openid_credential',
113
+ format: supported.format,
114
+ types: getTypesFromCredentialSupported(supported),
115
+ } as AuthorizationDetails
116
+ })
117
+
118
+ const signCallbacks: ProofOfPossessionCallbacks<never> = requestObjectOpts.signCallbacks ?? {
119
+ signCallback: signCallback(vciClient, idOpts, context),
120
+ }
121
+ const authorizationRequestOpts = {
122
+ redirectUri,
123
+ clientId,
124
+ authorizationDetails,
125
+ requestObjectOpts: {
126
+ ...requestObjectOpts,
127
+ signCallbacks,
128
+ kid: requestObjectOpts.kid ?? kid,
129
+ },
130
+ } satisfies AuthorizationRequestOpts
131
+ // todo: Do we really need to do this, or can we just set the create option to true at this point? We are passing in the authzReq opts
132
+ const authorizationCodeURL = await vciClient.createAuthorizationRequestUrl({
133
+ authorizationRequest: authorizationRequestOpts,
134
+ })
135
+
136
+ return {
137
+ requestData: {
138
+ createAuthorizationRequestURL: false,
139
+ flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW,
140
+ uri: credentialIssuer,
141
+ existingClientState: JSON.parse(await vciClient.exportState()),
142
+ },
143
+ accessTokenOpts: {
144
+ clientOpts: {
145
+ alg: Alg[await signatureAlgorithmFromKey({ key: authKey })],
146
+ clientId,
147
+ kid,
148
+ signCallbacks,
149
+ clientAssertionType: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
150
+ },
151
+ },
152
+ authorizationRequestOpts,
153
+ authorizationCodeURL,
154
+ identifier,
155
+ // @ts-ignore
156
+ authKey,
157
+ didMethodPreferences: [SupportedDidMethodEnum.DID_EBSI, SupportedDidMethodEnum.DID_KEY],
158
+ }
159
+ }
160
+
161
+ export const ebsiGetAttestationInterpreter = async (
162
+ { clientId, authReqResult }: Omit<GetAttestationArgs, 'opts'>,
163
+ context: IRequiredContext,
164
+ ): Promise<OID4VCIMachineInterpreter> => {
165
+ const identifier = authReqResult.identifier
166
+ const vciStateCallbacks = new Map<OID4VCIMachineStates, (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => Promise<void>>()
167
+ const vpStateCallbacks = new Map<Siopv2MachineStates, (oid4vpMachine: Siopv2MachineInterpreter, state: Siopv2MachineState) => Promise<void>>()
168
+
169
+ const oid4vciMachine = await context.agent.oid4vciHolderGetMachineInterpreter({
170
+ ...authReqResult,
171
+ issuanceOpt: {
172
+ identifier,
173
+ didMethod: SupportedDidMethodEnum.DID_EBSI,
174
+ kid: authReqResult.authKey.meta?.jwkThumbprint ?? authReqResult.authKey.kid,
175
+ },
176
+ clientOpts: {
177
+ clientAssertionType: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
178
+ kid: authReqResult.authKey.meta?.jwkThumbprint ?? authReqResult.authKey.kid,
179
+ clientId,
180
+ },
181
+ didMethodPreferences: [SupportedDidMethodEnum.DID_EBSI, SupportedDidMethodEnum.DID_KEY],
182
+ stateNavigationListener: OID4VCICallbackStateListener(vciStateCallbacks),
183
+ })
184
+ const vpLinkHandler = new Siopv2OID4VPLinkHandler({
185
+ protocols: ['openid:'],
186
+ // @ts-ignore
187
+ context,
188
+ noStateMachinePersistence: true,
189
+ stateNavigationListener: OID4VPCallbackStateListener(vpStateCallbacks),
190
+ })
191
+
192
+ vpStateCallbacks
193
+ .set(Siopv2MachineStates.done, siopDoneCallback({ oid4vciMachine }, context))
194
+ .set(Siopv2MachineStates.handleError, handleErrorCallback(context))
195
+
196
+ vciStateCallbacks
197
+ .set(OID4VCIMachineStates.handleError, handleErrorCallback(context))
198
+ .set(OID4VCIMachineStates.addContact, addContactCallback(context))
199
+ .set(OID4VCIMachineStates.selectCredentials, selectCredentialsCallback(context))
200
+ .set(
201
+ OID4VCIMachineStates.initiateAuthorizationRequest,
202
+ authorizationCodeUrlCallback(
203
+ {
204
+ authReqResult,
205
+ vpLinkHandler,
206
+ },
207
+ context,
208
+ ),
209
+ )
210
+ .set(OID4VCIMachineStates.reviewCredentials, reviewCredentialsCallback(context))
211
+
212
+ return oid4vciMachine.interpreter
213
+ }
214
+
215
+ export const ebsiGetAttestation = async (
216
+ { clientId, authReqResult, opts = { timeout: 30_000 } }: GetAttestationArgs,
217
+ context: IRequiredContext,
218
+ ): Promise<AttestationResult> => {
219
+ const interpreter = await ebsiGetAttestationInterpreter({ clientId, authReqResult }, context)
220
+ const state = await waitFor(interpreter.start(), (state) => state.matches('done') || state.matches('handleError'), {
221
+ timeout: opts.timeout ?? 30_000,
222
+ })
223
+ if (state.matches('handleError')) {
224
+ console.error(JSON.stringify(state.context.error))
225
+ throw Error(JSON.stringify(state.context.error))
226
+ }
227
+
228
+ return {
229
+ contactAlias: state.context.contactAlias,
230
+ contact: state.context.contact!,
231
+ credentialBranding: state.context.credentialBranding,
232
+ identifier: state.context.issuanceOpt?.identifier ?? authReqResult.identifier,
233
+ error: state.context.error,
234
+ credentials: state.context.credentialsToAccept,
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Normally you would use the browser to let the user make this call in the front channel,
240
+ * however EBSI mainly uses mocks at present, and we want to be able to test as well
241
+ */
242
+ export const ebsiAuthRequestExecution = async (authRequestResult: AttestationAuthRequestUrlResult, opts?: {}) => {
243
+ const { requestData, authorizationCodeURL } = authRequestResult
244
+ const vciClient = await OpenID4VCIClient.fromState({ state: requestData?.existingClientState! })
245
+
246
+ logger.debug(`URL: ${authorizationCodeURL}, according to client: ${vciClient.authorizationURL}`)
247
+
248
+ const authResponse = await getJson<any>(authorizationCodeURL)
249
+ const location: string | null = authResponse.origResponse.headers.get('location')
250
+ logger.debug(`LOCATION: ${location}`)
251
+ }
252
+
253
+ export const ebsiGetIssuer = ({ credentialIssuer, environment = 'pilot' }: { credentialIssuer?: string; environment?: EbsiEnvironment }): string => {
254
+ if (credentialIssuer) {
255
+ return credentialIssuer
256
+ }
257
+ if (environment !== 'pilot') {
258
+ return `${getEbsiApiBaseUrl({ environment, version: 'v3' })}/issuer-mock`
259
+ }
260
+
261
+ throw Error(`EBSI environment ${environment} needs explicit credential issuer`)
262
+ }
@@ -0,0 +1,242 @@
1
+ import { getIssuerName } from '@sphereon/oid4vci-common'
2
+ import {
3
+ ConnectionType,
4
+ CorrelationIdentifierType,
5
+ CredentialRole,
6
+ IdentityOrigin,
7
+ NonPersistedParty,
8
+ Party,
9
+ PartyOrigin,
10
+ PartyTypeType,
11
+ } from '@sphereon/ssi-sdk.data-store'
12
+ import { OID4VCIMachine, OID4VCIMachineEvents, OID4VCIMachineInterpreter, OID4VCIMachineState } from '@sphereon/ssi-sdk.oid4vci-holder'
13
+ import { Siopv2MachineInterpreter, Siopv2MachineState } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
14
+ import { Siopv2OID4VPLinkHandler } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth/dist/link-handler'
15
+ import fetch from 'cross-fetch'
16
+ import { logger } from '../index'
17
+ import { IRequiredContext } from '../types/IEbsiSupport'
18
+ import { AttestationAuthRequestUrlResult } from './Attestation'
19
+
20
+ export const addContactCallback = (context: IRequiredContext) => {
21
+ return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
22
+ const { serverMetadata, hasContactConsent, contactAlias } = state.context
23
+
24
+ if (!serverMetadata) {
25
+ return Promise.reject(Error('Missing serverMetadata in context'))
26
+ }
27
+
28
+ const issuerUrl: URL = new URL(serverMetadata.issuer)
29
+ const correlationId: string = `${issuerUrl.protocol}//${issuerUrl.hostname}`
30
+ let issuerName: string = getIssuerName(correlationId, serverMetadata.credentialIssuerMetadata)
31
+
32
+ const party: NonPersistedParty = {
33
+ contact: {
34
+ displayName: issuerName,
35
+ legalName: issuerName,
36
+ },
37
+ // FIXME maybe its nicer if we can also just use the id only
38
+ // TODO using the predefined party type from the contact migrations here
39
+ // TODO this is not used as the screen itself adds one, look at the params of the screen, this is not being passed in
40
+ partyType: {
41
+ id: '3875c12e-fdaa-4ef6-a340-c936e054b627',
42
+ origin: PartyOrigin.EXTERNAL,
43
+ type: PartyTypeType.ORGANIZATION,
44
+ name: 'Sphereon_default_type',
45
+ tenantId: '95e09cfc-c974-4174-86aa-7bf1d5251fb4',
46
+ },
47
+ uri: correlationId,
48
+ identities: [
49
+ {
50
+ alias: correlationId,
51
+ roles: [CredentialRole.ISSUER],
52
+ origin: IdentityOrigin.EXTERNAL,
53
+ identifier: {
54
+ type: CorrelationIdentifierType.URL,
55
+ correlationId: issuerUrl.hostname,
56
+ },
57
+ // TODO WAL-476 add support for correct connection
58
+ connection: {
59
+ type: ConnectionType.OPENID_CONNECT,
60
+ config: {
61
+ clientId: '138d7bf8-c930-4c6e-b928-97d3a4928b01',
62
+ clientSecret: '03b3955f-d020-4f2a-8a27-4e452d4e27a0',
63
+ scopes: ['auth'],
64
+ issuer: 'https://example.com/app-test',
65
+ redirectUrl: 'app:/callback',
66
+ dangerouslyAllowInsecureHttpRequests: true,
67
+ clientAuthMethod: 'post' as const,
68
+ },
69
+ },
70
+ },
71
+ ],
72
+ }
73
+
74
+ const onCreate = async ({
75
+ party,
76
+ issuerUrl,
77
+ issuerName,
78
+ correlationId,
79
+ }: {
80
+ party: NonPersistedParty
81
+ issuerUrl: string
82
+ issuerName: string
83
+ correlationId: string
84
+ }): Promise<void> => {
85
+ const displayName = party.contact.displayName ?? issuerName
86
+ const contacts: Array<Party> = await context.agent.cmGetContacts({
87
+ filter: [
88
+ {
89
+ contact: {
90
+ // Searching on legalName as displayName is not unique, and we only support organizations for now
91
+ legalName: displayName,
92
+ },
93
+ },
94
+ ],
95
+ })
96
+ if (contacts.length === 0 || !contacts[0]?.contact) {
97
+ const contact = await context.agent.cmAddContact({
98
+ ...party,
99
+ displayName,
100
+ legalName: displayName,
101
+ contactType: {
102
+ type: PartyTypeType.ORGANIZATION,
103
+ name: displayName,
104
+ origin: PartyOrigin.EXTERNAL,
105
+ tenantId: party.tenantId ?? '1',
106
+ },
107
+ })
108
+ oid4vciMachine.send({
109
+ type: OID4VCIMachineEvents.CREATE_CONTACT,
110
+ data: contact,
111
+ })
112
+ }
113
+ }
114
+
115
+ const onConsentChange = async (hasConsent: boolean): Promise<void> => {
116
+ oid4vciMachine.send({
117
+ type: OID4VCIMachineEvents.SET_CONTACT_CONSENT,
118
+ data: hasConsent,
119
+ })
120
+ }
121
+
122
+ const onAliasChange = async (alias: string): Promise<void> => {
123
+ oid4vciMachine.send({
124
+ type: OID4VCIMachineEvents.SET_CONTACT_ALIAS,
125
+ data: alias,
126
+ })
127
+ }
128
+
129
+ if (!issuerName) {
130
+ issuerName = `EBSI unknown (${issuerUrl})`
131
+ } else if (issuerName.startsWith('http')) {
132
+ issuerName = `EBSI ${issuerName.replace(/https?:\/\//, '')}`
133
+ }
134
+ if (!contactAlias) {
135
+ return await onAliasChange(issuerName)
136
+ }
137
+ issuerName = contactAlias
138
+ if (!hasContactConsent) {
139
+ return await onConsentChange(true)
140
+ }
141
+ await onCreate({ party, issuerName, issuerUrl: issuerUrl.toString(), correlationId })
142
+ }
143
+ }
144
+
145
+ export const handleErrorCallback = (context: IRequiredContext) => {
146
+ return async (oid4vciMachine: OID4VCIMachineInterpreter | Siopv2MachineInterpreter, state: OID4VCIMachineState | Siopv2MachineState) => {
147
+ console.error(`error callback event: ${state.event}`, state.context.error)
148
+ }
149
+ }
150
+
151
+ export const selectCredentialsCallback = (context: IRequiredContext) => {
152
+ return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
153
+ const { contact, credentialToSelectFrom, selectedCredentials } = state.context
154
+
155
+ if (selectedCredentials && selectedCredentials.length > 0) {
156
+ logger.info(`selected: ${selectedCredentials.join(', ')}`)
157
+ oid4vciMachine.send({
158
+ type: OID4VCIMachineEvents.NEXT,
159
+ })
160
+ return
161
+ } else if (!contact) {
162
+ return Promise.reject(Error('Missing contact in context'))
163
+ }
164
+
165
+ const onSelectType = async (selectedCredentials: Array<string>): Promise<void> => {
166
+ console.log(`Selected credentials: ${selectedCredentials.join(', ')}`)
167
+ oid4vciMachine.send({
168
+ type: OID4VCIMachineEvents.SET_SELECTED_CREDENTIALS,
169
+ data: selectedCredentials,
170
+ })
171
+ }
172
+
173
+ await onSelectType(credentialToSelectFrom.map((sel) => sel.credentialId))
174
+ }
175
+ }
176
+
177
+ export const authorizationCodeUrlCallback = (
178
+ {
179
+ authReqResult,
180
+ vpLinkHandler,
181
+ }: {
182
+ authReqResult: AttestationAuthRequestUrlResult
183
+ vpLinkHandler: Siopv2OID4VPLinkHandler
184
+ },
185
+ context: IRequiredContext,
186
+ ) => {
187
+ return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
188
+ const url = state.context.authorizationCodeURL
189
+ console.log('navigateAuthorizationCodeURL: ', url)
190
+ if (!url) {
191
+ return Promise.reject(Error('Missing authorization URL in context'))
192
+ }
193
+ const onOpenAuthorizationUrl = async (url: string): Promise<void> => {
194
+ console.log('onOpenAuthorizationUrl being invoked: ', url)
195
+ oid4vciMachine.send({
196
+ type: OID4VCIMachineEvents.INVOKED_AUTHORIZATION_CODE_REQUEST,
197
+ data: url,
198
+ })
199
+ const response = await fetch(url, { redirect: 'manual' })
200
+ if (response.status < 301 || response.status > 302) {
201
+ throw Error(`When doing a headless auth, we expect to be redirected on getting the authz URL`)
202
+ }
203
+ const openidUri = response.headers.get('location')
204
+ if (!openidUri || !openidUri.startsWith('openid://')) {
205
+ throw Error(
206
+ `Expected a openid:// URI to be returned from EBSI in headless mode. Returned: ${openidUri}, ${JSON.stringify(await response.text())}`,
207
+ )
208
+ }
209
+
210
+ console.log(`onOpenAuthorizationUrl after openUrl: ${url}`)
211
+ const kid = authReqResult.authKey.meta?.jwkThumbprint
212
+ ? `${authReqResult.identifier.did}#${authReqResult.authKey.meta.jwkThumbprint}`
213
+ : authReqResult.authKey.kid
214
+ await vpLinkHandler.handle(openidUri, { idOpts: { identifier: authReqResult.identifier, kid } })
215
+ }
216
+ await onOpenAuthorizationUrl(url)
217
+ }
218
+ }
219
+
220
+ export const reviewCredentialsCallback = (context: IRequiredContext) => {
221
+ return async (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState) => {
222
+ console.log(`# REVIEW CREDENTIALS:`)
223
+ console.log(JSON.stringify(state.context.credentialsToAccept, null, 2))
224
+ oid4vciMachine.send({
225
+ type: OID4VCIMachineEvents.NEXT,
226
+ })
227
+ }
228
+ }
229
+
230
+ export const siopDoneCallback = ({ oid4vciMachine }: { oid4vciMachine: OID4VCIMachine }, context: IRequiredContext) => {
231
+ return async (oid4vpMachine: Siopv2MachineInterpreter, state: Siopv2MachineState) => {
232
+ // console.log('SIOP result:')
233
+ // console.log(JSON.stringify(state.context, null , 2))
234
+ if (!state.context.authorizationResponseData?.queryParams?.code) {
235
+ throw Error(`No code was returned from the authorization step`)
236
+ }
237
+ oid4vciMachine.interpreter.send({
238
+ type: OID4VCIMachineEvents.PROVIDE_AUTHORIZATION_CODE_RESPONSE,
239
+ data: state.context.authorizationResponseData.url!,
240
+ })
241
+ }
242
+ }
@@ -0,0 +1,15 @@
1
+ import { ApiOpts, EbsiEnvironment } from '../types/IEbsiSupport'
2
+
3
+ export const getEbsiApiBaseUrl = ({
4
+ environment = 'pilot',
5
+ version,
6
+ system = 'pilot',
7
+ }: ApiOpts & { system?: 'authorisation' | 'conformance' | 'did-registry' | EbsiEnvironment }) => {
8
+ return `https://api-${environment}.ebsi.eu/${system}/${version}`
9
+ }
10
+
11
+ export const wait = async (timeoutInMS: number) => {
12
+ return new Promise((resolve) => setTimeout(resolve, timeoutInMS))
13
+ }
14
+
15
+ export * from './Attestation'
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { Loggers } from '@sphereon/ssi-types'
2
+
3
+ export const logger = Loggers.DEFAULT.get('sphereon:ebsi-support')
4
+ const schema = require('../plugin.schema.json')
5
+ export { schema }
6
+ export { EbsiSupport } from './agent/EbsiSupport'
7
+ export * from './types/IEbsiSupport'
8
+ export { EbsiDidProvider } from './did'