@sphereon/ssi-sdk.siopv2-oid4vp-op-auth 0.34.1-feature.SSISDK.26.RP.58 → 0.34.1-feature.SSISDK.44.finish.dcql.310

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.
@@ -1,322 +1,216 @@
1
- import { PresentationDefinitionWithLocation, PresentationExchange } from '@sphereon/did-auth-siop'
2
- import { SelectResults, Status, SubmissionRequirementMatch } from '@sphereon/pex'
3
- import { Format } from '@sphereon/pex-models'
1
+ import type { PartialSdJwtKbJwt } from '@sphereon/pex/dist/main/lib/index.js'
2
+ import { calculateSdHash } from '@sphereon/pex/dist/main/lib/utils/index.js'
3
+ import { isManagedIdentifierDidResult, ManagedIdentifierOptsOrResult } from '@sphereon/ssi-sdk-ext.identifier-resolution'
4
+ import { UniqueDigitalCredential } from '@sphereon/ssi-sdk.credential-store'
5
+ import { defaultGenerateDigest } from '@sphereon/ssi-sdk.sd-jwt'
4
6
  import {
5
- isManagedIdentifierDidResult,
6
- isOID4VCIssuerIdentifier,
7
- ManagedIdentifierOptsOrResult,
8
- ManagedIdentifierResult,
9
- } from '@sphereon/ssi-sdk-ext.identifier-resolution'
10
- import { defaultHasher, ProofOptions } from '@sphereon/ssi-sdk.core'
11
- import { UniqueDigitalCredential, verifiableCredentialForRoleFilter } from '@sphereon/ssi-sdk.credential-store'
12
- import { CredentialRole, FindDigitalCredentialArgs } from '@sphereon/ssi-sdk.data-store'
13
- import { CompactJWT, HasherSync, IProof, OriginalVerifiableCredential } from '@sphereon/ssi-types'
14
- import {
15
- DEFAULT_JWT_PROOF_TYPE,
16
- IGetPresentationExchangeArgs,
17
- IOID4VPArgs,
18
- VerifiableCredentialsWithDefinition,
19
- VerifiablePresentationWithDefinition,
20
- } from '../types'
21
- import { createOID4VPPresentationSignCallback } from './functions'
22
- import { OpSession } from './OpSession'
23
-
24
- export class OID4VP {
25
- private readonly session: OpSession
26
- private readonly allIdentifiers: string[]
27
- private readonly hasher?: HasherSync
28
-
29
- private constructor(args: IOID4VPArgs) {
30
- const { session, allIdentifiers, hasher = defaultHasher } = args
31
-
32
- this.session = session
33
- this.allIdentifiers = allIdentifiers ?? []
34
- this.hasher = hasher
35
- }
7
+ CredentialMapper,
8
+ HasherSync,
9
+ Loggers,
10
+ OriginalVerifiableCredential,
11
+ SdJwtDecodedVerifiableCredential,
12
+ WrappedVerifiableCredential,
13
+ } from '@sphereon/ssi-types'
14
+ import { LOGGER_NAMESPACE, RequiredContext } from '../types'
15
+
16
+ const CLOCK_SKEW = 120
17
+ const logger = Loggers.DEFAULT.get(LOGGER_NAMESPACE)
18
+
19
+ export interface PresentationBuilderContext {
20
+ nonce: string
21
+ audience: string // clientId or origin
22
+ agent: RequiredContext['agent']
23
+ clockSkew?: number
24
+ hasher?: HasherSync
25
+ }
36
26
 
