feat: add multi-challenge mppx.compose() combinator#166
Merged
Conversation
Adds an instance method that lets a single route present multiple payment
methods to the client via multiple WWW-Authenticate headers (RFC 9110 §11.6.1).
Usage:
const result = await mppx.challenge(
[tempoCharge, { amount: '100' }],
[stripeCharge, { amount: '100' }],
)(request)
- No credential: calls all handlers, merges WWW-Authenticate headers into one 402
- With credential: dispatches to matching handler by method+intent
- Cross-route protection preserved through offer()
- Also exports static Mppx.offer() for pre-configured handler composition
- 8 new tests covering dispatch, merge, cross-route, withReceipt, error cases
…-based selection, pre-dispatch narrowing - Rename static offer() to challenge(), OfferEntry to ChallengeEntry, all test/comment references - Add Challenge.fromResponseList/fromHeadersList/deserializeList for parsing merged WWW-Authenticate - Client Fetch.ts: use fromResponseList and select in methods declaration order (preference stack) - Pre-dispatch narrowing: compare currency/recipient from echoed credential against _internal metadata - Protect _internal.name/intent from being clobbered by user options (spread last) - Add tests for same name/intent with different currencies and recipients - Add comment on Wrap type explaining why challenge is passed through unwrapped
commit: |
…wing - Only enter credential dispatch when Authorization header contains a Payment scheme. Non-payment auth (Bearer, Basic) now falls through to the merged-402 path so all offers are presented. - Add 'amount' to pre-dispatch narrowing fields so same-method offers differing only by amount are correctly disambiguated.
jxom
reviewed
Mar 10, 2026
src/server/Mppx.ts
Outdated
| * }) | ||
| * | ||
| * app.get('/api/resource', async (req) => { | ||
| * const result = await Mppx.challenge( |
Member
There was a problem hiding this comment.
Is it meant to be mppx.challenge?
Suggested change
| * const result = await Mppx.challenge( | |
| * const result = await mppx.challenge( |
Member
There was a problem hiding this comment.
I wonder if mppx.compose might be better naming, in case we want to use mppx.challenge for something else (like arbitrary server-defined digests like we discussed earlier? feel like that might be a good name for it)
Collaborator
Author
There was a problem hiding this comment.
yeah lets' try compose, I think that is more explicit
jxom
reviewed
Mar 10, 2026
Comment on lines
+45
to
+46
| * mppx.tempo.charge({ amount: '100' }), | ||
| * mppx.stripe.charge({ amount: '100' }), |
Member
There was a problem hiding this comment.
can we make sure the types are hooked up well here for these variants with .test-d.ts type tests?
- Rename challenge → compose per review feedback (reserve 'challenge' for future server-defined digest API). - Fix JSDoc example to use instance method form (mppx.compose not Mppx.challenge). - Add Mppx.test-d.ts with vitest typecheck tests covering compose() with method reference tuples, string key tuples, nested handlers, and slash key handlers. - Update middleware Wrap type exclusion and tests accordingly.
jxom
approved these changes
Mar 10, 2026
mppx.challenge() combinatormppx.compose() combinator
This was referenced Mar 10, 2026
brendanjryan
added a commit
that referenced
this pull request
Mar 12, 2026
…-info) Implements mppx discovery per draft-payment-discovery-00 (mpp-specs #166). Phase 1 — Schema + Validator + CLI: - Zod schemas for PaymentInfo, ServiceInfo, DiscoveryDocument - validate() with structural parsing + semantic checks - 'discover validate <url-or-file>' CLI subcommand - 26 tests (Discovery + Validate) Phase 2 — OpenAPI Generator: - generate(mppx, config) builds OpenAPI 3.1.0 doc from mppx.methods - Annotates routes with x-service-info and x-payment-info extensions - 6 tests Phase 3 — Framework Plugins: - Hono: discovery(app, mppx, config) mounts GET /openapi.json - Express: discovery(app, mppx, config) mounts GET /openapi.json - Next.js: discovery(mppx, config) returns a route handler Phase 4 — Auto-introspection (Hono): - discovery(app, mppx, { auto: true }) walks app.routes and matches handlers with _internal metadata to build RouteConfig[] automatically Infrastructure: - ./discovery export in package.json - mppx/discovery alias in vitest.config.ts - src/discovery/index.ts re-exports
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
mppx.compose()— an instance method that lets a single route present multiple payment methods (or the same method with different options) to the client via multipleWWW-Authenticateheaders (RFC 9110 §11.6.1). Also addsMppx.compose()as a static function,Challenge.fromResponseList()for parsing merged responses, and updates the client to automatically select from multiple challenges using the preference order declared in themethodsarray.Usage
Server: nested accessors
Server: multi-challenge combinator
Server:
Mppx.compose()static with nested accessorsServer: same method, different currencies
Middleware: nested accessors (Hono)
Client: automatic preference-based selection
The client automatically handles multi-challenge 402 responses. When multiple challenges are present, it selects the first match based on the order of the
methodsarray — the client controls priority:Client: manual parsing
The 402 response contains multiple
WWW-Authenticateheaders:Test coverage
mppx.alpha.charge(...))Mppx.compose()staticwrap()nested supportcompose/realm/transportcompose()deserializeListquote handling"Payment "inside quoted param valueswithReceipt, currency/recipient disambiguation, pre-dispatch narrowing