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
|
@@ -679,11 +679,12 @@ export async function probeWebSocketSession(parameters) {
|
|
|
679
679
|
}
|
|
680
680
|
/** Probes a WebSocket endpoint over HTTP and creates the opening credential. */
|
|
681
681
|
export async function prepareWebSocketSession(parameters) {
|
|
682
|
+
parameters.signal?.throwIfAborted();
|
|
682
683
|
const probed = await probeWebSocketSession(parameters);
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
};
|
|
684
|
+
parameters.signal?.throwIfAborted();
|
|
685
|
+
const credential = await parameters.createSessionCredential(probed.challenge, {});
|
|
686
|
+
parameters.signal?.throwIfAborted();
|
|
687
|
+
return { ...probed, credential };
|
|
687
688
|
}
|
|
688
689
|
/** Creates the initial runtime state for a paid WebSocket from its opening credential. */
|
|
689
690
|
export function createActiveSocketSession(parameters) {
|
|
@@ -741,6 +742,8 @@ export function validateSocketPaymentReceipt(parameters) {
|
|
|
741
742
|
*/
|
|
742
743
|
export async function openWebSocketSession(parameters) {
|
|
743
744
|
const { challenge, credential, options, WebSocket: WebSocketImpl, wsUrl } = parameters;
|
|
745
|
+
const signal = options?.signal;
|
|
746
|
+
signal?.throwIfAborted();
|
|
744
747
|
const rawSocket = new WebSocketImpl(wsUrl, options?.protocols);
|
|
745
748
|
const socketState = createActiveSocketSession({
|
|
746
749
|
challenge,
|
|
@@ -757,7 +760,13 @@ export async function openWebSocketSession(parameters) {
|
|
|
757
760
|
rawSocket.close(ClientWebSocketProtocolErrorCloseCode, message);
|
|
758
761
|
}
|
|
759
762
|
};
|
|
763
|
+
const onAbort = () => {
|
|
764
|
+
parameters.rejectReceipt(new Error('WebSocket payment flow aborted.'));
|
|
765
|
+
parameters.rejectCloseReady(new Error('WebSocket payment flow aborted.'));
|
|
766
|
+
rawSocket.close();
|
|
767
|
+
};
|
|
760
768
|
rawSocket.addEventListener('close', (event) => {
|
|
769
|
+
signal?.removeEventListener('abort', onAbort);
|
|
761
770
|
socketState.socket = null;
|
|
762
771
|
socketState.expectedCloseAmount = null;
|
|
763
772
|
parameters.rejectReceipt(new Error('WebSocket closed before the payment flow completed.'));
|
|
@@ -785,29 +794,47 @@ export async function openWebSocketSession(parameters) {
|
|
|
785
794
|
socketState,
|
|
786
795
|
});
|
|
787
796
|
});
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
797
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
798
|
+
try {
|
|
799
|
+
await waitForSocketOpen(rawSocket, managedSocket.emit, wsUrl, signal);
|
|
800
|
+
signal?.throwIfAborted();
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
signal?.removeEventListener('abort', onAbort);
|
|
791
804
|
rawSocket.close();
|
|
792
|
-
|
|
793
|
-
|
|
805
|
+
throw error;
|
|
806
|
+
}
|
|
794
807
|
rawSocket.send(Ws.formatAuthorizationMessage(credential));
|
|
795
808
|
await parameters.waitForReceipt();
|
|
796
809
|
return managedSocket.socket;
|
|
797
810
|
}
|
|
798
|
-
function waitForSocketOpen(rawSocket, managedEmit, wsUrl) {
|
|
811
|
+
function waitForSocketOpen(rawSocket, managedEmit, wsUrl, signal) {
|
|
799
812
|
return new Promise((resolve, reject) => {
|
|
800
|
-
const
|
|
813
|
+
const cleanup = () => {
|
|
814
|
+
rawSocket.removeEventListener('open', onOpen);
|
|
801
815
|
rawSocket.removeEventListener('error', onError);
|
|
816
|
+
rawSocket.removeEventListener('close', onError);
|
|
817
|
+
signal?.removeEventListener('abort', onAbort);
|
|
818
|
+
};
|
|
819
|
+
const onOpen = () => {
|
|
820
|
+
cleanup();
|
|
802
821
|
managedEmit('open', { type: 'open' });
|
|
803
822
|
resolve();
|
|
804
823
|
};
|
|
805
824
|
const onError = () => {
|
|
806
|
-
|
|
825
|
+
cleanup();
|
|
807
826
|
reject(new Error(`WebSocket connection to ${wsUrl} failed to open.`));
|
|
808
827
|
};
|
|
828
|
+
const onAbort = () => {
|
|
829
|
+
cleanup();
|
|
830
|
+
reject(signal?.reason);
|
|
831
|
+
};
|
|
809
832
|
rawSocket.addEventListener('open', onOpen, { once: true });
|
|
810
833
|
rawSocket.addEventListener('error', onError, { once: true });
|
|
834
|
+
rawSocket.addEventListener('close', onError, { once: true });
|
|
835
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
836
|
+
if (signal?.aborted)
|
|
837
|
+
onAbort();
|
|
811
838
|
});
|
|
812
839
|
}
|
|
813
840
|
async function handleSocketMessage(parameters) {
|
|
@@ -71,6 +71,8 @@ export type TopUpReceiptFields = {
|
|
|
71
71
|
};
|
|
72
72
|
/** Typed fields decoded from a Settled receipt event. */
|
|
73
73
|
export type SettledReceiptFields = {
|
|
74
|
+
/** Amount this settlement paid to the payee. */
|
|
75
|
+
deltaPaid: bigint;
|
|
74
76
|
/** New cumulative amount settled on-chain. */
|
|
75
77
|
newSettled: bigint;
|
|
76
78
|
};
|
|
@@ -153,6 +155,8 @@ export type ReadbackRetryOptions = {
|
|
|
153
155
|
retries?: number | undefined;
|
|
154
156
|
/** Delay between attempts, in milliseconds. @default 250 */
|
|
155
157
|
delayMs?: number | undefined;
|
|
158
|
+
/** Whether a failed attempt may be retried. @default every failure */
|
|
159
|
+
shouldRetry?: ((error: unknown) => boolean) | undefined;
|
|
156
160
|
};
|
|
157
161
|
/**
|
|
158
162
|
* Retry an on-chain readback that is pinned to the block containing a just-sent
|
|
@@ -39,6 +39,7 @@ export function readTopUpReceiptFields(event) {
|
|
|
39
39
|
/** Reads and validates typed fields from a Settled receipt event. */
|
|
40
40
|
export function readSettledReceiptFields(event) {
|
|
41
41
|
return {
|
|
42
|
+
deltaPaid: readUint96(event.args.deltaPaid, 'Settled deltaPaid'),
|
|
42
43
|
newSettled: readUint96(event.args.newSettled, 'Settled newSettled'),
|
|
43
44
|
};
|
|
44
45
|
}
|
|
@@ -168,7 +169,7 @@ export async function getChannelStatesBatch(client, channelIds, escrow = tip20Ch
|
|
|
168
169
|
* verification even though the open transaction succeeded.
|
|
169
170
|
*/
|
|
170
171
|
export async function readbackWithRetry(read, options = {}) {
|
|
171
|
-
const { retries = 5, delayMs = 250 } = options;
|
|
172
|
+
const { retries = 5, delayMs = 250, shouldRetry = () => true } = options;
|
|
172
173
|
let lastError;
|
|
173
174
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
174
175
|
try {
|
|
@@ -176,6 +177,8 @@ export async function readbackWithRetry(read, options = {}) {
|
|
|
176
177
|
}
|
|
177
178
|
catch (error) {
|
|
178
179
|
lastError = error;
|
|
180
|
+
if (!shouldRetry(error))
|
|
181
|
+
break;
|
|
179
182
|
if (attempt < retries)
|
|
180
183
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
181
184
|
}
|
|
@@ -12,12 +12,13 @@ import * as Constants from '../../../Constants.js';
|
|
|
12
12
|
import * as Errors from '../../../Errors.js';
|
|
13
13
|
import * as Expires from '../../../Expires.js';
|
|
14
14
|
import * as Method from '../../../Method.js';
|
|
15
|
-
import * as
|
|
15
|
+
import * as ChallengeMeta from '../../../server/internal/challengeMeta.js';
|
|
16
16
|
import * as Client from '../../../viem/Client.js';
|
|
17
17
|
import * as Account from '../../internal/account.js';
|
|
18
18
|
import * as defaults from '../../internal/defaults.js';
|
|
19
19
|
import * as Methods from '../../Methods.js';
|
|
20
20
|
import * as ChargeServer from '../../server/Charge.js';
|
|
21
|
+
import { fallbackStore } from '../../server/internal/fallback-store.js';
|
|
21
22
|
import * as Transport from '../../server/internal/transport.js';
|
|
22
23
|
import { tip20ChannelEscrow } from '../precompile/Protocol.js';
|
|
23
24
|
import { deserializeSnapshot as deserializeSessionSnapshot, serializeSnapshot as serializeSessionSnapshot, } from '../Snapshot.js';
|
|
@@ -58,23 +59,21 @@ function isBootstrapChargeCredential(credential) {
|
|
|
58
59
|
function createBootstrapChallenge(parameters) {
|
|
59
60
|
return Challenge.fromMethod(Methods.charge, {
|
|
60
61
|
expires: parameters.expires,
|
|
62
|
+
meta: ChallengeMeta.withNonce(undefined),
|
|
61
63
|
realm: parameters.realm,
|
|
62
64
|
request: parameters.request,
|
|
63
65
|
secretKey: parameters.secretKey,
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
function respondBootstrapChallenge(challenge, error) {
|
|
67
|
-
const headers = new Headers({
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const headers = new Headers({ 'Cache-Control': 'no-store' });
|
|
70
|
+
// A 403 is final: omit the challenge, as `Transport.http()` does.
|
|
71
|
+
if (error?.status !== 403)
|
|
72
|
+
headers.set(Constants.Headers.wwwAuthenticate, Challenge.serialize(challenge));
|
|
71
73
|
if (!error)
|
|
72
74
|
return new Response(null, { status: 402, headers });
|
|
73
75
|
headers.set('Content-Type', 'application/problem+json');
|
|
74
|
-
return new Response(JSON.stringify(error.toProblemDetails(challenge.id)), {
|
|
75
|
-
status: error.status,
|
|
76
|
-
headers,
|
|
77
|
-
});
|
|
76
|
+
return new Response(JSON.stringify(error.toProblemDetails(error.status === 403 ? undefined : challenge.id)), { status: error.status, headers });
|
|
78
77
|
}
|
|
79
78
|
function assertBootstrapChallengeMatches(expected, actual) {
|
|
80
79
|
if (actual.method !== Constants.Methods.tempo || actual.intent !== Constants.Intents.charge)
|
|
@@ -162,7 +161,9 @@ async function handleBootstrapPreflight(parameters) {
|
|
|
162
161
|
});
|
|
163
162
|
}
|
|
164
163
|
catch (error) {
|
|
165
|
-
return respondBootstrapChallenge(challenge, error instanceof Errors.PaymentError
|
|
164
|
+
return respondBootstrapChallenge(challenge, error instanceof Errors.PaymentError
|
|
165
|
+
? error
|
|
166
|
+
: new Errors.InternalPaymentError({ cause: error }));
|
|
166
167
|
}
|
|
167
168
|
const channelId = await resolveSessionChannelId({
|
|
168
169
|
capturedRequest: parameters.capturedRequest,
|
|
@@ -193,7 +194,7 @@ async function handleBootstrapPreflight(parameters) {
|
|
|
193
194
|
/** Creates a server-side TIP20EscrowChannel precompile session payment method. */
|
|
194
195
|
export function session(p) {
|
|
195
196
|
const parameters = p;
|
|
196
|
-
const { amount, channelStateTtl = 5_000, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, operator, store: rawStore =
|
|
197
|
+
const { amount, channelStateTtl = 5_000, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, operator, store: rawStore = fallbackStore('tempo.session'), suggestedDeposit, unitType, } = parameters;
|
|
197
198
|
const settlementSchedule = resolveSettlementSchedule(parameters.settlementSchedule, decimals);
|
|
198
199
|
const onSessionSettlement = parameters.onSessionSettlement;
|
|
199
200
|
const onSessionSettlementFailure = parameters.onSessionSettlementFailure;
|
|
@@ -250,6 +250,13 @@ export declare class SettlementCheckpointError extends Error {
|
|
|
250
250
|
txHash: Hex;
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
|
+
/** Raised when a scheduled settlement could not claim its lease; no transaction was submitted. */
|
|
254
|
+
export declare class SettlementLeaseError extends Error {
|
|
255
|
+
readonly name = "SettlementLeaseError";
|
|
256
|
+
constructor(options: {
|
|
257
|
+
cause: unknown;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
253
260
|
/** Settles multiple precompile-backed session channels with the same validation as {@link settle}. */
|
|
254
261
|
export declare function settleBatch(store: SessionStoreInput, client: Chain.TransactionClient, channelIds: readonly Hex[], options?: SettlementTransactionOptions): Promise<Hex[]>;
|
|
255
262
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseError as viem_BaseError, HttpRequestError, isAddress, isAddressEqual, LimitExceededRpcError, parseUnits, ResourceUnavailableRpcError, SocketClosedError, TimeoutError, WebSocketRequestError, zeroAddress, } from 'viem';
|
|
1
|
+
import { BaseError as viem_BaseError, ExecutionRevertedError, HttpRequestError, InternalRpcError, isAddress, isAddressEqual, LimitExceededRpcError, parseUnits, ResourceUnavailableRpcError, RpcRequestError, SocketClosedError, TimeoutError, WaitForTransactionReceiptTimeoutError, WebSocketRequestError, zeroAddress, } from 'viem';
|
|
2
2
|
import { BadRequestError, ChannelClosedError, ChannelNotFoundError, InsufficientBalanceError, VerificationFailedError, } from '../../../Errors.js';
|
|
3
3
|
import { isSessionContentRequest } from '../../server/internal/request-body.js';
|
|
4
4
|
import * as Chain from '../precompile/Chain.js';
|
|
@@ -251,7 +251,8 @@ export async function maybeSettleScheduled(parameters) {
|
|
|
251
251
|
store,
|
|
252
252
|
}).catch(async (error) => {
|
|
253
253
|
await report(error);
|
|
254
|
-
|
|
254
|
+
// Nothing was submitted, so the next settlement can still collect this charge.
|
|
255
|
+
throw new SettlementLeaseError({ cause: error });
|
|
255
256
|
});
|
|
256
257
|
if (!leaseOwner)
|
|
257
258
|
return undefined;
|
|
@@ -274,10 +275,10 @@ export async function maybeSettleScheduled(parameters) {
|
|
|
274
275
|
})
|
|
275
276
|
.catch(async (error) => {
|
|
276
277
|
await release();
|
|
278
|
+
await report(error);
|
|
277
279
|
// The transaction confirmed and collected the charge; only the local record failed.
|
|
278
280
|
if (error instanceof SettlementCheckpointError)
|
|
279
281
|
return error.txHash;
|
|
280
|
-
await report(error);
|
|
281
282
|
throw error;
|
|
282
283
|
})
|
|
283
284
|
.finally(() => clearInterval(renewal));
|
|
@@ -306,7 +307,10 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
306
307
|
sender: account?.address,
|
|
307
308
|
});
|
|
308
309
|
const amount = uint96(channel.highestVoucher.cumulativeAmount);
|
|
309
|
-
const
|
|
310
|
+
const { signature } = channel.highestVoucher;
|
|
311
|
+
// A replica behind the open or top-up that admitted this voucher rejects the settle while
|
|
312
|
+
// preparing it (e.g. `ChannelNotFound`); nothing was broadcast, so retry until it catches up.
|
|
313
|
+
const txHash = await Chain.readbackWithRetry(() => Chain.settleOnChain(client, channel.descriptor, amount, signature, escrow, account
|
|
310
314
|
? {
|
|
311
315
|
account,
|
|
312
316
|
...(options?.feePayer ? { feePayer: options.feePayer } : {}),
|
|
@@ -314,13 +318,14 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
314
318
|
...(options?.feeToken ? { feeToken: options.feeToken } : {}),
|
|
315
319
|
candidateFeeTokens: options?.candidateFeeTokens ?? [channel.token],
|
|
316
320
|
}
|
|
317
|
-
: undefined);
|
|
321
|
+
: undefined), { shouldRetry: isExecutionRevert });
|
|
318
322
|
const receipt = await Chain.waitForSuccessfulReceipt(client, txHash);
|
|
319
323
|
const settled = readSettledReceiptFields(Chain.getChannelEvent(receipt, 'Settled', channelId));
|
|
320
|
-
const { newSettled } = settled;
|
|
324
|
+
const { deltaPaid, newSettled } = settled;
|
|
321
325
|
if (newSettled < amount)
|
|
322
326
|
throw new VerificationFailedError({ reason: 'Settled event is below voucher amount' });
|
|
323
|
-
|
|
327
|
+
// A replica behind the receipt's block would report the pre-settlement state.
|
|
328
|
+
const state = await Chain.readbackWithRetry(() => Chain.getChannelState(client, channelId, escrow, receipt.blockNumber));
|
|
324
329
|
if (state.settled !== newSettled)
|
|
325
330
|
throw new VerificationFailedError({
|
|
326
331
|
reason: 'on-chain channel state does not match settle receipt',
|
|
@@ -346,7 +351,8 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
346
351
|
channelId,
|
|
347
352
|
trigger: 'settle',
|
|
348
353
|
amount: newSettled,
|
|
349
|
-
|
|
354
|
+
// The stored checkpoint can be stale after a failed write; the receipt is authoritative.
|
|
355
|
+
delta: deltaPaid,
|
|
350
356
|
});
|
|
351
357
|
}
|
|
352
358
|
if (checkpointError)
|
|
@@ -363,6 +369,13 @@ export class SettlementCheckpointError extends Error {
|
|
|
363
369
|
this.txHash = options.txHash;
|
|
364
370
|
}
|
|
365
371
|
}
|
|
372
|
+
/** Raised when a scheduled settlement could not claim its lease; no transaction was submitted. */
|
|
373
|
+
export class SettlementLeaseError extends Error {
|
|
374
|
+
name = 'SettlementLeaseError';
|
|
375
|
+
constructor(options) {
|
|
376
|
+
super('Scheduled settlement lease could not be claimed.', { cause: options.cause });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
366
379
|
/** Settles multiple precompile-backed session channels with the same validation as {@link settle}. */
|
|
367
380
|
export async function settleBatch(store, client, channelIds, options) {
|
|
368
381
|
const hashes = [];
|
|
@@ -379,16 +392,28 @@ export function ignoreRetryableSettlementFailure(error) {
|
|
|
379
392
|
return undefined;
|
|
380
393
|
throw error;
|
|
381
394
|
}
|
|
395
|
+
/** Whether a settlement failed because the node's execution reverted it. */
|
|
396
|
+
function isExecutionRevert(error) {
|
|
397
|
+
return (error instanceof viem_BaseError &&
|
|
398
|
+
Boolean(error.walk((cause) => cause instanceof ExecutionRevertedError)));
|
|
399
|
+
}
|
|
382
400
|
function isRetryableSettlementFailure(error) {
|
|
401
|
+
if (error instanceof SettlementLeaseError)
|
|
402
|
+
return true;
|
|
383
403
|
if (!(error instanceof viem_BaseError))
|
|
384
404
|
return false;
|
|
385
405
|
// Only unavailable or overloaded upstreams clear on retry; node-rejected transactions repeat.
|
|
386
406
|
return Boolean(error.walk((cause) => (cause instanceof HttpRequestError &&
|
|
387
407
|
(cause.status === undefined || cause.status === 429 || cause.status >= 500)) ||
|
|
408
|
+
// Rate limits some providers return in a JSON-RPC body rather than as HTTP 429.
|
|
409
|
+
(cause instanceof RpcRequestError && cause.code === 429) ||
|
|
410
|
+
cause instanceof InternalRpcError ||
|
|
388
411
|
cause instanceof LimitExceededRpcError ||
|
|
389
412
|
cause instanceof ResourceUnavailableRpcError ||
|
|
390
413
|
cause instanceof SocketClosedError ||
|
|
391
414
|
cause instanceof TimeoutError ||
|
|
415
|
+
// The settlement was broadcast; a later settlement collects the charge if it never lands.
|
|
416
|
+
cause instanceof WaitForTransactionReceiptTimeoutError ||
|
|
392
417
|
cause instanceof WebSocketRequestError));
|
|
393
418
|
}
|
|
394
419
|
/** @internal Reports a settlement failure without letting observer errors replace it. */
|
|
@@ -7,6 +7,7 @@ import * as Credential_ from '../../Credential.js';
|
|
|
7
7
|
import { VerificationFailedError } from '../../Errors.js';
|
|
8
8
|
import * as Types from '../../evm/Types.js';
|
|
9
9
|
import * as PaymentRequest from '../../PaymentRequest.js';
|
|
10
|
+
import * as ChallengeMeta from '../../server/internal/challengeMeta.js';
|
|
10
11
|
import * as Scope from '../../server/internal/scope.js';
|
|
11
12
|
import * as x402_Header from '../Header.js';
|
|
12
13
|
import * as x402_RouteBinding from '../internal/RouteBinding.js';
|
|
@@ -83,8 +84,9 @@ export function createPath(config) {
|
|
|
83
84
|
const clientNonce = paymentPayload.extensions?.[mppxExtensionKey]?.info.nonce;
|
|
84
85
|
const isRouteBound = clientNonce !== undefined;
|
|
85
86
|
const routeRequiresBinding = challenge.digest !== undefined ||
|
|
86
|
-
challenge.
|
|
87
|
-
|
|
87
|
+
(challenge.meta === undefined
|
|
88
|
+
? challenge.opaque !== undefined
|
|
89
|
+
: ChallengeMeta.routeMeta(challenge.meta) !== undefined);
|
|
88
90
|
// `extensions.mppx` binding is not part of the x402 spec, so a client mppx
|
|
89
91
|
// did not write cannot produce it. Requiring it makes every scoped route —
|
|
90
92
|
// and everything behind `Proxy`, which scopes what it serves — unpayable by
|
|
@@ -296,7 +298,12 @@ function routeExtensions(challenge, input) {
|
|
|
296
298
|
binding[Scope.reservedMetaKey] = scope;
|
|
297
299
|
if (challenge.digest !== undefined)
|
|
298
300
|
binding.digest = challenge.digest;
|
|
299
|
-
const
|
|
301
|
+
const meta = ChallengeMeta.routeMeta(challenge.meta);
|
|
302
|
+
const opaque = challenge.meta === undefined
|
|
303
|
+
? challenge.opaque
|
|
304
|
+
: meta
|
|
305
|
+
? PaymentRequest.serialize(meta)
|
|
306
|
+
: undefined;
|
|
300
307
|
if (opaque !== undefined)
|
|
301
308
|
binding.opaque = opaque;
|
|
302
309
|
return {
|