37
- public static async init(session: OpSession, allIdentifiers: string[], hasher?: HasherSync): Promise<OID4VP> {
38
- return new OID4VP({ session, allIdentifiers: allIdentifiers ?? (await session.getSupportedDIDs()), hasher })
27
+ /**
28
+ * Extracts the original credential from a UniqueDigitalCredential or WrappedVerifiableCredential
29
+ */
30
+ function extractOriginalCredential(
31
+ credential: UniqueDigitalCredential | WrappedVerifiableCredential | OriginalVerifiableCredential,
32
+ ): OriginalVerifiableCredential {
33
+ if (typeof credential === 'string') {
34
+ return credential
39
35
  }
40
36
 
41
- public async getPresentationDefinitions(): Promise<PresentationDefinitionWithLocation[] | undefined> {
42
- const definitions = await this.session.getPresentationDefinitions()
43
- if (definitions) {
44
- PresentationExchange.assertValidPresentationDefinitionWithLocations(definitions)
37
+ if ('digitalCredential' in credential) {
38
+ // UniqueDigitalCredential
39
+ const udc = credential as UniqueDigitalCredential
40
+ if (udc.originalVerifiableCredential) {
41
+ return udc.originalVerifiableCredential
45
42
  }
46
- return definitions
43
+ return udc.uniformVerifiableCredential as OriginalVerifiableCredential
47
44
  }
48
45
 
49
- private getPresentationExchange(args: IGetPresentationExchangeArgs): PresentationExchange {
50
- const { verifiableCredentials, allIdentifiers, hasher } = args
51
-
52
- return new PresentationExchange({
53
- allDIDs: allIdentifiers ?? this.allIdentifiers,
54
- allVerifiableCredentials: verifiableCredentials,
55
- hasher: hasher ?? this.hasher,
56
- })
46
+ if ('original' in credential) {
47
+ // WrappedVerifiableCredential
48
+ return credential.original
57
49
  }
58
50
 
59
- public async createVerifiablePresentations(
60
- credentialRole: CredentialRole,
61
- credentialsWithDefinitions: VerifiableCredentialsWithDefinition[],
62
- opts?: {
63
- forceNoCredentialsInVP?: boolean // Allow to create a VP without credentials, like EBSI is using it. Defaults to false
64
- restrictToFormats?: Format
65
- restrictToDIDMethods?: string[]
66
- proofOpts?: ProofOptions
67
- idOpts?: ManagedIdentifierOptsOrResult
68
- skipDidResolution?: boolean
69
- holderDID?: string
70
- subjectIsHolder?: boolean
71
- hasher?: HasherSync
72
- applyFilter?: boolean
73
- },
74
- ): Promise<VerifiablePresentationWithDefinition[]> {
75
- return await Promise.all(credentialsWithDefinitions.map((cred) => this.createVerifiablePresentation(credentialRole, cred, opts)))
76
- }
51
+ // Already an OriginalVerifiableCredential
52
+ return credential as OriginalVerifiableCredential
53
+ }
77
54
 
78
- public async createVerifiablePresentation(
79
- credentialRole: CredentialRole,
80
- selectedVerifiableCredentials: VerifiableCredentialsWithDefinition,
81
- opts?: {
82
- forceNoCredentialsInVP?: boolean // Allow to create a VP without credentials, like EBSI is using it. Defaults to false
83
- restrictToFormats?: Format
84
- restrictToDIDMethods?: string[]
85
- proofOpts?: ProofOptions
86
- idOpts?: ManagedIdentifierOptsOrResult
87
- skipDidResolution?: boolean
88
- holder?: string
89
- subjectIsHolder?: boolean
90
- applyFilter?: boolean
91
- hasher?: HasherSync
92
- },
93
- ): Promise<VerifiablePresentationWithDefinition> {
94
- const { subjectIsHolder, holder, forceNoCredentialsInVP = false } = { ...opts }
95
- if (subjectIsHolder && holder) {
96
- throw Error('Cannot both have subject is holder and a holderDID value at the same time (programming error)')
55
+ /**
56
+ * Determines the format of a credential
57
+ */
58
+ function detectCredentialFormat(credential: OriginalVerifiableCredential): string {
59
+ if (typeof credential === 'string') {
60
+ // Could be JWT or SD-JWT
61
+ if (credential.includes('~')) {
62
+ return 'dc+sd-jwt'
97
63
  }
98
- if (forceNoCredentialsInVP) {
99
- selectedVerifiableCredentials.credentials = []
100
- } else if (!selectedVerifiableCredentials?.credentials || selectedVerifiableCredentials.credentials.length === 0) {
101
- throw Error('No verifiable verifiableCredentials provided for presentation definition')
64
+ // Check if it's a compact JWT format (3 parts)
65
+ const parts = credential.split('.')
66
+ if (parts.length === 3) {
67
+ return 'jwt_vc_json'
102
68
  }
103
-
104
- const proofOptions: ProofOptions = {
105
- ...opts?.proofOpts,
106
- challenge: opts?.proofOpts?.nonce ?? opts?.proofOpts?.challenge ?? this.session.nonce,
107
- domain: opts?.proofOpts?.domain ?? (await this.session.getRedirectUri()),
69
+ } else if (typeof credential === 'object') {
70
+ // Check for SD-JWT decoded format
71
+ if ('compactSdJwtVc' in credential) {
72
+ return 'dc+sd-jwt'
73
+ }
74
+ // Check for JSON-LD
75
+ if ('@context' in credential || 'proof' in credential) {
76
+ return 'ldp_vc'
108
77
  }
78
+ // Check for mdoc
79
+ if ('doctype' in credential || 'namespaces' in credential) {
80
+ return 'mso_mdoc'
81
+ }
82
+ }
109
83
 
110
- let idOpts = opts?.idOpts
111
- if (!idOpts) {
112
- if (opts?.subjectIsHolder) {
113
- if (forceNoCredentialsInVP) {
114
- return Promise.reject(
115
- Error(
116
- `Cannot have subject is holder, when force no credentials is being used, as we could never determine the holder then. Please provide holderDID`,
117
- ),
118
- )
119
- }
120
- const firstUniqueDC = selectedVerifiableCredentials.credentials[0]
121
- // const firstVC = firstUniqueDC.uniformVerifiableCredential!
122
- if (typeof firstUniqueDC !== 'object' || !('digitalCredential' in firstUniqueDC)) {
123
- return Promise.reject(Error('If no opts provided, credentials should be of type UniqueDigitalCredential'))
124
- }
84
+ // Default to JWT
85
+ return 'jwt_vc_json'
86
+ }
125
87
 
126
- idOpts = isOID4VCIssuerIdentifier(firstUniqueDC.digitalCredential.kmsKeyRef)
127
- ? await this.session.context.agent.identifierManagedGetByIssuer({
128
- identifier: firstUniqueDC.digitalCredential.kmsKeyRef,
129
- })
130
- : await this.session.context.agent.identifierManagedGetByKid({
131
- identifier: firstUniqueDC.digitalCredential.kmsKeyRef,
132
- kmsKeyRef: firstUniqueDC.digitalCredential.kmsKeyRef,
133
- })
88
+ /**
89
+ * Gets the issuer/holder identifier from ManagedIdentifierOptsOrResult
90
+ */
91
+ function getIdentifierString(identifier: ManagedIdentifierOptsOrResult): string {
92
+ // Check if it's a result type (has 'method' and 'opts' properties)
93
+ if ('opts' in identifier && 'method' in identifier) {
94
+ // It's a ManagedIdentifierResult
95
+ if (isManagedIdentifierDidResult(identifier)) {
96
+ return identifier.did
97
+ }
98
+ }
99
+ // For opts types or other result types, use issuer if available, otherwise kid
100
+ return identifier.issuer ?? identifier.kid ?? ''
101
+ }
134
102
 
135
- /*
136
- const holder = CredentialMapper.isSdJwtDecodedCredential(firstVC)
137
- ? firstVC.decodedPayload.cnf?.jwk
138
- ? //TODO SDK-19: convert the JWK to hex and search for the appropriate key and associated DID
139
- //doesn't apply to did:jwk only, as you can represent any DID key as a JWK. So whenever you encounter a JWK it doesn't mean it had to come from a did:jwk in the system. It just can always be represented as a did:jwk
140
- `did:jwk:${encodeJoseBlob(firstVC.decodedPayload.cnf?.jwk)}#0`
141
- : firstVC.decodedPayload.sub
142
- : Array.isArray(firstVC.credentialSubject)
143
- ? firstVC.credentialSubject[0].id
144
- : firstVC.credentialSubject.id
145
- if (holder) {
146
- idOpts = { identifier: holder }
147
- }
148
- */
149
- } else if (opts?.holder) {
150
- idOpts = { identifier: opts.holder }
103
+ /**
104
+ * Creates a Verifiable Presentation for a given credential in the appropriate format
105
+ * Ensures nonce/aud (or challenge/domain) are set according to OID4VP draft 28
106
+ */
107
+ export async function createVerifiablePresentationForFormat(
108
+ credential: UniqueDigitalCredential | WrappedVerifiableCredential | OriginalVerifiableCredential,
109
+ identifier: ManagedIdentifierOptsOrResult,
110
+ context: PresentationBuilderContext,
111
+ ): Promise<string | object> {
112
+ // FIXME find proper types
113
+ const { nonce, audience, agent, clockSkew = CLOCK_SKEW } = context
114
+
115
+ const originalCredential = extractOriginalCredential(credential)
116
+ const format = detectCredentialFormat(originalCredential)
117
+
118
+ logger.debug(`Creating VP for format: ${format}`)
119
+
120
+ switch (format) {
121
+ case 'dc+sd-jwt': {
122
+ // SD-JWT with KB-JWT
123
+ const decodedSdJwt = await CredentialMapper.decodeSdJwtVcAsync(
124
+ typeof originalCredential === 'string' ? originalCredential : (originalCredential as SdJwtDecodedVerifiableCredential).compactSdJwtVc,
125
+ defaultGenerateDigest,
126
+ )
127
+
128
+ const hashAlg = decodedSdJwt.signedPayload._sd_alg ?? 'sha-256'
129
+ const sdHash = calculateSdHash(decodedSdJwt.compactSdJwtVc, hashAlg, defaultGenerateDigest)
130
+
131
+ const kbJwtPayload: PartialSdJwtKbJwt['payload'] = {
132
+ iat: Math.floor(Date.now() / 1000 - clockSkew),
133
+ sd_hash: sdHash,
134
+ nonce, // Always use the Authorization Request nonce
135
+ aud: audience, // Always use the Client Identifier or Origin
151
136
  }
152
- }
153
137
 
154
- // We are making sure to filter, in case the user submitted all verifiableCredentials in the wallet/agent. We also make sure to get original formats back
155
- const vcs = forceNoCredentialsInVP
156
- ? selectedVerifiableCredentials
157
- : opts?.applyFilter
158
- ? await this.filterCredentials(credentialRole, selectedVerifiableCredentials.definition, {
159
- restrictToFormats: opts?.restrictToFormats,
160
- restrictToDIDMethods: opts?.restrictToDIDMethods,
161
- filterOpts: {
162
- verifiableCredentials: selectedVerifiableCredentials.credentials,
163
- },
164
- })
165
- : {
166
- definition: selectedVerifiableCredentials.definition,
167
- credentials: selectedVerifiableCredentials.credentials,
168
- }
138
+ const presentationResult = await agent.createSdJwtPresentation({
139
+ presentation: decodedSdJwt.compactSdJwtVc,
140
+ kb: {
141
+ payload: kbJwtPayload as any, // FIXME
142
+ },
143
+ })
169
144
 
170
- if (!idOpts) {
171
- return Promise.reject(Error(`No identifier options present at this point`))
145
+ return presentationResult.presentation
172
146
  }
173
147
 
174
- const signCallback = await createOID4VPPresentationSignCallback({
175
- presentationSignCallback: this.session.options.presentationSignCallback,
176
- idOpts,
177
- context: this.session.context,
178
- domain: proofOptions.domain,
179
- challenge: proofOptions.challenge,
180
- format: opts?.restrictToFormats ?? selectedVerifiableCredentials.definition.definition.format,
181
- skipDidResolution: opts?.skipDidResolution ?? false,
182
- })
183
- const identifier: ManagedIdentifierResult = await this.session.context.agent.identifierManagedGet(idOpts)
184
- const verifiableCredentials = vcs.credentials.map((credential) =>
185
- typeof credential === 'object' && 'digitalCredential' in credential ? credential.originalVerifiableCredential! : credential,
186
- )
187
- const presentationResult = await this.getPresentationExchange({
188
- verifiableCredentials: verifiableCredentials,
189
- allIdentifiers: this.allIdentifiers,
190
- hasher: opts?.hasher,
191
- }).createVerifiablePresentation(vcs.definition.definition, verifiableCredentials, signCallback, {
192
- proofOptions,
193
- // fixme: Update to newer siop-vp to not require dids here. But when Veramo is creating the VP it's still looking at this field to pass into didManagerGet
194
- ...(identifier && isManagedIdentifierDidResult(identifier) && { holderDID: identifier.did }),
195
- })
148
+ case 'jwt_vc_json': {
149
+ // JWT VC - create JWT VP with nonce and aud in payload
150
+ const vcJwt = typeof originalCredential === 'string' ? originalCredential : JSON.stringify(originalCredential)
151
+
152
+ const identifierString = getIdentifierString(identifier)
153
+
154
+ // Create VP JWT using agent method
155
+ const vpPayload = {
156
+ iss: identifierString,
157
+ aud: audience, // Client Identifier or Origin
158
+ nonce, // Authorization Request nonce
159
+ vp: {
160
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
161
+ type: ['VerifiablePresentation'],
162
+ holder: identifierString,
163
+ verifiableCredential: [vcJwt],
164
+ },
165
+ iat: Math.floor(Date.now() / 1000 - clockSkew),
166
+ exp: Math.floor(Date.now() / 1000 + 600), // 10 minutes
167
+ }
196
168
 
197
- const verifiablePresentations = presentationResult.verifiablePresentations.map((verifiablePresentation) =>
198
- typeof verifiablePresentation !== 'string' &&
199
- 'proof' in verifiablePresentation &&
200
- 'jwt' in verifiablePresentation.proof &&
201
- verifiablePresentation.proof.jwt
202
- ? verifiablePresentation.proof.jwt
203
- : verifiablePresentation,
204
- )
169
+ // Use the agent's JWT creation capability
170
+ const vpJwt = await agent.createVerifiablePresentation({
171
+ presentation: vpPayload.vp,
172
+ proofFormat: 'jwt',
173
+ domain: audience,
174
+ challenge: nonce,
175
+ keyRef: identifier.kmsKeyRef || identifier.kid,
176
+ })
205
177
 
206
- return {
207
- ...presentationResult,
208
- verifiablePresentations,
209
- verifiableCredentials: verifiableCredentials,
210
- definition: selectedVerifiableCredentials.definition,
211
- idOpts,
178
+ return vpJwt.proof?.jwt || vpJwt
212
179
  }
213
- }
214
180
 
215
- public async filterCredentialsAgainstAllDefinitions(
216
- credentialRole: CredentialRole,
217
- opts?: {
218
- filterOpts?: {
219
- verifiableCredentials?: UniqueDigitalCredential[]
220
- filter?: FindDigitalCredentialArgs
221
- }
222
- holderDIDs?: string[]
223
- restrictToFormats?: Format
224
- restrictToDIDMethods?: string[]
225
- },
226
- ): Promise<VerifiableCredentialsWithDefinition[]> {
227
- const defs = await this.getPresentationDefinitions()
228
- const result: VerifiableCredentialsWithDefinition[] = []
229
- if (defs) {
230
- for (const definition of defs) {
231
- result.push(await this.filterCredentials(credentialRole, definition, opts))
232
- }
233
- }
234
- return result
235
- }
181
+ case 'ldp_vc': {
182
+ // JSON-LD VC - create JSON-LD VP with challenge and domain in proof
183
+ const vcObject = typeof originalCredential === 'string' ? JSON.parse(originalCredential) : originalCredential
236
184
 
237
- public async filterCredentials(
238
- credentialRole: CredentialRole,
239
- presentationDefinition: PresentationDefinitionWithLocation,
240
- opts?: {
241
- filterOpts?: { verifiableCredentials?: (UniqueDigitalCredential | OriginalVerifiableCredential)[]; filter?: FindDigitalCredentialArgs }
242
- holderDIDs?: string[]
243
- restrictToFormats?: Format
244
- restrictToDIDMethods?: string[]
245
- },
246
- ): Promise<VerifiableCredentialsWithDefinition> {
247
- const udcMap = new Map<OriginalVerifiableCredential, UniqueDigitalCredential | OriginalVerifiableCredential>()
248
- opts?.filterOpts?.verifiableCredentials?.forEach((credential) => {
249
- if (typeof credential === 'object' && 'digitalCredential' in credential) {
250
- udcMap.set(credential.originalVerifiableCredential!, credential)
251
- } else {
252
- udcMap.set(credential, credential)
185
+ const vpObject = {
186
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
187
+ type: ['VerifiablePresentation'],
188
+ verifiableCredential: [vcObject],
253
189
  }
254
- })
255
190
 
256
- const credentials = (
257
- await this.filterCredentialsWithSelectionStatus(credentialRole, presentationDefinition, {
258
- ...opts,
259
- filterOpts: {
260
- verifiableCredentials: opts?.filterOpts?.verifiableCredentials?.map((credential) => {
261
- if (typeof credential === 'object' && 'digitalCredential' in credential) {
262
- return credential.originalVerifiableCredential!
263
- } else {
264
- return credential
265
- }
266
- }),
267
- },
191
+ // Create JSON-LD VP with proof
192
+ return await agent.createVerifiablePresentation({
193
+ presentation: vpObject,
194
+ proofFormat: 'lds',
195
+ challenge: nonce, // Authorization Request nonce as challenge
196
+ domain: audience, // Client Identifier or Origin as domain
197
+ keyRef: identifier.kmsKeyRef || identifier.kid,
268
198
  })
269
- ).verifiableCredential
270
- return {
271
- definition: presentationDefinition,
272
- credentials: credentials?.map((vc) => udcMap.get(vc)!) ?? [],
273
- }
274
- }
275
-
276
- public async filterCredentialsWithSelectionStatus(
277
- credentialRole: CredentialRole,
278
- presentationDefinition: PresentationDefinitionWithLocation,
279
- opts?: {
280
- filterOpts?: { verifiableCredentials?: OriginalVerifiableCredential[]; filter?: FindDigitalCredentialArgs }
281
- holderDIDs?: string[]
282
- restrictToFormats?: Format
283
- restrictToDIDMethods?: string[]
284
- },
285
- ): Promise<SelectResults> {
286
- const selectionResults: SelectResults = await this.getPresentationExchange({
287
- verifiableCredentials: await this.getCredentials(credentialRole, opts?.filterOpts),
288
- }).selectVerifiableCredentialsForSubmission(presentationDefinition.definition, opts)
289
- if (selectionResults.errors && selectionResults.errors.length > 0) {
290
- throw Error(JSON.stringify(selectionResults.errors))
291
- } else if (selectionResults.areRequiredCredentialsPresent === Status.ERROR) {
292
- throw Error(`Not all required credentials are available to satisfy the relying party's request`)
293
199
  }
294
200
 
295
- const matches: SubmissionRequirementMatch[] | undefined = selectionResults.matches
296
- if (!matches || matches.length === 0 || !selectionResults.verifiableCredential || selectionResults.verifiableCredential.length === 0) {
297
- throw Error(JSON.stringify(selectionResults.errors))
298
- }
299
- return selectionResults
300
- }
201
+ case 'mso_mdoc': {
202
+ // ISO mdoc - create mdoc VP token
203
+ // This is a placeholder implementation
204
+ // Full implementation would require:
205
+ // 1. Decode the mdoc using CredentialMapper or mdoc utilities
206
+ // 2. Build proper mdoc VP token with session transcript
207
+ // 3. Include nonce/audience in the session transcript
208
+ logger.warning('mso_mdoc format has basic support - production use requires proper mdoc VP token implementation')
301
209
 
302
- private async getCredentials(
303
- credentialRole: CredentialRole,
304
- filterOpts?: {
305
- verifiableCredentials?: OriginalVerifiableCredential[]
306
- filter?: FindDigitalCredentialArgs
307
- },
308
- ): Promise<OriginalVerifiableCredential[]> {
309
- if (filterOpts?.verifiableCredentials && filterOpts.verifiableCredentials.length > 0) {
310
- return filterOpts.verifiableCredentials
210
+ return originalCredential
311
211
  }
312
212
 
313
- const filter = verifiableCredentialForRoleFilter(credentialRole, filterOpts?.filter)
314
- const uniqueCredentials = await this.session.context.agent.crsGetUniqueCredentials({ filter })
315
- return uniqueCredentials.map((uniqueVC: UniqueDigitalCredential) => {
316
- const vc = uniqueVC.uniformVerifiableCredential!
317
- const proof = Array.isArray(vc.proof) ? vc.proof : [vc.proof]
318
- const jwtProof = proof.find((p: IProof) => p?.type === DEFAULT_JWT_PROOF_TYPE)
319
- return jwtProof ? (jwtProof.jwt as CompactJWT) : vc
320
- })
213
+ default:
214
+ return Promise.reject(Error(`Unsupported credential format: ${format}`))
321
215
  }
322
216
  }