mppx 0.17.0 → 0.17.3

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 (83) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/BodyDigest.js +13 -1
  3. package/dist/Method.d.ts +5 -0
  4. package/dist/Store.d.ts +225 -0
  5. package/dist/Store.js +358 -0
  6. package/dist/cli/cli.js +136 -135
  7. package/dist/cli/plugins/index.js +12 -11
  8. package/dist/cli/sessions/Manager.js +12 -1
  9. package/dist/cli/sessions/store.js +7 -1
  10. package/dist/cli/utils.d.ts +2 -0
  11. package/dist/cli/utils.js +6 -0
  12. package/dist/cli/validate/helpers.d.ts +1 -1
  13. package/dist/cli/validate/helpers.js +14 -2
  14. package/dist/client/Mppx.d.ts +1 -1
  15. package/dist/client/Mppx.js +18 -6
  16. package/dist/client/internal/Fetch.d.ts +3 -1
  17. package/dist/client/internal/Fetch.js +68 -3
  18. package/dist/client/internal/MethodResponse.d.ts +8 -0
  19. package/dist/client/internal/MethodResponse.js +9 -0
  20. package/dist/client/internal/ProofRealm.d.ts +3 -3
  21. package/dist/client/internal/ProofRealm.js +4 -4
  22. package/dist/client/internal/protocols/Mcp.d.ts +4 -0
  23. package/dist/client/internal/protocols/Mcp.js +27 -14
  24. package/dist/identity/Client.js +7 -1
  25. package/dist/internal/AcceptPayment.d.ts +12 -2
  26. package/dist/internal/AcceptPayment.js +12 -3
  27. package/dist/internal/sse.d.ts +21 -0
  28. package/dist/internal/sse.js +65 -0
  29. package/dist/internal/version.d.ts +2 -2
  30. package/dist/internal/version.js +1 -1
  31. package/dist/mcp/client/McpClient.d.ts +2 -2
  32. package/dist/mcp/client/McpClient.js +6 -3
  33. package/dist/proxy/Proxy.d.ts +17 -0
  34. package/dist/proxy/Proxy.js +116 -3
  35. package/dist/proxy/internal/Replay.d.ts +105 -0
  36. package/dist/proxy/internal/Replay.js +257 -0
  37. package/dist/server/Mppx.d.ts +2 -0
  38. package/dist/server/Mppx.js +7 -5
  39. package/dist/server/Transport.js +1 -1
  40. package/dist/server/internal/html/config.d.ts +7 -1
  41. package/dist/server/internal/html/config.js +4 -0
  42. package/dist/stripe/internal/types.d.ts +82 -6
  43. package/dist/stripe/server/Charge.js +1 -2
  44. package/dist/stripe/server/internal/html.gen.js +1 -1
  45. package/dist/tempo/client/Charge.d.ts +9 -0
  46. package/dist/tempo/client/Charge.js +13 -9
  47. package/dist/tempo/client/Methods.d.ts +2 -0
  48. package/dist/tempo/internal/fee-payer.d.ts +85 -10
  49. package/dist/tempo/internal/fee-payer.js +183 -19
  50. package/dist/tempo/legacy/session/Chain.js +2 -0
  51. package/dist/tempo/server/Charge.js +6 -3
  52. package/dist/tempo/server/Subscription.js +1 -0
  53. package/dist/tempo/server/internal/html.gen.js +1 -1
  54. package/dist/tempo/server/internal/transport.d.ts +6 -0
  55. package/dist/tempo/server/internal/transport.js +21 -21
  56. package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
  57. package/dist/tempo/session/client/ChannelStore.d.ts +2 -0
  58. package/dist/tempo/session/client/ChannelStore.js +5 -0
  59. package/dist/tempo/session/client/CredentialState.js +2 -0
  60. package/dist/tempo/session/client/Session.d.ts +37 -2
  61. package/dist/tempo/session/client/Session.js +111 -7
  62. package/dist/tempo/session/client/SessionManager.d.ts +2 -1
  63. package/dist/tempo/session/client/SessionManager.js +60 -9
  64. package/dist/tempo/session/client/Transports.d.ts +4 -0
  65. package/dist/tempo/session/client/Transports.js +35 -3
  66. package/dist/tempo/session/precompile/Chain.js +1 -0
  67. package/dist/tempo/session/precompile/Voucher.js +20 -7
  68. package/dist/tempo/session/server/ChannelStore.d.ts +9 -0
  69. package/dist/tempo/session/server/ChannelStore.js +8 -1
  70. package/dist/tempo/session/server/MeteredStream.d.ts +4 -0
  71. package/dist/tempo/session/server/MeteredStream.js +6 -1
  72. package/dist/tempo/session/server/Session.d.ts +2 -1
  73. package/dist/tempo/session/server/Session.js +2 -1
  74. package/dist/tempo/session/server/Settlement.d.ts +4 -2
  75. package/dist/tempo/session/server/Settlement.js +2 -2
  76. package/dist/tempo/session/server/Sse.d.ts +59 -0
  77. package/dist/tempo/session/server/Sse.js +175 -10
  78. package/dist/tempo/session/server/Ws.js +29 -12
  79. package/dist/zod.d.ts +2 -1
  80. package/dist/zod.js +1 -1
  81. package/package.json +1 -3
  82. package/dist/stripe/server/internal/html/types.d.ts +0 -2
  83. package/dist/stripe/server/internal/html/types.js +0 -2
