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.
- package/CHANGELOG.md +14 -0
- package/dist/cli/cli.js +137 -137
- package/dist/cli/internal.d.ts +4 -1
- package/dist/cli/internal.js +38 -5
- package/dist/cli/plugins/index.js +13 -13
- package/dist/cli/validate/discovery.d.ts +3 -1
- package/dist/cli/validate/discovery.js +28 -1
- package/dist/cli/validate/helpers.d.ts +3 -2
- package/dist/cli/validate/helpers.js +3 -0
- package/dist/cli/validate/index.js +3 -1
- package/dist/cli/validate/payment.js +106 -53
- package/dist/client/Mppx.d.ts +32 -0
- package/dist/client/Mppx.js +180 -6
- package/dist/client/internal/Fetch.d.ts +6 -0
- package/dist/client/internal/Fetch.js +4 -2
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/tempo/client/Charge.d.ts +8 -2
- package/dist/tempo/client/Charge.js +19 -11
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +2 -0
- package/dist/tempo/session/client/CredentialState.js +9 -3
- package/dist/tempo/session/client/Session.d.ts +6 -0
- package/dist/tempo/session/client/Session.js +4 -1
- package/dist/tempo/session/client/SessionManager.d.ts +6 -0
- package/dist/tempo/session/client/SessionManager.js +7 -0
- package/dist/validation/core.d.ts +1 -0
- package/dist/validation/core.js +7 -2
- package/dist/viem/Client.d.ts +4 -0
- package/dist/viem/Client.js +13 -0
- package/package.json +1 -1
package/dist/client/Mppx.js
CHANGED
|
@@ -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
|
|
33
|
-
?
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2
|
+
export declare const version = "0.10.0";
|
|
3
3
|
/** Canonical SDK identifier for this mppx release. */
|
|
4
|
-
export declare const sdkIdentifier = "mppx/0.
|
|
4
|
+
export declare const sdkIdentifier = "mppx/0.10.0";
|
|
5
5
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/internal/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/sync-version.ts.
|
|
2
2
|
/** Current mppx package version. */
|
|
3
|
-
export const version = '0.
|
|
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
|
-
*
|
|
129
|
-
*
|
|
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
|
-
|
|
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 });
|