mppx 0.13.3 → 0.13.5
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 +32 -0
- package/dist/Errors.d.ts +63 -2
- package/dist/Errors.js +52 -2
- package/dist/Method.d.ts +10 -0
- package/dist/cli/cli.js +133 -133
- package/dist/cli/plugins/index.js +12 -12
- package/dist/cli/validate/helpers.d.ts +1 -0
- package/dist/cli/validate/helpers.js +2 -1
- package/dist/client/Mppx.d.ts +2 -0
- package/dist/client/Mppx.js +8 -10
- package/dist/client/internal/Fetch.js +107 -8
- package/dist/client/internal/MethodResponse.d.ts +6 -0
- package/dist/client/internal/MethodResponse.js +16 -0
- package/dist/client/internal/protocols/Mcp.js +36 -5
- package/dist/evm/server/Charge.js +3 -1
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/internal/warnOnce.d.ts +3 -0
- package/dist/internal/warnOnce.js +9 -0
- package/dist/server/Mppx.d.ts +6 -1
- package/dist/server/Mppx.js +20 -16
- package/dist/server/Transport.js +10 -0
- package/dist/server/internal/challengeMeta.d.ts +5 -0
- package/dist/server/internal/challengeMeta.js +14 -0
- package/dist/tempo/Attribution.d.ts +24 -9
- package/dist/tempo/Attribution.js +25 -5
- package/dist/tempo/client/Charge.d.ts +116 -10
- package/dist/tempo/client/Charge.js +116 -22
- package/dist/tempo/client/Methods.d.ts +88 -9
- package/dist/tempo/index.d.ts +1 -0
- package/dist/tempo/index.js +1 -0
- package/dist/tempo/internal/fee-payer.js +4 -0
- package/dist/tempo/server/Charge.d.ts +11 -0
- package/dist/tempo/server/Charge.js +160 -29
- package/dist/tempo/server/Methods.d.ts +5 -0
- package/dist/tempo/server/Methods.js +18 -4
- package/dist/tempo/server/Subscription.js +2 -1
- package/dist/tempo/server/internal/fallback-store.d.ts +9 -0
- package/dist/tempo/server/internal/fallback-store.js +13 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/client/Transports.js +39 -12
- package/dist/tempo/session/precompile/Chain.d.ts +4 -0
- package/dist/tempo/session/precompile/Chain.js +4 -1
- package/dist/tempo/session/server/Session.js +12 -11
- package/dist/tempo/session/server/Settlement.d.ts +7 -0
- package/dist/tempo/session/server/Settlement.js +33 -8
- package/dist/x402/server/EvmCharge.js +10 -3
- package/package.json +1 -1
|
@@ -37,6 +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
41
|
export declare function fetchWithTimeout(url: RequestInfo | URL, init: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
41
42
|
export declare function formatBytes(bytes: number): string;
|
|
42
43
|
export declare const HTTP_METHODS: Set<string>;
|
|
@@ -57,11 +57,12 @@ export function printResults(results, counts) {
|
|
|
57
57
|
counts.skipped++;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
/** Fetches the validation target without allowing it to redirect probes elsewhere. */
|
|
60
61
|
export async function fetchWithTimeout(url, init, timeoutMs = 15_000) {
|
|
61
62
|
const controller = new AbortController();
|
|
62
63
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
64
|
try {
|
|
64
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
65
|
+
return await fetch(url, { ...init, redirect: 'error', signal: controller.signal });
|
|
65
66
|
}
|
|
66
67
|
finally {
|
|
67
68
|
clearTimeout(timeout);
|
package/dist/client/Mppx.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
|
|
|
62
62
|
transport: transport;
|
|
63
63
|
/**
|
|
64
64
|
* Selects a payment challenge without creating its credential.
|
|
65
|
+
* Native HTTP responses in payment events preserve status and headers only;
|
|
66
|
+
* the caller retains ownership of the original response body.
|
|
65
67
|
*
|
|
66
68
|
* @example
|
|
67
69
|
* ```ts
|
package/dist/client/Mppx.js
CHANGED
|
@@ -405,17 +405,15 @@ function snapshotMethod(method) {
|
|
|
405
405
|
}
|
|
406
406
|
function snapshotResponse(response) {
|
|
407
407
|
if (response instanceof Response) {
|
|
408
|
-
|
|
408
|
+
if (response.body === null)
|
|
409
409
|
return response.clone();
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
});
|
|
418
|
-
}
|
|
410
|
+
// Event context can outlive an SSE response. Retaining a clone would queue
|
|
411
|
+
// every chunk consumed by the caller on an unread body branch.
|
|
412
|
+
return new Response(null, {
|
|
413
|
+
headers: response.headers,
|
|
414
|
+
status: response.status,
|
|
415
|
+
statusText: response.statusText,
|
|
416
|
+
});
|
|
419
417
|
}
|
|
420
418
|
return snapshotValue(response);
|
|
421
419
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Challenge from '../../Challenge.js';
|
|
2
2
|
import * as Constants from '../../Constants.js';
|
|
3
|
+
import { PaymentOutcomeUnknownError } from '../../Errors.js';
|
|
3
4
|
import * as Expires from '../../Expires.js';
|
|
4
5
|
import * as AcceptPayment from '../../internal/AcceptPayment.js';
|
|
5
6
|
import * as Transport from '../Transport.js';
|
|
@@ -10,8 +11,64 @@ import * as MethodResponse from './MethodResponse.js';
|
|
|
10
11
|
// an unrelated fetch installed by user code or another library.
|
|
11
12
|
const MPPX_FETCH_WRAPPER = Symbol.for('mppx.fetch.wrapper');
|
|
12
13
|
const defaultMaxPaymentRetries = 3;
|
|
14
|
+
const maxErrorBodyBytes = 64 * 1024;
|
|
15
|
+
const errorBodyTimeoutMs = 2_000;
|
|
16
|
+
/** Response header set when a paid-retry error body was cut off by {@link bufferErrorBody}. */
|
|
17
|
+
const bodyTruncatedHeader = 'Mppx-Body-Truncated';
|
|
13
18
|
let originalFetch;
|
|
14
19
|
const eventObserverChecks = new WeakMap();
|
|
20
|
+
/**
|
|
21
|
+
* Buffers a paid-retry error body within a size and time bound so its connection is
|
|
22
|
+
* released. A complete body stays in the original response. The body is diagnostic
|
|
23
|
+
* only: one larger than 64 KiB, or still arriving after 2 seconds, is cancelled and
|
|
24
|
+
* replaced with what arrived, marked with an `Mppx-Body-Truncated: true` header.
|
|
25
|
+
*/
|
|
26
|
+
async function bufferErrorBody(response) {
|
|
27
|
+
if (!response.body)
|
|
28
|
+
return response;
|
|
29
|
+
// Reading a clone pulls the shared source, buffering it in the original response.
|
|
30
|
+
const reader = response.clone().body.getReader();
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let received = 0;
|
|
33
|
+
let truncated = false;
|
|
34
|
+
const timeout = setTimeout(() => {
|
|
35
|
+
truncated = true;
|
|
36
|
+
void reader.cancel().catch(() => { });
|
|
37
|
+
}, errorBodyTimeoutMs);
|
|
38
|
+
try {
|
|
39
|
+
for (;;) {
|
|
40
|
+
const { done, value } = await reader.read();
|
|
41
|
+
if (done)
|
|
42
|
+
break;
|
|
43
|
+
chunks.push(value);
|
|
44
|
+
received += value.byteLength;
|
|
45
|
+
if (received > maxErrorBodyBytes) {
|
|
46
|
+
truncated = true;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
// A tee's cancellation can wait for the other branch; do not await it.
|
|
54
|
+
void reader.cancel().catch(() => { });
|
|
55
|
+
}
|
|
56
|
+
if (!truncated)
|
|
57
|
+
return response;
|
|
58
|
+
// Cancelling both branches cancels the source and releases the connection.
|
|
59
|
+
void response.body.cancel().catch(() => { });
|
|
60
|
+
const headers = new Headers(response.headers);
|
|
61
|
+
headers.delete('content-length');
|
|
62
|
+
headers.set(bodyTruncatedHeader, 'true');
|
|
63
|
+
const truncatedResponse = new Response(new Blob(chunks).slice(0, maxErrorBodyBytes), {
|
|
64
|
+
headers,
|
|
65
|
+
status: response.status,
|
|
66
|
+
statusText: response.statusText,
|
|
67
|
+
});
|
|
68
|
+
for (const key of ['redirected', 'type', 'url'])
|
|
69
|
+
Object.defineProperty(truncatedResponse, key, { value: response[key] });
|
|
70
|
+
return truncatedResponse;
|
|
71
|
+
}
|
|
15
72
|
async function settleAttempt(prepared, outcome) {
|
|
16
73
|
const settle = prepared?.attempt.settle;
|
|
17
74
|
if (!settle)
|
|
@@ -67,6 +124,8 @@ export function from(config) {
|
|
|
67
124
|
let challenges;
|
|
68
125
|
let mi;
|
|
69
126
|
let preparedCredential;
|
|
127
|
+
// Set when the paid request throws after the credential was sent.
|
|
128
|
+
let outcomeUnknown;
|
|
70
129
|
try {
|
|
71
130
|
for (let retry = 0; retry < maxPaymentRetries; retry++) {
|
|
72
131
|
challenges = await transport.getChallenges(response, transportRequest);
|
|
@@ -166,14 +225,26 @@ export function from(config) {
|
|
|
166
225
|
method: selected.method,
|
|
167
226
|
response,
|
|
168
227
|
}));
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// Runtimes do not strip custom payment headers on cross-origin redirects.
|
|
175
|
-
redirect: 'manual',
|
|
228
|
+
const unknownOutcome = (cause) => new PaymentOutcomeUnknownError({
|
|
229
|
+
cause,
|
|
230
|
+
challenge: selectedChallenge,
|
|
231
|
+
credential,
|
|
232
|
+
transactionHash: MethodResponse.getTransactionHash(selected.method, credential),
|
|
176
233
|
});
|
|
234
|
+
try {
|
|
235
|
+
response = await baseFetch(paymentInput, {
|
|
236
|
+
...transport.setCredential({
|
|
237
|
+
...fetchInit,
|
|
238
|
+
headers: initialRequest.headers,
|
|
239
|
+
}, credential, { challenge: selectedChallenge }),
|
|
240
|
+
// Runtimes do not strip custom payment headers on cross-origin redirects.
|
|
241
|
+
redirect: 'manual',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (cause) {
|
|
245
|
+
outcomeUnknown = unknownOutcome(cause);
|
|
246
|
+
throw cause;
|
|
247
|
+
}
|
|
177
248
|
const paymentRequired = await transport.isPaymentRequired(response, transportRequest);
|
|
178
249
|
if (!paymentRequired) {
|
|
179
250
|
await settleAttempt(prepared, {
|
|
@@ -198,6 +269,33 @@ export function from(config) {
|
|
|
198
269
|
response,
|
|
199
270
|
}));
|
|
200
271
|
if (!paymentRequired) {
|
|
272
|
+
// The server may have settled before failing: report the credential for
|
|
273
|
+
// reconciliation and return the response. Retrying could pay twice.
|
|
274
|
+
if (response.status >= 500 && hasEventObservers(events, 'payment.failed')) {
|
|
275
|
+
try {
|
|
276
|
+
// Release the connection before observers can make requests through it.
|
|
277
|
+
response = await bufferErrorBody(response);
|
|
278
|
+
}
|
|
279
|
+
catch (cause) {
|
|
280
|
+
outcomeUnknown = unknownOutcome(cause);
|
|
281
|
+
// The failed body cannot be cloned for observers; keep its status and headers.
|
|
282
|
+
response = new Response(null, {
|
|
283
|
+
headers: response.headers,
|
|
284
|
+
status: response.status,
|
|
285
|
+
statusText: response.statusText,
|
|
286
|
+
});
|
|
287
|
+
throw cause;
|
|
288
|
+
}
|
|
289
|
+
await events.emit('payment.failed', createPaymentFailedPayload({
|
|
290
|
+
challenge: selectedChallenge,
|
|
291
|
+
challenges,
|
|
292
|
+
error: unknownOutcome(),
|
|
293
|
+
init,
|
|
294
|
+
input,
|
|
295
|
+
method: selected.method,
|
|
296
|
+
response,
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
201
299
|
if (!response.ok)
|
|
202
300
|
return response;
|
|
203
301
|
return MethodResponse.handle(selected.method, {
|
|
@@ -237,7 +335,8 @@ export function from(config) {
|
|
|
237
335
|
await events.emit('payment.failed', createPaymentFailedPayload({
|
|
238
336
|
challenge,
|
|
239
337
|
challenges,
|
|
240
|
-
error
|
|
338
|
+
// Observers get the credential to reconcile; callers keep the original error.
|
|
339
|
+
error: outcomeUnknown ?? error,
|
|
241
340
|
init,
|
|
242
341
|
input,
|
|
243
342
|
method: mi,
|
|
@@ -23,6 +23,8 @@ export type HandlerParameters = {
|
|
|
23
23
|
response: Response;
|
|
24
24
|
signal?: AbortSignal | undefined;
|
|
25
25
|
};
|
|
26
|
+
/** Derives the on-chain hash a credential's payment will have, when it is final before settlement. */
|
|
27
|
+
export type TransactionHash = (credential: string) => string | undefined;
|
|
26
28
|
/** Internal client-method response adapter. */
|
|
27
29
|
export type Handler = (parameters: HandlerParameters) => MaybePromise<Response>;
|
|
28
30
|
/** Registers an internal response adapter without changing the public method shape. */
|
|
@@ -35,4 +37,8 @@ export declare function attachAttempt(method: Method.AnyClient, parameters: obje
|
|
|
35
37
|
export declare function getAttempt(parameters: object): Attempt | undefined;
|
|
36
38
|
/** Lets the selected client method handle a successful paid response. */
|
|
37
39
|
export declare function handle(method: Method.AnyClient, parameters: HandlerParameters): Promise<Response>;
|
|
40
|
+
/** Registers credential hash derivation without changing the public method shape. */
|
|
41
|
+
export declare function registerTransactionHash<const method extends Method.AnyClient>(method: method, derive: TransactionHash): method;
|
|
42
|
+
/** Returns the credential's on-chain hash when its method can derive it. */
|
|
43
|
+
export declare function getTransactionHash(method: Method.AnyClient, credential: string): string | undefined;
|
|
38
44
|
//# sourceMappingURL=MethodResponse.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const handlers = new WeakMap();
|
|
2
2
|
const attempts = new WeakMap();
|
|
3
|
+
const transactionHashes = new WeakMap();
|
|
3
4
|
/** Registers an internal response adapter without changing the public method shape. */
|
|
4
5
|
export function register(method, handler) {
|
|
5
6
|
handlers.set(method, handler);
|
|
@@ -22,4 +23,19 @@ export function getAttempt(parameters) {
|
|
|
22
23
|
export function handle(method, parameters) {
|
|
23
24
|
return Promise.resolve(handlers.get(method)?.(parameters) ?? parameters.response);
|
|
24
25
|
}
|
|
26
|
+
/** Registers credential hash derivation without changing the public method shape. */
|
|
27
|
+
export function registerTransactionHash(method, derive) {
|
|
28
|
+
transactionHashes.set(method, derive);
|
|
29
|
+
return method;
|
|
30
|
+
}
|
|
31
|
+
/** Returns the credential's on-chain hash when its method can derive it. */
|
|
32
|
+
export function getTransactionHash(method, credential) {
|
|
33
|
+
try {
|
|
34
|
+
return transactionHashes.get(method)?.(credential);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Reconciliation metadata must never mask the payment outcome itself.
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
25
41
|
//# sourceMappingURL=MethodResponse.js.map
|
|
@@ -18,6 +18,8 @@ function jsonRpcRequestId(body) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
// Bound automatic inspection; the caller retains the original response stream.
|
|
22
|
+
const maxInspectionBytes = 1024 * 1024;
|
|
21
23
|
const responseCache = new WeakMap();
|
|
22
24
|
function mcpHttpRequestId(request) {
|
|
23
25
|
const id = jsonRpcRequestId(request?.body);
|
|
@@ -94,6 +96,7 @@ async function parseSseJsonRpcResponse(response) {
|
|
|
94
96
|
if (!reader)
|
|
95
97
|
return undefined;
|
|
96
98
|
const decoder = new TextDecoder();
|
|
99
|
+
let received = 0;
|
|
97
100
|
let dataEventSeen = false;
|
|
98
101
|
let message;
|
|
99
102
|
const parser = createParser({
|
|
@@ -119,6 +122,9 @@ async function parseSseJsonRpcResponse(response) {
|
|
|
119
122
|
parser.reset({ consume: true });
|
|
120
123
|
return message;
|
|
121
124
|
}
|
|
125
|
+
received += value.byteLength;
|
|
126
|
+
if (received > maxInspectionBytes)
|
|
127
|
+
return undefined;
|
|
122
128
|
parser.feed(decoder.decode(value, { stream: true }));
|
|
123
129
|
}
|
|
124
130
|
}
|
|
@@ -126,6 +132,33 @@ async function parseSseJsonRpcResponse(response) {
|
|
|
126
132
|
void reader.cancel().catch(() => { });
|
|
127
133
|
}
|
|
128
134
|
}
|
|
135
|
+
/** Reads only a bounded JSON response, leaving the caller's stream untouched. */
|
|
136
|
+
async function parseJsonResponse(response) {
|
|
137
|
+
const reader = response.clone().body?.getReader();
|
|
138
|
+
if (!reader)
|
|
139
|
+
return undefined;
|
|
140
|
+
const decoder = new TextDecoder();
|
|
141
|
+
let received = 0;
|
|
142
|
+
let text = '';
|
|
143
|
+
try {
|
|
144
|
+
for (;;) {
|
|
145
|
+
const { done, value } = await reader.read();
|
|
146
|
+
if (done)
|
|
147
|
+
return parseMessage(JSON.parse(text + decoder.decode()));
|
|
148
|
+
received += value.byteLength;
|
|
149
|
+
if (received > maxInspectionBytes)
|
|
150
|
+
return undefined;
|
|
151
|
+
text += decoder.decode(value, { stream: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
// A tee's cancellation can wait for the original stream; do not await it.
|
|
159
|
+
void reader.cancel().catch(() => { });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
129
162
|
/** Reads a cloned HTTP body into the first JSON-RPC response message, if any. */
|
|
130
163
|
function parseJsonRpcResponse(response) {
|
|
131
164
|
const cached = responseCache.get(response);
|
|
@@ -133,12 +166,10 @@ function parseJsonRpcResponse(response) {
|
|
|
133
166
|
return cached;
|
|
134
167
|
const promise = (async () => {
|
|
135
168
|
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
|
169
|
+
if (Number(response.headers.get('content-length')) > maxInspectionBytes)
|
|
170
|
+
return undefined;
|
|
136
171
|
if (contentType.includes('application/json'))
|
|
137
|
-
return response
|
|
138
|
-
.clone()
|
|
139
|
-
.json()
|
|
140
|
-
.then(parseMessage)
|
|
141
|
-
.catch(() => undefined);
|
|
172
|
+
return parseJsonResponse(response);
|
|
142
173
|
if (contentType.includes('text/event-stream'))
|
|
143
174
|
return parseSseJsonRpcResponse(response);
|
|
144
175
|
return undefined;
|
|
@@ -77,12 +77,14 @@ export function charge(parameters) {
|
|
|
77
77
|
source,
|
|
78
78
|
};
|
|
79
79
|
await config.validate?.(authorization);
|
|
80
|
+
const payer = getAddress(payload.from);
|
|
80
81
|
return {
|
|
81
82
|
challenge: credential.challenge,
|
|
82
83
|
credential,
|
|
83
|
-
details: { payer
|
|
84
|
+
details: { payer },
|
|
84
85
|
intent: Methods.charge.intent,
|
|
85
86
|
method: Methods.charge.name,
|
|
87
|
+
payer,
|
|
86
88
|
request: credential.challenge.request,
|
|
87
89
|
source,
|
|
88
90
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Current mppx package version. */
|
|
2
|
-
export declare const version = "0.13.
|
|
2
|
+
export declare const version = "0.13.5";
|
|
3
3
|
/** Canonical SDK identifier for this mppx release. */
|
|
4
|
-
export declare const sdkIdentifier = "mppx/0.13.
|
|
4
|
+
export declare const sdkIdentifier = "mppx/0.13.5";
|
|
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.13.
|
|
3
|
+
export const version = '0.13.5';
|
|
4
4
|
/** Canonical SDK identifier for this mppx release. */
|
|
5
5
|
export const sdkIdentifier = `mppx/${version}`;
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const warned = new Set();
|
|
2
|
+
/** Logs `message` with `console.warn` the first time `key` is seen in this process. */
|
|
3
|
+
export function warnOnce(key, message) {
|
|
4
|
+
if (warned.has(key))
|
|
5
|
+
return;
|
|
6
|
+
warned.add(key);
|
|
7
|
+
console.warn(`[mppx] ${message}`);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=warnOnce.js.map
|
package/dist/server/Mppx.d.ts
CHANGED
|
@@ -104,6 +104,11 @@ export type PaymentSuccessContext<method extends Method.Method = Method.Method,
|
|
|
104
104
|
envelope?: Method.VerifiedChallengeEnvelope<z.output<method['schema']['request']>, z.output<method['schema']['credential']['payload']>, method['intent'], method['name']> | undefined;
|
|
105
105
|
input?: Transport.InputOf<transport> | undefined;
|
|
106
106
|
method: ServerMethodDescriptor<method>;
|
|
107
|
+
/**
|
|
108
|
+
* Payer identity verified by the method before settlement (for example, the
|
|
109
|
+
* Tempo sender address). Absent when the method does not report one.
|
|
110
|
+
*/
|
|
111
|
+
payer?: string | undefined;
|
|
107
112
|
receipt: Receipt.Receipt;
|
|
108
113
|
/** Canonical request represented by the challenge. */
|
|
109
114
|
request: z.output<method['schema']['request']>;
|
|
@@ -359,7 +364,7 @@ declare namespace MethodFn {
|
|
|
359
364
|
description?: string | undefined;
|
|
360
365
|
/** Optional challenge expiration timestamp (ISO 8601) or Date. */
|
|
361
366
|
expires?: z.DatetimeInput | undefined;
|
|
362
|
-
/** Optional server-defined correlation data
|
|
367
|
+
/** Optional server-defined correlation data serialized as `opaque`. `_mppx_nonce` is reserved for a fresh server-generated issuance nonce; clients MUST echo opaque unchanged. */
|
|
363
368
|
meta?: Record<string, string> | undefined;
|
|
364
369
|
/** Optional route/resource scope bound via reserved challenge metadata. */
|
|
365
370
|
scope?: string | undefined;
|
package/dist/server/Mppx.js
CHANGED
|
@@ -7,11 +7,13 @@ import * as Errors from '../Errors.js';
|
|
|
7
7
|
import * as Expires from '../Expires.js';
|
|
8
8
|
import * as AcceptPayment from '../internal/AcceptPayment.js';
|
|
9
9
|
import * as Env from '../internal/env.js';
|
|
10
|
+
import { warnOnce } from '../internal/warnOnce.js';
|
|
10
11
|
import * as Mcp from '../Mcp.js';
|
|
11
12
|
import * as PaymentRequest from '../PaymentRequest.js';
|
|
12
13
|
import * as x402_Header from '../x402/Header.js';
|
|
13
14
|
import * as x402_Types from '../x402/Types.js';
|
|
14
15
|
import * as z from '../zod.js';
|
|
16
|
+
import * as ChallengeMeta from './internal/challengeMeta.js';
|
|
15
17
|
import * as Html from './internal/html/config.js';
|
|
16
18
|
import { serviceWorker } from './internal/html/serviceWorker.gen.js';
|
|
17
19
|
import * as Scope from './internal/scope.js';
|
|
@@ -331,9 +333,10 @@ export function create(config) {
|
|
|
331
333
|
}));
|
|
332
334
|
};
|
|
333
335
|
let receipt;
|
|
336
|
+
let validation;
|
|
334
337
|
try {
|
|
335
338
|
if (mi.broadcast && mi.validate)
|
|
336
|
-
await mi.validate({
|
|
339
|
+
validation = await mi.validate({
|
|
337
340
|
credential: parsedCredential,
|
|
338
341
|
envelope,
|
|
339
342
|
operation: 'broadcast',
|
|
@@ -343,7 +346,7 @@ export function create(config) {
|
|
|
343
346
|
receipt = await broadcast({ credential: parsedCredential, envelope, request });
|
|
344
347
|
}
|
|
345
348
|
catch (e) {
|
|
346
|
-
const error = e instanceof Errors.PaymentError ? e : new Errors.InternalPaymentError();
|
|
349
|
+
const error = e instanceof Errors.PaymentError ? e : new Errors.InternalPaymentError({ cause: e });
|
|
347
350
|
await emitStandalonePaymentFailed({
|
|
348
351
|
challenge: prepared.credential.challenge,
|
|
349
352
|
credential: parsedCredential,
|
|
@@ -359,6 +362,7 @@ export function create(config) {
|
|
|
359
362
|
credential: parsedCredential,
|
|
360
363
|
envelope,
|
|
361
364
|
method: mi,
|
|
365
|
+
payer: validation?.payer,
|
|
362
366
|
receipt,
|
|
363
367
|
request: parsedRequest,
|
|
364
368
|
...(requestInput !== undefined && { requestInput }),
|
|
@@ -607,7 +611,7 @@ function createMethodFn(parameters) {
|
|
|
607
611
|
? credentialError
|
|
608
612
|
: reason
|
|
609
613
|
? new Errors.MalformedCredentialError({ reason })
|
|
610
|
-
: new Errors.InternalPaymentError();
|
|
614
|
+
: new Errors.InternalPaymentError({ cause: credentialError });
|
|
611
615
|
await emitPaymentFailed({
|
|
612
616
|
challenge,
|
|
613
617
|
credential: null,
|
|
@@ -673,7 +677,7 @@ function createMethodFn(parameters) {
|
|
|
673
677
|
catch (e) {
|
|
674
678
|
if (!(e instanceof Errors.PaymentError))
|
|
675
679
|
console.error('mppx: internal authorization error', e);
|
|
676
|
-
const error = e instanceof Errors.PaymentError ? e : new Errors.InternalPaymentError();
|
|
680
|
+
const error = e instanceof Errors.PaymentError ? e : new Errors.InternalPaymentError({ cause: e });
|
|
677
681
|
await emitPaymentFailed({
|
|
678
682
|
challenge,
|
|
679
683
|
credential: null,
|
|
@@ -833,9 +837,10 @@ function createMethodFn(parameters) {
|
|
|
833
837
|
// User-provided verification (e.g., check signature, submit tx, verify payment).
|
|
834
838
|
// If verification fails, re-issue the challenge so the client can retry.
|
|
835
839
|
let receiptData;
|
|
840
|
+
let validation;
|
|
836
841
|
try {
|
|
837
842
|
if (broadcast && validate)
|
|
838
|
-
await validate({
|
|
843
|
+
validation = await validate({
|
|
839
844
|
credential: parsedCredential,
|
|
840
845
|
envelope,
|
|
841
846
|
operation: 'broadcast',
|
|
@@ -851,7 +856,7 @@ function createMethodFn(parameters) {
|
|
|
851
856
|
catch (e) {
|
|
852
857
|
if (!(e instanceof Errors.PaymentError))
|
|
853
858
|
console.error('mppx: internal verification error', e);
|
|
854
|
-
const error = e instanceof Errors.PaymentError ? e : new Errors.InternalPaymentError();
|
|
859
|
+
const error = e instanceof Errors.PaymentError ? e : new Errors.InternalPaymentError({ cause: e });
|
|
855
860
|
await emitPaymentFailed({
|
|
856
861
|
challenge,
|
|
857
862
|
credential: parsedCredential,
|
|
@@ -889,6 +894,7 @@ function createMethodFn(parameters) {
|
|
|
889
894
|
envelope,
|
|
890
895
|
input,
|
|
891
896
|
method,
|
|
897
|
+
payer: validation?.payer,
|
|
892
898
|
receipt: receiptData,
|
|
893
899
|
request: parsedRequest,
|
|
894
900
|
requestInput: request,
|
|
@@ -985,6 +991,7 @@ function createServerEventDispatcher() {
|
|
|
985
991
|
await method.onPaymentSuccess({
|
|
986
992
|
challenge: success.challenge,
|
|
987
993
|
input: success.input,
|
|
994
|
+
...(success.payer !== undefined && { payer: success.payer }),
|
|
988
995
|
receipt: success.receipt,
|
|
989
996
|
request: success.request,
|
|
990
997
|
...(success.requestInput !== undefined && { requestInput: success.requestInput }),
|
|
@@ -1065,6 +1072,7 @@ function createPaymentSuccessContext(parameters) {
|
|
|
1065
1072
|
...(parameters.envelope ? { envelope: snapshotVerifiedEnvelope(parameters.envelope) } : {}),
|
|
1066
1073
|
...snapshotInputProperty(parameters.input),
|
|
1067
1074
|
method: snapshotMethod(parameters.method),
|
|
1075
|
+
...(parameters.payer !== undefined && { payer: parameters.payer }),
|
|
1068
1076
|
receipt: snapshotValue(parameters.receipt),
|
|
1069
1077
|
request: snapshotValue(parameters.request),
|
|
1070
1078
|
...(parameters.requestInput !== undefined && {
|
|
@@ -1083,6 +1091,7 @@ function snapshotError(error) {
|
|
|
1083
1091
|
return error;
|
|
1084
1092
|
const snapshot = Object.assign(Object.create(Object.getPrototypeOf(error)), error);
|
|
1085
1093
|
Object.defineProperties(snapshot, {
|
|
1094
|
+
...('cause' in error && { cause: { value: error.cause, enumerable: false } }),
|
|
1086
1095
|
message: { value: error.message, enumerable: false },
|
|
1087
1096
|
name: { value: error.name, enumerable: false },
|
|
1088
1097
|
});
|
|
@@ -1260,13 +1269,6 @@ export function isMissingReceiptResponseError(error) {
|
|
|
1260
1269
|
function normalizeExpires(expires) {
|
|
1261
1270
|
return expires === undefined ? undefined : z.toDatetimeString(expires);
|
|
1262
1271
|
}
|
|
1263
|
-
const _warned = new Set();
|
|
1264
|
-
function warnOnce(key, message) {
|
|
1265
|
-
if (_warned.has(key))
|
|
1266
|
-
return;
|
|
1267
|
-
_warned.add(key);
|
|
1268
|
-
console.warn(`[mppx] ${message}`);
|
|
1269
|
-
}
|
|
1270
1272
|
/** Extracts hostname from the captured request URL, falling back to a default. */
|
|
1271
1273
|
function resolveRealmFromCapturedRequest(capturedRequest) {
|
|
1272
1274
|
try {
|
|
@@ -1300,7 +1302,7 @@ async function resolveRouteChallenge(parameters) {
|
|
|
1300
1302
|
description: parameters.description,
|
|
1301
1303
|
expires: parameters.expires,
|
|
1302
1304
|
header: parameters.header,
|
|
1303
|
-
meta: parameters.meta,
|
|
1305
|
+
meta: ChallengeMeta.withNonce(parameters.meta),
|
|
1304
1306
|
realm: effectiveRealm,
|
|
1305
1307
|
request: request,
|
|
1306
1308
|
secretKey: parameters.secretKey,
|
|
@@ -1316,7 +1318,7 @@ function createFallbackChallenge(parameters) {
|
|
|
1316
1318
|
description: parameters.description,
|
|
1317
1319
|
expires: parameters.expires,
|
|
1318
1320
|
header: parameters.header,
|
|
1319
|
-
meta: parameters.meta,
|
|
1321
|
+
meta: ChallengeMeta.withNonce(parameters.meta),
|
|
1320
1322
|
realm: parameters.realm ??
|
|
1321
1323
|
(parameters.capturedRequest
|
|
1322
1324
|
? resolveRealmFromCapturedRequest(parameters.capturedRequest)
|
|
@@ -1478,7 +1480,9 @@ function normalizeComparable(value) {
|
|
|
1478
1480
|
return typeof value === 'string' ? normalizeHex(value) : value;
|
|
1479
1481
|
}
|
|
1480
1482
|
function opaqueValuesMatch(expected, actual) {
|
|
1481
|
-
|
|
1483
|
+
// The echoed nonce is authenticated by the challenge HMAC, but is not a
|
|
1484
|
+
// stable route requirement. Every other metadata field remains pinned.
|
|
1485
|
+
return isDeepStrictEqual(ChallengeMeta.routeMeta(expected), ChallengeMeta.routeMeta(actual));
|
|
1482
1486
|
}
|
|
1483
1487
|
function hydrateCredentialMeta(credential) {
|
|
1484
1488
|
const { challenge } = credential;
|
package/dist/server/Transport.js
CHANGED
|
@@ -59,6 +59,16 @@ export function http(options = {}) {
|
|
|
59
59
|
},
|
|
60
60
|
async respondChallenge(options) {
|
|
61
61
|
const { challenge, error, input } = options;
|
|
62
|
+
// A 403 means the payment was valid but cannot pay for this request.
|
|
63
|
+
// Omit the challenge so clients do not pay again.
|
|
64
|
+
if (error?.status === 403)
|
|
65
|
+
return new Response(JSON.stringify(error.toProblemDetails()), {
|
|
66
|
+
status: error.status,
|
|
67
|
+
headers: {
|
|
68
|
+
'Cache-Control': 'no-store',
|
|
69
|
+
'Content-Type': 'application/problem+json',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
62
72
|
if (options.html && new URL(input.url).searchParams.has(Html.params.serviceWorker))
|
|
63
73
|
return new Response(serviceWorker, {
|
|
64
74
|
status: 200,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Adds fresh issuance randomness while preserving application and route metadata. */
|
|
2
|
+
export declare function withNonce(meta: Record<string, string> | undefined): Record<string, string>;
|
|
3
|
+
/** Removes issuance-only metadata when comparing stable route requirements. */
|
|
4
|
+
export declare function routeMeta(meta: Record<string, string> | undefined): Record<string, string> | undefined;
|
|
5
|
+
//# sourceMappingURL=challengeMeta.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Secp256k1 } from 'ox';
|
|
2
|
+
/** Reserved metadata key for HMAC-bound, per-issuance challenge randomness. */
|
|
3
|
+
const nonceKey = '_mppx_nonce';
|
|
4
|
+
/** Adds fresh issuance randomness while preserving application and route metadata. */
|
|
5
|
+
export function withNonce(meta) {
|
|
6
|
+
// Ox uses runtime-specific secure randomness, including Node without global Web Crypto.
|
|
7
|
+
return { ...meta, [nonceKey]: Secp256k1.randomPrivateKey() };
|
|
8
|
+
}
|
|
9
|
+
/** Removes issuance-only metadata when comparing stable route requirements. */
|
|
10
|
+
export function routeMeta(meta) {
|
|
11
|
+
const { [nonceKey]: _, ...rest } = meta ?? {};
|
|
12
|
+
return Object.keys(rest).length ? rest : undefined;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=challengeMeta.js.map
|