mppx 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/Method.d.ts +7 -1
  3. package/dist/Method.js +7 -2
  4. package/dist/Receipt.d.ts +2 -0
  5. package/dist/Receipt.js +2 -0
  6. package/dist/cli/cli.d.ts +8 -0
  7. package/dist/cli/cli.js +134 -136
  8. package/dist/cli/plugins/index.js +12 -14
  9. package/dist/cli/plugins/tempo.js +9 -2
  10. package/dist/cli/utils.d.ts +11 -1
  11. package/dist/cli/utils.js +15 -2
  12. package/dist/cli/validate/challenge.d.ts +2 -0
  13. package/dist/cli/validate/challenge.js +19 -4
  14. package/dist/cli/validate/discovery.d.ts +1 -0
  15. package/dist/cli/validate/discovery.js +10 -1
  16. package/dist/cli/validate/payment.js +34 -12
  17. package/dist/client/Methods.d.ts +0 -2
  18. package/dist/client/Methods.js +0 -2
  19. package/dist/client/Mppx.js +1 -0
  20. package/dist/client/index.d.ts +1 -1
  21. package/dist/client/index.js +1 -1
  22. package/dist/client/internal/Fetch.d.ts +2 -0
  23. package/dist/client/internal/Fetch.js +8 -2
  24. package/dist/client/internal/MethodChallenge.d.ts +1 -0
  25. package/dist/evm/Types.js +1 -1
  26. package/dist/evm/server/Charge.js +5 -0
  27. package/dist/integrations/x402/Evm.js +4 -3
  28. package/dist/internal/HeaderCodec.d.ts +2 -2
  29. package/dist/internal/version.d.ts +2 -2
  30. package/dist/internal/version.js +1 -1
  31. package/dist/mcp/client/McpClient.js +4 -7
  32. package/dist/middlewares/elysia.d.ts +11 -8
  33. package/dist/middlewares/elysia.js +67 -14
  34. package/dist/middlewares/express.js +13 -6
  35. package/dist/middlewares/hono.js +4 -4
  36. package/dist/middlewares/nextjs.js +9 -5
  37. package/dist/proxy/internal/Headers.js +65 -0
  38. package/dist/proxy/services/stripe.js +1 -0
  39. package/dist/server/Mppx.d.ts +13 -1
  40. package/dist/server/Mppx.js +85 -44
  41. package/dist/server/NodeListener.js +71 -6
  42. package/dist/server/Request.d.ts +11 -1
  43. package/dist/server/Request.js +132 -13
  44. package/dist/server/Transport.d.ts +14 -1
  45. package/dist/server/Transport.js +1 -0
  46. package/dist/stripe/server/Methods.d.ts +10 -3
  47. package/dist/stripe/server/Methods.js +17 -2
  48. package/dist/stripe/server/internal/html.gen.js +1 -1
  49. package/dist/tempo/Methods.d.ts +1 -0
  50. package/dist/tempo/Methods.js +1 -0
  51. package/dist/tempo/client/Charge.js +17 -13
  52. package/dist/tempo/client/Methods.d.ts +1 -5
  53. package/dist/tempo/client/Methods.js +10 -5
  54. package/dist/tempo/client/ResolveAccount.d.ts +3 -0
  55. package/dist/tempo/client/index.d.ts +0 -3
  56. package/dist/tempo/client/index.js +0 -2
  57. package/dist/tempo/internal/fee-payer.d.ts +6 -3
  58. package/dist/tempo/internal/fee-payer.js +23 -6
  59. package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
  60. package/dist/tempo/internal/machine-token-charge.js +2 -1
  61. package/dist/tempo/legacy/index.d.ts +1 -3
  62. package/dist/tempo/legacy/index.js +1 -3
  63. package/dist/tempo/server/Charge.js +75 -46
  64. package/dist/tempo/server/Methods.d.ts +55 -10
  65. package/dist/tempo/server/Subscription.d.ts +1 -1
  66. package/dist/tempo/server/Subscription.js +31 -4
  67. package/dist/tempo/server/internal/html.gen.js +1 -1
  68. package/dist/tempo/server/internal/transport.js +28 -11
  69. package/dist/tempo/session/client/ChannelOps.js +5 -0
  70. package/dist/tempo/session/client/Session.d.ts +1 -0
  71. package/dist/tempo/session/client/Session.js +7 -4
  72. package/dist/tempo/session/client/SessionManager.js +3 -0
  73. package/dist/tempo/session/client/Transports.d.ts +8 -1
  74. package/dist/tempo/session/client/Transports.js +12 -3
  75. package/dist/tempo/session/precompile/Chain.js +16 -1
  76. package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
  77. package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
  78. package/dist/tempo/session/precompile/Voucher.js +48 -0
  79. package/dist/tempo/session/server/ChannelStore.d.ts +20 -2
  80. package/dist/tempo/session/server/ChannelStore.js +25 -4
  81. package/dist/tempo/session/server/CredentialVerification.d.ts +4 -1
  82. package/dist/tempo/session/server/CredentialVerification.js +34 -6
  83. package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
  84. package/dist/tempo/session/server/MeteredStream.js +85 -29
  85. package/dist/tempo/session/server/Session.d.ts +2 -1
  86. package/dist/tempo/session/server/Session.js +2 -1
  87. package/dist/tempo/session/server/Settlement.d.ts +21 -0
  88. package/dist/tempo/session/server/Settlement.js +92 -17
  89. package/dist/tempo/session/server/Sse.d.ts +3 -1
  90. package/dist/tempo/session/server/Sse.js +105 -32
  91. package/dist/tempo/session/server/Transports.d.ts +24 -19
  92. package/dist/tempo/session/server/Transports.js +169 -58
  93. package/dist/tempo/session/server/Ws.d.ts +5 -1
  94. package/dist/tempo/session/server/Ws.js +75 -20
  95. package/dist/tempo/subscription/Store.d.ts +2 -0
  96. package/dist/tempo/subscription/Store.js +3 -0
  97. package/dist/validation/core.js +2 -1
  98. package/dist/x402/server/EvmCharge.d.ts +3 -1
  99. package/dist/x402/server/EvmCharge.js +14 -1
  100. package/package.json +6 -6
  101. package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
  102. package/dist/tempo/legacy/client/ChannelOps.js +0 -168
  103. package/dist/tempo/legacy/client/Session.d.ts +0 -185
  104. package/dist/tempo/legacy/client/Session.js +0 -279
  105. package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
  106. package/dist/tempo/legacy/client/SessionManager.js +0 -706
  107. package/dist/tempo/legacy/client/index.d.ts +0 -7
  108. package/dist/tempo/legacy/client/index.js +0 -5
