mppx 0.14.1 → 0.16.0

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 (105) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +1 -1
  3. package/dist/Expires.js +1 -1
  4. package/dist/Store.d.ts +2 -2
  5. package/dist/Store.js +13 -3
  6. package/dist/cli/cli.js +137 -131
  7. package/dist/cli/plugins/index.js +11 -11
  8. package/dist/cli/validate/payment.d.ts +12 -0
  9. package/dist/cli/validate/payment.js +113 -14
  10. package/dist/client/Mppx.d.ts +13 -3
  11. package/dist/client/Mppx.js +47 -22
  12. package/dist/client/internal/Fetch.d.ts +11 -0
  13. package/dist/client/internal/Fetch.js +46 -8
  14. package/dist/client/internal/ProofRealm.d.ts +21 -0
  15. package/dist/client/internal/ProofRealm.js +55 -0
  16. package/dist/{attestation → extensions/identity}/Constants.d.ts +2 -2
  17. package/dist/{attestation → extensions/identity}/Constants.js +2 -2
  18. package/dist/extensions/identity/Types.d.ts +21 -0
  19. package/dist/extensions/identity/index.d.ts +7 -0
  20. package/dist/extensions/identity/index.js +6 -0
  21. package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.d.ts +3 -3
  22. package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.js +12 -10
  23. package/dist/extensions/identity/internal/SigningContext.d.ts +6 -0
  24. package/dist/{attestation → extensions/identity}/internal/SigningContext.js +3 -3
  25. package/dist/extensions/identity/internal/Verification.d.ts +9 -0
  26. package/dist/extensions/identity/internal/Verification.js +10 -0
  27. package/dist/{tap → extensions/identity/tap}/Constants.js +1 -1
  28. package/dist/extensions/identity/tap/client/index.d.ts +32 -0
  29. package/dist/{tap → extensions/identity/tap}/client/index.js +13 -9
  30. package/dist/extensions/identity/tap/server/index.d.ts +27 -0
  31. package/dist/{tap → extensions/identity/tap}/server/index.js +12 -4
  32. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.js +1 -1
  33. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.d.ts +6 -5
  34. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.js +5 -5
  35. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.d.ts +6 -5
  36. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.js +6 -4
  37. package/dist/identity/Client.d.ts +34 -0
  38. package/dist/identity/Client.js +133 -0
  39. package/dist/identity/Server.d.ts +46 -0
  40. package/dist/identity/Server.js +145 -0
  41. package/dist/identity/Types.d.ts +90 -0
  42. package/dist/identity/Types.js +2 -0
  43. package/dist/{attestation → identity}/index.d.ts +2 -2
  44. package/dist/{attestation → identity}/index.js +1 -2
  45. package/dist/identity/internal/SigningContext.d.ts +6 -0
  46. package/dist/identity/internal/SigningContext.js +11 -0
  47. package/dist/integrations/x402/Evm.d.ts +2 -0
  48. package/dist/internal/version.d.ts +2 -2
  49. package/dist/internal/version.js +1 -1
  50. package/dist/mcp/client/McpClient.d.ts +5 -0
  51. package/dist/mcp/client/McpClient.js +2 -0
  52. package/dist/middlewares/express.js +18 -6
  53. package/dist/proxy/Proxy.js +10 -5
  54. package/dist/proxy/Service.d.ts +42 -4
  55. package/dist/proxy/Service.js +21 -5
  56. package/dist/server/Mppx.d.ts +32 -8
  57. package/dist/server/Mppx.js +144 -54
  58. package/dist/stripe/server/internal/html.gen.js +1 -1
  59. package/dist/tempo/Methods.d.ts +5 -0
  60. package/dist/tempo/Methods.js +1 -0
  61. package/dist/tempo/client/Subscription.d.ts +5 -0
  62. package/dist/tempo/internal/charge.js +2 -1
  63. package/dist/tempo/internal/fee-payer.d.ts +0 -2
  64. package/dist/tempo/internal/fee-payer.js +0 -4
  65. package/dist/tempo/server/Charge.js +4 -0
  66. package/dist/tempo/server/Methods.d.ts +26 -0
  67. package/dist/tempo/server/Subscription.d.ts +5 -0
  68. package/dist/tempo/server/Subscription.js +8 -5
  69. package/dist/tempo/server/internal/html.gen.js +1 -1
  70. package/dist/tempo/session/client/Runtime.d.ts +2 -0
  71. package/dist/tempo/session/client/Session.js +1 -1
  72. package/dist/tempo/session/client/SessionManager.d.ts +6 -0
  73. package/dist/tempo/session/client/SessionManager.js +14 -7
  74. package/dist/tempo/session/client/Transports.d.ts +12 -3
  75. package/dist/tempo/session/client/Transports.js +37 -11
  76. package/dist/tempo/session/precompile/Chain.js +0 -3
  77. package/dist/tempo/session/server/Ws.d.ts +5 -2
  78. package/dist/tempo/session/server/Ws.js +11 -2
  79. package/dist/tempo/subscription/Store.d.ts +7 -1
  80. package/dist/tempo/subscription/Store.js +66 -13
  81. package/dist/tempo/subscription/Types.d.ts +2 -0
  82. package/package.json +15 -11
  83. package/dist/attestation/Client.d.ts +0 -17
  84. package/dist/attestation/Client.js +0 -28
  85. package/dist/attestation/Server.d.ts +0 -4
  86. package/dist/attestation/Server.js +0 -8
  87. package/dist/attestation/Types.d.ts +0 -76
  88. package/dist/attestation/internal/SigningContext.d.ts +0 -4
  89. package/dist/tap/client/index.d.ts +0 -26
  90. package/dist/tap/server/index.d.ts +0 -20
  91. /package/dist/{attestation → extensions/identity}/Types.js +0 -0
  92. /package/dist/{tap → extensions/identity/tap}/Constants.d.ts +0 -0
  93. /package/dist/{tap → extensions/identity/tap}/Types.d.ts +0 -0
  94. /package/dist/{tap → extensions/identity/tap}/Types.js +0 -0
  95. /package/dist/{tap → extensions/identity/tap}/index.d.ts +0 -0
  96. /package/dist/{tap → extensions/identity/tap}/index.js +0 -0
  97. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.d.ts +0 -0
  98. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.d.ts +0 -0
  99. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.js +0 -0
  100. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.d.ts +0 -0
  101. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.js +0 -0
  102. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.d.ts +0 -0
  103. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.js +0 -0
  104. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.d.ts +0 -0
  105. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.js +0 -0
