Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 89 additions & 11 deletions agent/src/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import { agent } from './agent.js'
import { funkeDeployedAccessCertificate, funkeDeployedRegistrationCertificate } from './eudiTrust.js'
import { getIssuerIdForCredentialConfigurationId, type IssuanceMetadata } from './issuer.js'
import { issuers } from './issuers/index.js'
import {
updatePaymentStatusForWeroCredential,
weroScaConfiguration,
weroScaThirdPartyConfiguration,
} from './issuers/openHorizonBank.js'
import { getX509DcsCertificate, getX509RootCertificate } from './keyMethods/index.js'
import { oidcUrl } from './oidcProvider/provider.js'
import { LimitedSizeCollection } from './utils/LimitedSizeCollection.js'
Expand Down Expand Up @@ -174,6 +179,35 @@ apiRouter.get('/verifier', async (_, response: Response) => {
})
})

apiRouter.post('transaction-status', async (request: Request, response: Response) => {
const authHeader = request.headers.authorization
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : undefined
Comment thread
berendsliedrecht marked this conversation as resolved.

const parseResult = await z.object({ transaction: z.string() }).safeParseAsync(request.body)
if (!parseResult.success) {
agent.config.logger.warn('transaction-status: missing transactionId in request body')
return response.status(400).json({ error: 'Missing transactionId' })
}

const { transaction } = parseResult.data
agent.config.logger.info(`transaction-status: looking up record for transaction ${transaction}`)
const record = await agent.genericRecords.findById(`transaction-status-${transaction}`)

if (!record || record.content.transaction_status_token !== token || !('statusCode' in record.content)) {
agent.config.logger.warn(
`transaction-status: unauthorized - record ${record ? 'found but token mismatch or no statusCode' : 'not found'}`
)
return response.status(401)
}

agent.config.logger.info(
`transaction-status: returning status ${record.content.statusCode} for transaction ${transaction}`
)
return response.json({
status_code: record.content.statusCode,
})
})

// apiRouter.post('/trust-chains', async (request: Request, response: Response) => {
// const parseResult = await z
// .object({
Expand Down Expand Up @@ -262,13 +296,14 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
}
}

console.log('Requesting definition', JSON.stringify(definition, null, 2))
agent.config.logger.debug(`Requesting definition ${JSON.stringify(definition, null, 2)}`)

const queryLanguageDefinition = dcqlQueryFromRequest(definition, purpose)
const credentialIds = queryLanguageDefinition.credentials.map((query) => query.id)

const responseCode = randomUUID()
const redirectUri = redirectUriBase ? `${redirectUriBase}?response_code=${responseCode}` : undefined
const paymentTransactionId = transactionAuthorizationType === 'payment' ? randomUUID() : undefined

