From bc1fd3eaf839817a928074ef45c1a9d8504f8ec0 Mon Sep 17 00:00:00 2001 From: Ryan Aubrey Date: Mon, 24 Aug 2026 23:44:40 -0400 Subject: [PATCH 1/4] Honor Payment-Authorization in mpp pay. When a 402 challenge advertises header="Payment-Authorization", send the Payment credential in that field so ordinary Authorization can coexist. Co-authored-by: Cursor Committed-By-Agent: cursor --- .changeset/payment-authorization-header.md | 5 ++ packages/cli/src/__tests__/cli.test.ts | 47 ++++++++++- .../commands/mpp/credential-header.test.ts | 83 +++++++++++++++++++ .../cli/src/commands/mpp/credential-header.ts | 70 ++++++++++++++++ packages/cli/src/commands/mpp/decode.test.ts | 21 +++++ packages/cli/src/commands/mpp/decode.ts | 13 +++ packages/cli/src/commands/mpp/pay.tsx | 38 +++++++-- 7 files changed, 269 insertions(+), 8 deletions(-) create mode 100644 .changeset/payment-authorization-header.md create mode 100644 packages/cli/src/commands/mpp/credential-header.test.ts create mode 100644 packages/cli/src/commands/mpp/credential-header.ts diff --git a/.changeset/payment-authorization-header.md b/.changeset/payment-authorization-header.md new file mode 100644 index 00000000..d4a89d6e --- /dev/null +++ b/.changeset/payment-authorization-header.md @@ -0,0 +1,5 @@ +--- +"@stripe/link-cli": patch +--- + +Honor advertised Payment-Authorization headers in `mpp pay` so Payment credentials can coexist with ordinary Authorization diff --git a/packages/cli/src/__tests__/cli.test.ts b/packages/cli/src/__tests__/cli.test.ts index e717186f..3c3bcddf 100644 --- a/packages/cli/src/__tests__/cli.test.ts +++ b/packages/cli/src/__tests__/cli.test.ts @@ -2449,7 +2449,7 @@ describe('production mode', () => { ].join(' '); function decodeCredential(authorizationHeader: string): { - challenge: { intent: string }; + challenge: { intent: string; header?: string }; payload: Record; } { const encoded = authorizationHeader.replace(/^Payment\s+/i, ''); @@ -2484,6 +2484,51 @@ describe('production mode', () => { expect(merchantRequests[1].headers.authorization).toMatch(/^Payment /); }); + it('retries with Payment-Authorization when the challenge advertises that header', async () => { + const wwwAuthenticate = [ + 'Payment id="ch_001",', + 'realm="127.0.0.1",', + 'method="stripe",', + 'intent="charge",', + 'header="Payment-Authorization",', + `request="${Buffer.from(JSON.stringify({ networkId: 'net_001', amount: '1000', currency: 'usd', decimals: 2, paymentMethodTypes: ['card'] })).toString('base64')}",`, + 'expires="2099-01-01T00:00:00Z"', + ].join(' '); + + setNextResponse(200, APPROVED_SPT_REQUEST); + setMerchantResponse(402, '{"error":"payment required"}', { + 'www-authenticate': wwwAuthenticate, + }); + setMerchantResponse(200, '{"success":true}'); + + const result = await runProdCli( + 'mpp', + 'pay', + `http://127.0.0.1:${merchantPort}/api/charge`, + '--spend-request-id', + 'lsrq_spt_001', + '--header', + 'Authorization: Bearer app-token', + '--json', + ); + + expect(result.exitCode).toBe(0); + expect(merchantRequests).toHaveLength(2); + expect(merchantRequests[1].headers.authorization).toBe( + 'Bearer app-token', + ); + expect(merchantRequests[1].headers['payment-authorization']).toMatch( + /^Payment /, + ); + const credential = decodeCredential( + merchantRequests[1].headers['payment-authorization'] as string, + ); + expect(credential.challenge).toMatchObject({ + intent: 'charge', + header: 'Payment-Authorization', + }); + }); + it('returns structured response when the paid retry fails', async () => { setNextResponse(200, APPROVED_SPT_REQUEST); setMerchantResponse(402, '{"error":"payment required"}', { diff --git a/packages/cli/src/commands/mpp/credential-header.test.ts b/packages/cli/src/commands/mpp/credential-header.test.ts new file mode 100644 index 00000000..424690a7 --- /dev/null +++ b/packages/cli/src/commands/mpp/credential-header.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest'; +import { + DEFAULT_CREDENTIAL_HEADER, + PAYMENT_AUTHORIZATION_HEADER, + canonicalizeCredentialHeader, + resolvePaymentCredentialHeader, + shouldEchoCredentialHeader, +} from './credential-header'; + +const STRIPE_REQUEST = Buffer.from( + JSON.stringify({ + amount: '1000', + currency: 'usd', + methodDetails: { networkId: 'net_001', paymentMethodTypes: ['card'] }, + }), +).toString('base64'); + +describe('resolvePaymentCredentialHeader', () => { + it('defaults to Authorization when the challenge omits header', () => { + const wwwAuthenticate = [ + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + `request="${STRIPE_REQUEST}"`, + ].join(' '); + + expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( + DEFAULT_CREDENTIAL_HEADER, + ); + }); + + it('uses Payment-Authorization when the stripe challenge advertises it', () => { + const wwwAuthenticate = [ + 'Payment id="tempo_001", realm="merchant.example", method="tempo", intent="charge", request="e30=",', + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + 'header="Payment-Authorization",', + `request="${STRIPE_REQUEST}"`, + ].join(' '); + + expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( + PAYMENT_AUTHORIZATION_HEADER, + ); + }); + + it('does not inherit header from a different Payment challenge', () => { + const wwwAuthenticate = [ + 'Payment id="tempo_001", realm="merchant.example", method="tempo", intent="charge",', + 'header="Payment-Authorization", request="e30=",', + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + `request="${STRIPE_REQUEST}"`, + ].join(' '); + + expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( + DEFAULT_CREDENTIAL_HEADER, + ); + }); + + it('rejects an unsupported advertised header', () => { + const wwwAuthenticate = [ + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + 'header="X-Payment",', + `request="${STRIPE_REQUEST}"`, + ].join(' '); + + expect(() => + resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001'), + ).toThrow(/Unsupported payment credential header/i); + }); +}); + +describe('canonicalizeCredentialHeader', () => { + it('treats omitted and Authorization values as the default', () => { + expect(canonicalizeCredentialHeader(undefined)).toBe( + DEFAULT_CREDENTIAL_HEADER, + ); + expect(canonicalizeCredentialHeader('authorization')).toBe( + DEFAULT_CREDENTIAL_HEADER, + ); + }); + + it('echoes only non-default headers', () => { + expect(shouldEchoCredentialHeader(DEFAULT_CREDENTIAL_HEADER)).toBe(false); + expect(shouldEchoCredentialHeader(PAYMENT_AUTHORIZATION_HEADER)).toBe(true); + }); +}); diff --git a/packages/cli/src/commands/mpp/credential-header.ts b/packages/cli/src/commands/mpp/credential-header.ts new file mode 100644 index 00000000..d9b98ec9 --- /dev/null +++ b/packages/cli/src/commands/mpp/credential-header.ts @@ -0,0 +1,70 @@ +export const DEFAULT_CREDENTIAL_HEADER = 'Authorization'; +export const PAYMENT_AUTHORIZATION_HEADER = 'Payment-Authorization'; + +/** + * HTTP field the client must use for the Payment credential. + * + * mppx 0.8.x drops unknown WWW-Authenticate auth-params (including `header`) + * when parsing challenges, so this reads `header` from the raw challenge + * string. Omitted `header` defaults to Authorization. The only advertised + * alternate this CLI supports is Payment-Authorization. + */ +export function resolvePaymentCredentialHeader( + wwwAuthenticate: string, + challengeId: string, +): string { + const chunk = paymentSchemeChunks(wwwAuthenticate).find( + (scheme) => authParam(scheme, 'id') === challengeId, + ); + return canonicalizeCredentialHeader( + chunk ? authParam(chunk, 'header') : undefined, + ); +} + +export function canonicalizeCredentialHeader( + value: string | undefined, +): string { + if (value == null || value === '') { + return DEFAULT_CREDENTIAL_HEADER; + } + if (equalsHeaderName(value, DEFAULT_CREDENTIAL_HEADER)) { + return DEFAULT_CREDENTIAL_HEADER; + } + if (equalsHeaderName(value, PAYMENT_AUTHORIZATION_HEADER)) { + return PAYMENT_AUTHORIZATION_HEADER; + } + throw new Error( + `Unsupported payment credential header "${value}". Only Authorization (default) and Payment-Authorization are supported.`, + ); +} + +export function shouldEchoCredentialHeader(header: string): boolean { + return !equalsHeaderName(header, DEFAULT_CREDENTIAL_HEADER); +} + +function equalsHeaderName(left: string, right: string): boolean { + return left.toLowerCase() === right.toLowerCase(); +} + +function paymentSchemeChunks(wwwAuthenticate: string): string[] { + const starts: number[] = []; + for (const match of wwwAuthenticate.matchAll(/Payment\s+/gi)) { + if (match.index !== undefined) starts.push(match.index); + } + return starts.map((start, index) => { + const nextStart = starts[index + 1]; + const end = nextStart === undefined ? wwwAuthenticate.length : nextStart; + return wwwAuthenticate.slice(start, end).replace(/,\s*$/, ''); + }); +} + +function authParam(chunk: string, name: string): string | undefined { + const pattern = new RegExp( + `(?:^|[,\\s])${name}\\s*=\\s*(?:"((?:\\\\.|[^"\\\\])*)"|([^,\\s]+))`, + 'i', + ); + const match = chunk.match(pattern); + if (!match) return undefined; + if (match[1] !== undefined) return match[1].replace(/\\(.)/g, '$1'); + return match[2]; +} diff --git a/packages/cli/src/commands/mpp/decode.test.ts b/packages/cli/src/commands/mpp/decode.test.ts index ca5a102d..265d427b 100644 --- a/packages/cli/src/commands/mpp/decode.test.ts +++ b/packages/cli/src/commands/mpp/decode.test.ts @@ -35,6 +35,27 @@ describe('decodeStripeChallenge', () => { }); }); + it('includes header when the stripe challenge advertises Payment-Authorization', () => { + const header = [ + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + 'header="Payment-Authorization",', + `request="${encodeRequest({ + amount: '1000', + currency: 'usd', + methodDetails: { + networkId: 'net_001', + paymentMethodTypes: ['card'], + }, + })}"`, + ].join(' '); + + expect(decodeStripeChallenge(header)).toMatchObject({ + id: 'ch_001', + header: 'Payment-Authorization', + network_id: 'net_001', + }); + }); + it('handles escaped quoted-string values in challenge parameters', () => { const header = [ 'Payment id="ch_001",', diff --git a/packages/cli/src/commands/mpp/decode.ts b/packages/cli/src/commands/mpp/decode.ts index 55d4c21d..fa5174d3 100644 --- a/packages/cli/src/commands/mpp/decode.ts +++ b/packages/cli/src/commands/mpp/decode.ts @@ -1,5 +1,9 @@ import { Challenge } from 'mppx'; import { sanitizeDeep } from '../../utils/sanitize-text'; +import { + resolvePaymentCredentialHeader, + shouldEchoCredentialHeader, +} from './credential-header'; type StripeChargeChallenge = Challenge.Challenge< Record, @@ -21,6 +25,8 @@ export interface DecodedStripeChallenge { description?: string; digest?: string; expires?: string; + /** Present only when the challenge advertised a non-default credential field. */ + header?: string; network_id: string; request_json: Record; } @@ -118,6 +124,10 @@ export function decodeStripeChallenge( const { challenge, networkId, request } = resolveStripeChallenge( Challenge.deserializeList(challengeHeader), ); + const credentialHeader = resolvePaymentCredentialHeader( + challengeHeader, + challenge.id, + ); return sanitizeDeep({ id: challenge.id, @@ -127,6 +137,9 @@ export function decodeStripeChallenge( description: challenge.description, digest: challenge.digest, expires: challenge.expires, + ...(shouldEchoCredentialHeader(credentialHeader) + ? { header: credentialHeader } + : {}), network_id: networkId, request_json: request, }); diff --git a/packages/cli/src/commands/mpp/pay.tsx b/packages/cli/src/commands/mpp/pay.tsx index 0bb03062..22df1916 100644 --- a/packages/cli/src/commands/mpp/pay.tsx +++ b/packages/cli/src/commands/mpp/pay.tsx @@ -10,6 +10,10 @@ import { Methods as StripeMethods } from 'mppx/stripe'; import React, { useEffect, useState } from 'react'; import { pollUntilApproved } from '../../utils/poll-until-approved'; import { sanitizeDeep } from '../../utils/sanitize-text'; +import { + resolvePaymentCredentialHeader, + shouldEchoCredentialHeader, +} from './credential-header'; import { decodeStripeChallenge, getStripeChargeChallengeFromResponse, @@ -52,11 +56,19 @@ export async function readPayResult(response: Response): Promise { }); } -function createStripePaymentClient(spt: string) { +function withAdvertisedHeader( + challenge: T, + credentialHeader: string, +): T { + if (!shouldEchoCredentialHeader(credentialHeader)) return challenge; + return { ...challenge, header: credentialHeader } as T; +} + +function createStripePaymentClient(spt: string, credentialHeader: string) { const stripeCharge = Method.toClient(StripeMethods.charge, { async createCredential({ challenge }) { return Credential.serialize({ - challenge, + challenge: withAdvertisedHeader(challenge, credentialHeader), payload: { spt }, }); }, @@ -67,7 +79,7 @@ function createStripePaymentClient(spt: string) { { async createCredential({ challenge }) { return Credential.serialize({ - challenge, + challenge: withAdvertisedHeader(challenge, credentialHeader), payload: { action: 'open', grantedToken: spt }, }); }, @@ -87,7 +99,7 @@ function createStripePaymentClient(spt: string) { }, setCredential(request, credential) { const nextHeaders = new Headers(request.headers); - nextHeaders.set('Authorization', credential); + nextHeaders.set(credentialHeader, credential); return { ...request, headers: nextHeaders }; }, }), @@ -168,15 +180,27 @@ export async function payWithSpt( return readPayResult(initialResponse); } - const authHeader = - await createStripePaymentClient(spt).createCredential(initialResponse); + const wwwAuthenticate = initialResponse.headers.get('www-authenticate'); + if (!wwwAuthenticate) { + throw new Error('URL returned 402 but no WWW-Authenticate header'); + } + + const challenge = getStripeChargeChallengeFromResponse(initialResponse); + const credentialHeader = resolvePaymentCredentialHeader( + wwwAuthenticate, + challenge.id, + ); + const credential = await createStripePaymentClient( + spt, + credentialHeader, + ).createCredential(initialResponse); const retryResponse = await fetch(url, { method: httpMethod, body: data, headers: { ...requestHeaders, - Authorization: authHeader, + [credentialHeader]: credential, }, }); From c343ca857c933cbc386d70a4467a26eb25db78df Mon Sep 17 00:00:00 2001 From: Ryan Aubrey Date: Tue, 25 Aug 2026 11:48:27 -0400 Subject: [PATCH 2/4] Restrict mpp pay credentials to Authorization or Payment-Authorization. The protocol no longer allows arbitrary credential header names, so send and echo only those two fields. Co-authored-by: Cursor Committed-By-Agent: cursor --- .changeset/payment-authorization-header.md | 2 +- .../cli/src/commands/mpp/credential-header.ts | 18 +++++++---- packages/cli/src/commands/mpp/decode.ts | 2 +- packages/cli/src/commands/mpp/pay.tsx | 32 +++++++++++++++---- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/.changeset/payment-authorization-header.md b/.changeset/payment-authorization-header.md index d4a89d6e..19f881db 100644 --- a/.changeset/payment-authorization-header.md +++ b/.changeset/payment-authorization-header.md @@ -2,4 +2,4 @@ "@stripe/link-cli": patch --- -Honor advertised Payment-Authorization headers in `mpp pay` so Payment credentials can coexist with ordinary Authorization +Honor Payment-Authorization in `mpp pay` so Payment credentials can coexist with ordinary Authorization. Challenges may select only Authorization (default) or Payment-Authorization. diff --git a/packages/cli/src/commands/mpp/credential-header.ts b/packages/cli/src/commands/mpp/credential-header.ts index d9b98ec9..2af1658e 100644 --- a/packages/cli/src/commands/mpp/credential-header.ts +++ b/packages/cli/src/commands/mpp/credential-header.ts @@ -1,18 +1,22 @@ export const DEFAULT_CREDENTIAL_HEADER = 'Authorization'; export const PAYMENT_AUTHORIZATION_HEADER = 'Payment-Authorization'; +export type PaymentCredentialHeader = + | typeof DEFAULT_CREDENTIAL_HEADER + | typeof PAYMENT_AUTHORIZATION_HEADER; + /** * HTTP field the client must use for the Payment credential. * * mppx 0.8.x drops unknown WWW-Authenticate auth-params (including `header`) * when parsing challenges, so this reads `header` from the raw challenge - * string. Omitted `header` defaults to Authorization. The only advertised - * alternate this CLI supports is Payment-Authorization. + * string. The protocol only allows Authorization (omitted / default) or + * Payment-Authorization. */ export function resolvePaymentCredentialHeader( wwwAuthenticate: string, challengeId: string, -): string { +): PaymentCredentialHeader { const chunk = paymentSchemeChunks(wwwAuthenticate).find( (scheme) => authParam(scheme, 'id') === challengeId, ); @@ -23,7 +27,7 @@ export function resolvePaymentCredentialHeader( export function canonicalizeCredentialHeader( value: string | undefined, -): string { +): PaymentCredentialHeader { if (value == null || value === '') { return DEFAULT_CREDENTIAL_HEADER; } @@ -38,8 +42,10 @@ export function canonicalizeCredentialHeader( ); } -export function shouldEchoCredentialHeader(header: string): boolean { - return !equalsHeaderName(header, DEFAULT_CREDENTIAL_HEADER); +export function shouldEchoCredentialHeader( + header: PaymentCredentialHeader, +): boolean { + return header === PAYMENT_AUTHORIZATION_HEADER; } function equalsHeaderName(left: string, right: string): boolean { diff --git a/packages/cli/src/commands/mpp/decode.ts b/packages/cli/src/commands/mpp/decode.ts index fa5174d3..25d570b3 100644 --- a/packages/cli/src/commands/mpp/decode.ts +++ b/packages/cli/src/commands/mpp/decode.ts @@ -25,7 +25,7 @@ export interface DecodedStripeChallenge { description?: string; digest?: string; expires?: string; - /** Present only when the challenge advertised a non-default credential field. */ + /** Present only when the challenge advertised Payment-Authorization. */ header?: string; network_id: string; request_json: Record; diff --git a/packages/cli/src/commands/mpp/pay.tsx b/packages/cli/src/commands/mpp/pay.tsx index 22df1916..07c0a4a3 100644 --- a/packages/cli/src/commands/mpp/pay.tsx +++ b/packages/cli/src/commands/mpp/pay.tsx @@ -11,6 +11,9 @@ import React, { useEffect, useState } from 'react'; import { pollUntilApproved } from '../../utils/poll-until-approved'; import { sanitizeDeep } from '../../utils/sanitize-text'; import { + DEFAULT_CREDENTIAL_HEADER, + PAYMENT_AUTHORIZATION_HEADER, + type PaymentCredentialHeader, resolvePaymentCredentialHeader, shouldEchoCredentialHeader, } from './credential-header'; @@ -58,13 +61,28 @@ export async function readPayResult(response: Response): Promise { function withAdvertisedHeader( challenge: T, - credentialHeader: string, + credentialHeader: PaymentCredentialHeader, ): T { if (!shouldEchoCredentialHeader(credentialHeader)) return challenge; return { ...challenge, header: credentialHeader } as T; } -function createStripePaymentClient(spt: string, credentialHeader: string) { +function setPaymentCredential( + headers: Headers, + credentialHeader: PaymentCredentialHeader, + credential: string, +): void { + if (credentialHeader === PAYMENT_AUTHORIZATION_HEADER) { + headers.set(PAYMENT_AUTHORIZATION_HEADER, credential); + return; + } + headers.set(DEFAULT_CREDENTIAL_HEADER, credential); +} + +function createStripePaymentClient( + spt: string, + credentialHeader: PaymentCredentialHeader, +) { const stripeCharge = Method.toClient(StripeMethods.charge, { async createCredential({ challenge }) { return Credential.serialize({ @@ -99,7 +117,7 @@ function createStripePaymentClient(spt: string, credentialHeader: string) { }, setCredential(request, credential) { const nextHeaders = new Headers(request.headers); - nextHeaders.set(credentialHeader, credential); + setPaymentCredential(nextHeaders, credentialHeader, credential); return { ...request, headers: nextHeaders }; }, }), @@ -195,13 +213,13 @@ export async function payWithSpt( credentialHeader, ).createCredential(initialResponse); + const retryHeaders = new Headers(requestHeaders); + setPaymentCredential(retryHeaders, credentialHeader, credential); + const retryResponse = await fetch(url, { method: httpMethod, body: data, - headers: { - ...requestHeaders, - [credentialHeader]: credential, - }, + headers: retryHeaders, }); return readPayResult(retryResponse); From f0cc83ff98aca3fc65188233f19f136e615f3385 Mon Sep 17 00:00:00 2001 From: Ryan Aubrey Date: Fri, 28 Aug 2026 13:01:17 -0400 Subject: [PATCH 3/4] Parse Payment scheme boundaries outside quoted WWW-Authenticate values. Quoted auth-params like description="Payment required" were treated as a new scheme start, which could drop header="Payment-Authorization". Co-authored-by: Cursor Committed-By-Agent: cursor --- .../commands/mpp/credential-header.test.ts | 43 +++++++++++++++ .../cli/src/commands/mpp/credential-header.ts | 53 +++++++++++++++++-- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/mpp/credential-header.test.ts b/packages/cli/src/commands/mpp/credential-header.test.ts index 424690a7..bf8bc76c 100644 --- a/packages/cli/src/commands/mpp/credential-header.test.ts +++ b/packages/cli/src/commands/mpp/credential-header.test.ts @@ -53,6 +53,49 @@ describe('resolvePaymentCredentialHeader', () => { ); }); + it('does not treat Payment inside a quoted auth-param as a new scheme', () => { + const wwwAuthenticate = [ + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + 'description="Payment required",', + 'header="Payment-Authorization",', + `request="${STRIPE_REQUEST}"`, + ].join(' '); + + expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( + PAYMENT_AUTHORIZATION_HEADER, + ); + }); + + it('still splits real Payment schemes after a quoted Payment substring', () => { + const wwwAuthenticate = [ + 'Payment id="tempo_001", realm="merchant.example", method="tempo", intent="charge",', + 'description="Payment required", request="e30=",', + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + 'header="Payment-Authorization",', + `request="${STRIPE_REQUEST}"`, + ].join(' '); + + expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( + PAYMENT_AUTHORIZATION_HEADER, + ); + expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'tempo_001')).toBe( + DEFAULT_CREDENTIAL_HEADER, + ); + }); + + it('ignores escaped quotes when locating Payment scheme boundaries', () => { + const wwwAuthenticate = [ + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + 'description="Say \\"Payment required\\", then pay",', + 'header="Payment-Authorization",', + `request="${STRIPE_REQUEST}"`, + ].join(' '); + + expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( + PAYMENT_AUTHORIZATION_HEADER, + ); + }); + it('rejects an unsupported advertised header', () => { const wwwAuthenticate = [ 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', diff --git a/packages/cli/src/commands/mpp/credential-header.ts b/packages/cli/src/commands/mpp/credential-header.ts index 2af1658e..e13a6e08 100644 --- a/packages/cli/src/commands/mpp/credential-header.ts +++ b/packages/cli/src/commands/mpp/credential-header.ts @@ -53,10 +53,7 @@ function equalsHeaderName(left: string, right: string): boolean { } function paymentSchemeChunks(wwwAuthenticate: string): string[] { - const starts: number[] = []; - for (const match of wwwAuthenticate.matchAll(/Payment\s+/gi)) { - if (match.index !== undefined) starts.push(match.index); - } + const starts = paymentSchemeStarts(wwwAuthenticate); return starts.map((start, index) => { const nextStart = starts[index + 1]; const end = nextStart === undefined ? wwwAuthenticate.length : nextStart; @@ -64,6 +61,54 @@ function paymentSchemeChunks(wwwAuthenticate: string): string[] { }); } +/** + * Scheme starts are `Payment` followed by whitespace, ignoring that same + * substring inside quoted auth-param values (e.g. description="Payment required"). + */ +function paymentSchemeStarts(wwwAuthenticate: string): number[] { + const starts: number[] = []; + let i = 0; + while (i < wwwAuthenticate.length) { + const char = wwwAuthenticate[i]; + if (char === '"') { + i = skipQuotedString(wwwAuthenticate, i); + continue; + } + if (isPaymentSchemeAt(wwwAuthenticate, i)) { + starts.push(i); + i += 'Payment'.length; + continue; + } + i += 1; + } + return starts; +} + +function skipQuotedString(value: string, quoteIndex: number): number { + let i = quoteIndex + 1; + while (i < value.length) { + const char = value[i]; + if (char === '\\') { + i += 2; + continue; + } + if (char === '"') { + return i + 1; + } + i += 1; + } + return value.length; +} + +function isPaymentSchemeAt(value: string, index: number): boolean { + const scheme = 'Payment'; + if (index + scheme.length >= value.length) return false; + if (value.slice(index, index + scheme.length).toLowerCase() !== 'payment') { + return false; + } + return /\s/.test(value[index + scheme.length] ?? ''); +} + function authParam(chunk: string, name: string): string | undefined { const pattern = new RegExp( `(?:^|[,\\s])${name}\\s*=\\s*(?:"((?:\\\\.|[^"\\\\])*)"|([^,\\s]+))`, From 19948401d71a81a3292964cce55073af18bb8397 Mon Sep 17 00:00:00 2001 From: Ryan Aubrey Date: Fri, 28 Aug 2026 13:45:30 -0400 Subject: [PATCH 4/4] Use mppx 0.9.1 to parse the Payment credential header. mppx now keeps challenge.header, so mpp pay no longer re-parses WWW-Authenticate scheme boundaries itself. Co-authored-by: Cursor Committed-By-Agent: cursor --- packages/cli/package.json | 2 +- .../commands/mpp/credential-header.test.ts | 107 ++---------------- .../cli/src/commands/mpp/credential-header.ts | 85 +------------- packages/cli/src/commands/mpp/decode.test.ts | 59 ++++++++++ packages/cli/src/commands/mpp/decode.ts | 7 +- packages/cli/src/commands/mpp/pay.tsx | 42 +++---- pnpm-lock.yaml | 55 +++++---- 7 files changed, 123 insertions(+), 234 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 9e8d5d9b..44498e11 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -35,7 +35,7 @@ "incur": "^0.4.26", "ink": "^5.2.1", "ink-spinner": "^5.0.0", - "mppx": "0.8.15", + "mppx": "0.9.1", "qrcode": "^1.5.4", "react": "^18.3.1", "strip-ansi": "^7.2.0", diff --git a/packages/cli/src/commands/mpp/credential-header.test.ts b/packages/cli/src/commands/mpp/credential-header.test.ts index bf8bc76c..35ed7579 100644 --- a/packages/cli/src/commands/mpp/credential-header.test.ts +++ b/packages/cli/src/commands/mpp/credential-header.test.ts @@ -3,119 +3,28 @@ import { DEFAULT_CREDENTIAL_HEADER, PAYMENT_AUTHORIZATION_HEADER, canonicalizeCredentialHeader, - resolvePaymentCredentialHeader, shouldEchoCredentialHeader, } from './credential-header'; -const STRIPE_REQUEST = Buffer.from( - JSON.stringify({ - amount: '1000', - currency: 'usd', - methodDetails: { networkId: 'net_001', paymentMethodTypes: ['card'] }, - }), -).toString('base64'); - -describe('resolvePaymentCredentialHeader', () => { - it('defaults to Authorization when the challenge omits header', () => { - const wwwAuthenticate = [ - 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', - `request="${STRIPE_REQUEST}"`, - ].join(' '); - - expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( - DEFAULT_CREDENTIAL_HEADER, - ); - }); - - it('uses Payment-Authorization when the stripe challenge advertises it', () => { - const wwwAuthenticate = [ - 'Payment id="tempo_001", realm="merchant.example", method="tempo", intent="charge", request="e30=",', - 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', - 'header="Payment-Authorization",', - `request="${STRIPE_REQUEST}"`, - ].join(' '); - - expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( - PAYMENT_AUTHORIZATION_HEADER, - ); - }); - - it('does not inherit header from a different Payment challenge', () => { - const wwwAuthenticate = [ - 'Payment id="tempo_001", realm="merchant.example", method="tempo", intent="charge",', - 'header="Payment-Authorization", request="e30=",', - 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', - `request="${STRIPE_REQUEST}"`, - ].join(' '); - - expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( +describe('canonicalizeCredentialHeader', () => { + it('treats omitted and Authorization values as the default', () => { + expect(canonicalizeCredentialHeader(undefined)).toBe( DEFAULT_CREDENTIAL_HEADER, ); - }); - - it('does not treat Payment inside a quoted auth-param as a new scheme', () => { - const wwwAuthenticate = [ - 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', - 'description="Payment required",', - 'header="Payment-Authorization",', - `request="${STRIPE_REQUEST}"`, - ].join(' '); - - expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( - PAYMENT_AUTHORIZATION_HEADER, - ); - }); - - it('still splits real Payment schemes after a quoted Payment substring', () => { - const wwwAuthenticate = [ - 'Payment id="tempo_001", realm="merchant.example", method="tempo", intent="charge",', - 'description="Payment required", request="e30=",', - 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', - 'header="Payment-Authorization",', - `request="${STRIPE_REQUEST}"`, - ].join(' '); - - expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( - PAYMENT_AUTHORIZATION_HEADER, - ); - expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'tempo_001')).toBe( + expect(canonicalizeCredentialHeader('authorization')).toBe( DEFAULT_CREDENTIAL_HEADER, ); }); - it('ignores escaped quotes when locating Payment scheme boundaries', () => { - const wwwAuthenticate = [ - 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', - 'description="Say \\"Payment required\\", then pay",', - 'header="Payment-Authorization",', - `request="${STRIPE_REQUEST}"`, - ].join(' '); - - expect(resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001')).toBe( + it('accepts Payment-Authorization', () => { + expect(canonicalizeCredentialHeader('Payment-Authorization')).toBe( PAYMENT_AUTHORIZATION_HEADER, ); }); it('rejects an unsupported advertised header', () => { - const wwwAuthenticate = [ - 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', - 'header="X-Payment",', - `request="${STRIPE_REQUEST}"`, - ].join(' '); - - expect(() => - resolvePaymentCredentialHeader(wwwAuthenticate, 'ch_001'), - ).toThrow(/Unsupported payment credential header/i); - }); -}); - -describe('canonicalizeCredentialHeader', () => { - it('treats omitted and Authorization values as the default', () => { - expect(canonicalizeCredentialHeader(undefined)).toBe( - DEFAULT_CREDENTIAL_HEADER, - ); - expect(canonicalizeCredentialHeader('authorization')).toBe( - DEFAULT_CREDENTIAL_HEADER, + expect(() => canonicalizeCredentialHeader('X-Payment')).toThrow( + /Unsupported payment credential header/i, ); }); diff --git a/packages/cli/src/commands/mpp/credential-header.ts b/packages/cli/src/commands/mpp/credential-header.ts index e13a6e08..99eaf95a 100644 --- a/packages/cli/src/commands/mpp/credential-header.ts +++ b/packages/cli/src/commands/mpp/credential-header.ts @@ -8,23 +8,10 @@ export type PaymentCredentialHeader = /** * HTTP field the client must use for the Payment credential. * - * mppx 0.8.x drops unknown WWW-Authenticate auth-params (including `header`) - * when parsing challenges, so this reads `header` from the raw challenge - * string. The protocol only allows Authorization (omitted / default) or + * `mppx` parses the challenge `header` auth-param; this only canonicalizes the + * advertised value. The protocol allows Authorization (omitted / default) or * Payment-Authorization. */ -export function resolvePaymentCredentialHeader( - wwwAuthenticate: string, - challengeId: string, -): PaymentCredentialHeader { - const chunk = paymentSchemeChunks(wwwAuthenticate).find( - (scheme) => authParam(scheme, 'id') === challengeId, - ); - return canonicalizeCredentialHeader( - chunk ? authParam(chunk, 'header') : undefined, - ); -} - export function canonicalizeCredentialHeader( value: string | undefined, ): PaymentCredentialHeader { @@ -51,71 +38,3 @@ export function shouldEchoCredentialHeader( function equalsHeaderName(left: string, right: string): boolean { return left.toLowerCase() === right.toLowerCase(); } - -function paymentSchemeChunks(wwwAuthenticate: string): string[] { - const starts = paymentSchemeStarts(wwwAuthenticate); - return starts.map((start, index) => { - const nextStart = starts[index + 1]; - const end = nextStart === undefined ? wwwAuthenticate.length : nextStart; - return wwwAuthenticate.slice(start, end).replace(/,\s*$/, ''); - }); -} - -/** - * Scheme starts are `Payment` followed by whitespace, ignoring that same - * substring inside quoted auth-param values (e.g. description="Payment required"). - */ -function paymentSchemeStarts(wwwAuthenticate: string): number[] { - const starts: number[] = []; - let i = 0; - while (i < wwwAuthenticate.length) { - const char = wwwAuthenticate[i]; - if (char === '"') { - i = skipQuotedString(wwwAuthenticate, i); - continue; - } - if (isPaymentSchemeAt(wwwAuthenticate, i)) { - starts.push(i); - i += 'Payment'.length; - continue; - } - i += 1; - } - return starts; -} - -function skipQuotedString(value: string, quoteIndex: number): number { - let i = quoteIndex + 1; - while (i < value.length) { - const char = value[i]; - if (char === '\\') { - i += 2; - continue; - } - if (char === '"') { - return i + 1; - } - i += 1; - } - return value.length; -} - -function isPaymentSchemeAt(value: string, index: number): boolean { - const scheme = 'Payment'; - if (index + scheme.length >= value.length) return false; - if (value.slice(index, index + scheme.length).toLowerCase() !== 'payment') { - return false; - } - return /\s/.test(value[index + scheme.length] ?? ''); -} - -function authParam(chunk: string, name: string): string | undefined { - const pattern = new RegExp( - `(?:^|[,\\s])${name}\\s*=\\s*(?:"((?:\\\\.|[^"\\\\])*)"|([^,\\s]+))`, - 'i', - ); - const match = chunk.match(pattern); - if (!match) return undefined; - if (match[1] !== undefined) return match[1].replace(/\\(.)/g, '$1'); - return match[2]; -} diff --git a/packages/cli/src/commands/mpp/decode.test.ts b/packages/cli/src/commands/mpp/decode.test.ts index 265d427b..b4e2d9b1 100644 --- a/packages/cli/src/commands/mpp/decode.test.ts +++ b/packages/cli/src/commands/mpp/decode.test.ts @@ -56,6 +56,65 @@ describe('decodeStripeChallenge', () => { }); }); + it('does not inherit header from a different Payment challenge', () => { + const header = [ + 'Payment id="tempo_001", realm="merchant.example", method="tempo", intent="charge",', + 'header="Payment-Authorization", request="e30=",', + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + `request="${encodeRequest({ + amount: '1000', + currency: 'usd', + methodDetails: { + networkId: 'net_001', + paymentMethodTypes: ['card'], + }, + })}"`, + ].join(' '); + + expect(decodeStripeChallenge(header)).not.toHaveProperty('header'); + }); + + it('keeps header when a quoted description contains Payment', () => { + const header = [ + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + 'description="Payment required",', + 'header="Payment-Authorization",', + `request="${encodeRequest({ + amount: '1000', + currency: 'usd', + methodDetails: { + networkId: 'net_001', + paymentMethodTypes: ['card'], + }, + })}"`, + ].join(' '); + + expect(decodeStripeChallenge(header)).toMatchObject({ + header: 'Payment-Authorization', + description: 'Payment required', + network_id: 'net_001', + }); + }); + + it('rejects an unsupported advertised header', () => { + const header = [ + 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge",', + 'header="X-Payment",', + `request="${encodeRequest({ + amount: '1000', + currency: 'usd', + methodDetails: { + networkId: 'net_001', + paymentMethodTypes: ['card'], + }, + })}"`, + ].join(' '); + + expect(() => decodeStripeChallenge(header)).toThrow( + /Unsupported payment credential header/i, + ); + }); + it('handles escaped quoted-string values in challenge parameters', () => { const header = [ 'Payment id="ch_001",', diff --git a/packages/cli/src/commands/mpp/decode.ts b/packages/cli/src/commands/mpp/decode.ts index 25d570b3..b58a0337 100644 --- a/packages/cli/src/commands/mpp/decode.ts +++ b/packages/cli/src/commands/mpp/decode.ts @@ -1,7 +1,7 @@ import { Challenge } from 'mppx'; import { sanitizeDeep } from '../../utils/sanitize-text'; import { - resolvePaymentCredentialHeader, + canonicalizeCredentialHeader, shouldEchoCredentialHeader, } from './credential-header'; @@ -124,10 +124,7 @@ export function decodeStripeChallenge( const { challenge, networkId, request } = resolveStripeChallenge( Challenge.deserializeList(challengeHeader), ); - const credentialHeader = resolvePaymentCredentialHeader( - challengeHeader, - challenge.id, - ); + const credentialHeader = canonicalizeCredentialHeader(challenge.header); return sanitizeDeep({ id: challenge.id, diff --git a/packages/cli/src/commands/mpp/pay.tsx b/packages/cli/src/commands/mpp/pay.tsx index 07c0a4a3..60288d7e 100644 --- a/packages/cli/src/commands/mpp/pay.tsx +++ b/packages/cli/src/commands/mpp/pay.tsx @@ -14,8 +14,7 @@ import { DEFAULT_CREDENTIAL_HEADER, PAYMENT_AUTHORIZATION_HEADER, type PaymentCredentialHeader, - resolvePaymentCredentialHeader, - shouldEchoCredentialHeader, + canonicalizeCredentialHeader, } from './credential-header'; import { decodeStripeChallenge, @@ -59,14 +58,6 @@ export async function readPayResult(response: Response): Promise { }); } -function withAdvertisedHeader( - challenge: T, - credentialHeader: PaymentCredentialHeader, -): T { - if (!shouldEchoCredentialHeader(credentialHeader)) return challenge; - return { ...challenge, header: credentialHeader } as T; -} - function setPaymentCredential( headers: Headers, credentialHeader: PaymentCredentialHeader, @@ -79,14 +70,12 @@ function setPaymentCredential( headers.set(DEFAULT_CREDENTIAL_HEADER, credential); } -function createStripePaymentClient( - spt: string, - credentialHeader: PaymentCredentialHeader, -) { +function createStripePaymentClient(spt: string) { const stripeCharge = Method.toClient(StripeMethods.charge, { async createCredential({ challenge }) { + canonicalizeCredentialHeader(challenge.header); return Credential.serialize({ - challenge: withAdvertisedHeader(challenge, credentialHeader), + challenge, payload: { spt }, }); }, @@ -96,8 +85,9 @@ function createStripePaymentClient( { ...StripeMethods.charge, intent: 'session' as const }, { async createCredential({ challenge }) { + canonicalizeCredentialHeader(challenge.header); return Credential.serialize({ - challenge: withAdvertisedHeader(challenge, credentialHeader), + challenge, payload: { action: 'open', grantedToken: spt }, }); }, @@ -112,10 +102,13 @@ function createStripePaymentClient( isPaymentRequired(response) { return response.status === 402; }, - getChallenge(response) { - return getStripeChargeChallengeFromResponse(response); + getChallenges(response) { + return [getStripeChargeChallengeFromResponse(response)]; }, - setCredential(request, credential) { + setCredential(request, credential, options) { + const credentialHeader = canonicalizeCredentialHeader( + options?.challenge?.header, + ); const nextHeaders = new Headers(request.headers); setPaymentCredential(nextHeaders, credentialHeader, credential); return { ...request, headers: nextHeaders }; @@ -204,14 +197,9 @@ export async function payWithSpt( } const challenge = getStripeChargeChallengeFromResponse(initialResponse); - const credentialHeader = resolvePaymentCredentialHeader( - wwwAuthenticate, - challenge.id, - ); - const credential = await createStripePaymentClient( - spt, - credentialHeader, - ).createCredential(initialResponse); + const credentialHeader = canonicalizeCredentialHeader(challenge.header); + const credential = + await createStripePaymentClient(spt).createCredential(initialResponse); const retryHeaders = new Headers(requestHeaders); setPaymentCredential(retryHeaders, credentialHeader, credential); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe481498..f2232aa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: ^5.0.0 version: 5.0.0(ink@5.2.1(@types/react@18.3.29)(react@18.3.1))(react@18.3.1) mppx: - specifier: 0.8.15 - version: 0.8.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.34)(typescript@5.9.3)(viem@2.55.10(typescript@5.9.3)(zod@4.4.3)) + specifier: 0.9.1 + version: 0.9.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.34)(typescript@5.9.3)(viem@2.55.10(typescript@5.9.3)(zod@4.4.3)) qrcode: specifier: ^1.5.4 version: 1.5.4 @@ -886,8 +886,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@stripe/stripe-js@9.9.0': - resolution: {integrity: sha512-Vwqe6Q5cU4i82tPyAv2BpaW/fQSNdOSO4/J8EeDLPp5/oIZiMmdB+Hgh863zFH+rtoxpuWGvD1L7QPh8k1Rdvw==} + '@stripe/stripe-js@9.13.0': + resolution: {integrity: sha512-/0c72BUgzzVkVTlsw5uBn8x3waTdVJ/PZGfQ6jY1eu6K7olUPf4d9lgDPA9/0sIdsR8j7o3QIG8fOCO6ItcL7A==} engines: {node: '>=12.16'} '@toon-format/toon@2.3.0': @@ -1374,10 +1374,6 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - eventsource-parser@3.1.0: - resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} - engines: {node: '>=18.0.0'} - eventsource-parser@3.1.1: resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} engines: {node: '>=18.0.0'} @@ -1822,24 +1818,42 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mppx@0.8.15: - resolution: {integrity: sha512-+4jQRYB3AbgATfsZZAen7SxDC4miAPhUokTmBgda5OORZKvPnbWKAKHMHK1oMKsxWTVMYBwfgxmiJYAEe6I47g==} + mppx@0.9.1: + resolution: {integrity: sha512-mmzOHcUnyvxXBFJQWWFeaT6+uwRuphqmzFZXfJjDKlmEQ559oEseQusaXs68husPKSYZU9WCwRElhi55+5I+5A==} hasBin: true peerDependencies: '@modelcontextprotocol/sdk': '>=1.25.0' + '@x402/core': '>=2.22.0' + '@x402/express': '>=2.22.0' + '@x402/hono': '>=2.22.0' + '@x402/mcp': '>=2.22.0' + '@x402/next': '>=2.22.0' elysia: '>=1' express: '>=5' hono: '>=4.12.25' + next: '>=16.2.6' viem: '>=2.54.0' peerDependenciesMeta: '@modelcontextprotocol/sdk': optional: true + '@x402/core': + optional: true + '@x402/express': + optional: true + '@x402/hono': + optional: true + '@x402/mcp': + optional: true + '@x402/next': + optional: true elysia: optional: true express: optional: true hono: optional: true + next: + optional: true mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} @@ -2244,6 +2258,10 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + structured-headers@2.0.3: + resolution: {integrity: sha512-4g5yxhlDMClRwCcfKfLeS7Z8yAVdOWGDADwm80Poh1iReU2KVKLGBlqwpHWJ2qovq0+ZIf1atAEO1eua2o9Rgg==} + engines: {node: '>=18', npm: '>=6'} + stubborn-fs@2.0.0: resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} @@ -3159,7 +3177,7 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@stripe/stripe-js@9.9.0': {} + '@stripe/stripe-js@9.13.0': {} '@toon-format/toon@2.3.0': {} @@ -3662,10 +3680,7 @@ snapshots: eventemitter3@5.0.1: {} - eventsource-parser@3.1.0: {} - - eventsource-parser@3.1.1: - optional: true + eventsource-parser@3.1.1: {} eventsource@3.0.7: dependencies: @@ -4116,12 +4131,12 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 - mppx@0.8.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.34)(typescript@5.9.3)(viem@2.55.10(typescript@5.9.3)(zod@4.4.3)): + mppx@0.9.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.34)(typescript@5.9.3)(viem@2.55.10(typescript@5.9.3)(zod@4.4.3)): dependencies: - '@stripe/stripe-js': 9.9.0 - eventsource-parser: 3.1.0 - incur: 0.4.26 + '@stripe/stripe-js': 9.13.0 + eventsource-parser: 3.1.1 ox: 0.14.33(typescript@5.9.3)(zod@4.4.3) + structured-headers: 2.0.3 viem: 2.55.10(typescript@5.9.3)(zod@4.4.3) zod: 4.4.3 optionalDependencies: @@ -4570,6 +4585,8 @@ snapshots: strip-json-comments@2.0.1: {} + structured-headers@2.0.3: {} + stubborn-fs@2.0.0: dependencies: stubborn-utils: 1.0.2