/**
* AP2 Mandate Verifier
* Verifies JWS mandate tokens (Intent, Cart, Payment) and validates
* the full mandate chain for agentic commerce transactions.
*/
import { jwtVerify, createRemoteJWKSet, importJWK } from 'jose';
import type {
JWK,
JWTVerifyResult,
JWTPayload,
JWTVerifyGetKey,
CryptoKey,
} from 'jose';
import type { Mandate, IntentPayload, CartPayload, PaymentPayload } from '../types.js';
import type { MandateVerificationResult } from './types.js';
export class MandateVerifier {
private directKey: CryptoKey | null = null;
private getKeyFn: JWTVerifyGetKey | null = null;
private initialized = false;
private readonly jwkOrUrl: JWK | string;
/**
* @param jwkOrUrl - Either a public JWK object for local verification,
* or a JWKS URL string for remote key resolution.
*/
constructor(jwkOrUrl: JWK | string) {
this.jwkOrUrl = jwkOrUrl;
}
/** Lazily initialize the verification key / keyset. */
private async ensureInitialized(): Promise<void> {
if (this.initialized) return;
if (typeof this.jwkOrUrl === 'string') {
// Remote JWKS endpoint — produces a key-resolver function
this.getKeyFn = createRemoteJWKSet(new URL(this.jwkOrUrl));
} else {
// Local JWK — import into a CryptoKey
this.directKey = await importJWK(this.jwkOrUrl, 'ES256') as CryptoKey;
}
this.initialized = true;
}
/**
* Call jwtVerify with the correct overload depending on key type.
* When using a remote JWKS, calls the getKey overload.
* When using a local JWK, calls the direct-key overload.
*/
private async verifyToken(token: string): Promise<JWTVerifyResult<JWTPayload>> {
await this.ensureInitialized();
const options = { algorithms: ['ES256'] };
if (this.getKeyFn) {
return jwtVerify(token, this.getKeyFn, options);
}
// directKey is guaranteed non-null when getKeyFn is null after initialization
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return jwtVerify(token, this.directKey!, options);
}
// ────────────────────────────────────────
// Single-mandate verification
// ────────────────────────────────────────
/**
* Verify an Intent Mandate JWS.
* Checks: signature, expiry, typ header, payload constraints.
*/
async verifyIntent(token: string): Promise<MandateVerificationResult> {
try {
const { payload, protectedHeader } = await this.verifyToken(token);
// Type header check
if (protectedHeader.typ !== 'mandate+jwt') {
return { valid: false, mandate: null, error: 'Invalid token type: expected mandate+jwt' };
}
const mandate = payload as unknown as Mandate;
// Must be an intent mandate
if (mandate.type !== 'intent') {
return { valid: false, mandate: null, error: `Expected intent mandate, got "${mandate.type}"` };
}
// Expiry check
const expiryError = this.checkExpiry(mandate);
if (expiryError) {
return { valid: false, mandate: null, error: expiryError };
}
// Status check
if (mandate.status !== 'active') {
return { valid: false, mandate: null, error: `Mandate status is "${mandate.status}", expected "active"` };
}
// Intent-specific constraint validation
const intentPayload = mandate.payload as IntentPayload;
if (intentPayload.max_amount <= 0) {
return { valid: false, mandate: null, error: 'Intent max_amount must be positive' };
}
if (!intentPayload.currency || intentPayload.currency.length !== 3) {
return { valid: false, mandate: null, error: 'Intent currency must be a 3-letter ISO code' };
}
return { valid: true, mandate };
} catch (err) {
return { valid: false, mandate: null, error: this.formatError(err) };
}
}
/**
* Verify a Cart Mandate JWS.
* Checks: signature, expiry, merchant signature present.
*/
async verifyCart(token: string): Promise<MandateVerificationResult> {
try {
const { payload, protectedHeader } = await this.verifyToken(token);
if (protectedHeader.typ !== 'mandate+jwt') {
return { valid: false, mandate: null, error: 'Invalid token type: expected mandate+jwt' };
}
const mandate = payload as unknown as Mandate;
if (mandate.type !== 'cart') {
return { valid: false, mandate: null, error: `Expected cart mandate, got "${mandate.type}"` };
}
const expiryError = this.checkExpiry(mandate);
if (expiryError) {
return { valid: false, mandate: null, error: expiryError };
}
if (mandate.status !== 'active') {
return { valid: false, mandate: null, error: `Mandate status is "${mandate.status}", expected "active"` };
}
// Cart-specific: merchant signature must be present
const cartPayload = mandate.payload as CartPayload;
if (!cartPayload.merchant_signature) {
return { valid: false, mandate: null, error: 'Cart mandate missing merchant_signature' };
}
if (!cartPayload.checkout_id) {
return { valid: false, mandate: null, error: 'Cart mandate missing checkout_id' };
}
if (!cartPayload.line_items || cartPayload.line_items.length === 0) {
return { valid: false, mandate: null, error: 'Cart mandate must contain at least one line item' };
}
return { valid: true, mandate };
} catch (err) {
return { valid: false, mandate: null, error: this.formatError(err) };
}
}
/**
* Verify a Payment Mandate JWS.
* Checks: signature, expiry, amount and references.
*/
async verifyPayment(token: string): Promise<MandateVerificationResult> {
try {
const { payload, protectedHeader } = await this.verifyToken(token);
if (protectedHeader.typ !== 'mandate+jwt') {
return { valid: false, mandate: null, error: 'Invalid token type: expected mandate+jwt' };
}
const mandate = payload as unknown as Mandate;
if (mandate.type !== 'payment') {
return { valid: false, mandate: null, error: `Expected payment mandate, got "${mandate.type}"` };
}
const expiryError = this.checkExpiry(mandate);
if (expiryError) {
return { valid: false, mandate: null, error: expiryError };
}
if (mandate.status !== 'active') {
return { valid: false, mandate: null, error: `Mandate status is "${mandate.status}", expected "active"` };
}
// Payment-specific validation
const paymentPayload = mandate.payload as PaymentPayload;
if (paymentPayload.amount <= 0) {
return { valid: false, mandate: null, error: 'Payment amount must be positive' };
}
if (!paymentPayload.cart_mandate_id) {
return { valid: false, mandate: null, error: 'Payment mandate missing cart_mandate_id reference' };
}
if (!paymentPayload.payment_handler_id) {
return { valid: false, mandate: null, error: 'Payment mandate missing payment_handler_id' };
}
return { valid: true, mandate };
} catch (err) {
return { valid: false, mandate: null, error: this.formatError(err) };
}
}
// ────────────────────────────────────────
// Chain verification
// ────────────────────────────────────────
/**
* Verify that three mandates (intent -> cart -> payment) form a valid chain.
*
* Checks:
* 1. Each individual mandate is valid.
* 2. Cart total does not exceed intent max_amount.
* 3. Cart currency matches intent currency.
* 4. Payment amount matches cart total.
* 5. Payment currency matches cart currency.
* 6. Payment references the cart mandate by ID.
*/
async verifyMandateChain(
intent: string,
cart: string,
payment: string,
): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
// Step 1: verify each mandate individually
const [intentResult, cartResult, paymentResult] = await Promise.all([
this.verifyIntent(intent),
this.verifyCart(cart),
this.verifyPayment(payment),
]);
if (!intentResult.valid) {
errors.push(`Intent mandate invalid: ${intentResult.error ?? 'unknown'}`);
}
if (!cartResult.valid) {
errors.push(`Cart mandate invalid: ${cartResult.error ?? 'unknown'}`);
}
if (!paymentResult.valid) {
errors.push(`Payment mandate invalid: ${paymentResult.error ?? 'unknown'}`);
}
// If any individual mandate failed, skip cross-checks
if (errors.length > 0) {
return { valid: false, errors };
}
// mandates are guaranteed non-null when errors array is empty
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const intentMandate = intentResult.mandate!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const cartMandate = cartResult.mandate!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const paymentMandate = paymentResult.mandate!;
const intentPayload = intentMandate.payload as IntentPayload;
const cartPayload = cartMandate.payload as CartPayload;
const paymentPayload = paymentMandate.payload as PaymentPayload;
// Step 2: Cart total must not exceed intent max_amount
if (cartPayload.totals.total > intentPayload.max_amount) {
errors.push(
`Cart total (${cartPayload.totals.total}) exceeds intent max_amount (${intentPayload.max_amount})`,
);
}
// Step 3: Currency must match between intent and cart
if (cartPayload.totals.currency !== intentPayload.currency) {
errors.push(
`Cart currency (${cartPayload.totals.currency}) does not match intent currency (${intentPayload.currency})`,
);
}
// Step 4: Payment amount must equal cart total
if (paymentPayload.amount !== cartPayload.totals.total) {
errors.push(
`Payment amount (${paymentPayload.amount}) does not match cart total (${cartPayload.totals.total})`,
);
}
// Step 5: Payment currency must match cart currency
if (paymentPayload.currency !== cartPayload.totals.currency) {
errors.push(
`Payment currency (${paymentPayload.currency}) does not match cart currency (${cartPayload.totals.currency})`,
);
}
// Step 6: Payment must reference the cart mandate
if (paymentPayload.cart_mandate_id !== cartMandate.id) {
errors.push(
`Payment cart_mandate_id (${paymentPayload.cart_mandate_id}) does not match cart mandate ID (${cartMandate.id})`,
);
}
// Step 7: If intent restricts merchant_ids, verify cart merchant (issuer)
if (intentPayload.merchant_ids && intentPayload.merchant_ids.length > 0) {
if (!intentPayload.merchant_ids.includes(cartMandate.issuer)) {
errors.push(
`Cart issuer (${cartMandate.issuer}) is not in intent allowed merchant_ids`,
);
}
}
return {
valid: errors.length === 0,
errors,
};
}
// ────────────────────────────────────────
// Internal helpers
// ────────────────────────────────────────
/** Check if a mandate has expired based on expires_at. */
private checkExpiry(mandate: Mandate): string | null {
const now = Date.now();
const expiresAt = new Date(mandate.expires_at).getTime();
if (isNaN(expiresAt)) {
return 'Invalid expires_at timestamp';
}
if (now > expiresAt) {
return `Mandate expired at ${mandate.expires_at}`;
}
return null;
}
/** Format unknown errors into readable strings. */
private formatError(err: unknown): string {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
}