mppx 0.15.0 → 0.17.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 (101) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/Challenge.d.ts +6 -0
  3. package/dist/Challenge.js +12 -0
  4. package/dist/Method.d.ts +2 -0
  5. package/dist/cli/cli.js +143 -131
  6. package/dist/cli/plugins/index.js +11 -11
  7. package/dist/cli/validate/payment.d.ts +12 -0
  8. package/dist/cli/validate/payment.js +137 -14
  9. package/dist/client/Mppx.d.ts +13 -3
  10. package/dist/client/Mppx.js +47 -22
  11. package/dist/client/internal/Fetch.d.ts +11 -0
  12. package/dist/client/internal/Fetch.js +46 -8
  13. package/dist/client/internal/ProofRealm.d.ts +21 -0
  14. package/dist/client/internal/ProofRealm.js +55 -0
  15. package/dist/client/internal/protocols/Mpp.js +4 -1
  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 +10 -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 +59 -0
  40. package/dist/identity/Server.js +194 -0
  41. package/dist/identity/Types.d.ts +112 -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 +1 -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/proxy/Proxy.js +10 -5
  53. package/dist/proxy/Service.d.ts +42 -4
  54. package/dist/proxy/Service.js +21 -5
  55. package/dist/server/Mppx.d.ts +27 -8
  56. package/dist/server/Mppx.js +164 -54
  57. package/dist/server/internal/challengeMeta.d.ts +6 -0
  58. package/dist/server/internal/challengeMeta.js +17 -1
  59. package/dist/stripe/server/internal/html.gen.js +1 -1
  60. package/dist/tempo/client/Charge.d.ts +11 -0
  61. package/dist/tempo/client/Charge.js +30 -1
  62. package/dist/tempo/internal/charge.js +2 -1
  63. package/dist/tempo/internal/defaults.d.ts +9 -0
  64. package/dist/tempo/internal/defaults.js +9 -0
  65. package/dist/tempo/internal/fee-payer.d.ts +0 -2
  66. package/dist/tempo/internal/fee-payer.js +2 -6
  67. package/dist/tempo/server/Charge.js +12 -1
  68. package/dist/tempo/server/Subscription.js +0 -1
  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/subscription/Store.js +10 -9
  78. package/package.json +15 -11
  79. package/dist/attestation/Client.d.ts +0 -17
  80. package/dist/attestation/Client.js +0 -28
  81. package/dist/attestation/Server.d.ts +0 -4
  82. package/dist/attestation/Server.js +0 -8
  83. package/dist/attestation/Types.d.ts +0 -76
  84. package/dist/attestation/internal/SigningContext.d.ts +0 -4
  85. package/dist/tap/client/index.d.ts +0 -26
  86. package/dist/tap/server/index.d.ts +0 -20
  87. /package/dist/{attestation → extensions/identity}/Types.js +0 -0
  88. /package/dist/{tap → extensions/identity/tap}/Constants.d.ts +0 -0
  89. /package/dist/{tap → extensions/identity/tap}/Types.d.ts +0 -0
  90. /package/dist/{tap → extensions/identity/tap}/Types.js +0 -0
  91. /package/dist/{tap → extensions/identity/tap}/index.d.ts +0 -0
  92. /package/dist/{tap → extensions/identity/tap}/index.js +0 -0
  93. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.d.ts +0 -0
  94. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.d.ts +0 -0
  95. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.js +0 -0
  96. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.d.ts +0 -0
  97. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.js +0 -0
  98. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.d.ts +0 -0
  99. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.js +0 -0
  100. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.d.ts +0 -0
  101. /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,109 @@ 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.get(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
+ // Normalized pieces of the current partial event, joined only when the event ends, so a
750
+ // long event costs linear time. A CR that may start a CRLF is held apart.
751
+ let parts = [];
752
+ let size = 0;
753
+ let heldCr = false;
754
+ // Set after an oversized event is dropped, until its end, so its remaining lines are
755
+ // never parsed as an event of their own.
756
+ let skipping = false;
757
+ const scan = (text, done) => {
758
+ let chunk = heldCr ? `\r${text}` : text;
759
+ heldCr = !done && chunk.endsWith('\r');
760
+ if (heldCr)
761
+ chunk = chunk.slice(0, -1);
762
+ let rest = chunk.replace(/\r\n|\r/g, '\n');
763
+ const events = [];
764
+ // A boundary split across chunks: the partial event ends in a newline and this text starts with one.
765
+ if (rest.startsWith('\n') && parts.at(-1)?.endsWith('\n')) {
766
+ events.push(parts.join('').slice(0, -1));
767
+ parts = [];
768
+ size = 0;
769
+ rest = rest.slice(1);
770
+ }
771
+ for (let index = rest.indexOf('\n\n'); index !== -1; index = rest.indexOf('\n\n')) {
772
+ events.push(parts.join('') + rest.slice(0, index));
773
+ parts = [];
774
+ size = 0;
775
+ rest = rest.slice(index + 2);
776
+ }
777
+ if (rest) {
778
+ parts.push(rest);
779
+ size += rest.length;
780
+ }
781
+ if (done) {
782
+ events.push(parts.join(''));
783
+ parts = [];
784
+ size = 0;
785
+ }
786
+ if (skipping && events.length > 0) {
787
+ events.shift();
788
+ skipping = false;
789
+ }
790
+ if (size > maxPendingEventBytes) {
791
+ // Keep a trailing newline: it may be the first half of the event's boundary.
792
+ parts = parts.at(-1)?.endsWith('\n') ? ['\n'] : [];
793
+ size = parts.length;
794
+ skipping = true;
795
+ }
796
+ for (const raw of events) {
797
+ const event = parseEvent(raw);
798
+ if (event?.type === 'payment-receipt' && !receipt)
799
+ receipt = event.data;
800
+ }
801
+ };
802
+ const body = response.body.pipeThrough(new TransformStream({
803
+ transform(chunk, controller) {
804
+ scan(decoder.decode(chunk, { stream: true }), false);
805
+ controller.enqueue(chunk);
806
+ },
807
+ flush() {
808
+ scan(decoder.decode(), true);
809
+ },
810
+ }));
811
+ const scanned = new Response(body, {
812
+ headers: response.headers,
813
+ status: response.status,
814
+ statusText: response.statusText,
815
+ });
816
+ Object.defineProperty(scanned, 'url', { value: response.url });
817
+ return scanned;
818
+ },
819
+ receipt: () => receipt,
820
+ };
821
+ }
699
822
  //# 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 */