@sphereon/oid4vci-client
Version:
OpenID for Verifiable Credential Issuance (OpenID4VCI) client
1 lines • 263 kB
Source Map (JSON)
{"version":3,"sources":["../lib/index.ts","../lib/AccessTokenClient.ts","../lib/functions/AuthorizationUtil.ts","../lib/functions/notifications.ts","../lib/types/index.ts","../lib/functions/OpenIDUtils.ts","../lib/functions/AccessTokenUtil.ts","../lib/ProofOfPossessionBuilder.ts","../lib/functions/CredentialOfferCommons.ts","../lib/functions/dpopUtil.ts","../lib/MetadataClientV1_0_15.ts","../lib/AuthorizationCodeClient.ts","../lib/MetadataClient.ts","../lib/CredentialRequestClient.ts","../lib/CredentialOfferClient.ts","../lib/CredentialOfferClientV1_0_15.ts","../lib/CredentialRequestClientBuilder.ts","../lib/CredentialRequestClientBuilderV1_0_15.ts","../lib/OpenID4VCIClient.ts","../lib/OpenID4VCIClientV1_0_15.ts","../lib/NonceClient.ts"],"sourcesContent":["import { VCI_LOGGERS } from '@sphereon/oid4vci-common'\nimport { ISimpleLogger } from '@sphereon/ssi-types'\n\nexport const LOG: ISimpleLogger<string> = VCI_LOGGERS.get('sphereon:oid4vci:client')\n\nexport * from './AccessTokenClient'\nexport * from './AuthorizationCodeClient'\nexport * from './CredentialRequestClient'\nexport * from './CredentialOfferClient'\nexport * from './CredentialOfferClientV1_0_15'\nexport * from './CredentialRequestClientBuilder'\nexport * from './CredentialRequestClientBuilderV1_0_15'\nexport * from './functions'\nexport * from './MetadataClient'\nexport * from './MetadataClientV1_0_15'\nexport * from './OpenID4VCIClient'\nexport * from './OpenID4VCIClientV1_0_15'\nexport * from './ProofOfPossessionBuilder'\n","import { createDPoP, CreateDPoPClientOpts, getCreateDPoPOptions } from '@sphereon/oid4vc-common'\nimport {\n AccessTokenRequest,\n AccessTokenRequestOpts,\n AccessTokenResponse,\n assertedUniformCredentialOffer,\n AuthorizationServerOpts,\n AuthzFlowType,\n convertJsonToURI,\n DPoPResponseParams,\n EndpointMetadata,\n formPost,\n getIssuerFromCredentialOfferPayload,\n GrantTypes,\n IssuerOpts,\n JsonURIMode,\n OpenIDResponse,\n PRE_AUTH_CODE_LITERAL,\n PRE_AUTH_GRANT_LITERAL,\n TokenErrorResponse,\n toUniformCredentialOfferRequest,\n TxCodeAndPinRequired,\n UniformCredentialOfferPayload,\n} from '@sphereon/oid4vci-common'\nimport { ObjectUtils } from '@sphereon/ssi-types'\n\nimport { createJwtBearerClientAssertion } from './functions'\nimport { shouldRetryTokenRequestWithDPoPNonce } from './functions/dpopUtil'\nimport { LOG } from './types'\nimport { MetadataClientV1_0_15 } from './MetadataClientV1_0_15'\n\nexport class AccessTokenClient {\n public async acquireAccessToken(opts: AccessTokenRequestOpts): Promise<OpenIDResponse<AccessTokenResponse, DPoPResponseParams>> {\n const { asOpts, pin, codeVerifier, code, redirectUri, metadata, createDPoPOpts } = opts\n\n const credentialOffer = opts.credentialOffer ? await assertedUniformCredentialOffer(opts.credentialOffer) : undefined\n const pinMetadata: TxCodeAndPinRequired | undefined = credentialOffer && this.getPinMetadata(credentialOffer.credential_offer)\n const issuer =\n opts.credentialIssuer ??\n (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : (metadata?.issuer as string))\n if (!issuer) {\n throw Error('Issuer required at this point')\n }\n const issuerOpts = {\n issuer,\n }\n\n return await this.acquireAccessTokenUsingRequest({\n accessTokenRequest: await this.createAccessTokenRequest({\n credentialOffer,\n asOpts,\n codeVerifier,\n code,\n redirectUri,\n pin,\n credentialIssuer: issuer,\n metadata,\n additionalParams: opts.additionalParams,\n pinMetadata,\n }),\n pinMetadata,\n metadata,\n asOpts,\n issuerOpts,\n createDPoPOpts: createDPoPOpts,\n })\n }\n\n public async acquireAccessTokenUsingRequest({\n accessTokenRequest,\n pinMetadata,\n metadata,\n asOpts,\n issuerOpts,\n createDPoPOpts,\n }: {\n accessTokenRequest: AccessTokenRequest\n pinMetadata?: TxCodeAndPinRequired\n metadata?: EndpointMetadata\n asOpts?: AuthorizationServerOpts\n issuerOpts?: IssuerOpts\n createDPoPOpts?: CreateDPoPClientOpts\n }): Promise<OpenIDResponse<AccessTokenResponse, DPoPResponseParams>> {\n this.validate(accessTokenRequest, pinMetadata)\n\n const requestTokenURL = AccessTokenClient.determineTokenURL({\n asOpts,\n issuerOpts,\n metadata: metadata\n ? metadata\n : issuerOpts?.fetchMetadata\n ? await MetadataClientV1_0_15.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false })\n : undefined,\n })\n\n const useDpop = createDPoPOpts?.dPoPSigningAlgValuesSupported && createDPoPOpts.dPoPSigningAlgValuesSupported.length > 0\n let dPoP = useDpop ? await createDPoP(getCreateDPoPOptions(createDPoPOpts, requestTokenURL)) : undefined\n\n let response = await this.sendAuthCode(requestTokenURL, accessTokenRequest, dPoP ? { headers: { dpop: dPoP } } : undefined)\n\n let nextDPoPNonce = createDPoPOpts?.jwtPayloadProps.nonce\n const retryWithNonce = shouldRetryTokenRequestWithDPoPNonce(response)\n if (retryWithNonce.ok && createDPoPOpts) {\n createDPoPOpts.jwtPayloadProps.nonce = retryWithNonce.dpopNonce\n\n dPoP = await createDPoP(getCreateDPoPOptions(createDPoPOpts, requestTokenURL))\n response = await this.sendAuthCode(requestTokenURL, accessTokenRequest, dPoP ? { headers: { dpop: dPoP } } : undefined)\n const successDPoPNonce = response.origResponse.headers.get('DPoP-Nonce')\n\n nextDPoPNonce = successDPoPNonce ?? retryWithNonce.dpopNonce\n }\n\n if (response.successBody && createDPoPOpts && response.successBody.token_type !== 'DPoP') {\n throw new Error('Invalid token type returned. Expected DPoP. Received: ' + response.successBody.token_type)\n }\n\n return {\n ...response,\n ...(nextDPoPNonce && { params: { dpop: { dpopNonce: nextDPoPNonce } } }),\n }\n }\n\n public async createAccessTokenRequest(opts: Omit<AccessTokenRequestOpts, 'createDPoPOpts'>): Promise<AccessTokenRequest> {\n const { asOpts, pin, codeVerifier, code, redirectUri } = opts\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n const credentialOfferRequest = opts.credentialOffer ? await toUniformCredentialOfferRequest(opts.credentialOffer) : undefined\n const request: Partial<AccessTokenRequest> = { ...opts.additionalParams }\n if (asOpts?.clientOpts?.clientId) {\n request.client_id = asOpts.clientOpts.clientId\n }\n const credentialIssuer = opts.credentialIssuer ?? credentialOfferRequest?.credential_offer?.credential_issuer ?? opts.metadata?.issuer\n await createJwtBearerClientAssertion(request, { ...opts, credentialIssuer })\n\n // Prefer AUTHORIZATION_CODE over PRE_AUTHORIZED_CODE_FLOW\n if (!credentialOfferRequest || credentialOfferRequest.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW)) {\n request.grant_type = GrantTypes.AUTHORIZATION_CODE\n request.code = code\n request.redirect_uri = redirectUri\n\n if (codeVerifier) {\n request.code_verifier = codeVerifier\n }\n\n return request as AccessTokenRequest\n }\n\n if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {\n this.assertAlphanumericPin(opts.pinMetadata, pin)\n request.user_pin = pin\n request.tx_code = pin\n\n request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE\n // we actually know it is there because of the isPreAuthCode call\n request[PRE_AUTH_CODE_LITERAL] = credentialOfferRequest?.credential_offer.grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL]\n\n return request as AccessTokenRequest\n }\n\n throw new Error('Credential offer request follows neither pre-authorized code nor authorization code flow requirements.')\n }\n\n private assertPreAuthorizedGrantType(grantType: GrantTypes): void {\n if (GrantTypes.PRE_AUTHORIZED_CODE !== grantType) {\n throw new Error(\"grant type must be 'urn:ietf:params:oauth:grant-type:pre-authorized_code'\")\n }\n }\n\n private assertAuthorizationGrantType(grantType: GrantTypes): void {\n if (GrantTypes.AUTHORIZATION_CODE !== grantType) {\n throw new Error(\"grant type must be 'authorization_code'\")\n }\n }\n\n private getPinMetadata(requestPayload: UniformCredentialOfferPayload): TxCodeAndPinRequired {\n if (!requestPayload) {\n throw new Error(TokenErrorResponse.invalid_request)\n }\n const issuer = getIssuerFromCredentialOfferPayload(requestPayload)\n\n const grantDetails = requestPayload.grants?.[PRE_AUTH_GRANT_LITERAL]\n const isPinRequired = !!(grantDetails?.tx_code ?? false)\n\n LOG.warning(`Pin required for issuer ${issuer}: ${isPinRequired}`)\n return {\n txCode: grantDetails?.tx_code,\n isPinRequired,\n }\n }\n\n private assertAlphanumericPin(pinMeta?: TxCodeAndPinRequired, pin?: string): void {\n if (pinMeta && pinMeta.isPinRequired) {\n let regex\n\n if (pinMeta.txCode) {\n const { input_mode, length } = pinMeta.txCode\n\n if (input_mode === 'numeric') {\n // Create a regex for numeric input. If no length specified, allow any length of numeric input.\n regex = length ? new RegExp(`^\\\\d{1,${length}}$`) : /^\\d+$/\n } else if (input_mode === 'text') {\n // Create a regex for text input. If no length specified, allow any length of alphanumeric input.\n regex = length ? new RegExp(`^[a-zA-Z0-9]{1,${length}}$`) : /^[a-zA-Z0-9]+$/\n }\n }\n\n // Default regex for alphanumeric with no specific length limit if no input_mode is specified.\n regex = regex || /^[a-zA-Z0-9]+$|^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$/\n\n if (!pin || !regex.test(pin)) {\n LOG.warning(\n `Pin is not valid. Expected format: ${pinMeta?.txCode?.input_mode || 'alphanumeric'}, Length: up to ${pinMeta?.txCode?.length || 'any number of'} characters`,\n )\n throw new Error('A valid pin must be present according to the specified transaction code requirements.')\n }\n } else if (pin) {\n LOG.warning('Pin set, whilst not required')\n throw new Error('Cannot set a pin when the pin is not required.')\n }\n }\n\n private assertNonEmptyPreAuthorizedCode(accessTokenRequest: AccessTokenRequest): void {\n if (!accessTokenRequest[PRE_AUTH_CODE_LITERAL]) {\n LOG.warning(`No pre-authorized code present, whilst it is required`, accessTokenRequest)\n throw new Error('Pre-authorization must be proven by presenting the pre-authorized code. Code must be present.')\n }\n }\n\n private assertNonEmptyCodeVerifier(accessTokenRequest: AccessTokenRequest): void {\n if (!accessTokenRequest.code_verifier) {\n LOG.warning('No code_verifier present, whilst it is required', accessTokenRequest)\n throw new Error('Authorization flow requires the code_verifier to be present')\n }\n }\n\n private assertNonEmptyCode(accessTokenRequest: AccessTokenRequest): void {\n if (!accessTokenRequest.code) {\n LOG.warning('No code present, whilst it is required')\n throw new Error('Authorization flow requires the code to be present')\n }\n }\n\n private validate(accessTokenRequest: AccessTokenRequest, pinMeta?: TxCodeAndPinRequired): void {\n if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) {\n this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type)\n this.assertNonEmptyPreAuthorizedCode(accessTokenRequest)\n this.assertAlphanumericPin(pinMeta, accessTokenRequest.tx_code ?? accessTokenRequest.user_pin)\n } else if (accessTokenRequest.grant_type === GrantTypes.AUTHORIZATION_CODE) {\n this.assertAuthorizationGrantType(accessTokenRequest.grant_type)\n this.assertNonEmptyCodeVerifier(accessTokenRequest)\n this.assertNonEmptyCode(accessTokenRequest)\n } else {\n this.throwNotSupportedFlow()\n }\n }\n\n private async sendAuthCode(\n requestTokenURL: string,\n accessTokenRequest: AccessTokenRequest,\n opts?: { headers?: Record<string, string> },\n ): Promise<OpenIDResponse<AccessTokenResponse, DPoPResponseParams>> {\n return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), {\n customHeaders: opts?.headers ? opts.headers : undefined,\n })\n }\n\n public static determineTokenURL({\n asOpts,\n issuerOpts,\n metadata,\n }: {\n asOpts?: AuthorizationServerOpts\n issuerOpts?: IssuerOpts\n metadata?: EndpointMetadata\n }): string {\n if (!asOpts && !metadata?.token_endpoint && !issuerOpts) {\n throw new Error('Cannot determine token URL if no issuer, metadata and no Authorization Server values are present')\n }\n let url\n if (asOpts && asOpts.as) {\n url = this.creatTokenURLFromURL(asOpts.as, asOpts?.allowInsecureEndpoints, asOpts.tokenEndpoint)\n } else if (metadata?.token_endpoint) {\n url = metadata.token_endpoint\n } else {\n if (!issuerOpts?.issuer) {\n throw Error('Either authorization server options, a token endpoint or issuer options are required at this point')\n }\n url = this.creatTokenURLFromURL(issuerOpts.issuer, asOpts?.allowInsecureEndpoints, issuerOpts.tokenEndpoint)\n }\n\n if (!url || !ObjectUtils.isString(url)) {\n throw new Error('No authorization server token URL present. Cannot acquire access token')\n }\n LOG.debug(`Token endpoint determined to be ${url}`)\n return url\n }\n\n private static creatTokenURLFromURL(url: string, allowInsecureEndpoints?: boolean, tokenEndpoint?: string): string {\n if (allowInsecureEndpoints !== true && url.startsWith('http:')) {\n throw Error(\n `Unprotected token endpoints are not allowed ${url}. Use the 'allowInsecureEndpoints' param if you really need this for dev/testing!`,\n )\n }\n const hostname = url.replace(/https?:\\/\\//, '').replace(/\\/$/, '')\n const endpoint = tokenEndpoint ? (tokenEndpoint.startsWith('/') ? tokenEndpoint : tokenEndpoint.substring(1)) : '/token'\n const scheme = url.split('://')[0]\n return `${scheme ? scheme + '://' : 'https://'}${hostname}${endpoint}`\n }\n\n private throwNotSupportedFlow(): void {\n LOG.warning(`Only pre-authorized or authorization code flows supported.`)\n throw new Error('Only pre-authorized-code or authorization code flows are supported')\n }\n}\n","import { assertValidCodeVerifier, CodeChallengeMethod, createCodeChallenge, generateCodeVerifier, PKCEOpts } from '@sphereon/oid4vci-common'\n\nexport const generateMissingPKCEOpts = (pkce: PKCEOpts) => {\n if (pkce.disabled) {\n return pkce\n }\n if (!pkce.codeChallengeMethod) {\n pkce.codeChallengeMethod = CodeChallengeMethod.S256\n }\n if (!pkce.codeVerifier) {\n pkce.codeVerifier = generateCodeVerifier()\n }\n assertValidCodeVerifier(pkce.codeVerifier)\n if (!pkce.codeChallenge) {\n pkce.codeChallenge = createCodeChallenge(pkce.codeVerifier, pkce.codeChallengeMethod)\n }\n return pkce\n}\n","import { NotificationErrorResponse, NotificationRequest, NotificationResponseResult, post } from '@sphereon/oid4vci-common'\n\nimport { CredentialRequestOpts } from '../CredentialRequestClient'\nimport { LOG } from '../types'\n\nexport async function sendNotification(\n credentialRequestOpts: Partial<CredentialRequestOpts>,\n request: NotificationRequest,\n accessToken?: string,\n): Promise<NotificationResponseResult> {\n LOG.info(`Sending status notification event '${request.event}' for id ${request.notification_id}`)\n if (!credentialRequestOpts.notificationEndpoint) {\n throw Error(`Cannot send notification when no notification endpoint is provided`)\n }\n const token = accessToken ?? credentialRequestOpts.token\n const response = await post<NotificationErrorResponse>(credentialRequestOpts.notificationEndpoint, JSON.stringify(request), {\n ...(token && { bearerToken: token }),\n })\n const error = response.errorBody?.error !== undefined\n const result = {\n error,\n response: error ? response.errorBody : undefined,\n }\n if (error) {\n LOG.warning(`Notification endpoint returned an error for event '${request.event}' and id ${request.notification_id}: ${response.errorBody}`)\n } else {\n LOG.debug(`Notification endpoint returned success for event '${request.event}' and id ${request.notification_id}`)\n }\n return result\n}\n","import { VCI_LOGGERS } from '@sphereon/oid4vci-common'\nimport { ISimpleLogger, LogMethod } from '@sphereon/ssi-types'\n\nexport const LOG: ISimpleLogger<string> = VCI_LOGGERS.options('sphereon:oid4vci:client', { methods: [LogMethod.EVENT, LogMethod.DEBUG_PKG] }).get(\n 'sphereon:oid4vci:client',\n)\n","import { getJson, OpenIDResponse, WellKnownEndpoints } from '@sphereon/oid4vci-common'\nimport { Loggers } from '@sphereon/ssi-types'\n\nconst logger = Loggers.DEFAULT.get('sphereon:openid4vci:openid-utils')\n/**\n * Allows to retrieve information from a well-known location\n *\n * @param host The host\n * @param endpointType The endpoint type, currently supports OID4VCI, OIDC and OAuth2 endpoint types\n * @param opts Options, like for instance whether an error should be thrown in case the endpoint doesn't exist\n */\nexport const retrieveWellknown = async <T>(\n host: string,\n endpointType: WellKnownEndpoints,\n opts?: { errorOnNotFound?: boolean },\n): Promise<OpenIDResponse<T>> => {\n const result: OpenIDResponse<T> = await getJson(`${host.endsWith('/') ? host.slice(0, -1) : host}${endpointType}`, {\n exceptionOnHttpErrorStatus: opts?.errorOnNotFound,\n })\n if (result.origResponse.status >= 400) {\n // We only get here when error on not found is false\n logger.debug(`host ${host} with endpoint type ${endpointType} status: ${result.origResponse.status}, ${result.origResponse.statusText}`)\n }\n return result\n}\n","import { uuidv4 } from '@sphereon/oid4vc-common'\nimport { AccessTokenRequest, AccessTokenRequestOpts, Jwt, OpenId4VCIVersion } from '@sphereon/oid4vci-common'\n\nimport { ProofOfPossessionBuilder } from '../ProofOfPossessionBuilder'\n\nexport const createJwtBearerClientAssertion = async (\n request: Partial<AccessTokenRequest>,\n opts: AccessTokenRequestOpts & {\n version?: OpenId4VCIVersion\n },\n): Promise<void> => {\n const { asOpts, credentialIssuer } = opts\n if (asOpts?.clientOpts?.clientAssertionType === 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer') {\n const { clientId = request.client_id, signCallbacks, alg } = asOpts.clientOpts\n let { kid } = asOpts.clientOpts\n if (!clientId) {\n return Promise.reject(Error(`Not client_id supplied, but client-assertion jwt-bearer requested.`))\n } else if (!kid) {\n return Promise.reject(Error(`No kid supplied, but client-assertion jwt-bearer requested.`))\n } else if (typeof signCallbacks?.signCallback !== 'function') {\n return Promise.reject(Error(`No sign callback supplied, but client-assertion jwt-bearer requested.`))\n } else if (!credentialIssuer) {\n return Promise.reject(Error(`No credential issuer supplied, but client-assertion jwt-bearer requested.`))\n }\n if (clientId.startsWith('http') && kid.includes('#')) {\n kid = kid.split('#')[1]\n }\n const jwt: Jwt = {\n header: {\n typ: 'JWT',\n kid,\n alg: alg ?? 'ES256',\n },\n payload: {\n iss: clientId,\n sub: clientId,\n aud: credentialIssuer,\n jti: uuidv4(),\n exp: Math.floor(Date.now()) / 1000 + 60,\n iat: Math.floor(Date.now()) / 1000 - 60,\n },\n }\n const pop = await ProofOfPossessionBuilder.fromJwt({\n jwt,\n callbacks: signCallbacks,\n version: opts.version ?? OpenId4VCIVersion.VER_1_0_15,\n mode: 'JWT',\n }).build()\n request.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'\n request.client_assertion = pop.jwt\n }\n}\n","import { JWK } from '@sphereon/oid4vc-common'\nimport {\n AccessTokenResponse,\n Alg,\n createProofOfPossession,\n EndpointMetadata,\n Jwt,\n NO_JWT_PROVIDED,\n OpenId4VCIVersion,\n PoPMode,\n PROOF_CANT_BE_CONSTRUCTED,\n ProofOfPossession,\n ProofOfPossessionCallbacks,\n Typ,\n} from '@sphereon/oid4vci-common'\n\nexport class ProofOfPossessionBuilder<DIDDoc = never> {\n private readonly proof?: ProofOfPossession\n private readonly callbacks?: ProofOfPossessionCallbacks\n // private readonly version: OpenId4VCIVersion\n private readonly mode: PoPMode = 'pop'\n\n private kid?: string\n private jwk?: JWK\n private aud?: string | string[]\n private clientId?: string\n private issuer?: string\n private jwt?: Jwt\n private alg?: string\n private jti?: string\n private cNonce?: string\n private typ?: Typ\n\n private constructor({\n proof,\n callbacks,\n jwt,\n accessTokenResponse,\n version,\n mode = 'pop',\n }: {\n proof?: ProofOfPossession\n callbacks?: ProofOfPossessionCallbacks\n accessTokenResponse?: AccessTokenResponse\n jwt?: Jwt\n version: OpenId4VCIVersion\n mode?: PoPMode\n }) {\n this.mode = mode\n this.proof = proof\n this.callbacks = callbacks\n // this.version = version\n if (jwt) {\n this.withJwt(jwt)\n } else {\n this.withTyp(mode === 'JWT' ? 'JWT' : 'openid4vci-proof+jwt')\n }\n if (accessTokenResponse) {\n this.withAccessTokenResponse(accessTokenResponse)\n }\n }\n\n static manual({\n jwt,\n callbacks,\n version,\n mode = 'JWT',\n }: {\n jwt?: Jwt\n callbacks: ProofOfPossessionCallbacks\n version: OpenId4VCIVersion\n mode?: PoPMode\n }): ProofOfPossessionBuilder {\n return new ProofOfPossessionBuilder({ callbacks, jwt, version, mode })\n }\n\n static fromJwt({\n jwt,\n callbacks,\n version,\n mode = 'pop',\n }: {\n jwt: Jwt\n callbacks: ProofOfPossessionCallbacks\n version: OpenId4VCIVersion\n mode?: PoPMode\n }): ProofOfPossessionBuilder {\n return new ProofOfPossessionBuilder({ callbacks, jwt, version, mode })\n }\n\n static fromAccessTokenResponse({\n accessTokenResponse,\n callbacks,\n version,\n mode = 'pop',\n }: {\n accessTokenResponse: AccessTokenResponse\n callbacks: ProofOfPossessionCallbacks\n version: OpenId4VCIVersion\n mode?: PoPMode\n }): ProofOfPossessionBuilder {\n return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse, version, mode })\n }\n\n static fromProof(proof: ProofOfPossession, version: OpenId4VCIVersion): ProofOfPossessionBuilder {\n return new ProofOfPossessionBuilder({ proof, version })\n }\n\n withAud(aud: string | string[]): this {\n this.aud = aud\n return this\n }\n\n withClientId(clientId: string): this {\n this.clientId = clientId\n return this\n }\n\n withKid(kid: string): this {\n this.kid = kid\n return this\n }\n\n withJWK(jwk: JWK): this {\n this.jwk = jwk\n return this\n }\n\n withIssuer(issuer: string): this {\n this.issuer = issuer\n return this\n }\n\n withAlg(alg: Alg | string): this {\n this.alg = alg\n return this\n }\n\n withJti(jti: string): this {\n this.jti = jti\n return this\n }\n\n withTyp(typ: Typ): this {\n if (this.mode === 'pop') {\n if (!!typ && typ !== 'openid4vci-proof+jwt') {\n throw Error(`typ must be openid4vci-proof+jwt for version 1.0.11 and up. Provided: ${typ}`)\n }\n } else {\n if (!!typ && typ !== 'JWT') {\n throw Error(`typ must be jwt for version 1.0.10 and below. Provided: ${typ}`)\n }\n }\n this.typ = typ\n return this\n }\n\n withAccessTokenNonce(cNonce: string): this {\n this.cNonce = cNonce\n return this\n }\n\n withAccessTokenResponse(accessToken: AccessTokenResponse): this {\n if (accessToken.c_nonce) {\n this.withAccessTokenNonce(accessToken.c_nonce)\n }\n return this\n }\n\n withEndpointMetadata(endpointMetadata: EndpointMetadata): this {\n this.withIssuer(endpointMetadata.issuer)\n return this\n }\n\n withJwt(jwt: Jwt): this {\n if (!jwt) {\n throw new Error(NO_JWT_PROVIDED)\n }\n this.jwt = jwt\n if (!jwt.header) {\n throw Error(`No JWT header present`)\n } else if (!jwt.payload) {\n throw Error(`No JWT payload present`)\n }\n\n if (jwt.header.kid) {\n this.withKid(jwt.header.kid)\n }\n if (jwt.header.typ) {\n this.withTyp(jwt.header.typ as Typ)\n }\n if (!this.typ) {\n this.withTyp('openid4vci-proof+jwt')\n }\n this.withAlg(jwt.header.alg)\n\n if (Array.isArray(jwt.payload.aud)) {\n // Rather do this than take the first value, as it might be very hard to figure out why something is failing\n throw Error('We cannot handle multiple aud values currently')\n }\n\n if (jwt.payload) {\n if (jwt.payload.iss) this.mode === 'pop' ? this.withClientId(jwt.payload.iss) : this.withIssuer(jwt.payload.iss)\n if (jwt.payload.aud) this.mode === 'pop' ? this.withIssuer(jwt.payload.aud) : this.withAud(jwt.payload.aud)\n if (jwt.payload.jti) this.withJti(jwt.payload.jti)\n if (jwt.payload.nonce) this.withAccessTokenNonce(jwt.payload.nonce)\n }\n return this\n }\n\n public async build(): Promise<ProofOfPossession> {\n if (this.proof) {\n return Promise.resolve(this.proof)\n } else if (this.callbacks) {\n return await createProofOfPossession(\n this.mode,\n this.callbacks,\n {\n typ: this.typ ?? (this.mode === 'JWT' ? 'JWT' : 'openid4vci-proof+jwt'),\n kid: this.kid,\n jwk: this.jwk,\n jti: this.jti,\n alg: this.alg,\n aud: this.aud,\n issuer: this.issuer,\n clientId: this.clientId,\n ...(this.cNonce && { nonce: this.cNonce }),\n },\n this.jwt,\n )\n }\n throw new Error(PROOF_CANT_BE_CONSTRUCTED)\n }\n}\n","import {\n decodeJsonProperties,\n getClientIdFromCredentialOfferPayload,\n getURIComponentsAsArray,\n PRE_AUTH_CODE_LITERAL,\n PRE_AUTH_GRANT_LITERAL,\n UniformCredentialOfferRequest,\n} from '@sphereon/oid4vci-common'\nimport fetch from 'cross-fetch'\n\nexport function isUriEncoded(str: string): boolean {\n const pattern = /%[0-9A-F]{2}/i\n return pattern.test(str)\n}\n\nexport async function handleCredentialOfferUri(uri: string) {\n const uriObj = getURIComponentsAsArray(uri) as unknown as Record<string, string>\n const credentialOfferUri = decodeURIComponent(uriObj['credential_offer_uri'])\n const decodedUri = isUriEncoded(credentialOfferUri) ? decodeURIComponent(credentialOfferUri) : credentialOfferUri\n const response = await fetch(decodedUri)\n\n if (!(response && response.status >= 200 && response.status < 400)) {\n return Promise.reject(\n Error(`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`),\n )\n }\n\n if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {\n return Promise.reject(Error('the credential offer URI endpoint did not return content type application/json'))\n }\n\n return {\n credential_offer: decodeJsonProperties(await response.json()),\n }\n}\n\nexport function constructBaseResponse(request: UniformCredentialOfferRequest, scheme: string, baseUrl: string) {\n const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer)\n const grants = request.credential_offer?.grants\n\n return {\n scheme,\n baseUrl,\n ...(clientId && { clientId }),\n ...request,\n ...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),\n ...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {\n preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL],\n }),\n ...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {\n txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code,\n }),\n }\n}\n","import { dpopTokenRequestNonceError } from '@sphereon/oid4vc-common'\nimport { OpenIDResponse } from '@sphereon/oid4vci-common'\n\nexport type RetryRequestWithDPoPNonce = { ok: true; dpopNonce: string } | { ok: false }\n\nexport function shouldRetryTokenRequestWithDPoPNonce(response: OpenIDResponse<unknown, unknown>): RetryRequestWithDPoPNonce {\n if (!response.errorBody || response.errorBody.error !== dpopTokenRequestNonceError) {\n return { ok: false }\n }\n\n const dPoPNonce = response.origResponse.headers.get('DPoP-Nonce')\n if (!dPoPNonce) {\n throw new Error('Missing required DPoP-Nonce header.')\n }\n\n return { ok: true, dpopNonce: dPoPNonce }\n}\n\nexport function shouldRetryResourceRequestWithDPoPNonce(response: OpenIDResponse<unknown, unknown>): RetryRequestWithDPoPNonce {\n if (!response.errorBody || response.origResponse.status !== 401) {\n return { ok: false }\n }\n\n const wwwAuthenticateHeader = response.origResponse.headers.get('WWW-Authenticate')\n if (!wwwAuthenticateHeader?.includes(dpopTokenRequestNonceError)) {\n return { ok: false }\n }\n\n const dPoPNonce = response.origResponse.headers.get('DPoP-Nonce')\n if (!dPoPNonce) {\n throw new Error('Missing required DPoP-Nonce header.')\n }\n\n return { ok: true, dpopNonce: dPoPNonce }\n}\n","import {\n AuthorizationServerMetadata,\n AuthorizationServerType,\n CredentialIssuerMetadataV1_0_15,\n CredentialOfferPayloadV1_0_15,\n CredentialOfferRequestWithBaseUrl,\n EndpointMetadataResultV1_0_15,\n getIssuerFromCredentialOfferPayload,\n IssuerMetadataV1_0_15,\n OpenIDResponse,\n WellKnownEndpoints,\n} from '@sphereon/oid4vci-common'\nimport { Loggers } from '@sphereon/ssi-types'\n\nimport { retrieveWellknown } from './functions'\n\nconst logger = Loggers.DEFAULT.get('sphereon:oid4vci:metadata')\n\nexport class MetadataClientV1_0_15 {\n /**\n * Retrieve metadata using the Initiation obtained from a previous step\n *\n * @param credentialOffer\n */\n public static async retrieveAllMetadataFromCredentialOffer(\n credentialOffer: CredentialOfferRequestWithBaseUrl,\n ): Promise<EndpointMetadataResultV1_0_15> {\n return MetadataClientV1_0_15.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer as CredentialOfferPayloadV1_0_15)\n }\n\n /**\n * Retrieve the metada using the initiation request obtained from a previous step\n * @param request\n */\n public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayloadV1_0_15): Promise<EndpointMetadataResultV1_0_15> {\n const issuer = getIssuerFromCredentialOfferPayload(request)\n if (issuer) {\n return MetadataClientV1_0_15.retrieveAllMetadata(issuer)\n }\n throw new Error(\"can't retrieve metadata from CredentialOfferRequest. No issuer field is present\")\n }\n\n /**\n * Retrieve all metadata from an issuer\n * @param issuer The issuer URL\n * @param opts\n */\n public static async retrieveAllMetadata(\n issuer: string,\n opts?: {\n errorOnNotFound: boolean\n },\n ): Promise<EndpointMetadataResultV1_0_15> {\n let token_endpoint: string | undefined\n let credential_endpoint: string | undefined\n let nonce_endpoint: string | undefined\n let deferred_credential_endpoint: string | undefined\n let notification_endpoint: string | undefined\n let authorization_endpoint: string | undefined\n let authorization_challenge_endpoint: string | undefined\n let authorizationServerType: AuthorizationServerType = 'OID4VCI'\n let authorization_servers: string[] = [issuer]\n const oid4vciResponse = await MetadataClientV1_0_15.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }) // We will handle errors later, given we will also try other metadata locations\n let credentialIssuerMetadata = oid4vciResponse?.successBody\n if (credentialIssuerMetadata) {\n logger.debug(`Issuer ${issuer} OID4VCI well-known server metadata\\r\\n${JSON.stringify(credentialIssuerMetadata)}`)\n credential_endpoint = credentialIssuerMetadata.credential_endpoint\n nonce_endpoint = credentialIssuerMetadata.nonce_endpoint\n deferred_credential_endpoint = credentialIssuerMetadata.deferred_credential_endpoint\n notification_endpoint = credentialIssuerMetadata.notification_endpoint\n if (credentialIssuerMetadata.token_endpoint) {\n token_endpoint = credentialIssuerMetadata.token_endpoint\n }\n authorization_challenge_endpoint = credentialIssuerMetadata.authorization_challenge_endpoint\n if (credentialIssuerMetadata.authorization_servers) {\n authorization_servers = credentialIssuerMetadata.authorization_servers\n }\n }\n // No specific OID4VCI endpoint. Either can be an OAuth2 AS or an OIDC IDP. Let's start with OIDC first\n // TODO: for now we're taking just the first one\n let response: OpenIDResponse<AuthorizationServerMetadata> = await retrieveWellknown(\n authorization_servers[0],\n WellKnownEndpoints.OPENID_CONFIGURATION,\n { errorOnNotFound: false },\n )\n let authMetadata = response.successBody\n if (authMetadata) {\n logger.debug(`Issuer ${issuer} has OpenID Connect Server metadata in well-known location`)\n authorizationServerType = 'OIDC'\n } else {\n // Now let's do OAuth2\n // TODO: for now we're taking just the first one\n response = await retrieveWellknown(authorization_servers[0], WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false })\n authMetadata = response.successBody\n }\n if (!authMetadata) {\n // We will always throw an error, no matter whether the user provided the option not to, because this is bad.\n if (!authorization_servers.includes(issuer)) {\n throw Error(`Issuer ${issuer} provided a separate authorization server ${authorization_servers}, but that server did not provide metadata`)\n }\n } else {\n logger.debug(`Issuer ${issuer} has ${authorizationServerType} Server metadata in well-known location`)\n if (!authMetadata.authorization_endpoint) {\n console.warn(\n `Issuer ${issuer} of type ${authorizationServerType} has no authorization_endpoint! Will use ${authorization_endpoint}. This only works for pre-authorized flows`,\n )\n } else if (authorization_endpoint && authMetadata.authorization_endpoint !== authorization_endpoint) {\n throw Error(\n `Credential issuer has a different authorization_endpoint (${authorization_endpoint}) from the Authorization Server (${authMetadata.authorization_endpoint})`,\n )\n }\n authorization_endpoint = authMetadata.authorization_endpoint\n if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) {\n throw Error(\n `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`,\n )\n }\n authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint\n if (!authMetadata.token_endpoint) {\n throw Error(`Authorization Server ${authorization_servers} did not provide a token_endpoint`)\n } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {\n throw Error(\n `Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`,\n )\n }\n token_endpoint = authMetadata.token_endpoint\n if (authMetadata.credential_endpoint) {\n if (credential_endpoint && authMetadata.credential_endpoint !== credential_endpoint) {\n logger.debug(\n `Credential issuer has a different credential_endpoint (${credential_endpoint}) from the Authorization Server (${authMetadata.credential_endpoint}). Will use the issuer value`,\n )\n } else {\n credential_endpoint = authMetadata.credential_endpoint\n }\n }\n if (authMetadata.deferred_credential_endpoint) {\n if (deferred_credential_endpoint && authMetadata.deferred_credential_endpoint !== deferred_credential_endpoint) {\n logger.debug(\n `Credential issuer has a different deferred_credential_endpoint (${deferred_credential_endpoint}) from the Authorization Server (${authMetadata.deferred_credential_endpoint}). Will use the issuer value`,\n )\n } else {\n deferred_credential_endpoint = authMetadata.deferred_credential_endpoint\n }\n }\n if (authMetadata.notification_endpoint) {\n if (notification_endpoint && authMetadata.notification_endpoint !== notification_endpoint) {\n logger.debug(\n `Credential issuer has a different notification_endpoint (${notification_endpoint}) from the Authorization Server (${authMetadata.notification_endpoint}). Will use the issuer value`,\n )\n } else {\n notification_endpoint = authMetadata.notification_endpoint\n }\n }\n }\n\n if (!authorization_endpoint) {\n logger.debug(`Issuer ${issuer} does not expose authorization_endpoint, so only pre-auth will be supported`)\n }\n if (!token_endpoint) {\n logger.debug(`Issuer ${issuer} does not have a token_endpoint listed in well-known locations!`)\n if (opts?.errorOnNotFound) {\n throw Error(`Could not deduce the token_endpoint for ${issuer}`)\n } else {\n token_endpoint = `${issuer}${issuer.endsWith('/') ? 'token' : '/token'}`\n }\n }\n if (!credential_endpoint) {\n logger.debug(`Issuer ${issuer} does not have a credential_endpoint listed in well-known locations!`)\n if (opts?.errorOnNotFound) {\n throw Error(`Could not deduce the credential endpoint for ${issuer}`)\n } else {\n credential_endpoint = `${issuer}${issuer.endsWith('/') ? 'credential' : '/credential'}`\n }\n }\n\n // We keep nonce_endpoint internally if you rely on it elsewhere, but do not expose it at top-level in v15 result\n\n if (!credentialIssuerMetadata && authMetadata) {\n // Apparently everything worked out and the issuer is exposing everything in oAuth2/OIDC well-knowns. Spec is vague about this situation, but we can support it\n credentialIssuerMetadata = authMetadata as CredentialIssuerMetadataV1_0_15\n }\n\n // Ensure v15 array form under CI metadata\n const ci = (credentialIssuerMetadata ?? {}) as Partial<CredentialIssuerMetadataV1_0_15>\n const ciAuthorizationServers =\n Array.isArray(ci.authorization_servers) && ci.authorization_servers.length > 0 ? ci.authorization_servers : authorization_servers\n\n const v15CredentialIssuerMetadata: CredentialIssuerMetadataV1_0_15 = {\n credential_issuer: ci.credential_issuer ?? issuer,\n credential_endpoint: credential_endpoint as string, // guaranteed above by your defaults\n authorization_servers: ciAuthorizationServers,\n credential_configurations_supported: ci.credential_configurations_supported ?? {},\n display: ci.display ?? [],\n ...(nonce_endpoint && { nonce_endpoint }),\n ...(deferred_credential_endpoint && { deferred_credential_endpoint }),\n ...(notification_endpoint && { notification_endpoint }),\n }\n\n logger.debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`)\n\n // Return v15-only fields (no legacy top-level authorization_server/authorization_endpoint and no nonce/deferred at top-level)\n return {\n issuer,\n token_endpoint,\n credential_endpoint,\n authorization_challenge_endpoint,\n notification_endpoint,\n authorizationServerType,\n credentialIssuerMetadata: v15CredentialIssuerMetadata,\n authorizationServerMetadata: authMetadata,\n }\n }\n\n /**\n * Retrieve only the OID4VCI metadata for the issuer. So no OIDC/OAuth2 metadata\n *\n * @param issuerHost The issuer hostname\n * @param opts\n */\n public static async retrieveOpenID4VCIServerMetadata(\n issuerHost: string,\n opts?: {\n errorOnNotFound?: boolean\n },\n ): Promise<OpenIDResponse<IssuerMetadataV1_0_15> | undefined> {\n return retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, {\n errorOnNotFound: opts?.errorOnNotFound === undefined ? true : opts.errorOnNotFound,\n })\n }\n}\n","import {\n AuthorizationChallengeCodeResponse,\n AuthorizationChallengeRequestOpts,\n AuthorizationDetailsV1_0_15,\n AuthorizationRequestOpts,\n CodeChallengeMethod,\n CommonAuthorizationChallengeRequest,\n convertJsonToURI,\n CreateRequestObjectMode,\n CredentialConfigurationSupportedV1_0_15,\n CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_15,\n CredentialDefinitionJwtVcJsonV1_0_15,\n CredentialOfferPayloadV1_0_15,\n CredentialOfferRequestWithBaseUrl,\n determineSpecVersionFromOffer,\n EndpointMetadata,\n EndpointMetadataResultV1_0_15,\n formPost,\n IssuerOpts,\n isW3cCredentialSupported,\n JsonURIMode,\n Jwt,\n OpenId4VCIVersion,\n OpenIDResponse,\n PARMode,\n PKCEOpts,\n PushedAuthorizationResponse,\n RequestObjectOpts,\n ResponseType,\n} from '@sphereon/oid4vci-common'\nimport { Loggers } from '@sphereon/ssi-types'\n\nimport { MetadataClient } from './MetadataClient'\nimport { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'\n\nconst logger = Loggers.DEFAULT.get('sphereon:oid4vci')\n\nexport async function createSignedAuthRequestWhenNeeded(\n requestObject: Record<string, any>,\n opts: RequestObjectOpts & {\n aud?: string\n },\n) {\n if (opts.requestObjectMode === CreateRequestObjectMode.REQUEST_URI) {\n throw Error(`Request Object Mode ${opts.requestObjectMode} is not supported yet`)\n } else if (opts.requestObjectMode === CreateRequestObjectMode.REQUEST_OBJECT) {\n if (typeof opts.signCallbacks?.signCallback !== 'function') {\n throw Error(`No request object sign callback found, whilst request object mode was set to ${opts.requestObjectMode}`)\n } else if (!opts.kid) {\n throw Error(`No kid found, whilst request object mode was set to ${opts.requestObjectMode}`)\n }\n let client_metadata: any\n if (opts.clientMetadata || opts.jwksUri) {\n client_metadata = opts.clientMetadata ?? {}\n if (opts.jwksUri) {\n client_metadata['jwks_uri'] = opts.jwksUri\n }\n }\n let authorization_details = requestObject['authorization_details']\n if (typeof authorization_details === 'string') {\n authorization_details = JSON.parse(requestObject.authorization_details)\n }\n if (!requestObject.aud && opts.aud) {\n requestObject.aud = opts.aud\n }\n const iss = requestObject.iss ?? opts.iss ?? requestObject.client_id\n\n const jwt: Jwt = {\n header: { alg: 'ES256', kid: opts.kid, typ: 'JWT' },\n payload: { ...requestObject, iss, authorization_details, ...(client_metadata && { client_metadata }) },\n }\n const pop = await ProofOfPossessionBuilder.fromJwt({\n jwt,\n callbacks: opts.signCallbacks,\n version: OpenId4VCIVersion.VER_1_0_15,\n mode: 'JWT',\n }).build()\n requestObject['request'] = pop.jwt\n }\n}\n\nfunction filterSupportedCredentials(\n credentialOffer: CredentialOfferPayloadV1_0_15,\n credentialsSupported?: Record<string, CredentialConfigurationSupportedV1_0_15>,\n): (CredentialConfigurationSupportedV1_0_15 & {\n configuration_id: string\n})[] {\n if (!credentialOffer.credential_configuration_ids || !credentialsSupported) {\n return []\n }\n return Object.entries(credentialsSupported)\n .filter((entry) => credentialOffer.credential_configuration_ids?.includes(entry[0]))\n .map((entry) => {\n return { ...entry[1], configuration_id: entry[0] }\n })\n}\n\nexport const createAuthorizationRequestUrl = async ({\n pkce,\n endpointMetadata,\n authorizationRequest,\n credentialOffer,\n credentialConfigurationSupported,\n clientId,\n version,\n}: {\n pkce: PKCEOpts\n endpointMetadata: EndpointMetadataResultV1_0_15\n authorizationRequest: AuthorizationRequestOpts\n credentialOffer?: CredentialOfferRequestWithBaseUrl\n credentialConfigurationSupported?: Record<string, CredentialConfigurationSupportedV1_0_15>\n clientId?: string\n version?: OpenId4VCIVersion\n}): Promise<string> => {\n function removeDisplayAndValueTypes(obj: any): any {\n if (Array.isArray(obj)) {\n return obj.map((item) => removeDisplayAndValueTypes(item))\n }\n\n if (typeof obj !== 'object' || obj === null) {\n return obj\n }\n\n const newObj = { ...obj }\n for (const prop in newObj) {\n if (['display', 'value_type'].includes(prop)) {\n delete newObj[prop]\n } else if (typeof newObj[prop] === 'object' && newObj[prop] !== null) {\n newObj[prop] = removeDisplayAndValueTypes(newObj[prop])\n }\n }\n\n return newObj\n }\n\n const { redirectUri, requestObjectOpts = { requestObjectMode: CreateRequestObjectMode.NONE } } = authorizationRequest\n const client_id = clientId ?? authorizationRequest.clientId\n\n // Authorization server metadata takes precedence\n const authorizationMetadata = endpointMetadata.authorizationServerMetadata ?? endpointMetadata.credentialIssuerMetadata\n\n let { authorizationDetails } = authorizationRequest\n const parMode = authorizationMetadata?.require_pushed_authorization_requests\n ? PARMode.REQUIRE\n : (authorizationRequest.parMode ?? (client_id ? PARMode.AUTO : PARMode.NEVER))\n // Scope and authorization_details can be used in the same authorization request\n // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param\n if (!authorizationRequest.scope && !authorizationDetails) {\n if (!credentialOffer) {\n throw Error('Please provide a scope or authorization_details if no credential offer is present')\n }\n if ('credentials' in credentialOffer.credential_offer) {\n throw new Error('CredentialOffer format is wrong.')\n }\n const ver = version ?? determineSpecVersionFromOffer(credentialOffer.credential_offer) ?? OpenId4VCIVersion.VER_1_0_15\n const creds =\n ver === OpenId4VCIVersion.VER_1_0_15\n ? filterSupportedCredentials(credentialOffer.credential_offer as CredentialOfferPayloadV1_0_15, credentialConfigurationSupported)\n : []\n\n authorizationDetails = creds.flatMap((cred) => {\n const locations = [credentialOffer?.credential_offer.credential_issuer ?? endpointMetadata.issuer]\n\n // TODO: credential_configuration_id seems to always be defined?\n const credential_configuration_id: string | undefined = cred.configuration_id\n const format = credential_configuration_id ? undefined : cred.format\n\n if (!credential_configuration_id && !cred.format) {\n throw Error('format is required in authorization details')\n }\n\n // SD-JWT VC\n const vct = cred.format === 'dc+sd-jwt' ? cred.vct : undefined\n const doctype = cred.format === 'mso_mdoc' ? cred.doctype : undefined\n\n // W3C credentials have a credential definition, the rest does not\n let credential_definition: undefined | Partial<CredentialDefinitionJwtVcJsonV1_0_15 | CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_15> =\n undefined\n if (isW3cCredentialSupported(cred) && hasCredentialDefinition(cred)) {\n credential_definition = {\n ...cred.credential_definition,\n // type: OPTIONAL. Array as defined in Appendix A.1.1.2. This claim contains the type values the Wallet requests authorization for at the Credential Issuer. It MUST be present if the claim format is present in the root of the authorization details object. It MUST not be present otherwise.\n // It meens we have a config_id, already mapping it to an explicit format and types\n type: format ? cred.credential_definition.type : undefined,\n credentialSubject: cred.credential_definition.credentialSubject\n ? removeDisplayAndValueTypes(cred.credential_definition.credentialSubject)\n : undefined,\n }\n }\n\n return {\n type: 'openid_credential',\n locations,\n ...(credential_definition && { credential_definition }),\n ...(credential_configuration_id && { credential_configuration_id }),\n ...(format && { format }),\n ...(vct && { vct, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : undefined }),\n ...(doctype && { doctype, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : undefined }),\n } as AuthorizationDetailsV1_0_15\n })\n if (!authorizationDetails || authorizationDetails.length === 0) {\n throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`)\n }\n }\n // v15: authorization endpoint is under Authorization Server metadata (or duplicated in CI metadata)\n const authorizationEndpoint =\n (endpointMetadata as any).authorization_endpoint ??\n (endpointMetadata as EndpointMetadataResultV1_0_15).authorizationServerMetadata?.authorization_endpoint ??\n (endpointMetadata as EndpointMetadataResultV1_0_15).credentialIssuerMetadata?.authorization_endpoint\n\n if (!authorizationEndpoint) {\n throw Error('Server metadata does not contain authorization endpoint')\n }\n const parEndpoint = authorizationMetadata?.pushed_authorization_request_endpoint\n\n let queryObj: Record<s