@@ -1,4 +1,5 @@
1
1
  import * as Challenge from '../../Challenge.js';
2
+ import * as ProofRealm from '../../client/internal/ProofRealm.js';
2
3
  import { resolveEscrow } from '../../tempo/session/client/ChannelOps.js';
3
4
  import { getSessionManagerInternals } from '../../tempo/session/client/internal/SessionManager.js';
4
5
  import { sessionManager } from '../../tempo/session/client/SessionManager.js';
@@ -25,6 +26,14 @@ function assertCloseChallengeScope(challenge, channel) {
25
26
  /** Rehydrates durable session context and cooperatively closes it through the manager. */
26
27
  export async function closeWithSessionManager(parameters) {
27
28
  assertCloseChallengeScope(parameters.challenge, parameters.channel);
29
+ let channel = parameters.channel;
30
+ if (channel.origin === undefined) {
31
+ const url = new URL(parameters.input instanceof Request ? parameters.input.url : parameters.input);
32
+ if (!['http:', 'https:'].includes(url.protocol))
33
+ throw new Error('Legacy session close requires an HTTP origin');
34
+ ProofRealm.assert(parameters.challenge, url, parameters.manager.trustedTempoProofRealms);
35
+ channel = { ...channel, origin: url.origin };
36
+ }
28
37
  const networkFetch = resolveFetch(parameters.fetch);
29
38
  let pendingChallenge;
30
39
  const validatedFetch = async (input, init) => {
@@ -48,7 +57,9 @@ export async function closeWithSessionManager(parameters) {
48
57
  escrow: parameters.channel.escrow,
49
58
  fetch: validatedFetch,
50
59
  });
51
- getSessionManagerInternals(manager).rehydrate(parameters);
60
+ getSessionManagerInternals(manager).rehydrate({ ...parameters, channel });
61
+ if (channel !== parameters.channel)
62
+ await parameters.manager.channelStore?.set(channel);
52
63
  const receipt = await manager.close();
53
64
  if (!receipt)
54
65
  throw new Error('Session close response did not include a payment receipt.');
@@ -33,6 +33,7 @@ export class SessionBusyError extends Error {
33
33
  }
34
34
  }
35
35
  const storedChannelSchema = z.object({
36
+ origin: z.optional(z.string()),
36
37
  channelId: z.hash(),
37
38
  cumulativeAmount: z.string(),
38
39
  deposit: z.string(),
@@ -183,6 +184,11 @@ export function createSessionRegistry(options = {}) {
183
184
  const previous = previousValue === undefined ? undefined : parseStoredSession(previousValue, file);
184
185
  if (previous)
185
186
  assertSameSession(previous, input, file);
187
+ const origin = previous?.channel.origin ?? channel.origin;
188
+ if (previous?.channel.origin && channel.origin && previous.channel.origin !== channel.origin)
189
+ throw stateError(file, 'Session payment origin mismatch.');
190
+ if (origin !== undefined && new URL(origin).origin !== origin)
191
+ throw stateError(file, 'Session payment origin must be a normalized origin.');
186
192
  const challenge = parseSessionChallenge(input.challenge, file);
187
193
  assertChallengeMatchesChannel(challenge, channel, file);
188
194
  const receipt = input.receipt
@@ -193,7 +199,7 @@ export function createSessionRegistry(options = {}) {
193
199
  const spent = maxBigInt(input.spent ?? 0n, receipt ? BigInt(receipt.spent) : 0n, previous ? BigInt(previous.spent) : 0n);
194
200
  if (spent > cumulativeAmount)
195
201
  throw stateError(file, 'Session spend exceeds the locally authorized cumulative amount.');
196
- const storedChannel = sanitizeStoredChannel(serializeEntry({ ...channel, cumulativeAmount }), file);
202
+ const storedChannel = sanitizeStoredChannel(serializeEntry({ ...channel, cumulativeAmount, ...(origin !== undefined && { origin }) }), file);
197
203
  const timestamp = monotonicTimestamp(previous?.updatedAt, now(), file);
198
204
  const latestReceipt = selectLatestReceipt(previous?.receipt, receipt);
199
205
  const account = sanitizeAccount(input.account, file);
@@ -58,6 +58,8 @@ export declare function fmtRequestValue(key: string, value: unknown, ctx: {
58
58
  }): string;
59
59
  export declare function decodeMemo(hex: string): string | undefined;
60
60
  export declare function fmtChallengeValue(key: string, value: unknown): string;
61
+ /** Formats a problem-details field for display, serializing nested objects and arrays as JSON. */
62
+ export declare function fmtProblemValue(value: unknown): string;
61
63
  export declare function link(url: string, text: string): string;
62
64
  export declare function parseMethodOpts(raw: string | string[] | undefined): Record<string, string>;
63
65
  /** Returns true when the CLI is running inside an agent wrapper that prefers machine-readable output. */
package/dist/cli/utils.js CHANGED
@@ -141,6 +141,12 @@ export function fmtChallengeValue(key, value) {
141
141
  }
142
142
  return String(value);
143
143
  }
144
+ /** Formats a problem-details field for display, serializing nested objects and arrays as JSON. */
145
+ export function fmtProblemValue(value) {
146
+ if (value !== null && typeof value === 'object')
147
+ return JSON.stringify(value);
148
+ return String(value);
149
+ }
144
150
  export function link(url, text) {
145
151
  return pc.link(url, text);
146
152
  }
@@ -37,7 +37,7 @@ export type Counts = {
37
37
  skipped: number;
38
38
  };
39
39
  export declare function printResults(results: CheckResult[], counts: Counts): void;
40
- /** Fetches the validation target without allowing it to redirect probes elsewhere. */
40
+ /** Fetches a validation target without automatic payments or redirected probes. */
41
41
  export declare function fetchWithTimeout(url: RequestInfo | URL, init: RequestInit, timeoutMs?: number): Promise<Response>;
42
42
  export declare function formatBytes(bytes: number): string;
43
43
  export declare const HTTP_METHODS: Set<string>;
@@ -1,3 +1,4 @@
1
+ import { identityOf, skipPayment, unwrapFetch } from '../../client/internal/Fetch.js';
1
2
  import { pc } from '../utils.js';
2
3
  export function check(label, detail) {
3
4
  return detail ? { label, detail, severity: 'pass' } : { label, severity: 'pass' };
@@ -57,12 +58,23 @@ export function printResults(results, counts) {
57
58
  counts.skipped++;
58
59
  }
59
60
  }
60
- /** Fetches the validation target without allowing it to redirect probes elsewhere. */
61
+ /** Fetches a validation target without automatic payments or redirected probes. */
61
62
  export async function fetchWithTimeout(url, init, timeoutMs = 15_000) {
62
63
  const controller = new AbortController();
63
64
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
64
65
  try {
65
- return await fetch(url, { ...init, redirect: 'error', signal: controller.signal });
66
+ const fetch = globalThis.fetch;
67
+ const base = unwrapFetch(fetch);
68
+ const identity = identityOf(fetch);
69
+ const options = {
70
+ [skipPayment]: true,
71
+ ...init,
72
+ redirect: 'error',
73
+ signal: controller.signal,
74
+ };
75
+ return identity
76
+ ? (await identity.send((input, init) => base(input, { ...init, [skipPayment]: true }), new Request(url, options))).response
77
+ : await base(url, options);
66
78
  }
67
79
  finally {
68
80
  clearTimeout(timeout);
@@ -189,7 +189,7 @@ export declare namespace create {
189
189
  /** Transport to use (defaults to HTTP). */
190
190
  transport?: transport | undefined;
191
191
  /**
192
- * Server origins allowed to request zero-amount Tempo proofs for a realm other than their
192
+ * Server origins allowed to request zero-amount Tempo proofs or session payments for a realm other than their
193
193
  * hostname, mapped to that realm, such as `{ 'https://api.example.com': 'MPP Payment' }`.
194
194
  * With `Transport.mcp()`, which has no origin, only realms listed here are trusted,
195
195
  * whatever their origin.
@@ -4,6 +4,7 @@ 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 MethodResponse from './internal/MethodResponse.js';
7
8
  import * as ProofRealm from './internal/ProofRealm.js';
8
9
  import * as Transport from './Transport.js';
9
10
  const preparedPaymentMethods = new WeakMap();
@@ -71,7 +72,10 @@ export function create(config) {
71
72
  function onPaymentResponse(handler) {
72
73
  return events.on('payment.response', handler);
73
74
  }
74
- async function preparePayment(response, options) {
75
+ async function preparePayment(response, options,
76
+ // Credential context known before selection, such as an explicit session close passed to
77
+ // `createCredential`, so methods can accept offers they would otherwise skip.
78
+ selectionContext) {
75
79
  const eventResponse = snapshotResponse(response);
76
80
  const challenges = await transport.getChallenges(response, options?.request);
77
81
  const challengeSnapshots = Object.freeze(challenges.map((candidate) => snapshotValue(candidate)));
@@ -79,7 +83,12 @@ export function create(config) {
79
83
  let challenge;
80
84
  let mi;
81
85
  try {
82
- const candidates = AcceptPayment.selectChallengeCandidates(challenges, methods, preferences);
86
+ let candidates = AcceptPayment.selectChallengeCandidates(challenges, methods, preferences, selectionContext);
87
+ // Without a context yet, as in `preparePayment(response).createCredential(context)`, fall back
88
+ // to offers a method skips only by policy: an explicit close may still use them, and the
89
+ // method enforces the policy for anything else when the credential is created.
90
+ if (candidates.length === 0 && selectionContext === undefined)
91
+ candidates = AcceptPayment.selectChallengeCandidates(challenges, methods, preferences, AcceptPayment.deferredContext);
83
92
  const orderedCandidates = await resolveChallengeOrder(candidates, options?.orderChallenges ?? orderChallenges);
84
93
  const selected = orderedCandidates[0];
85
94
  if (!selected)
@@ -104,7 +113,7 @@ export function create(config) {
104
113
  try {
105
114
  if (selectedChallenge.expires)
106
115
  Expires.assert(selectedChallenge.expires, selectedChallenge.id);
107
- const createCredential = memoizeCreateCredential((overrideContext) => createCredentialForMethod(selectedChallenge, selectedMethod, overrideContext ?? context));
116
+ const createCredential = memoizeCreateCredential((overrideContext) => createCredentialForMethod(selectedChallenge, selectedMethod, overrideContext ?? context, responseUrl?.origin));
108
117
  const eventCredential = await events.emit('challenge.received', createChallengeReceivedPayload({
109
118
  challenge: selectedChallenge,
110
119
  challenges,
@@ -237,7 +246,7 @@ export function create(config) {
237
246
  preparePayment,
238
247
  prepareRequest: prepareRequest,
239
248
  async createCredential(response, context, options) {
240
- const prepared = await preparePayment(response, options);
249
+ const prepared = await preparePayment(response, options, context);
241
250
  return prepared.createCredential(context);
242
251
  },
243
252
  };
@@ -474,9 +483,12 @@ function resolveChallengePreferences(fallback, override) {
474
483
  async function resolveChallengeOrder(candidates, orderChallenges) {
475
484
  return orderChallenges ? orderChallenges(candidates) : candidates;
476
485
  }
477
- async function createCredentialForMethod(challenge, mi, context) {
486
+ async function createCredentialForMethod(challenge, mi, context, origin) {
478
487
  const parsedContext = mi.context && context !== undefined ? mi.context.parse(context) : undefined;
479
- return mi.createCredential(parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge });
488
+ const parameters = parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge };
489
+ if (origin)
490
+ MethodResponse.attachOrigin(parameters, origin);
491
+ return mi.createCredential(parameters);
480
492
  }
481
493
  /** Returns the URL of the HTTP server that sent `response`, or `undefined` when it has none. */
482
494
  function resolveResponseUrl(response) {
@@ -6,6 +6,8 @@ import type * as Method from '../../Method.js';
6
6
  import type * as z from '../../zod.js';
7
7
  import * as Transport from '../Transport.js';
8
8
  import * as ProofRealm from './ProofRealm.js';
9
+ /** Per-request payment opt-out preserved by forwarding fetch layers. @internal */
10
+ export declare const skipPayment: unique symbol;
9
11
  export type ClientEventMap<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], response = Response> = {
10
12
  'challenge.received': ChallengeReceivedPayload<methods, response>;
11
13
  'credential.created': CredentialCreatedPayload<methods, response>;
@@ -114,7 +116,7 @@ export declare namespace from {
114
116
  /** Transport to use for challenge extraction and credential attachment. */
115
117
  transport?: Transport.AnyTransport | undefined;
116
118
  /**
117
- * Server origins allowed to request zero-amount Tempo proofs for a realm other than their
119
+ * Server origins allowed to request zero-amount Tempo proofs or session payments for a realm other than their
118
120
  * hostname, mapped to that realm, such as `{ 'https://api.example.com': 'MPP Payment' }`.
119
121
  */
120
122
  trustedTempoProofRealms?: ProofRealm.TrustedRealms | undefined;
@@ -8,12 +8,15 @@ import * as Transport from '../Transport.js';
8
8
  import * as MethodChallenge from './MethodChallenge.js';
9
9
  import * as MethodResponse from './MethodResponse.js';
10
10
  import * as ProofRealm from './ProofRealm.js';
11
+ import { adapterOwnsPayment } from './protocols/Mcp.js';
11
12
  // We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
12
13
  // even across multiple module instances/bundles. This lets restore() avoid clobbering
13
14
  // an unrelated fetch installed by user code or another library.
14
15
  const MPPX_FETCH_WRAPPER = Symbol.for('mppx.fetch.wrapper');
15
16
  /** Identity runner of an mppx fetch wrapper, inherited when that wrapper is wrapped again. */
16
17
  const MPPX_FETCH_IDENTITY = Symbol.for('mppx.fetch.identity');
18
+ /** Per-request payment opt-out preserved by forwarding fetch layers. @internal */
19
+ export const skipPayment = Symbol.for('mppx.fetch.skipPayment');
17
20
  const defaultMaxPaymentRetries = 3;
18
21
  const maxErrorBodyBytes = 64 * 1024;
19
22
  const errorBodyTimeoutMs = 2_000;
@@ -125,6 +128,10 @@ export function from(config) {
125
128
  const initialRequest = prepareInitialRequest(input, init, callerHeaders, paymentPreferences.header, hasExplicitAcceptPayment, acceptPaymentPolicy);
126
129
  let response = await send(cloneRequestInput(initialRequest.input), initialRequest.init);
127
130
  const transportRequest = withCapturedBody(initialRequest.init, await capturedBody);
131
+ if (init?.[skipPayment])
132
+ return response;
133
+ if (adapterOwnsPayment(transportRequest))
134
+ return response;
128
135
  if (!(await transport.isPaymentRequired(response, transportRequest)))
129
136
  return response;
130
137
  // Only extract context for payment handling after confirming 402.
@@ -139,7 +146,7 @@ export function from(config) {
139
146
  try {
140
147
  for (let retry = 0; retry < maxPaymentRetries; retry++) {
141
148
  challenges = await transport.getChallenges(response, transportRequest);
142
- const candidates = AcceptPayment.selectChallengeCandidates(challenges, methods, paymentPreferences.entries);
149
+ const candidates = AcceptPayment.selectChallengeCandidates(challenges, methods, paymentPreferences.entries, context);
143
150
  const orderedCandidates = await resolveChallengeOrder(candidates, requestOrderChallenges ??
144
151
  orderChallenges);
145
152
  const selected = orderedCandidates[0];
@@ -161,7 +168,7 @@ export function from(config) {
161
168
  if (challenge.expires)
162
169
  Expires.assert(challenge.expires, challenge.id);
163
170
  await identity?.challenge(response, { challenge: selectedChallenge }, resolveRequestUrl(initialRequest.input).href);
164
- const paymentInput = resolvePaymentRetryInput(response, initialRequest.input, initialRequest.input);
171
+ const paymentInput = await resolvePaymentRetryInput(response, initialRequest.input, initialRequest.input, fetchInit);
165
172
  ProofRealm.assert(selectedChallenge, () => resolveRequestUrl(paymentInput), trustedTempoProofRealms);
166
173
  const previousCredential = preparedCredential;
167
174
  const key = Challenge.serialize(selectedChallenge);
@@ -184,10 +191,13 @@ export function from(config) {
184
191
  await settleAttempt(previousCredential, { status: 'rejected' });
185
192
  preparedCredential = undefined;
186
193
  }
194
+ // A new payment must not start for a request the caller already aborted.
195
+ resolveRequestSignal(input, init)?.throwIfAborted();
187
196
  const credentialContext = overrideContext ?? context;
188
197
  if (!MethodChallenge.has(selected.method))
189
198
  return resolveCredential(selectedChallenge, selected.method, credentialContext);
190
199
  const attempt = {
200
+ origin: resolveRequestUrl(paymentInput).origin,
191
201
  prepare: () => MethodChallenge.handle(selected.method, {
192
202
  challenge: selectedChallenge,
193
203
  context: credentialContext,
@@ -205,6 +215,7 @@ export function from(config) {
205
215
  .prepare()
206
216
  .then(() => resolveCredential(selectedChallenge, selected.method, credentialContext, attempt));
207
217
  preparedCredential = {
218
+ context: credentialContext,
208
219
  attempt,
209
220
  key: overrideContext === undefined ? key : undefined,
210
221
  method: selected.method,
@@ -233,6 +244,7 @@ export function from(config) {
233
244
  await settleAttempt(prepared, { status: 'rejected' });
234
245
  preparedCredential = undefined;
235
246
  }
247
+ const effectiveContext = preparedCredential ? preparedCredential.context : context;
236
248
  validateCredentialHeaderValue(credential);
237
249
  await events.emit('credential.created', createCredentialCreatedPayload({
238
250
  challenge: selectedChallenge,
@@ -316,6 +328,7 @@ export function from(config) {
316
328
  if (!response.ok)
317
329
  return response;
318
330
  return MethodResponse.handle(selected.method, {
331
+ context: effectiveContext,
319
332
  challenge: selectedChallenge,
320
333
  credential,
321
334
  fetch: baseFetch,
@@ -747,6 +760,8 @@ export function validateCredentialHeaderValue(credential) {
747
760
  async function resolveCredential(challenge, mi, context, attempt) {
748
761
  const parsedContext = mi.context && context !== undefined ? mi.context.parse(context) : undefined;
749
762
  const parameters = parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge };
763
+ if (attempt?.origin)
764
+ MethodResponse.attachOrigin(parameters, attempt.origin);
750
765
  if (attempt)
751
766
  MethodResponse.attachAttempt(mi, parameters, attempt);
752
767
  return mi.createCredential(parameters);
@@ -807,7 +822,21 @@ function resolveRequestUrl(input) {
807
822
  return new URL(input.url);
808
823
  return new URL(input, isBrowser() ? globalThis.location.href : undefined);
809
824
  }
810
- function resolvePaymentRetryInput(response, fallback, initialInput) {
825
+ /** Rejects Request modes whose paid retry cannot carry a credential to the server. */
826
+ function assertPayableRequest(input, init) {
827
+ const request = input instanceof Request ? input : undefined;
828
+ const mode = init.mode ?? request?.mode;
829
+ // Fetch strips the Authorization header from no-cors requests.
830
+ if (mode === 'no-cors')
831
+ throw new Error('Cannot automatically pay a no-cors request: it cannot carry a credential.');
832
+ const cache = init.cache ?? request?.cache;
833
+ // A cache-only retry never reaches the server.
834
+ if (cache === 'only-if-cached')
835
+ throw new Error('Cannot automatically pay an only-if-cached request: its retry cannot reach the server.');
836
+ }
837
+ /** Retains Request state while targeting the same-origin challenge response URL. */
838
+ async function resolvePaymentRetryInput(response, fallback, initialInput, init) {
839
+ assertPayableRequest(fallback, init);
811
840
  if (!response.url)
812
841
  return fallback;
813
842
  const responseUrl = new URL(response.url);
@@ -815,6 +844,42 @@ function resolvePaymentRetryInput(response, fallback, initialInput) {
815
844
  if (responseUrl.origin !== initialUrl.origin) {
816
845
  throw new Error(`Refusing to send payment credential across redirect from ${initialUrl.origin} to ${responseUrl.origin}`);
817
846
  }
847
+ const method = (init.method ?? (fallback instanceof Request ? fallback.method : 'GET')).toUpperCase();
848
+ // Fetch does not expose the redirect chain, so a non-GET/HEAD request may
849
+ // have become a GET before reaching the challenge (301/302/303).
850
+ if (response.redirected && method !== 'GET' && method !== 'HEAD')
851
+ throw new Error(`Cannot automatically pay a redirected ${method} request. Use prepareRequest() to preserve redirect semantics.`);
852
+ if (fallback instanceof Request) {
853
+ // A replacement body makes a consumed or locked original body irrelevant.
854
+ // Otherwise retain the original body for any later payment challenges.
855
+ const source = init.body != null ? fallback : fallback.clone();
856
+ // User-created requests cannot use navigate mode; retry a navigation as same-origin.
857
+ const request = new Request(source, fallback.mode === 'navigate' && init.mode === undefined
858
+ ? { ...init, mode: 'same-origin' }
859
+ : init);
860
+ // Fetch never sends a URL fragment, and the response URL has none.
861
+ if (request.url.split('#')[0] === response.url)
862
+ return request;
863
+ if (request.body) {
864
+ // Retargeting would turn the body into a stream without a length (and keepalive rejects
865
+ // streams). Retarget a buffered copy so the paid retry keeps its Content-Length.
866
+ return new Request(response.url, {
867
+ body: await request.arrayBuffer(),
868
+ cache: request.cache,
869
+ credentials: request.credentials,
870
+ headers: request.headers,
871
+ integrity: request.integrity,
872
+ keepalive: request.keepalive,
873
+ method: request.method,
874
+ mode: request.mode,
875
+ redirect: request.redirect,
876
+ referrer: request.referrer,
877
+ referrerPolicy: request.referrerPolicy,
878
+ signal: request.signal,
879
+ });
880
+ }
881
+ return new Request(response.url, request);
882
+ }
818
883
  return response.url;
819
884
  }
820
885
  //# sourceMappingURL=Fetch.js.map
@@ -7,6 +7,8 @@ export type AttemptOutcome = {
7
7
  status: 'accepted' | 'pending' | 'rejected';
8
8
  };
9
9
  export type Attempt = {
10
+ /** Authorized HTTP origin, checked before any credential is signed. */
11
+ origin?: string | undefined;
10
12
  /** Re-runs method preparation when serialized state changed before signing. */
11
13
  prepare: () => Promise<void>;
12
14
  /** Settles state created for one credential attempt. */
@@ -14,6 +16,8 @@ export type Attempt = {
14
16
  };
15
17
  /** Inputs available when a client method handles a successful paid response. */
16
18
  export type HandlerParameters = {
19
+ /** Original request policy for automatic follow-up payments. */
20
+ context?: unknown;
17
21
  challenge: Challenge.Challenge;
18
22
  credential: string;
19
23
  fetch: typeof globalThis.fetch;
@@ -41,4 +45,8 @@ export declare function handle(method: Method.AnyClient, parameters: HandlerPara
41
45
  export declare function registerTransactionHash<const method extends Method.AnyClient>(method: method, derive: TransactionHash): method;
42
46
  /** Returns the credential's on-chain hash when its method can derive it. */
43
47
  export declare function getTransactionHash(method: Method.AnyClient, credential: string): string | undefined;
48
+ /** Associates an explicitly verified HTTP origin with credential creation. */
49
+ export declare function attachOrigin(parameters: object, origin: string): void;
50
+ /** Returns the authorized origin for direct or fetch-owned credential creation. */
51
+ export declare function getOrigin(parameters: object): string | undefined;
44
52
  //# sourceMappingURL=MethodResponse.d.ts.map
@@ -1,5 +1,6 @@
1
1
  const handlers = new WeakMap();
2
2
  const attempts = new WeakMap();
3
+ const origins = new WeakMap();
3
4
  const transactionHashes = new WeakMap();
4
5
  /** Registers an internal response adapter without changing the public method shape. */
5
6
  export function register(method, handler) {
@@ -38,4 +39,12 @@ export function getTransactionHash(method, credential) {
38
39
  return undefined;
39
40
  }
40
41
  }
42
+ /** Associates an explicitly verified HTTP origin with credential creation. */
43
+ export function attachOrigin(parameters, origin) {
44
+ origins.set(parameters, origin);
45
+ }
46
+ /** Returns the authorized origin for direct or fetch-owned credential creation. */
47
+ export function getOrigin(parameters) {
48
+ return origins.get(parameters) ?? attempts.get(parameters)?.origin;
49
+ }
41
50
  //# sourceMappingURL=MethodResponse.js.map
@@ -1,12 +1,12 @@
1
1
  import type * as Challenge from '../../Challenge.js';
2
2
  /**
3
- * Pins that let a server origin request zero-amount Tempo proofs for a realm other than its
3
+ * Pins that let a server origin request zero-amount Tempo proofs or session payments for a realm other than its
4
4
  * hostname, keyed by origin (`scheme://host[:port]`), such as
5
5
  * `{ 'https://api.example.com': 'MPP Payment' }`.
6
6
  */
7
7
  export type TrustedRealms = Readonly<Record<string, string>>;
8
8
  /**
9
- * Refuses a zero-amount Tempo proof whose realm the HTTP server that sent the Challenge does not
9
+ * Refuses a Tempo proof or session payment whose realm the HTTP server that sent the Challenge does not
10
10
  * own. A proof authenticates the payer at the Challenge realm, so a foreign origin could otherwise
11
11
  * relay another server's Challenge and replay the signed proof there. The server owns the realm
12
12
  * when it is the request hostname, or when `trustedRealms` pins it to the request origin. Pass
@@ -14,7 +14,7 @@ export type TrustedRealms = Readonly<Record<string, string>>;
14
14
  */
15
15
  export declare function assert(challenge: Challenge.Challenge, url: URL | (() => URL), trustedRealms: TrustedRealms | undefined): void;
16
16
  /**
17
- * Refuses a zero-amount Tempo proof on a transport without an origin, such as MCP, unless
17
+ * Refuses a Tempo proof or session payment on a transport without an origin, such as MCP, unless
18
18
  * `trustedRealms` lists its realm.
19
19
  */
20
20
  export declare function assertWithoutOrigin(challenge: Challenge.Challenge, trustedRealms: readonly string[] | undefined): void;
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Refuses a zero-amount Tempo proof whose realm the HTTP server that sent the Challenge does not
2
+ * Refuses a Tempo proof or session payment whose realm the HTTP server that sent the Challenge does not
3
3
  * own. A proof authenticates the payer at the Challenge realm, so a foreign origin could otherwise
4
4
  * relay another server's Challenge and replay the signed proof there. The server owns the realm
5
5
  * when it is the request hostname, or when `trustedRealms` pins it to the request origin. Pass
6
6
  * `url` as a function to resolve it only for proof Challenges.
7
7
  */
8
8
  export function assert(challenge, url, trustedRealms) {
9
- if (!isProof(challenge))
9
+ if (!isProof(challenge) && !(challenge.method === 'tempo' && challenge.intent === 'session'))
10
10
  return;
11
11
  url = typeof url === 'function' ? url() : url;
12
12
  const { realm } = challenge;
@@ -19,11 +19,11 @@ export function assert(challenge, url, trustedRealms) {
19
19
  throw new Error(`Refusing to sign Tempo proof for realm ${JSON.stringify(realm)} from ${url.origin}. Pin the realm to this origin with \`trustedTempoProofRealms\` if this server is expected to issue it.`);
20
20
  }
21
21
  /**
22
- * Refuses a zero-amount Tempo proof on a transport without an origin, such as MCP, unless
22
+ * Refuses a Tempo proof or session payment on a transport without an origin, such as MCP, unless
23
23
  * `trustedRealms` lists its realm.
24
24
  */
25
25
  export function assertWithoutOrigin(challenge, trustedRealms) {
26
- if (!isProof(challenge))
26
+ if (!isProof(challenge) && !(challenge.method === 'tempo' && challenge.intent === 'session'))
27
27
  return;
28
28
  if (trustedRealms?.includes(challenge.realm))
29
29
  return;
@@ -1,5 +1,9 @@
1
1
  import * as Mcp from '../../../Mcp.js';
2
2
  import type { Protocol } from './Protocol.js';
3
+ /** Metadata that delegates all payment decisions to the MCP client adapter. */
4
+ export declare const adapterPaymentMetaKey = "mppx/payment-client";
5
+ /** Whether the caller's MCP adapter owns approval and payment retry for this request. */
6
+ export declare function adapterOwnsPayment(request?: RequestInit): boolean;
3
7
  type CorePaymentRequiredData = NonNullable<Mcp.ErrorObject['data']>;
4
8
  export type PaymentRequiredData = Pick<CorePaymentRequiredData, 'challenges'> & Partial<Pick<CorePaymentRequiredData, 'httpStatus' | 'problem'>>;
5
9
  /** Extracts validated payment-required data from MCP errors or tool result metadata. */
@@ -1,6 +1,6 @@
1
- import { createParser } from 'eventsource-parser';
2
1
  import * as Challenge from '../../../Challenge.js';
3
2
  import * as Credential from '../../../Credential.js';
3
+ import { createParser } from '../../../internal/sse.js';
4
4
  import * as Mcp from '../../../Mcp.js';
5
5
  import { paymentRequiredStatus } from './Shared.js';
6
6
  /** Returns the JSON-RPC request id — only requests can receive a `-32042` response. */
@@ -18,6 +18,22 @@ function jsonRpcRequestId(body) {
18
18
  return undefined;
19
19
  }
20
20
  }
21
+ /** Metadata that delegates all payment decisions to the MCP client adapter. */
22
+ export const adapterPaymentMetaKey = 'mppx/payment-client';
23
+ /** Whether the caller's MCP adapter owns approval and payment retry for this request. */
24
+ export function adapterOwnsPayment(request) {
25
+ if (typeof request?.body !== 'string')
26
+ return false;
27
+ try {
28
+ const message = JSON.parse(request.body);
29
+ return (message?.jsonrpc === '2.0' &&
30
+ message?.method === 'tools/call' &&
31
+ message.params?._meta?.[adapterPaymentMetaKey] === true);
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
21
37
  // Bound automatic inspection; the caller retains the original response stream.
22
38
  const maxInspectionBytes = 1024 * 1024;
23
39
  const responseCache = new WeakMap();
@@ -99,18 +115,16 @@ async function parseSseJsonRpcResponse(response) {
99
115
  let received = 0;
100
116
  let dataEventSeen = false;
101
117
  let message;
102
- const parser = createParser({
103
- onEvent(event) {
104
- if (dataEventSeen || !event.data)
105
- return;
106
- dataEventSeen = true;
107
- if (event.event && event.event !== 'message')
108
- return;
109
- try {
110
- message = parseMessage(JSON.parse(event.data));
111
- }
112
- catch { }
113
- },
118
+ const parser = createParser((event) => {
119
+ if (dataEventSeen || !event.data)
120
+ return;
121
+ dataEventSeen = true;
122
+ if (event.event !== 'message')
123
+ return;
124
+ try {
125
+ message = parseMessage(JSON.parse(event.data));
126
+ }
127
+ catch { }
114
128
  });
115
129
  try {
116
130
  for (;;) {
@@ -119,7 +133,6 @@ async function parseSseJsonRpcResponse(response) {
119
133
  const { done, value } = await reader.read();
120
134
  if (done) {
121
135
  parser.feed(decoder.decode());
122
- parser.reset({ consume: true });
123
136
  return message;
124
137
  }
125
138
  received += value.byteLength;
@@ -1,6 +1,7 @@
1
1
  import * as Challenge from '../Challenge.js';
2
2
  import * as Credential from '../Credential.js';
3
3
  import * as SigningContext from './internal/SigningContext.js';
4
+ const skipPayment = Symbol.for('mppx.fetch.skipPayment');
4
5
  /** x402 credential headers (v2 and v1); a request carrying one is a paid attempt. */
5
6
  const x402CredentialHeaders = ['payment-signature', 'x-payment'];
6
7
  /**
@@ -99,7 +100,12 @@ export function wrapFetch(fetch, client, context) {
99
100
  // A retry may answer a refreshed challenge: bind proofs to the challenge its credential
100
101
  // carries, and let provers prepare for a refreshed 402 of the same method and intent.
101
102
  const challenge = credentialChallenge(request.headers) ?? context?.challenge;
102
- const { response } = await client.send(fetch, request, { ...context, challenge });
103
+ const { response } = await client.send((input, options) => fetch(input, {
104
+ ...options,
105
+ ...(init?.[skipPayment]
106
+ ? { [skipPayment]: true }
107
+ : {}),
108
+ }), request, { ...context, challenge });
103
109
  if (response.status === 402 && challenge)
104
110
  // A 402 without a parseable Payment challenge is returned as is: the caller decides.
105
111
  for (const next of challengesOf(response))
@@ -3,10 +3,17 @@ import type { MaybePromise } from './types.js';
3
3
  type MethodLike = {
4
4
  canHandleChallenge?: ((parameters: {
5
5
  challenge: Challenge.Challenge;
6
+ context?: unknown;
6
7
  }) => boolean) | undefined;
7
8
  intent: string;
8
9
  name: string;
9
10
  };
11
+ /**
12
+ * Selection context for flows that receive the credential context only after an offer is chosen,
13
+ * such as `preparePayment()`. Methods may accept offers they would otherwise skip by policy and
14
+ * enforce that policy when the credential is created.
15
+ */
16
+ export declare const deferredContext: unique symbol;
10
17
  /** Supported challenge paired with the configured client method that can sign it. */
11
18
  export type ChallengeCandidate<method extends MethodLike = MethodLike> = method extends MethodLike ? {
12
19
  challenge: Challenge.Challenge<Record<string, unknown>, method['intent'], method['name']>;
@@ -79,8 +86,11 @@ export declare function selectChallenge<const methods extends readonly MethodLik
79
86
  challenge: Challenge.Challenge;
80
87
  method: methods[number];
81
88
  } | undefined;
82
- /** Returns supported challenge candidates ordered by client payment preferences. */
83
- export declare function selectChallengeCandidates<const methods extends readonly MethodLike[]>(challenges: readonly Challenge.Challenge[], methods: methods, preferences: readonly Entry[]): ChallengeCandidate<methods[number]>[];
89
+ /**
90
+ * Returns supported challenge candidates ordered by client payment preferences.
91
+ * `context` is the caller's credential context, when known before selection.
92
+ */
93
+ export declare function selectChallengeCandidates<const methods extends readonly MethodLike[]>(challenges: readonly Challenge.Challenge[], methods: methods, preferences: readonly Entry[], context?: unknown): ChallengeCandidate<methods[number]>[];
84
94
  /** Returns the canonical `method/intent` key for a method or challenge-like value. */
85
95
  export declare function keyOf(value: {
86
96
  intent: string;