@@ -1,5 +1,7 @@
1
1
  import { Elysia } from 'elysia';
2
+ import { mapResponse as mapElysiaResponse } from 'elysia/adapter/web-standard/handler';
2
3
  import { generate } from '../discovery/OpenApi.js';
4
+ import * as Scope from '../server/internal/scope.js';
3
5
  import * as Mppx_core from '../server/Mppx.js';
4
6
  import * as Mppx_internal from './internal/mppx.js';
5
7
  export * from '../server/Methods.js';
@@ -7,10 +9,10 @@ export var Mppx;
7
9
  (function (Mppx) {
8
10
  /**
9
11
  * Creates an Elysia-aware payment handler where each intent
10
- * returns an Elysia `beforeHandle` hook.
12
+ * returns Elysia lifecycle hooks.
11
13
  *
12
- * Use with `.guard()` to scope payment to specific routes,
13
- * or `.onBeforeHandle()` to apply globally.
14
+ * Use with `.guard()` so both verification and response wrapping are scoped
15
+ * to the same routes.
14
16
  *
15
17
  * @example
16
18
  * ```ts
@@ -21,7 +23,7 @@ export var Mppx;
21
23
  *
22
24
  * const app = new Elysia()
23
25
  * .guard(
24
- * { beforeHandle: mppx.charge({ amount: '1' }) },
26
+ * mppx.charge({ amount: '1' }),
25
27
  * (app) => app.get('/premium', () => ({ data: 'paid content' })),
26
28
  * )
27
29
  * ```
@@ -32,7 +34,7 @@ export var Mppx;
32
34
  Mppx.create = create;
33
35
  })(Mppx || (Mppx = {}));
34
36
  /**
35
- * Elysia `beforeHandle` hook that gates a route behind a payment intent.
37
+ * Elysia lifecycle hooks that gate a route behind a payment intent.
36
38
  *
37
39
  * Returns a 402 challenge if no valid credential is provided.
38
40
  *
@@ -46,27 +48,78 @@ export var Mppx;
46
48
  *
47
49
  * const app = new Elysia()
48
50
  * .guard(
49
- * { beforeHandle: payment(mppx.charge, { amount: '1' }) },
51
+ * payment(mppx.charge, { amount: '1' }),
50
52
  * (app) => app.get('/premium', () => ({ data: 'paid content' })),
51
53
  * )
52
54
  * ```
53
55
  */
54
56
  export function payment(intent, options) {
55
- return async ({ request, set }) => {
56
- const result = await intent(options)(request);
57
+ const pending = new WeakMap();
58
+ const runBeforeHandle = async ({ request, route, set }, legacyRegistration) => {
59
+ const scopedRequest = options.scope === undefined && Scope.read(options.meta) === undefined
60
+ ? Scope.attach(request, `${request.method.toUpperCase()} ${route || new URL(request.url).pathname}`)
61
+ : request;
62
+ const result = await intent(options)(scopedRequest);
57
63
  if (result.status === 402)
58
64
  return result.challenge;
59
- const managementResponse = getManagementResponse(result);
65
+ const managementResponse = await getManagementResponse(result);
60
66
  if (managementResponse)
61
67
  return managementResponse;
62
- const receipt = result.withReceipt(new Response());
63
- for (const [key, value] of receipt.headers)
64
- set.headers[key] = value;
68
+ const supportsStreamingReceipts = Mppx_core.supportsStreamingReceipts(result);
69
+ if (legacyRegistration) {
70
+ if (supportsStreamingReceipts)
71
+ return new Response('Streaming payment hooks require paired beforeHandle and afterHandle registration.', { status: 500 });
72
+ const receiptResponse = await result.withReceipt(new Response(null, { status: 204 }));
73
+ if (!receiptResponse.ok)
74
+ return receiptResponse;
75
+ copyHeaders(receiptResponse, set.headers);
76
+ return undefined;
77
+ }
78
+ pending.set(request, { result, supportsStreamingReceipts });
79
+ return undefined;
80
+ };
81
+ const beforeHandle = (context) => runBeforeHandle(context, false);
82
+ const legacyBeforeHandle = (context) => runBeforeHandle(context, true);
83
+ const afterHandle = async ({ request, responseValue, set }) => {
84
+ const entry = pending.get(request);
85
+ if (!entry)
86
+ return;
87
+ const { result, supportsStreamingReceipts } = entry;
88
+ if (isAsyncIterableResponse(responseValue)) {
89
+ if (supportsStreamingReceipts) {
90
+ const response = await result.withReceipt(responseValue);
91
+ pending.delete(request);
92
+ return response;
93
+ }
94
+ const receiptResponse = await result.withReceipt(new Response(null, { status: 204 }));
95
+ pending.delete(request);
96
+ if (!receiptResponse.ok)
97
+ return receiptResponse;
98
+ copyHeaders(receiptResponse, set.headers);
99
+ return responseValue;
100
+ }
101
+ const response = await mapElysiaResponse(responseValue, set, request);
102
+ const wrapped = await result.withReceipt(response);
103
+ pending.delete(request);
104
+ return wrapped;
65
105
  };
106
+ return Object.assign(legacyBeforeHandle, { afterHandle, beforeHandle });
107
+ }
108
+ function isAsyncIterableResponse(response) {
109
+ if (typeof response === 'function')
110
+ return true;
111
+ return (typeof response === 'object' &&
112
+ response !== null &&
113
+ Symbol.asyncIterator in response &&
114
+ typeof response[Symbol.asyncIterator] === 'function');
115
+ }
116
+ function copyHeaders(response, headers) {
117
+ for (const [key, value] of response.headers)
118
+ headers[key] = value;
66
119
  }
67
- function getManagementResponse(result) {
120
+ async function getManagementResponse(result) {
68
121
  try {
69
- return result.withReceipt();
122
+ return await result.withReceipt();
70
123
  }
71
124
  catch (error) {
72
125
  if (Mppx_core.isMissingReceiptResponseError(error)) {
@@ -1,4 +1,5 @@
1
1
  import { generate } from '../discovery/OpenApi.js';
2
+ import * as Scope from '../server/internal/scope.js';
2
3
  import * as Mppx_core from '../server/Mppx.js';
3
4
  import * as ExpressAdapter from './internal/express.js';
4
5
  import * as Mppx_internal from './internal/mppx.js';
@@ -49,16 +50,20 @@ export var Mppx;
49
50
  */
50
51
  export function payment(intent, options) {
51
52
  return async (req, res, next) => {
52
- const request = ExpressAdapter.toRequest(req);
53
+ const rawRequest = ExpressAdapter.toRequest(req);
54
+ const routePath = typeof req.route?.path === 'string' ? req.route.path : req.path;
55
+ const request = options.scope === undefined && Scope.read(options.meta) === undefined
56
+ ? Scope.attach(rawRequest, `${req.method.toUpperCase()} ${req.baseUrl}${routePath}`)
57
+ : rawRequest;
53
58
  const result = await intent(options)(request);
54
59
  if (result.status === 402) {
55
60
  const challenge = result.challenge;
56
61
  await ExpressAdapter.sendResponse(res, challenge);
57
62
  return;
58
63
  }
59
- const managementResponse = (() => {
64
+ const managementResponse = await (async () => {
60
65
  try {
61
- return result.withReceipt();
66
+ return await result.withReceipt();
62
67
  }
63
68
  catch (error) {
64
69
  if (Mppx_core.isMissingReceiptResponseError(error))
@@ -78,9 +83,11 @@ export function payment(intent, options) {
78
83
  }
79
84
  const originalJson = res.json.bind(res);
80
85
  res.json = (body) => {
81
- const wrapped = result.withReceipt(Response.json(body));
82
- ExpressAdapter.copyHeaders(res, wrapped.headers);
83
- return originalJson(body);
86
+ void Promise.resolve(result.withReceipt(Response.json(body))).then((wrapped) => {
87
+ ExpressAdapter.copyHeaders(res, wrapped.headers);
88
+ originalJson(body);
89
+ }, next);
90
+ return res;
84
91
  };
85
92
  next();
86
93
  };
@@ -55,16 +55,16 @@ export function payment(intent, options) {
55
55
  const result = await intent(options)(request);
56
56
  if (result.status === 402)
57
57
  return result.challenge;
58
- const managementResponse = getManagementResponse(result);
58
+ const managementResponse = await getManagementResponse(result);
59
59
  if (managementResponse)
60
60
  return managementResponse;
61
61
  await next();
62
- c.res = result.withReceipt(c.res);
62
+ c.res = await result.withReceipt(c.res);
63
63
  };
64
64
  }
65
- function getManagementResponse(result) {
65
+ async function getManagementResponse(result) {
66
66
  try {
67
- return result.withReceipt();
67
+ return await result.withReceipt();
68
68
  }
69
69
  catch (error) {
70
70
  if (Mppx_core.isMissingReceiptResponseError(error)) {
@@ -1,4 +1,5 @@
1
1
  import { generate } from '../discovery/OpenApi.js';
2
+ import * as Scope from '../server/internal/scope.js';
2
3
  import * as Mppx_core from '../server/Mppx.js';
3
4
  import * as Mppx_internal from './internal/mppx.js';
4
5
  export * from '../server/Methods.js';
@@ -48,19 +49,22 @@ export var Mppx;
48
49
  */
49
50
  export function payment(intent, options, handler) {
50
51
  return async (request) => {
51
- const result = await intent(options)(request);
52
+ const scopedRequest = options.scope === undefined && Scope.read(options.meta) === undefined
53
+ ? Scope.attach(request, `${request.method.toUpperCase()} ${new URL(request.url).pathname}`)
54
+ : request;
55
+ const result = await intent(options)(scopedRequest);
52
56
  if (result.status === 402)
53
57
  return result.challenge;
54
- const managementResponse = getManagementResponse(result);
58
+ const managementResponse = await getManagementResponse(result);
55
59
  if (managementResponse)
56
60
  return managementResponse;
57
61
  const response = await handler(request);
58
- return result.withReceipt(response);
62
+ return await result.withReceipt(response);
59
63
  };
60
64
  }
61
- function getManagementResponse(result) {
65
+ async function getManagementResponse(result) {
62
66
  try {
63
- return result.withReceipt();
67
+ return await result.withReceipt();
64
68
  }
65
69
  catch (error) {
66
70
  if (Mppx_core.isMissingReceiptResponseError(error)) {
@@ -12,9 +12,12 @@ const hopByHopHeaders = new Set([
12
12
  const paymentHeaders = new Set([
13
13
  'accept-payment',
14
14
  'authorization',
15
+ 'payment-authorization',
15
16
  'payment-receipt',
16
17
  'payment-required',
17
18
  'payment-response',
19
+ 'payment-session',
20
+ 'payment-session-snapshot',
18
21
  'payment-signature',
19
22
  'www-authenticate',
20
23
  ]);
@@ -57,10 +60,72 @@ export function scrubResponse(response) {
57
60
  headers.delete('content-encoding');
58
61
  headers.delete('content-length');
59
62
  headers.delete('set-cookie');
63
+ for (const name of paymentHeaders) {
64
+ if (name !== 'authorization' && name !== 'www-authenticate')
65
+ headers.delete(name);
66
+ }
67
+ headers.delete('payment-authorization');
68
+ const authorization = headers.get('authorization');
69
+ if (authorization && /^\s*Payment(?:\s|$)/i.test(authorization))
70
+ headers.delete('authorization');
71
+ const authenticate = headers.get('www-authenticate');
72
+ if (authenticate) {
73
+ const remaining = removePaymentChallenges(authenticate);
74
+ if (remaining)
75
+ headers.set('www-authenticate', remaining);
76
+ else
77
+ headers.delete('www-authenticate');
78
+ }
60
79
  return new Response(response.body, {
61
80
  status: response.status,
62
81
  statusText: response.statusText,
63
82
  headers,
64
83
  });
65
84
  }
85
+ /** Removes Payment authentication challenges while preserving other schemes. */
86
+ function removePaymentChallenges(value) {
87
+ const starts = [];
88
+ let inQuotes = false;
89
+ let escaped = false;
90
+ for (let index = 0; index < value.length; index++) {
91
+ const character = value[index];
92
+ if (inQuotes) {
93
+ if (escaped)
94
+ escaped = false;
95
+ else if (character === '\\')
96
+ escaped = true;
97
+ else if (character === '"')
98
+ inQuotes = false;
99
+ continue;
100
+ }
101
+ if (character === '"') {
102
+ inQuotes = true;
103
+ continue;
104
+ }
105
+ const previous = value.slice(0, index).trimEnd().at(-1);
106
+ if (index !== 0 && previous !== ',')
107
+ continue;
108
+ const match = value.slice(index).match(/^\s*([!#$%&'*+.^_`|~0-9A-Za-z-]+)(?=\s|,|$)/);
109
+ if (!match)
110
+ continue;
111
+ const start = index + match[0].length - match[0].trimStart().length;
112
+ const afterScheme = start + match[1].length;
113
+ let next = afterScheme;
114
+ while (/\s/.test(value[next] ?? ''))
115
+ next++;
116
+ if (value[next] === '=')
117
+ continue;
118
+ starts.push({ index: start, scheme: match[1].toLowerCase() });
119
+ index = afterScheme - 1;
120
+ }
121
+ return starts
122
+ .filter(({ scheme }) => scheme !== 'payment')
123
+ .map(({ index }) => {
124
+ const originalPosition = starts.findIndex((start) => start.index === index);
125
+ const end = starts[originalPosition + 1]?.index ?? value.length;
126
+ return value.slice(index, end).replace(/,\s*$/, '').trim();
127
+ })
128
+ .filter(Boolean)
129
+ .join(', ');
130
+ }
66
131
  //# sourceMappingURL=Headers.js.map
@@ -30,6 +30,7 @@ export function stripe(config) {
30
30
  rewriteRequest(request, ctx) {
31
31
  const apiKey = ctx.apiKey ?? config.apiKey;
32
32
  request.headers.delete('Stripe-Account');
33
+ request.headers.delete('Stripe-Context');
33
34
  request.headers.set('Authorization', `Basic ${btoa(`${apiKey}:`)}`);
34
35
  return request;
35
36
  },
@@ -8,8 +8,14 @@ import * as Method from '../Method.js';
8
8
  import type * as Receipt from '../Receipt.js';
9
9
  import * as z from '../zod.js';
10
10
  import * as Html from './internal/html/config.js';
11
+ import * as Request from './Request.js';
11
12
  import * as Transport from './Transport.js';
12
13
  export type Methods = readonly (Method.AnyServer | readonly Method.AnyServer[])[];
14
+ type ReceiptWrapper = (response?: unknown) => unknown;
15
+ /** Returns whether a successful payment result accepts streaming receipt responses. @internal */
16
+ export declare function supportsStreamingReceipts(result: {
17
+ withReceipt: ReceiptWrapper;
18
+ }): boolean;
13
19
  export type ServerEventMap<methods extends readonly Method.Method[] = readonly Method.Method[], transport extends Transport.AnyTransport = Transport.AnyTransport> = {
14
20
  'challenge.created': ChallengeContext<methods[number], transport>;
15
21
  'payment.failed': PaymentFailedContext<methods[number], transport>;
@@ -374,10 +380,12 @@ type ConfiguredHandler<transport extends Transport.AnyTransport = Transport.Http
374
380
  name: string;
375
381
  intent: string;
376
382
  html: Html.Options | undefined;
383
+ credentialHeaders?: readonly string[] | undefined;
377
384
  meta?: Record<string, string> | undefined;
378
385
  scope?: string | undefined;
379
386
  _canonicalRequest: Record<string, unknown>;
380
387
  _stableBinding?: Method.StableBindingFn<Method.Method> | undefined;
388
+ _transport: Transport.AnyTransport;
381
389
  };
382
390
  };
383
391
  type ComposedHandler<transport extends Transport.AnyTransport = Transport.Http> = ((input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>) & {
@@ -413,6 +421,9 @@ export declare function compose(...args: readonly unknown[]): ComposedHandler;
413
421
  * On 402: writes the challenge response and ends the connection.
414
422
  * On 200: sets the Payment-Receipt header; caller should write response body.
415
423
  *
424
+ * @param handler - Payment handler to adapt.
425
+ * @param options - Node request conversion and body-size options.
426
+ *
416
427
  * @example
417
428
  * ```ts
418
429
  * import * as http from 'node:http'
@@ -431,7 +442,8 @@ export declare function compose(...args: readonly unknown[]): ComposedHandler;
431
442
  * })
432
443
  * ```
433
444
  */
434
- export declare function toNodeListener(handler: (input: globalThis.Request) => Promise<MethodFn.Response<Transport.Http>>): (req: IncomingMessage, res: ServerResponse) => Promise<MethodFn.Response<Transport.Http>>;
445
+ export declare function toNodeListener(handler: (input: globalThis.Request) => Promise<MethodFn.Response<Transport.Http>>, options?: Request.NodeConversionOptions | undefined): (req: IncomingMessage, res: ServerResponse) => Promise<MethodFn.Response<Transport.Http>>;
446
+ export declare function toNodeListener(handler: (input: globalThis.Request) => Promise<MethodFn.Response<Transport.Sse>>, options?: Request.NodeConversionOptions | undefined): (req: IncomingMessage, res: ServerResponse) => Promise<MethodFn.Response<Transport.Sse>>;
435
447
  /**
436
448
  * Flattens a methods config tuple, preserving positional types.
437
449
  * @internal
@@ -19,6 +19,11 @@ import * as SecretKey from './internal/SecretKey.js';
19
19
  import * as NodeListener from './NodeListener.js';
20
20
  import * as Request from './Request.js';
21
21
  import * as Transport from './Transport.js';
22
+ const streamingReceiptWrappers = new WeakSet();
23
+ /** Returns whether a successful payment result accepts streaming receipt responses. @internal */
24
+ export function supportsStreamingReceipts(result) {
25
+ return streamingReceiptWrappers.has(result.withReceipt);
26
+ }
22
27
  const reservedMppxKeyValues = [
23
28
  'challenge',
24
29
  'compose',
@@ -306,6 +311,7 @@ export function create(config) {
306
311
  return prepared.method.validate({
307
312
  credential: prepared.parsedCredential,
308
313
  envelope: prepared.envelope,
314
+ operation: 'validate',
309
315
  request: prepared.request,
310
316
  });
311
317
  }
@@ -327,7 +333,12 @@ export function create(config) {
327
333
  let receipt;
328
334
  try {
329
335
  if (mi.broadcast && mi.validate)
330
- await mi.validate({ credential: parsedCredential, envelope, request });
336
+ await mi.validate({
337
+ credential: parsedCredential,
338
+ envelope,
339
+ operation: 'broadcast',
340
+ request,
341
+ });
331
342
  const broadcast = mi.broadcast ?? mi.verify;
332
343
  receipt = await broadcast({ credential: parsedCredential, envelope, request });
333
344
  }
@@ -429,8 +440,15 @@ function createMethodFn(parameters) {
429
440
  name: method.name,
430
441
  intent: method.intent,
431
442
  html: method.html,
443
+ credentialHeaders: [
444
+ ...new Set([
445
+ credentialHeader ?? Constants.Headers.authorization,
446
+ ...(transport.credentialHeaders ?? []),
447
+ ]),
448
+ ],
432
449
  _canonicalRequest: PaymentRequest.fromMethod(method, { ...defaults, ...rest }),
433
450
  _stableBinding: stableBinding,
451
+ _transport: transport,
434
452
  };
435
453
  Object.defineProperty(internal, '_method', { value: method });
436
454
  const handler = async (input) => {
@@ -608,7 +626,7 @@ function createMethodFn(parameters) {
608
626
  }
609
627
  const success = (receiptData, options = {}) => {
610
628
  const { challengeId = challenge.id, credentialForReceipt = { challenge, payload: {} }, envelopeForReceipt, managementResponse, } = options;
611
- return {
629
+ const result = {
612
630
  status: 200,
613
631
  withReceipt(response) {
614
632
  if (managementResponse) {
@@ -633,6 +651,9 @@ function createMethodFn(parameters) {
633
651
  });
634
652
  },
635
653
  };
654
+ if (transport.supportsStreamingReceipts)
655
+ streamingReceiptWrappers.add(result.withReceipt);
656
+ return result;
636
657
  };
637
658
  // No credential provided—issue challenge
638
659
  if (!credential) {
@@ -644,15 +665,6 @@ function createMethodFn(parameters) {
644
665
  request: challenge.request,
645
666
  });
646
667
  if (authorized) {
647
- await events.emit('payment.success', createPaymentSuccessContext({
648
- capturedRequest,
649
- challenge,
650
- input,
651
- method,
652
- receipt: authorized.receipt,
653
- request: parsedRequest,
654
- requestInput: request,
655
- }), method);
656
668
  return success(authorized.receipt, {
657
669
  managementResponse: authorized.response,
658
670
  });
@@ -823,7 +835,12 @@ function createMethodFn(parameters) {
823
835
  let receiptData;
824
836
  try {
825
837
  if (broadcast && validate)
826
- await validate({ credential: parsedCredential, envelope, request });
838
+ await validate({
839
+ credential: parsedCredential,
840
+ envelope,
841
+ operation: 'broadcast',
842
+ request,
843
+ });
827
844
  const broadcastCredential = broadcast ?? verify;
828
845
  receiptData = await broadcastCredential({
829
846
  credential: parsedCredential,
@@ -1555,7 +1572,10 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1555
1572
  // Try to extract a Payment credential to decide whether to dispatch or challenge.
1556
1573
  // Only gate on the Payment scheme — other auth schemes (Bearer, Basic, etc.)
1557
1574
  // should fall through to the merged-402 path so all offers are presented.
1558
- const paymentHeader = Array.from(input.headers.values())
1575
+ const credentialHeaders = new Set(handlers.flatMap((handler) => getConfiguredOffers(handler).flatMap((internal) => internal.credentialHeaders ?? [])));
1576
+ const paymentHeader = Array.from(credentialHeaders)
1577
+ .map((header) => input.headers.get(header))
1578
+ .filter((value) => value !== null)
1559
1579
  .map((value) => Credential.extractPaymentScheme(value))
1560
1580
  .find((value) => value !== null);
1561
1581
  if (paymentHeader) {
@@ -1594,6 +1614,24 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1594
1614
  // Unrecognized or malformed credentials still use the currently eligible
1595
1615
  // offer set before the first handler issues its rejection challenge.
1596
1616
  }
1617
+ const transportResults = new Map();
1618
+ for (const handler of handlers) {
1619
+ for (const internal of getConfiguredOffers(handler)) {
1620
+ const matchCredential = internal._transport.matchCredential;
1621
+ if (!matchCredential)
1622
+ continue;
1623
+ try {
1624
+ if (await matchCredential({ input, request: internal._canonicalRequest })) {
1625
+ const result = await handler(input);
1626
+ transportResults.set(handler, result);
1627
+ if (result.status === 200)
1628
+ return result;
1629
+ break;
1630
+ }
1631
+ }
1632
+ catch { }
1633
+ }
1634
+ }
1597
1635
  const selectedHandlers = hasOfferPolicy
1598
1636
  ? await selectOfferHandlers({
1599
1637
  entries: offerEntries,
@@ -1601,11 +1639,19 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1601
1639
  selectOffers: offerSelector,
1602
1640
  })
1603
1641
  : handlers;
1642
+ const transportCredentialHandler = selectedHandlers.find((handler) => getConfiguredOffers(handler).some((internal) => internal.credentialHeaders?.some((header) => {
1643
+ const value = input.headers.get(header);
1644
+ return (header.toLowerCase() !== Constants.Headers.authorization.toLowerCase() &&
1645
+ value !== null &&
1646
+ Credential.extractPaymentScheme(value) === null);
1647
+ })));
1648
+ if (transportCredentialHandler)
1649
+ return transportCredentialHandler(input);
1604
1650
  // No credential — evaluate handlers sequentially so authorize()/renewal hooks
1605
1651
  // can safely claim the request without racing each other.
1606
1652
  const results = [];
1607
1653
  for (const handler of selectedHandlers) {
1608
- const result = await handler(input);
1654
+ const result = transportResults.get(handler) ?? (await handler(input));
1609
1655
  if (result.status === 200)
1610
1656
  return result;
1611
1657
  if (result.challenge.status !== 402)
@@ -1920,52 +1966,47 @@ function mergeX402PaymentRequiredHeaders(values) {
1920
1966
  }),
1921
1967
  ];
1922
1968
  }
1923
- /**
1924
- * Wraps a payment handler to create a Node.js HTTP listener.
1925
- *
1926
- * On 402: writes the challenge response and ends the connection.
1927
- * On 200: sets the Payment-Receipt header; caller should write response body.
1928
- *
1929
- * @example
1930
- * ```ts
1931
- * import * as http from 'node:http'
1932
- * import { Mppx } from 'mppx/server'
1933
- *
1934
- * const payment = Mppx.create({ ... })
1935
- *
1936
- * http.createServer(async (req, res) => {
1937
- * const result = await Mppx.toNodeListener(
1938
- * payment.charge({
1939
- * amount: '1', currency: '...', recipient: '0x...',
1940
- * }),
1941
- * )(req, res)
1942
- * if (result.status === 402) return
1943
- * res.end('OK')
1944
- * })
1945
- * ```
1946
- */
1947
- export function toNodeListener(handler) {
1969
+ export function toNodeListener(handler, options) {
1948
1970
  return async (req, res) => {
1949
- const result = await handler(Request.fromNodeListener(req, res));
1971
+ let result;
1972
+ try {
1973
+ const request = Request.fromNodeListener(req, res, options);
1974
+ await Request.waitForBody(request);
1975
+ result = await handler(request);
1976
+ }
1977
+ catch (error) {
1978
+ if (!(error instanceof Request.RequestBodyTooLargeError))
1979
+ throw error;
1980
+ const response = new globalThis.Response('Payload Too Large', {
1981
+ status: 413,
1982
+ headers: { 'Content-Type': 'text/plain' },
1983
+ });
1984
+ await NodeListener.sendResponse(res, response);
1985
+ return { challenge: response, status: 402 };
1986
+ }
1950
1987
  if (result.status === 402) {
1951
1988
  await NodeListener.sendResponse(res, result.challenge);
1952
1989
  }
1953
1990
  else {
1954
- const managementResponse = getManagementResponse(result);
1991
+ const managementResponse = await getManagementResponse(result);
1955
1992
  if (managementResponse) {
1956
1993
  await NodeListener.sendResponse(res, managementResponse);
1957
1994
  return { challenge: managementResponse, status: 402 };
1958
1995
  }
1959
- const wrapped = result.withReceipt(new globalThis.Response());
1996
+ const wrapped = await result.withReceipt(new globalThis.Response(null, { status: 204 }));
1997
+ if (wrapped.status === 402) {
1998
+ await NodeListener.sendResponse(res, wrapped);
1999
+ return { challenge: wrapped, status: 402 };
2000
+ }
1960
2001
  for (const [name, value] of wrapped.headers)
1961
2002
  res.setHeader(name, value);
1962
2003
  }
1963
2004
  return result;
1964
2005
  };
1965
2006
  }
1966
- function getManagementResponse(result) {
2007
+ async function getManagementResponse(result) {
1967
2008
  try {
1968
- return result.withReceipt();
2009
+ return await result.withReceipt();
1969
2010
  }
1970
2011
  catch (error) {
1971
2012
  if (isMissingReceiptResponseError(error)) {