@@ -1,3 +1,4 @@
1
+ import { type SessionReceipt } from '../../tempo/session/precompile/Protocol.js';
1
2
  import type { CheckResult, EndpointSpec } from './helpers.js';
2
3
  export declare function validatePaymentFlow(baseUrl: string, endpoint: EndpointSpec, verbose: boolean, options: {
3
4
  body?: string | undefined;
@@ -8,4 +9,15 @@ export declare function validatePaymentFlow(baseUrl: string, endpoint: EndpointS
8
9
  interactive?: boolean | undefined;
9
10
  onResults?: (results: CheckResult[]) => void;
10
11
  }): Promise<CheckResult[]>;
12
+ /**
13
+ * Wraps a fetch so in-stream session receipts are seen as the session client reads them: the
14
+ * client consumes `payment-receipt` events before the caller sees the body. Event-stream
15
+ * responses without a `Payment-Receipt` header pass through a scanner that keeps only the
16
+ * current partial event, so nothing is buffered or left unread.
17
+ */
18
+ /** @internal */
19
+ export declare function captureEventStream(fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>): {
20
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
21
+ receipt: () => SessionReceipt | null | undefined;
22
+ };
11
23
  //# sourceMappingURL=payment.d.ts.map
@@ -6,12 +6,14 @@ import * as viemChains from 'viem/chains';
6
6
  import { Actions } from 'viem/tempo';
7
7
  import { tempoModerato, tempo as tempoMainnetChain } from 'viem/tempo/chains';
8
8
  import * as Challenge from '../../Challenge.js';
9
+ import * as ProofRealm from '../../client/internal/ProofRealm.js';
9
10
  import * as Mppx from '../../client/Mppx.js';
10
11
  import * as Constants from '../../Constants.js';
11
12
  import * as Receipt from '../../Receipt.js';
12
13
  import { tempo as tempoMethods } from '../../tempo/client/index.js';
13
14
  import { chainId as tempoChainIds } from '../../tempo/internal/defaults.js';
14
15
  import { isTempoSessionChallenge } from '../../tempo/session/client/Transports.js';
16
+ import { isEventStream, parseEvent, } from '../../tempo/session/precompile/Protocol.js';
15
17
  import { resolveAccount, resolveAccountName } from '../account.js';
16
18
  import { assertSamePaymentRequest, flattenConfigMethods, loadConfig, preparePayment, resolvePlugin, } from '../internal.js';
17
19
  import { fetchTokenInfo, confirm, pc } from '../utils.js';
@@ -189,6 +191,7 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
189
191
  const provisioned = await provisionAndPayTestnet(tempoTestnetChallenge, verbose, options.silent);
190
192
  if (provisioned) {
191
193
  try {
194
+ const raw = captureEventStream((input, init) => fetchWithTimeout(input, init ?? {}, 30_000));
192
195
  const mppx = Mppx.create({
193
196
  methods: provisioned.methods,
194
197
  polyfill: false,
@@ -199,14 +202,14 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
199
202
  results.push(check('Payment: submitted', 'ephemeral testnet wallet'));
200
203
  return credential;
201
204
  },
202
- fetch: (input, init) => fetchWithTimeout(input, init ?? {}, 30_000),
205
+ fetch: raw.fetch,
203
206
  });
204
207
  const response = await mppx.fetch(url, {
205
208
  method: endpoint.method,
206
209
  headers: fetchHeaders,
207
210
  body: fetchBody ?? null,
208
211
  });
209
- await validatePaymentResponse(results, response, verbose, tempoModerato);
212
+ await validatePaymentResponse(results, response, verbose, tempoModerato, isTempoSessionChallenge(tempoTestnetChallenge), raw.receipt);
210
213
  }
211
214
  catch (error) {
212
215
  results.push(fail('Payment: create credential', error.message));
@@ -408,6 +411,7 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
408
411
  }
409
412
  if (directMethod && isTempoSessionChallenge(challenge)) {
410
413
  let initial = true;
414
+ const raw = captureEventStream((input, init) => fetchWithTimeout(input, init ?? {}, 30_000));
411
415
  const mppx = Mppx.create({
412
416
  methods: [directMethod],
413
417
  polyfill: false,
@@ -428,7 +432,7 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
428
432
  },
429
433
  });
430
434
  }