// When credential_sets is used we need to add the wero card to each group so that it will always be requested when also requesting a payment
if (transactionAuthorizationType === 'payment') {
Expand Down Expand Up @@ -330,7 +365,7 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
credential_ids: [credentialIds[credentialIds.length - 1]] as [string, ...string[]],
transaction_data_hashes_alg: ['sha-256'],
payload: {
transaction_id: randomUUID(),
transaction_id: paymentTransactionId as string,
amount: `${paymentAmount} EUR`,
date_time: new Date().toISOString(),
payee: {
Expand Down Expand Up @@ -358,24 +393,29 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
responseCodeMap.set(responseCode, verificationSession.id)
}

if (transactionAuthorizationType === 'payment' && paymentTransactionId) {
agent.config.logger.info(
`requests/create: saving PDNG payment record for paymentTransactionId ${paymentTransactionId}`
)
await agent.genericRecords.save({
id: `transaction-status-${paymentTransactionId}`,
content: { statusCode: 'PDNG' },
})
}

const authorizationRequestJwt = verificationSession.authorizationRequestJwt
? Jwt.fromSerializedJwt(verificationSession.authorizationRequestJwt)
: undefined
const authorizationRequestPayload = verificationSession.requestPayload
const dcqlQuery = authorizationRequestPayload.dcql_query
const transactionData = authorizationRequestPayload.verifier_info?.map((e) => ({
...e,
data: typeof e.data === 'string' ? JsonEncoder.fromBase64(e.data) : e.data,
}))

console.log(JSON.stringify(authorizationRequestObject, null, 2))
agent.config.logger.debug(JSON.stringify(authorizationRequestObject, null, 2))
return response.json({
authorizationRequestObject,
authorizationRequestUri: authorizationRequest.replace('openid4vp://', requestScheme),
verificationSessionId: verificationSession.id,
responseStatus: verificationSession.state,
dcqlQuery,
transactionData,
authorizationRequest: authorizationRequestJwt
? {
payload: authorizationRequestJwt.payload.toJson(),
Expand Down Expand Up @@ -409,7 +449,45 @@ async function getVerificationStatus(verificationSession: OpenId4VcVerificationS

if (verificationSession.state === OpenId4VcVerificationSessionState.ResponseVerified) {
const verified = await agent.openid4vc.verifier.getVerifiedAuthorizationResponse(verificationSession.id)
console.log(verified.dcql?.presentationResult)
agent.config.logger.debug(JSON.stringify(verified.dcql?.presentationResult))

// Find the Wero SCA presentation to extract the credential jti, then signal the issuer to update payment status.
// The issuer owns the transaction_status_token — the verifier only passes the jti and payment details.
const weroVcts = [weroScaConfiguration.vct, weroScaThirdPartyConfiguration.vct]
const weroPresentation = Object.values(verified.dcql?.presentations ?? {})
.flat()
.find((p) => weroVcts.includes((p as { prettyClaims?: Record<string, unknown> }).prettyClaims?.vct as string))
const weroJti = (weroPresentation as { prettyClaims?: Record<string, unknown> } | undefined)?.prettyClaims?.jti as
| string
| undefined

const rawPaymentEntry = (authorizationRequestPayload.transaction_data as string[] | undefined)?.find((data) => {
try {
return (
(JSON.parse(Buffer.from(data, 'base64url').toString()) as { type?: string }).type ===
Comment thread
berendsliedrecht marked this conversation as resolved.
Outdated
'urn:eudi:sca:eu.europa.ec:payment:single:1'
)
} catch {
return false
}
})
if (rawPaymentEntry && weroJti) {
const decoded = JSON.parse(Buffer.from(rawPaymentEntry, 'base64url').toString()) as {
payload?: { amount?: string; transaction_id?: string }
}
const paymentTransactionId = decoded.payload?.transaction_id
const amount = parseFloat(decoded.payload?.amount ?? '0')
agent.config.logger.info(
`getVerificationStatus: Wero credential jti ${weroJti}, paymentTransactionId ${paymentTransactionId}, amount ${amount}`
)
if (paymentTransactionId) {
await updatePaymentStatusForWeroCredential(weroJti, paymentTransactionId, amount)
}
} else {
agent.config.logger.debug(
`getVerificationStatus: no payment update - weroJti ${weroJti ?? 'not found'}, rawPaymentEntry ${rawPaymentEntry ? 'found' : 'not found'}`
)
}

const presentations = await Promise.all(
Object.values(verified.dcql?.presentations ?? {})
Expand Down Expand Up @@ -497,7 +575,7 @@ async function getVerificationStatus(verificationSession: OpenId4VcVerificationS
}))
: undefined

console.log('presentations', presentations)
agent.config.logger.debug(`presentations ${JSON.stringify(presentations)}`)

return {
verificationSessionId: verificationSession.id,
Expand Down Expand Up @@ -559,7 +637,7 @@ apiRouter.get('/requests/:verificationSessionId', async (request, response) => {
})

apiRouter.use((error: Error, _request: Request, response: Response, _next: NextFunction) => {
console.error('Unhandled error', error)
agent.config.logger.error(`Unhandled error ${error}`)
return response.status(500).json({
error: error.message,
})
Expand Down
41 changes: 38 additions & 3 deletions agent/src/issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import {
import { cborDecode, cborEncode } from '@owf/mdoc'
import { randomUUID } from 'crypto'
import { agent } from './agent.js'
import { AGENT_HOST } from './constants.js'
import { bdrIssuer } from './issuers/bdr.js'
import { issuers, issuersCredentialsData } from './issuers/index.js'
import { kolnIssuer } from './issuers/koln.js'
import { krankenkasseIssuer } from './issuers/krankenkasse.js'
import { weroScaConfiguration, weroScaThirdPartyConfiguration } from './issuers/openHorizonBank.js'
import { steuernIssuer } from './issuers/steuern.js'
import { telOrgIssuer } from './issuers/telOrg.js'
import { getX509DcsCertificate } from './keyMethods/index.js'
Expand Down Expand Up @@ -504,9 +506,8 @@ export const credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToC
})),
} satisfies SerializableMdocSignOptions

console.log(
'decoded',
cborDecode(Buffer.from(signOptions.credentials[0].namespaces, 'base64url'), { mapsAsObjects: true })
agent.config.logger.debug(
`decoded ${JSON.stringify(cborDecode(Buffer.from(signOptions.credentials[0].namespaces, 'base64url'), { mapsAsObjects: true }))}`
)
}
}
Expand Down Expand Up @@ -545,6 +546,40 @@ export const credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToC
}
}

// For Wero SCA SD-JWT credentials, inject a per-credential transaction status token
// We manually set the `jti` here so the issuer can refer later to update the status
if (
signOptions &&
credentialData.format === ClaimFormat.SdJwtDc &&
(normalizedCredentialConfigurationId === weroScaConfiguration.scope ||
normalizedCredentialConfigurationId === weroScaThirdPartyConfiguration.scope)
) {
const jti = randomUUID() as string
const transaction_status_token = randomUUID() as string
const sdJwtSignOptions = signOptions as SerializableSdJwtVcSignOptions

signOptions = {
...sdJwtSignOptions,
credentials: sdJwtSignOptions.credentials.map((credential) => ({
...credential,
payload: {
...credential.payload,
jti,
credential_metadata: {
transaction_status_url: `${AGENT_HOST}/api/transaction-status`,
transaction_status_token,
},
Comment thread
berendsliedrecht marked this conversation as resolved.
Outdated
},
})),
} satisfies SerializableSdJwtVcSignOptions

await agent.genericRecords.save({
id: `wero-credential-token-${jti}`,
content: { transaction_status_token },
})
agent.config.logger.info(`issuer: saved Wero SCA transaction status record for jti ${jti}`)
}

const issuanceMetadata: IssuanceMetadata = issuanceSession.issuanceMetadata ?? {}
if (issuanceMetadata.deferInterval) {
issuanceMetadata.signOptions = signOptions
Expand Down
31 changes: 31 additions & 0 deletions agent/src/issuers/openHorizonBank.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Kms } from '@credo-ts/core'
import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc'
import { agent } from '../agent.js'
import { AGENT_HOST } from '../constants.js'
import type { CredentialConfigurationDisplay, PlaygroundIssuerOptions, SdJwtConfiguration } from '../issuer.js'
import type { StaticSdJwtSignInput } from '../types.js'
Expand Down Expand Up @@ -198,6 +199,36 @@ export const openHorizonbankCredentialMetadata = {
},
}

export async function updatePaymentStatusForWeroCredential(
jti: string,
paymentTransactionId: string,
amount: number
): Promise<void> {
const statusCode = amount > 100 ? 'RJCT' : 'ACSC'
agent.config.logger.info(
`openHorizonBank: updating payment ${paymentTransactionId} for credential ${jti}, amount ${amount} -> ${statusCode}`
)

const credentialTokenRecord = await agent.genericRecords.findById(`wero-credential-token-${jti}`)
if (!credentialTokenRecord) {
agent.config.logger.warn(`openHorizonBank: no credential token record found for jti ${jti}`)
return
}

const paymentRecord = await agent.genericRecords.findById(`transaction-status-${paymentTransactionId}`)
if (!paymentRecord || paymentRecord.content.statusCode !== 'PDNG') {
agent.config.logger.warn(
`openHorizonBank: payment record ${paymentTransactionId} not found or not PDNG (current: ${paymentRecord?.content.statusCode})`
)
return
}

paymentRecord.content.transaction_status_token = credentialTokenRecord.content.transaction_status_token
paymentRecord.content.statusCode = statusCode
await agent.genericRecords.update(paymentRecord)
agent.config.logger.info(`openHorizonBank: payment ${paymentTransactionId} updated to ${statusCode}`)
}

export const openHorizonBankCredentialsData = {
[weroScaData.credentialConfigurationId]: weroScaData,
[weroScaThirdPartyData.credentialConfigurationId]: weroScaThirdPartyData,
Expand Down
5 changes: 2 additions & 3 deletions agent/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import '@openwallet-foundation/askar-nodejs'
import { JwsService, JwtPayload, Kms } from '@credo-ts/core'
import type { Request, Response } from 'express'

import express from 'express'
import path from 'path'
import { agent } from './agent.js'
Expand Down Expand Up @@ -128,7 +127,7 @@ async function run() {

// Hack for making images available
if (AGENT_HOST.includes('ngrok') || AGENT_HOST.includes('.ts.net') || AGENT_HOST.includes('localhost')) {
console.log(path.join(dirname, '../../app/public/assets'))
agent.config.logger.debug(path.join(dirname, '../../app/public/assets'))
app.use('/assets', express.static(path.join(dirname, '../../app/public/assets')))
}

Expand Down Expand Up @@ -192,7 +191,7 @@ async function run() {

// @ts-expect-error
app.use((err, _, res, __) => {
console.error(err.stack)
agent.config.logger.error(err.stack)
res.status(500).send('Something broke!')
})
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"devDependencies": {
"@biomejs/biome": "2.3.11"
},
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
"packageManager": "pnpm@11.6.0"
}
7 changes: 7 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ packages:
- agent
- app

allowBuilds:
'@openwallet-foundation/askar-nodejs': true
cbor-extract: false
esbuild: false
koffi: true
sharp: false

ignoredBuiltDependencies:
- es5-ext
- esbuild
Expand Down
Loading