@sphereon/oid4vci-client
Version:
OpenID for Verifiable Credential Issuance (OpenID4VCI) client
1 lines • 377 kB
Source Map (JSON)
{"version":3,"sources":["../lib/index.ts","../lib/AccessTokenClient.ts","../lib/MetadataClientV1_0_13.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/AccessTokenClientV1_0_11.ts","../lib/AuthorizationCodeClient.ts","../lib/MetadataClient.ts","../lib/MetadataClientV1_0_11.ts","../lib/AuthorizationCodeClientV1_0_11.ts","../lib/CredentialRequestClient.ts","../lib/CredentialOfferClient.ts","../lib/CredentialOfferClientV1_0_11.ts","../lib/CredentialOfferClientV1_0_13.ts","../lib/CredentialRequestClientV1_0_11.ts","../lib/CredentialRequestClientBuilder.ts","../lib/CredentialRequestClientBuilderV1_0_11.ts","../lib/CredentialRequestClientBuilderV1_0_13.ts","../lib/OpenID4VCIClient.ts","../lib/OpenID4VCIClientV1_0_13.ts","../lib/OpenID4VCIClientV1_0_11.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 './AccessTokenClientV1_0_11'\nexport * from './AuthorizationCodeClient'\nexport * from './AuthorizationCodeClientV1_0_11'\nexport * from './CredentialRequestClient'\nexport * from './CredentialOfferClient'\nexport * from './CredentialOfferClientV1_0_11'\nexport * from './CredentialOfferClientV1_0_13'\nexport * from './CredentialRequestClientV1_0_11'\nexport * from './CredentialRequestClientBuilder'\nexport * from './CredentialRequestClientBuilderV1_0_13'\nexport * from './CredentialRequestClientBuilderV1_0_11'\nexport * from './functions'\nexport * from './MetadataClient'\nexport * from './MetadataClientV1_0_13'\nexport * from './MetadataClientV1_0_11'\nexport * from './OpenID4VCIClient'\nexport * from './OpenID4VCIClientV1_0_13'\nexport * from './OpenID4VCIClientV1_0_11'\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 { MetadataClientV1_0_13 } from './MetadataClientV1_0_13'\nimport { createJwtBearerClientAssertion } from './functions'\nimport { shouldRetryTokenRequestWithDPoPNonce } from './functions/dpopUtil'\nimport { LOG } from './types'\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_13.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 {\n AuthorizationServerMetadata,\n AuthorizationServerType,\n CredentialIssuerMetadataV1_0_13,\n CredentialOfferPayloadV1_0_13,\n CredentialOfferRequestWithBaseUrl,\n EndpointMetadataResultV1_0_13,\n getIssuerFromCredentialOfferPayload,\n IssuerMetadataV1_0_13,\n OpenIDResponse,\n WellKnownEndpoints,\n} from '@sphereon/oid4vci-common'\nimport { Loggers } from '@sphereon/ssi-types'\n\nimport { retrieveWellknown } from './functions'\nconst logger = Loggers.DEFAULT.get('sphereon:oid4vci:metadata')\n\nexport class MetadataClientV1_0_13 {\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_13> {\n return MetadataClientV1_0_13.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13)\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_13): Promise<EndpointMetadataResultV1_0_13> {\n const issuer = getIssuerFromCredentialOfferPayload(request)\n if (issuer) {\n return MetadataClientV1_0_13.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(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<EndpointMetadataResultV1_0_13> {\n let token_endpoint: string | undefined\n let credential_endpoint: string | undefined\n let deferred_credential_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_13.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 deferred_credential_endpoint = credentialIssuerMetadata.deferred_credential_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 {\n errorOnNotFound: false,\n },\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 if (!authorizationServerType) {\n authorizationServerType = 'OAuth 2.0'\n }\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 }\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 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_13\n }\n logger.debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`)\n return {\n issuer,\n token_endpoint,\n credential_endpoint,\n deferred_credential_endpoint,\n authorization_server: authorization_servers[0],\n authorization_endpoint,\n authorization_challenge_endpoint,\n authorizationServerType,\n credentialIssuerMetadata: credentialIssuerMetadata,\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_13> | undefined> {\n return retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, {\n errorOnNotFound: opts?.errorOnNotFound === undefined ? true : opts.errorOnNotFound,\n })\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_13,\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(version < OpenId4VCIVersion.VER_1_0_11 || 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' && this.version >= OpenId4VCIVersion.VER_1_0_11) {\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 && this.version >= OpenId4VCIVersion.VER_1_0_11) {\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.version < OpenId4VCIVersion.VER_1_0_11 || 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(`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`)\n }\n\n if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {\n return Promise.reject('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 { createDPoP, CreateDPoPClientOpts, getCreateDPoPOptions } from '@sphereon/oid4vc-common'\nimport {\n AccessTokenRequest,\n AccessTokenRequestOpts,\n AccessTokenResponse,\n assertedUniformCredentialOffer,\n AuthorizationServerOpts,\n AuthzFlowType,\n convertJsonToURI,\n CredentialOfferV1_0_11,\n CredentialOfferV1_0_13,\n DPoPResponseParams,\n EndpointMetadata,\n formPost,\n getIssuerFromCredentialOfferPayload,\n GrantTypes,\n IssuerOpts,\n JsonURIMode,\n OpenId4VCIVersion,\n OpenIDResponse,\n PRE_AUTH_CODE_LITERAL,\n PRE_AUTH_GRANT_LITERAL,\n TokenErrorResponse,\n toUniformCredentialOfferRequest,\n UniformCredentialOfferPayload,\n} from '@sphereon/oid4vci-common'\nimport { Loggers, ObjectUtils } from '@sphereon/ssi-types'\n\nimport { MetadataClientV1_0_13 } from './MetadataClientV1_0_13'\nimport { createJwtBearerClientAssertion } from './functions'\nimport { shouldRetryTokenRequestWithDPoPNonce } from './functions/dpopUtil'\n\nconst logger = Loggers.DEFAULT.get('sphereon:oid4vci:token')\n\nexport class AccessTokenClientV1_0_11 {\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 isPinRequired = credentialOffer && this.isPinRequiredValue(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: opts.pinMetadata,\n }),\n isPinRequired,\n metadata,\n asOpts,\n issuerOpts,\n createDPoPOpts,\n })\n }\n\n public async acquireAccessTokenUsingRequest({\n accessTokenRequest,\n isPinRequired,\n metadata,\n asOpts,\n createDPoPOpts,\n issuerOpts,\n }: {\n accessTokenRequest: AccessTokenRequest\n isPinRequired?: boolean\n metadata?: EndpointMetadata\n asOpts?: AuthorizationServerOpts\n issuerOpts?: IssuerOpts\n createDPoPOpts?: CreateDPoPClientOpts\n }): Promise<OpenIDResponse<AccessTokenResponse, DPoPResponseParams>> {\n this.validate(accessTokenRequest, isPinRequired)\n\n const requestTokenURL = AccessTokenClientV1_0_11.determineTokenURL({\n asOpts,\n issuerOpts,\n metadata: metadata\n ? metadata\n : issuerOpts?.fetchMetadata\n ? await MetadataClientV1_0_13.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 const credentialOfferRequest = opts.credentialOffer\n ? await toUniformCredentialOfferRequest(opts.credentialOffer as CredentialOfferV1_0_11 | CredentialOfferV1_0_13)\n : undefined\n const request: Partial<AccessTokenRequest> = { ...opts.additionalParams }\n const credentialIssuer = opts.credentialIssuer ?? credentialOfferRequest?.credential_offer?.credential_issuer ?? opts.metadata?.issuer\n\n if (asOpts?.clientOpts?.clientId) {\n request.client_id = asOpts.clientOpts.clientId\n }\n await createJwtBearerClientAssertion(request, { ...opts, version: OpenId4VCIVersion.VER_1_0_11, 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.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin)\n request.user_pin = 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 throw new Error('Credential offer request does not follow 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 PRE_AUTH_GRANT_LITERAL')\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 isPinRequiredValue(requestPayload: UniformCredentialOfferPayload): boolean {\n let isPinRequired = false\n if (!requestPayload) {\n throw new Error(TokenErrorResponse.invalid_request)\n }\n const issuer = getIssuerFromCredentialOfferPayload(requestPayload)\n if (requestPayload.grants?.[PRE_AUTH_GRANT_LITERAL]) {\n isPinRequired = requestPayload.grants[PRE_AUTH_GRANT_LITERAL]?.user_pin_required ?? false\n }\n logger.debug(`Pin required for issuer ${issuer}: ${isPinRequired}`)\n return isPinRequired\n }\n\n private assertNumericPin(isPinRequired?: boolean, pin?: string): void {\n if (isPinRequired) {\n if (!pin || !/^\\d{1,8}$/.test(pin)) {\n logger.debug(`Pin is not 1 to 8 digits long`)\n throw new Error('A valid pin consisting of maximal 8 numeric characters must be present.')\n }\n } else if (pin) {\n logger.debug(`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 logger.debug(`No pre-authorized code present, whilst it is required`)\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 logger.debug('No code_verifier present, whilst it is required')\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 logger.debug('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, isPinRequired?: boolean): void {\n if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) {\n this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type)\n this.assertNonEmptyPreAuthorizedCode(accessTokenRequest)\n this.assertNumericPin(isPinRequired, 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>> {\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,