431
- return fetchWithTimeout(input, init ?? {}, 30_000);
435
+ return raw.fetch(input, init);
432
436
  },
433
437
  });
434
438
  try {
@@ -437,18 +441,18 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
437
441
  headers: fetchHeaders,
438
442
  body: fetchBody ?? null,
439
443
  });
440
- await validatePaymentResponse(results, response, verbose, paymentChain);
444
+ await validatePaymentResponse(results, response, verbose, paymentChain, true, raw.receipt);
441
445
  }
442
446
  catch (error) {
443
447
  results.push(fail(tag, error.message));
444
448
  }
445
449
  return;
446
450
  }
447
- const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext);
451
+ const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext, url);
448
452
  if (!credential)
449
453
  return;
450
454
  plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders });
451
- await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, paymentChain, Challenge.credentialHeader(challenge));
455
+ await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, paymentChain, Challenge.credentialHeader(challenge), isTempoSessionChallenge(challenge));
452
456
  }
453
457
  async function attemptStripePayment(challenge, tag, ctx) {
454
458
  const { results, url, endpoint, fetchHeaders, fetchBody, verbose, loaded, options, stripeKey } = ctx;
@@ -511,7 +515,7 @@ async function attemptStripePayment(challenge, tag, ctx) {
511
515
  else {
512
516
  methods = [directMethod];
513
517
  }
514
- const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext);
518
+ const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext, url);
515
519
  if (!credential)
516
520
  return;
517
521
  plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders });
@@ -532,12 +536,14 @@ async function attemptStripePayment(challenge, tag, ctx) {
532
536
  }
533
537
  await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, undefined, Challenge.credentialHeader(challenge));
534
538
  }
