diff --git a/src/client/Mppx.ts b/src/client/Mppx.ts index ae3cbe7a..e919b67d 100644 --- a/src/client/Mppx.ts +++ b/src/client/Mppx.ts @@ -1,4 +1,5 @@ import type * as Challenge from '../Challenge.js' +import * as Errors from '../Errors.js' import type * as Method from '../Method.js' import type * as z from '../zod.js' import * as Fetch from './internal/Fetch.js' @@ -81,6 +82,11 @@ export function create< async createCredential(response: Transport.ResponseOf, context?: unknown) { const challenge = transport.getChallenge(response as never) as Challenge.Challenge + // Validate challenge expiration before creating credential (client-side early rejection) + if (challenge.expires && new Date(challenge.expires) < new Date()) { + throw new Errors.PaymentExpiredError({ expires: challenge.expires }) + } + const mi = methods.find((m) => m.name === challenge.method && m.intent === challenge.intent) if (!mi) throw new Error( diff --git a/src/client/internal/Fetch.ts b/src/client/internal/Fetch.ts index 4cd0093e..57468b9e 100644 --- a/src/client/internal/Fetch.ts +++ b/src/client/internal/Fetch.ts @@ -1,4 +1,5 @@ import * as Challenge from '../../Challenge.js' +import * as Errors from '../../Errors.js' import type * as Method from '../../Method.js' import type * as z from '../../zod.js' @@ -72,6 +73,9 @@ export function from( `No method found for challenges: ${challenges.map((c) => `${c.method}.${c.intent}`).join(', ')}. Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`, ) + // Validate challenge expiration before creating credential (client-side early rejection) + validateChallengeExpiration(challenge) + const onChallengeCredential = onChallenge ? await onChallenge(challenge, { createCredential: async (overrideContext?: AnyContextFor) => @@ -240,6 +244,13 @@ function validateCredentialHeaderValue(credential: string): void { } } +/** @internal Validates that a challenge has not expired. */ +function validateChallengeExpiration(challenge: Challenge.Challenge): void { + if (challenge.expires && new Date(challenge.expires) < new Date()) { + throw new Errors.PaymentExpiredError({ expires: challenge.expires }) + } +} + /** @internal */ async function resolveCredential( challenge: Challenge.Challenge,