mppx 0.9.3 → 0.10.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.
@@ -1,8 +1,11 @@
1
1
  import * as AttestationClient from '../attestation/Client.js';
2
+ import * as Constants from '../Constants.js';
2
3
  import * as Expires from '../Expires.js';
3
4
  import * as AcceptPayment from '../internal/AcceptPayment.js';
4
5
  import * as Fetch from './internal/Fetch.js';
6
+ import * as MethodChallenge from './internal/MethodChallenge.js';
5
7
  import * as Transport from './Transport.js';
8
+ const preparedPaymentMethods = new WeakMap();
6
9
  /**
7
10
  * Creates a client-side payment handler from an array of methods.
8
11
  *
@@ -28,9 +31,12 @@ export function create(config) {
28
31
  const { attestation, maxPaymentRetries, onChallenge, orderChallenges, polyfill = true, acceptPaymentPolicy = polyfill && typeof globalThis.location !== 'undefined'
29
32
  ? 'same-origin'
30
33
  : 'always', transport = Transport.http(), } = config;
31
- const rawFetch = config.fetch ?? globalThis.fetch;
32
- const attestedFetch = attestation
33
- ? createAttestedFetch(rawFetch, attestation)
34
+ 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)
34
40
  : rawFetch;
35
41
  const methods = config.methods.flat();
36
42
  const acceptPayment = AcceptPayment.resolve(methods, config.paymentPreferences);
@@ -117,7 +123,7 @@ export function create(config) {
117
123
  throw error;
118
124
  }
119
125
  });
120
- return Object.freeze({
126
+ const prepared = Object.freeze({
121
127
  challenge: selectedChallenge,
122
128
  challenges: challengeSnapshots,
123
129
  createCredential: createPreparedCredential,
@@ -127,6 +133,8 @@ export function create(config) {
127
133
  return transport.setCredential(request, credential, { challenge: transportChallenge });
128
134
  },
129
135
  });
136
+ preparedPaymentMethods.set(prepared, selectedMethod);
137
+ return prepared;
130
138
  }
131
139
  catch (error) {
132
140
  await events.emit('payment.failed', createPaymentFailedPayload({
@@ -139,6 +147,51 @@ export function create(config) {
139
147
  throw error;
140
148
  }
141
149
  }
150
+ async function prepareRequest(input, init, options) {
151
+ const { maxRedirects = 20, ...paymentOptions } = options ?? {};
152
+ const preparedHttp = await prepareHttpRequest({
153
+ acceptPayment: acceptPayment.header,
154
+ acceptPaymentPolicy,
155
+ fetch: rawFetch,
156
+ init,
157
+ input,
158
+ maxRedirects,
159
+ signer: attestationSigner,
160
+ });
161
+ const requestInit = requestToInit(preparedHttp.replayRequest, preparedHttp.body);
162
+ if (!(await transport.isPaymentRequired(preparedHttp.response, requestInit)))
163
+ throw new Error('Response does not require payment.');
164
+ const payment = (await preparePayment(preparedHttp.response, {
165
+ ...paymentOptions,
166
+ request: requestInit,
167
+ }));
168
+ const paymentMethod = preparedPaymentMethods.get(payment) ?? payment.method;
169
+ const createRequestCredential = memoizeCreateCredential(async (context) => {
170
+ if (MethodChallenge.has(paymentMethod))
171
+ await MethodChallenge.handle(paymentMethod, {
172
+ challenge: payment.challenge,
173
+ context,
174
+ fetch: attestedFetch,
175
+ input: preparedHttp.replayRequest,
176
+ });
177
+ return payment.createCredential(context);
178
+ });
179
+ return Object.freeze({
180
+ ...payment,
181
+ createCredential: createRequestCredential,
182
+ request: preparedHttp.request,
183
+ response: preparedHttp.response,
184
+ redirects: preparedHttp.redirects,
185
+ async pay(context) {
186
+ const credential = await createRequestCredential(context);
187
+ const paidInit = payment.setCredential(requestToInit(preparedHttp.replayRequest, preparedHttp.body), credential);
188
+ return attestedFetch(preparedHttp.replayRequest.url, {
189
+ ...paidInit,
190
+ redirect: 'manual',
191
+ });
192
+ },
193
+ });
194
+ }
142
195
  return {
143
196
  fetch,
144
197
  rawFetch,
@@ -150,6 +203,7 @@ export function create(config) {
150
203
  onPaymentFailed,
151
204
  onPaymentResponse,
152
205
  preparePayment,
206
+ prepareRequest: prepareRequest,
153
207
  async createCredential(response, context, options) {
154
208
  const prepared = await preparePayment(response, options);
155
209
  return prepared.createCredential(context);
@@ -173,11 +227,131 @@ export function create(config) {
173
227
  export function restore() {
174
228
  Fetch.restore();
175
229
  }
176
- function createAttestedFetch(fetch, signers) {
230
+ function createAttestationSigner(signers) {
177
231
  const values = Object.values(signers);
178
232
  if (values.length === 0)
179
233
  throw new TypeError('Mppx client attestation must configure at least one signer.');
180
- return AttestationClient.wrapFetch(fetch, AttestationClient.composeSigners(...values));
234
+ return AttestationClient.composeSigners(...values);
235
+ }
236
+ const redirectStatuses = new Set([301, 302, 303, 307, 308]);
237
+ const bodyHeaders = [
238
+ 'content-encoding',
239
+ 'content-language',
240
+ 'content-length',
241
+ 'content-location',
242
+ 'content-type',
243
+ 'transfer-encoding',
244
+ ];
245
+ const crossOriginHeaders = [
246
+ 'authorization',
247
+ 'cookie',
248
+ 'cookie2',
249
+ 'host',
250
+ 'payment-authorization',
251
+ 'payment-signature',
252
+ 'proxy-authorization',
253
+ 'x-payment',
254
+ ];
255
+ async function prepareHttpRequest(parameters) {
256
+ const { acceptPayment, acceptPaymentPolicy, fetch, init, input, maxRedirects, signer } = parameters;
257
+ if (!Number.isInteger(maxRedirects) || maxRedirects < 0)
258
+ throw new TypeError('maxRedirects must be a non-negative integer.');
259
+ let request = new Request(input, { ...init, redirect: 'manual' });
260
+ const explicitAcceptPayment = request.headers.has(Constants.Headers.acceptPayment);
261
+ let body = await replayBody(request, init?.body);
262
+ const redirects = [];
263
+ for (;;) {
264
+ request = withAcceptPayment(request, acceptPayment, explicitAcceptPayment, acceptPaymentPolicy);
265
+ const sentRequest = signer ? await signer.sign(request.clone()) : request.clone();
266
+ const response = await fetch(sentRequest);
267
+ if (response.type === 'opaqueredirect' || response.status === 0)
268
+ throw new Error('prepareRequest requires a runtime that exposes manual redirect responses.');
269
+ if (!redirectStatuses.has(response.status))
270
+ return {
271
+ body,
272
+ replayRequest: request,
273
+ request: sentRequest,
274
+ response,
275
+ redirects: Object.freeze(redirects),
276
+ };
277
+ const location = response.headers.get('location');
278
+ if (!location)
279
+ return {
280
+ body,
281
+ replayRequest: request,
282
+ request: sentRequest,
283
+ response,
284
+ redirects: Object.freeze(redirects),
285
+ };
286
+ if (redirects.length >= maxRedirects) {
287
+ await response.body?.cancel();
288
+ throw new Error(`Payment request exceeded ${maxRedirects} redirects.`);
289
+ }
290
+ const from = new URL(request.url);
291
+ const to = new URL(location, from);
292
+ if (from.protocol === 'https:' && to.protocol !== 'https:') {
293
+ await response.body?.cancel();
294
+ throw new Error(`Payment request refused HTTPS downgrade redirect to ${to.href}`);
295
+ }
296
+ const headers = new Headers(request.headers);
297
+ let method = request.method;
298
+ const switchesToGet = ((response.status === 301 || response.status === 302) && method === 'POST') ||
299
+ (response.status === 303 && method !== 'GET' && method !== 'HEAD');
300
+ if (switchesToGet) {
301
+ method = 'GET';
302
+ body = undefined;
303
+ for (const header of bodyHeaders)
304
+ headers.delete(header);
305
+ }
306
+ if (from.origin !== to.origin)
307
+ for (const header of [...headers.keys()]) {
308
+ const value = headers.get(header) ?? '';
309
+ if (crossOriginHeaders.includes(header) || value.startsWith('Payment '))
310
+ headers.delete(header);
311
+ }
312
+ redirects.push(Object.freeze({ from: from.href, status: response.status, to: to.href }));
313
+ await response.body?.cancel();
314
+ request = new Request(to, requestInit(request, headers, method, body));
315
+ }
316
+ }
317
+ function requestToInit(request, body) {
318
+ return requestInit(request, new Headers(request.headers), request.method, body);
319
+ }
320
+ async function replayBody(request, suppliedBody) {
321
+ if (typeof suppliedBody === 'string')
322
+ return suppliedBody;
323
+ if (!request.body)
324
+ return undefined;
325
+ const accept = request.headers.get('accept')?.toLowerCase() ?? '';
326
+ if (request.headers.has('mcp-method') ||
327
+ (accept.includes('application/json') && accept.includes('text/event-stream')))
328
+ return request.clone().text();
329
+ return request.clone().arrayBuffer();
330
+ }
331
+ function withAcceptPayment(request, acceptPayment, explicit, policy) {
332
+ if (explicit)
333
+ return request;
334
+ const headers = new Headers(request.headers);
335
+ headers.delete(Constants.Headers.acceptPayment);
336
+ if (acceptPayment && Fetch.shouldInjectForPolicy(request, policy))
337
+ headers.set(Constants.Headers.acceptPayment, acceptPayment);
338
+ return new Request(request, { headers });
339
+ }
340
+ function requestInit(request, headers, method, body) {
341
+ return {
342
+ ...(body ? { body } : {}),
343
+ cache: request.cache,
344
+ credentials: request.credentials,
345
+ headers,
346
+ integrity: request.integrity,
347
+ keepalive: request.keepalive,
348
+ method,
349
+ mode: request.mode,
350
+ redirect: 'manual',
351
+ referrer: request.referrer,
352
+ referrerPolicy: request.referrerPolicy,
353
+ signal: request.signal,
354
+ };
181
355
  }
182
356
  function memoizeCreateCredential(createCredential) {
183
357
  let promise;
@@ -168,6 +168,12 @@ export declare function normalizeHeaders(headers: unknown): Record<string, strin
168
168
  */