535
- async function createAndSend(challenge, methods, createCredentialFn, tag, results, extensions, initialCredentialContext) {
539
+ async function createAndSend(challenge, methods, createCredentialFn, tag, results, extensions, initialCredentialContext, url) {
536
540
  const fakeResponse = new Response(null, {
537
541
  status: 402,
538
542
  headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
539
543
  });
540
544
  try {
545
+ // The credential goes to url, so only sign a zero-amount proof for a realm its server owns.
546
+ ProofRealm.assert(challenge, new URL(url), undefined);
541
547
  const credentialContext = await preparePayment(challenge, extensions, initialCredentialContext);
542
548
  let credential;
543
549
  if (createCredentialFn) {
@@ -561,7 +567,7 @@ async function createAndSend(challenge, methods, createCredentialFn, tag, result
561
567
  return undefined;
562
568
  }
563
569
  }
564
- async function sendAndValidateResponse(results, url, endpoint, credential, baseHeaders, fetchBody, verbose, explorerChain, credentialHeader = Constants.Headers.authorization) {
570
+ async function sendAndValidateResponse(results, url, endpoint, credential, baseHeaders, fetchBody, verbose, explorerChain, credentialHeader = Constants.Headers.authorization, session = false) {
565
571
  let paymentResponse;
566
572
  try {
567
573
  paymentResponse = await fetchWithTimeout(url, {
@@ -574,9 +580,9 @@ async function sendAndValidateResponse(results, url, endpoint, credential, baseH
574
580
  results.push(fail('Payment: send credential', error.message));
575
581
  return results;
576
582
  }
577
- return validatePaymentResponse(results, paymentResponse, verbose, explorerChain);
583
+ return validatePaymentResponse(results, paymentResponse, verbose, explorerChain, session);
578
584
  }
579
- async function validatePaymentResponse(results, paymentResponse, verbose, explorerChain) {
585
+ async function validatePaymentResponse(results, paymentResponse, verbose, explorerChain, session = false, streamReceipt) {
580
586
  if (paymentResponse.status === 402) {
581
587
  const body = await paymentResponse.text().catch(() => '');
582
588
  let detail = 'Payment rejected';
@@ -599,8 +605,22 @@ async function validatePaymentResponse(results, paymentResponse, verbose, explor
599
605
  results.push(check('Payment: successful', `HTTP ${paymentResponse.status}`));
600
606
  }
601
607
  // Validate receipt
608
+ const contentType = paymentResponse.headers.get('content-type') ?? '';
602
609
  const receiptHeader = paymentResponse.headers.get(Constants.Headers.paymentReceipt);
603
- if (!receiptHeader) {
610
+ const stream = !receiptHeader && session && isEventStream(paymentResponse);
611
+ const body = await paymentResponse.text().catch(() => '');
612
+ if (stream) {
613
+ // SSE sessions deliver the receipt as a final payment-receipt event instead of a header.
614
+ // The session client consumes that event, so it is captured from the server's stream as
615
+ // the client reads it; a stream read directly is parsed here.
616
+ const captured = streamReceipt?.();
617
+ const receipt = captured === undefined ? findReceipt(body) : (captured ?? undefined);
618
+ if (receipt)
619
+ results.push(check('Payment receipt in event stream', receipt.reference));
620
+ else
621
+ results.push(fail('Payment receipt in event stream', undefined, 'SSE session responses without a Payment-Receipt header must end with an "event: payment-receipt" carrying the session receipt.'));
622
+ }
623
+ else if (!receiptHeader) {
604
624
  results.push(fail('Payment-Receipt header present', undefined, 'After accepting payment, include a Payment-Receipt header with a base64url-encoded JSON object containing: method, reference, status ("success"), and timestamp (ISO 8601).'));
605
625
  }
606
626
  else {
@@ -646,8 +666,6 @@ async function validatePaymentResponse(results, paymentResponse, verbose, explor
646
666
  }
647
667
  }
648
668
  // Validate response body
649
- const contentType = paymentResponse.headers.get('content-type') ?? '';
650
- const body = await paymentResponse.text().catch(() => '');
651
669
  if (body.length > 0) {
652
670
  results.push(check('Response body non-empty', `${contentType.split(';')[0]}, ${formatBytes(body.length)}`));
653
671
  }
@@ -696,4 +714,85 @@ async function validatePaymentResponse(results, paymentResponse, verbose, explor
696
714
  }
697
715
  return results;
698
716
  }
717
+ /** Returns the first session receipt in an event-stream body. */
718
+ function findReceipt(body) {
719
+ for (const raw of body.replace(/\r\n|\r/g, '\n').split('\n\n')) {
720
+ const event = parseEvent(raw);
721
+ if (event?.type === 'payment-receipt')
722
+ return event.data;
723
+ }
724
+ return undefined;
725
+ }
726
+ /** Longest partial event kept while scanning; a receipt event is far smaller. */
727
+ const maxPendingEventBytes = 1024 * 1024;
728
+ /**
729
+ * Wraps a fetch so in-stream session receipts are seen as the session client reads them: the
730
+ * client consumes `payment-receipt` events before the caller sees the body. Event-stream
731
+ * responses without a `Payment-Receipt` header pass through a scanner that keeps only the
732
+ * current partial event, so nothing is buffered or left unread.
733
+ */
734
+ /** @internal */
735
+ export function captureEventStream(fetch) {
736
+ // `undefined`: no stream scanned; `null`: scanned without a receipt.
737
+ let receipt;
738
+ return {
739
+ async fetch(input, init) {
740
+ const response = await fetch(input, init);
741
+ if (!response.body ||
742
+ !isEventStream(response) ||
743
+ response.headers.has(Constants.Headers.paymentReceipt))
744
+ return response;
745
+ // Session management requests (such as mid-stream voucher posts) share this fetch, so a
746
+ // later stream without a receipt must not discard one already seen.
747
+ receipt ??= null;
748
+ const decoder = new TextDecoder();
749
+ let pending = '';
750
+ // Set after an oversized event is dropped, until its end, so its remaining lines are
751
+ // never parsed as an event of their own.
752
+ let skipping = false;
753
+ const scan = (text, done) => {
754
+ pending += text;
755
+ // Keep a trailing CR: the next chunk may complete a CRLF.
756
+ const end = done || !pending.endsWith('\r') ? pending.length : pending.length - 1;
757
+ const events = pending
758
+ .slice(0, end)
759
+ .replace(/\r\n|\r/g, '\n')
760
+ .split('\n\n');
761
+ const rest = done ? '' : (events.pop() ?? '');
762
+ if (skipping && (events.length > 0 || done)) {
763
+ events.shift();
764
+ skipping = false;
765
+ }
766
+ pending = rest + pending.slice(end);
767
+ if (pending.length > maxPendingEventBytes) {
768
+ // Keep a trailing line ending: it may be the first half of the event's boundary.
769
+ pending = /[\r\n]*$/.exec(pending)?.[0] ?? '';
770
+ skipping = true;
771
+ }
772
+ for (const raw of events) {
773
+ const event = parseEvent(raw);
774
+ if (event?.type === 'payment-receipt' && !receipt)
775
+ receipt = event.data;
776
+ }
777
+ };
778
+ const body = response.body.pipeThrough(new TransformStream({
779
+ transform(chunk, controller) {
780
+ scan(decoder.decode(chunk, { stream: true }), false);
781
+ controller.enqueue(chunk);
782
+ },
783
+ flush() {
784
+ scan(decoder.decode(), true);
785
+ },
786
+ }));
787
+ const scanned = new Response(body, {
788
+ headers: response.headers,
789
+ status: response.status,
790
+ statusText: response.statusText,
791
+ });
792
+ Object.defineProperty(scanned, 'url', { value: response.url });
793
+ return scanned;
794
+ },
795
+ receipt: () => receipt,
796
+ };
797
+ }
699
798
  //# sourceMappingURL=payment.js.map
@@ -1,5 +1,5 @@
1
- import type * as Attestation from '../attestation/Types.js';
2
1
  import type * as Challenge from '../Challenge.js';
2
+ import type * as Identity from '../identity/Types.js';
3
3
  import * as AcceptPayment from '../internal/AcceptPayment.js';
4
4
  import type * as Method from '../Method.js';
5
5
  import type * as z from '../zod.js';
@@ -163,8 +163,11 @@ export declare namespace createCredential {
163
163
  export declare function restore(): void;
164
164
  export declare namespace create {
165
165
  type Config<methods extends Methods = Methods, transport extends Transport.AnyTransport = Transport.Transport> = {
166
- /** Request-attestation signers applied to every outbound HTTP attempt. */
167
- attestation?: transport extends Transport.Transport<RequestInit, Response> ? Attestation.SignerMap | undefined : never;
166
+ /**
167
+ * Identity provers applied to every in-scope HTTP attempt, in order.
168
+ * List request signers last so they cover headers added by earlier provers.
169
+ */
170
+ identity?: transport extends Transport.Transport<RequestInit, Response> ? readonly Identity.Prover[] | undefined : never;
168
171
  /** Controls when `Accept-Payment` is injected. */
169
172
  acceptPaymentPolicy?: Fetch.from.Config['acceptPaymentPolicy'] | undefined;
170
173
  /** Custom fetch function to wrap. Defaults to `globalThis.fetch`. */
@@ -185,6 +188,13 @@ export declare namespace create {
185
188
  polyfill?: boolean | undefined;
186
189
  /** Transport to use (defaults to HTTP). */
187
190
  transport?: transport | undefined;
191
+ /**
192
+ * Server origins allowed to request zero-amount Tempo proofs for a realm other than their
193
+ * hostname, mapped to that realm, such as `{ 'https://api.example.com': 'MPP Payment' }`.
194
+ * With `Transport.mcp()`, which has no origin, only realms listed here are trusted,
195
+ * whatever their origin.
196
+ */
197
+ trustedTempoProofRealms?: Fetch.from.Config['trustedTempoProofRealms'] | undefined;
188
198
  };
189
199
  }
190
200
  /**
@@ -1,9 +1,10 @@
1
- import * as AttestationClient from '../attestation/Client.js';
2
1
  import * as Constants from '../Constants.js';
3
2
  import * as Expires from '../Expires.js';
3
+ import * as IdentityClient from '../identity/Client.js';
4
4
  import * as AcceptPayment from '../internal/AcceptPayment.js';
5
5
  import * as Fetch from './internal/Fetch.js';
6
6
  import * as MethodChallenge from './internal/MethodChallenge.js';
7
+ import * as ProofRealm from './internal/ProofRealm.js';
7
8
  import * as Transport from './Transport.js';
8
9
  const preparedPaymentMethods = new WeakMap();
9
10
  /**
@@ -28,16 +29,16 @@ const preparedPaymentMethods = new WeakMap();
28
29
  * ```
29
30
  */
30
31
  export function create(config) {
31
- const { attestation, maxPaymentRetries, onChallenge, orderChallenges, polyfill = true, acceptPaymentPolicy = polyfill && typeof globalThis.location !== 'undefined'
32
+ const { identity, maxPaymentRetries, onChallenge, orderChallenges, polyfill = true, acceptPaymentPolicy = polyfill && typeof globalThis.location !== 'undefined'
32
33
  ? 'same-origin'
33
- : 'always', transport = Transport.http(), } = config;
34
+ : 'always', transport = Transport.http(), trustedTempoProofRealms, } = config;
35
+ if ('attestation' in config)
36
+ throw new TypeError('Mppx client `attestation` was replaced by `identity: [prover]`.');
34
37
  const rawFetch = Fetch.unwrapFetch(config.fetch ?? globalThis.fetch);
35
- const attestationSigner = attestation
36
- ? createAttestationSigner(attestation)
37
- : undefined;
38
- const attestedFetch = attestationSigner
39
- ? AttestationClient.wrapFetch(rawFetch, attestationSigner)
40
- : rawFetch;
38
+ // Rewrapping an mppx fetch keeps its identity provers unless new ones are configured.
39
+ const identityClient = identity
40
+ ? IdentityClient.from(identity)
41
+ : config.fetch && Fetch.identityOf(config.fetch);
41
42
  const methods = config.methods.flat();
42
43
  const acceptPayment = AcceptPayment.resolve(methods, config.paymentPreferences);
43
44
  const events = Fetch.createEventDispatcher();
@@ -45,13 +46,15 @@ export function create(config) {
45
46
  const config_fetch = {
46
47
  acceptPayment,
47
48
  acceptPaymentPolicy,
48
- ...((config.fetch || attestation) && { fetch: attestedFetch }),
49
+ ...(config.fetch && { fetch: rawFetch }),
50
+ ...(identityClient && { identity: identityClient }),
49
51
  eventDispatcher: events,
50
52
  ...(maxPaymentRetries !== undefined && { maxPaymentRetries }),
51
53
  ...(resolvedOnChallenge && { onChallenge: resolvedOnChallenge }),
52
54
  ...(orderChallenges && { orderChallenges }),
53
55
  methods,
54
56
  transport,
57
+ ...(trustedTempoProofRealms && { trustedTempoProofRealms }),
55
58
  };
56
59
  const fetch = Fetch.from(config_fetch);
57
60
  if (polyfill)
@@ -88,6 +91,13 @@ export function create(config) {
88
91
  mi = selected.method;
89
92
  if (challenge.expires)
90
93
  Expires.assert(challenge.expires, challenge.id);
94
+ const responseUrl = resolveResponseUrl(response);
95
+ if (responseUrl)
96
+ ProofRealm.assert(challenge, responseUrl, trustedTempoProofRealms);
97
+ // The MCP transport has no origin that could own the realm. Custom transports, such as the
98
+ // CLI's, check the realm against their own target.
99
+ else if (transport.name === 'mcp')
100
+ ProofRealm.assertWithoutOrigin(challenge, trustedTempoProofRealms && Object.values(trustedTempoProofRealms));
91
101
  const selectedChallenge = challenge;
92
102
  const selectedMethod = mi;
93
103
  const createPreparedCredential = memoizeCreateCredential(async (context) => {
@@ -156,7 +166,7 @@ export function create(config) {
156
166
  init,
157
167
  input,
158
168
  maxRedirects,
159
- signer: attestationSigner,
169
+ identity: identityClient,
160
170
  });
161
171
  const requestInit = requestToInit(preparedHttp.replayRequest, preparedHttp.body);
162
172
  if (!(await transport.isPaymentRequired(preparedHttp.response, requestInit))) {
@@ -173,13 +183,17 @@ export function create(config) {
173
183
  ...paymentOptions,
174
184
  request: requestInit,
175
185
  }));
186
+ ProofRealm.assert(payment.challenge, new URL(preparedHttp.replayRequest.url), trustedTempoProofRealms);
176
187
  const paymentMethod = preparedPaymentMethods.get(payment) ?? payment.method;
177
188
  const createRequestCredential = memoizeCreateCredential(async (context) => {
189
+ await identityClient?.challenge(preparedHttp.response, { challenge: payment.challenge }, preparedHttp.replayRequest.url);
178
190
  if (MethodChallenge.has(paymentMethod))
179
191
  await MethodChallenge.handle(paymentMethod, {
180
192
  challenge: payment.challenge,
181
193
  context,
182
- fetch: attestedFetch,
194
+ fetch: identityClient
195
+ ? IdentityClient.wrapFetch(rawFetch, identityClient, { challenge: payment.challenge })
196
+ : rawFetch,
183
197
  input: preparedHttp.replayRequest,
184
198
  signal: preparedHttp.replayRequest.signal,
185
199
  });
@@ -191,10 +205,16 @@ export function create(config) {
191
205
  async pay(context) {
192
206
  const credential = await createRequestCredential(context);
193
207
  const paidInit = payment.setCredential(requestToInit(preparedHttp.replayRequest, preparedHttp.body), credential);
194
- return attestedFetch(preparedHttp.replayRequest.url, {
208
+ const paidRequest = new Request(preparedHttp.replayRequest.url, {
195
209
  ...paidInit,
196
210
  redirect: 'manual',
197
211
  });
212
+ if (!identityClient)
213
+ return rawFetch(paidRequest);
214
+ const { response } = await identityClient.send(rawFetch, paidRequest, {
215
+ challenge: payment.challenge,
216
+ });
217
+ return response;
198
218
  },
199
219
  });
200
220
  return Object.freeze({
@@ -239,12 +259,6 @@ export function create(config) {
239
259
  export function restore() {
240
260
  Fetch.restore();
241
261
  }
242
- function createAttestationSigner(signers) {
243
- const values = Object.values(signers);
244
- if (values.length === 0)
245
- throw new TypeError('Mppx client attestation must configure at least one signer.');
246
- return AttestationClient.composeSigners(...values);
247
- }
248
262
  const redirectStatuses = new Set([301, 302, 303, 307, 308]);
249
263
  const bodyHeaders = [
250
264
  'content-encoding',
@@ -265,7 +279,7 @@ const crossOriginHeaders = [
265
279
  'x-payment',
266
280
  ];
267
281
  async function prepareHttpRequest(parameters) {
268
- const { acceptPayment, acceptPaymentPolicy, fetch, init, input, maxRedirects, signer } = parameters;
282
+ const { acceptPayment, acceptPaymentPolicy, fetch, identity, init, input, maxRedirects } = parameters;
269
283
  if (!Number.isInteger(maxRedirects) || maxRedirects < 0)
270
284
  throw new TypeError('maxRedirects must be a non-negative integer.');
271
285
  let request = new Request(input, { ...init, redirect: 'manual' });
@@ -274,8 +288,13 @@ async function prepareHttpRequest(parameters) {
274
288
  const redirects = [];
275
289
  for (;;) {
276
290
  request = withAcceptPayment(request, acceptPayment, explicitAcceptPayment, acceptPaymentPolicy);
277
- const sentRequest = signer ? await signer.sign(request.clone()) : request.clone();
278
- const response = await fetch(sentRequest);
291
+ const { request: sentRequest, response } = await (async () => {
292
+ // `request` stays unread for replay; the reported request is the one actually sent.
293
+ if (identity)
294
+ return identity.send(fetch, request.clone());
295
+ const sent = request.clone();
296
+ return { request: sent, response: await fetch(sent) };
297
+ })();
279
298
  if (response.type === 'opaqueredirect' || response.status === 0)
280
299
  throw new Error('prepareRequest requires a runtime that exposes manual redirect responses.');
281
300
  if (!redirectStatuses.has(response.status))
@@ -459,4 +478,10 @@ async function createCredentialForMethod(challenge, mi, context) {
459
478
  const parsedContext = mi.context && context !== undefined ? mi.context.parse(context) : undefined;
460
479
  return mi.createCredential(parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge });
461
480
  }
481
+ /** Returns the URL of the HTTP server that sent `response`, or `undefined` when it has none. */
482
+ function resolveResponseUrl(response) {
483
+ if (typeof Response !== 'undefined' && response instanceof Response && response.url)
484
+ return new URL(response.url);
485
+ return undefined;
486
+ }
462
487
  //# sourceMappingURL=Mppx.js.map
@@ -1,9 +1,11 @@
1
1
  import * as Challenge from '../../Challenge.js';
2
+ import * as IdentityClient from '../../identity/Client.js';
2
3
  import * as AcceptPayment from '../../internal/AcceptPayment.js';
3
4
  import type { MaybePromise } from '../../internal/types.js';
4
5
  import type * as Method from '../../Method.js';
5
6
  import type * as z from '../../zod.js';
6
7
  import * as Transport from '../Transport.js';
8
+ import * as ProofRealm from './ProofRealm.js';
7
9
  export type ClientEventMap<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], response = Response> = {
8
10
  'challenge.received': ChallengeReceivedPayload<methods, response>;
9
11
  'credential.created': CredentialCreatedPayload<methods, response>;
@@ -95,6 +97,8 @@ export declare namespace from {
95
97
  } | undefined;
96
98
  /** Custom fetch function to wrap. Defaults to `globalThis.fetch`. */
97
99
  fetch?: typeof globalThis.fetch;
100
+ /** Identity provers applied to every in-scope HTTP attempt. */
101
+ identity?: IdentityClient.Client | undefined;
98
102
  /** Advanced shared event dispatcher. `challenge.received` handlers run before `onChallenge`; the first non-empty credential returned by a handler skips `onChallenge`. */
99
103
  eventDispatcher?: ClientEventDispatcher<methods, any> | undefined;
100
104
  /** Maximum number of payment challenge retries after the initial response. @default 3 */
@@ -109,6 +113,11 @@ export declare namespace from {
109
113
  orderChallenges?: AcceptPayment.OrderChallenges<methods> | undefined;
110
114
  /** Transport to use for challenge extraction and credential attachment. */
111
115
  transport?: Transport.AnyTransport | undefined;
116
+ /**
117
+ * Server origins allowed to request zero-amount Tempo proofs for a realm other than their
118
+ * hostname, mapped to that realm, such as `{ 'https://api.example.com': 'MPP Payment' }`.
119
+ */
120
+ trustedTempoProofRealms?: ProofRealm.TrustedRealms | undefined;
112
121
  };
113
122
  type Fetch<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[]> = (input: RequestInfo | URL, init?: RequestInit<methods>) => Promise<Response>;
114
123
  type RequestInit<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[]> = globalThis.RequestInit & {
@@ -168,6 +177,8 @@ export declare function normalizeHeaders(headers: unknown): Record<string, strin
168
177
  */
169
178
  export declare function createEventDispatcher<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], response = Response>(): ClientEventDispatcher<methods, response>;
170
179
  /** @internal */
180
+ /** Returns the identity runner of an mppx fetch wrapper, if it has one. @internal */
181
+ export declare function identityOf(fetch: typeof globalThis.fetch): IdentityClient.Client | undefined;
171
182
  /** @internal */
172
183
  export declare function unwrapFetch(fetch: typeof globalThis.fetch): typeof globalThis.fetch;
173
184
  /** @internal */
@@ -2,14 +2,18 @@ import * as Challenge from '../../Challenge.js';
2
2
  import * as Constants from '../../Constants.js';
3
3
  import { PaymentOutcomeUnknownError } from '../../Errors.js';
4
4
  import * as Expires from '../../Expires.js';
5
+ import * as IdentityClient from '../../identity/Client.js';
5
6
  import * as AcceptPayment from '../../internal/AcceptPayment.js';
6
7
  import * as Transport from '../Transport.js';
7
8
  import * as MethodChallenge from './MethodChallenge.js';
8
9
  import * as MethodResponse from './MethodResponse.js';
10
+ import * as ProofRealm from './ProofRealm.js';
9
11
  // We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
10
12
  // even across multiple module instances/bundles. This lets restore() avoid clobbering
11
13
  // an unrelated fetch installed by user code or another library.
12
14
  const MPPX_FETCH_WRAPPER = Symbol.for('mppx.fetch.wrapper');
15
+ /** Identity runner of an mppx fetch wrapper, inherited when that wrapper is wrapped again. */
16
+ const MPPX_FETCH_IDENTITY = Symbol.for('mppx.fetch.identity');
13
17
  const defaultMaxPaymentRetries = 3;
14
18
  const maxErrorBodyBytes = 64 * 1024;
15
19
  const errorBodyTimeoutMs = 2_000;
@@ -101,19 +105,25 @@ async function settleAttempt(prepared, outcome) {
101
105
  *
102
106
  */
103
107
  export function from(config) {
104
- const { acceptPayment, acceptPaymentPolicy = 'always', fetch = globalThis.fetch, maxPaymentRetries = defaultMaxPaymentRetries, methods, onChallenge, orderChallenges, transport = Transport.http(), } = config;
108
+ const { acceptPayment, acceptPaymentPolicy = 'always', fetch = globalThis.fetch, maxPaymentRetries = defaultMaxPaymentRetries, methods, onChallenge, orderChallenges, transport = Transport.http(), trustedTempoProofRealms, } = config;
109
+ // Rewrapping an mppx fetch keeps its identity provers unless new ones are configured.
110
+ // Only an explicitly passed mppx fetch hands down its provers; the global fetch never does.
111
+ const identity = config.identity ?? (config.fetch && identityOf(config.fetch));
105
112
  const events = config.eventDispatcher ?? createEventDispatcher();
106
113
  const resolvedAcceptPayment = acceptPayment ?? AcceptPayment.resolve(methods);
107
114
  // Always operate on the true underlying fetch to avoid wrapper-on-wrapper stacking,
108
115
  // which can duplicate retries and make restore semantics fragile.
109
116
  const baseFetch = unwrapFetch(fetch);
117
+ const send = async (input, init, context) => identity
118
+ ? (await identity.send(baseFetch, new Request(input, init), context)).response
119
+ : baseFetch(input, init);
110
120
  const request = async (input, init, canRefetch) => {
111
121
  const callerHeaders = getCallerHeaders(input, init?.headers);
112
122
  const hasExplicitAcceptPayment = callerHeaders.has(Constants.Headers.acceptPayment);
113
123
  const paymentPreferences = resolvePaymentPreferences(callerHeaders, resolvedAcceptPayment);
114
124
  const capturedBody = captureRequestBody(input, init, callerHeaders);
115
125
  const initialRequest = prepareInitialRequest(input, init, callerHeaders, paymentPreferences.header, hasExplicitAcceptPayment, acceptPaymentPolicy);
116
- let response = await baseFetch(cloneRequestInput(initialRequest.input), initialRequest.init);
126
+ let response = await send(cloneRequestInput(initialRequest.input), initialRequest.init);
117
127
  const transportRequest = withCapturedBody(initialRequest.init, await capturedBody);
118
128
  if (!(await transport.isPaymentRequired(response, transportRequest)))
119
129
  return response;
@@ -150,7 +160,9 @@ export function from(config) {
150
160
  mi = selected.method;
151
161
  if (challenge.expires)
152
162
  Expires.assert(challenge.expires, challenge.id);
163
+ await identity?.challenge(response, { challenge: selectedChallenge }, resolveRequestUrl(initialRequest.input).href);
153
164
  const paymentInput = resolvePaymentRetryInput(response, initialRequest.input, initialRequest.input);
165
+ ProofRealm.assert(selectedChallenge, () => resolveRequestUrl(paymentInput), trustedTempoProofRealms);
154
166
  const previousCredential = preparedCredential;
155
167
  const key = Challenge.serialize(selectedChallenge);
156
168
  const sameAttempt = previousCredential?.method === selected.method && previousCredential?.key === key;
@@ -179,7 +191,12 @@ export function from(config) {
179
191
  prepare: () => MethodChallenge.handle(selected.method, {
180
192
  challenge: selectedChallenge,
181
193
  context: credentialContext,
182
- fetch: baseFetch,
194
+ // Management requests (e.g. session top-ups) carry the same identity proofs.
195
+ fetch: identity
196
+ ? IdentityClient.wrapFetch(baseFetch, identity, {
197
+ challenge: selectedChallenge,
198
+ })
199
+ : baseFetch,
183
200
  input: paymentInput,
184
201
  signal: resolveRequestSignal(input, init),
185
202
  }),
@@ -232,14 +249,14 @@ export function from(config) {
232
249
  transactionHash: MethodResponse.getTransactionHash(selected.method, credential),
233
250
  });
234
251
  try {
235
- response = await baseFetch(paymentInput, {
252
+ response = await send(paymentInput, {
236
253
  ...transport.setCredential({
237
254
  ...fetchInit,
238
255
  headers: initialRequest.headers,
239
256
  }, credential, { challenge: selectedChallenge }),
240
257
  // Runtimes do not strip custom payment headers on cross-origin redirects.
241
258
  redirect: 'manual',
242
- });
259
+ }, { challenge: selectedChallenge });
243
260
  }
244
261
  catch (cause) {
245
262
  outcomeUnknown = unknownOutcome(cause);
@@ -347,6 +364,8 @@ export function from(config) {
347
364
  };
348
365
  const wrappedFetch = (input, init) => request(input, init, true);
349
366
  wrappedFetch[MPPX_FETCH_WRAPPER] = baseFetch;
367
+ if (identity)
368
+ wrappedFetch[MPPX_FETCH_IDENTITY] = identity;
350
369
  return wrappedFetch;
351
370
  }
352
371
  /**
@@ -380,7 +399,8 @@ export function polyfill(config) {
380
399
  globalThis.fetch = from({
381
400
  ...config,
382
401
  acceptPaymentPolicy: config.acceptPaymentPolicy ?? (isBrowser() ? 'same-origin' : 'always'),
383
- fetch: globalThis.fetch,
402
+ // Unwrapped so a previous polyfill's identity provers are not inherited.
403
+ fetch: unwrapFetch(globalThis.fetch),
384
404
  });
385
405
  }
386
406
  /**
@@ -586,8 +606,22 @@ function snapshotMethod(method) {
586
606
  return freezeSnapshot(Object.assign({}, method));
587
607
  }
588
608
  function snapshotResponse(response) {
589
- if (response instanceof Response)
590
- return response.clone();
609
+ if (response instanceof Response) {
610
+ try {
611
+ return response.clone();
612
+ }
613
+ catch {
614
+ // Consumed or locked bodies cannot be cloned; retain isolated status, headers and URL.
615
+ const snapshot = new Response(null, {
616
+ headers: response.headers,
617
+ status: response.status,
618
+ statusText: response.statusText,
619
+ });
620
+ for (const key of ['redirected', 'type', 'url'])
621
+ Object.defineProperty(snapshot, key, { value: response[key] });
622
+ return snapshot;
623
+ }
624
+ }
591
625
  return snapshotValue(response);
592
626
  }
593
627
  function snapshotValue(value) {
@@ -667,6 +701,10 @@ function getCallerHeaders(input, headers) {
667
701
  return new Headers(input instanceof Request ? input.headers : undefined);
668
702
  }
669
703
  /** @internal */
704
+ /** Returns the identity runner of an mppx fetch wrapper, if it has one. @internal */
705
+ export function identityOf(fetch) {
706
+ return fetch[MPPX_FETCH_IDENTITY];
707
+ }
670
708
  /** @internal */
671
709
  export function unwrapFetch(fetch) {
672
710
  let current = fetch;