169
169
  export declare function createEventDispatcher<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], response = Response>(): ClientEventDispatcher<methods, response>;
170
170
  /** @internal */
171
+ /** @internal */
172
+ export declare function unwrapFetch(fetch: typeof globalThis.fetch): typeof globalThis.fetch;
173
+ /** @internal */
171
174
  export declare function validateCredentialHeaderValue(credential: string): void;
175
+ /** @internal */
176
+ /** @internal */
177
+ export declare function shouldInjectForPolicy(input: RequestInfo | URL, policy: NonNullable<from.Config['acceptPaymentPolicy']>): boolean;
172
178
  export {};
173
179
  //# sourceMappingURL=Fetch.d.ts.map
@@ -558,7 +558,8 @@ function getCallerHeaders(input, headers) {
558
558
  return new Headers(input instanceof Request ? input.headers : undefined);
559
559
  }
560
560
  /** @internal */
561
- function unwrapFetch(fetch) {
561
+ /** @internal */
562
+ export function unwrapFetch(fetch) {
562
563
  let current = fetch;
563
564
  while (current[MPPX_FETCH_WRAPPER]) {
564
565
  current = current[MPPX_FETCH_WRAPPER];
@@ -620,7 +621,8 @@ async function resolveChallengeOrder(candidates, orderChallenges) {
620
621
  return orderChallenges ? orderChallenges(candidates) : candidates;
621
622
  }
622
623
  /** @internal */
623
- function shouldInjectForPolicy(input, policy) {
624
+ /** @internal */
625
+ export function shouldInjectForPolicy(input, policy) {
624
626
  if (policy === 'always')
625
627
  return true;
626
628
  if (policy === 'never')
@@ -1,5 +1,5 @@
1
1
  /** Current mppx package version. */
2
- export declare const version = "0.9.3";
2
+ export declare const version = "0.10.0";
3
3
  /** Canonical SDK identifier for this mppx release. */
4
- export declare const sdkIdentifier = "mppx/0.9.3";
4
+ export declare const sdkIdentifier = "mppx/0.10.0";
5
5
  //# sourceMappingURL=version.d.ts.map
@@ -1,6 +1,6 @@
1
1
  // Generated by scripts/sync-version.ts.
2
2
  /** Current mppx package version. */
3
- export const version = '0.9.3';
3
+ export const version = '0.10.0';
4
4
  /** Canonical SDK identifier for this mppx release. */
5
5
  export const sdkIdentifier = `mppx/${version}`;
6
6
  //# sourceMappingURL=version.js.map
@@ -125,8 +125,14 @@ export declare namespace charge {
125
125
  */
126
126
  expectedChainId?: number | undefined;
127
127
  /**
128
- * Allowlist of expected split recipient addresses. When set, the client
129
- * rejects any challenge whose split recipients are not in this list.
128
+ * Chains permitted for payment credentials. Omitted allows any chain; empty rejects all.
129
+ * A single entry supplies an omitted challenge chain unless `expectedChainId` is set.
130
+ * When both policies are set, the selected chain must satisfy both.
131
+ */
132
+ allowedChainIds?: readonly number[] | undefined;
133
+ /**
134
+ * Allowlist of payment recipient addresses. When set, both the primary
135
+ * recipient and every split recipient must be included in this list.
130
136
  */
131
137
  expectedRecipients?: readonly Address[] | undefined;
132
138
  /**
@@ -51,15 +51,33 @@ export function charge(parameters = {}) {
51
51
  challengeChainId !== undefined &&
52
52
  challengeChainId !== parameters.expectedChainId)
53
53
  throw new Error(`Chain ID mismatch: expected ${parameters.expectedChainId}, got ${challengeChainId}.`);
54
- const resolvedChainId = challengeChainId ?? parameters.expectedChainId;
54
+ if (challengeChainId !== undefined || parameters.allowedChainIds?.length === 0)
55
+ Client.assertAllowedChainId(parameters.allowedChainIds, challengeChainId);
56
+ const resolvedChainId = challengeChainId ??
57
+ parameters.expectedChainId ??
58
+ (parameters.allowedChainIds?.length === 1 ? parameters.allowedChainIds[0] : undefined);
55
59
  const client = await getClient({ chainId: resolvedChainId });
60
+ Client.assertChainId(client, resolvedChainId);
56
61
  const chainId = resolvedChainId ?? client.chain?.id;
62
+ Client.assertAllowedChainId(parameters.allowedChainIds, chainId);
57
63
  if (chainId === undefined)
58
64
  throw new Error('No `chainId` provided. Pass a chain ID in the challenge or client.');
59
65
  const { request } = challenge;
60
66
  const { amount, methodDetails } = request;
61
67
  const supportedModes = methodDetails?.supportedModes ?? ['pull', 'push'];
62
68
  const defaultAccount = getAccount(client, context);
69
+ if (parameters.expectedRecipients) {
70
+ const allowed = new Set(parameters.expectedRecipients.map((a) => a.toLowerCase()));
71
+ if (!request.recipient || !allowed.has(request.recipient.toLowerCase()))
72
+ throw new Error(`Unexpected primary recipient: ${request.recipient}`);
73
+ const splits = methodDetails?.splits;
74
+ if (splits) {
75
+ for (const split of splits) {
76
+ if (!allowed.has(split.recipient.toLowerCase()))
77
+ throw new Error(`Unexpected split recipient: ${split.recipient}`);
78
+ }
79
+ }
80
+ }
63
81
  // Zero-amount: sign EIP-712 typed data instead of creating a transaction.
64
82
  if (BigInt(amount) === 0n) {
65
83
  const signature = await signTypedData(client, {
@@ -80,16 +98,6 @@ export function charge(parameters = {}) {
80
98
  });
81
99
  }
82
100
  const currency = request.currency;
83
- if (parameters.expectedRecipients) {
84
- const allowed = new Set(parameters.expectedRecipients.map((a) => a.toLowerCase()));
85
- const splits = methodDetails?.splits;
86
- if (splits) {
87
- for (const split of splits) {
88
- if (!allowed.has(split.recipient.toLowerCase()))
89
- throw new Error(`Unexpected split recipient: ${split.recipient}`);
90
- }
91
- }
92
- }
93
101
  const memo = methodDetails?.memo
94
102
  ? methodDetails.memo
95
103
  : Attribution.encode({ challengeId: challenge.id, clientId, serverId: challenge.realm });