mppx 0.9.2 → 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.
Files changed (78) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +19 -0
  3. package/dist/Method.d.ts +2 -0
  4. package/dist/cli/Extension.d.ts +20 -0
  5. package/dist/cli/Extension.js +5 -0
  6. package/dist/cli/cli.js +137 -137
  7. package/dist/cli/config.d.ts +4 -0
  8. package/dist/cli/config.js +1 -0
  9. package/dist/cli/internal.d.ts +7 -1
  10. package/dist/cli/internal.js +51 -5
  11. package/dist/cli/plugins/index.js +12 -12
  12. package/dist/cli/plugins/plugin.d.ts +2 -2
  13. package/dist/cli/sessions/request.d.ts +1 -0
  14. package/dist/cli/sessions/request.js +1 -0
  15. package/dist/cli/validate/discovery.d.ts +3 -1
  16. package/dist/cli/validate/discovery.js +28 -1
  17. package/dist/cli/validate/helpers.d.ts +3 -2
  18. package/dist/cli/validate/helpers.js +3 -0
  19. package/dist/cli/validate/index.js +3 -1
  20. package/dist/cli/validate/payment.js +118 -59
  21. package/dist/client/Mppx.d.ts +32 -0
  22. package/dist/client/Mppx.js +180 -6
  23. package/dist/client/internal/Fetch.d.ts +6 -0
  24. package/dist/client/internal/Fetch.js +4 -2
  25. package/dist/evm/client/Charge.d.ts +2 -2
  26. package/dist/evm/client/Charge.js +1 -1
  27. package/dist/evm/server/Charge.d.ts +7 -2
  28. package/dist/evm/server/Charge.js +28 -2
  29. package/dist/evm/server/Methods.d.ts +1 -1
  30. package/dist/internal/version.d.ts +5 -0
  31. package/dist/internal/version.js +6 -0
  32. package/dist/server/Mppx.d.ts +7 -6
  33. package/dist/server/Mppx.js +84 -2
  34. package/dist/server/internal/html/config.js +1 -3
  35. package/dist/stripe/Methods.d.ts +4 -3
  36. package/dist/stripe/Methods.js +14 -11
  37. package/dist/stripe/client/Charge.d.ts +3 -3
  38. package/dist/stripe/client/Methods.d.ts +3 -3
  39. package/dist/stripe/internal/parse-units.d.ts +8 -0
  40. package/dist/stripe/internal/parse-units.js +20 -0
  41. package/dist/stripe/internal/payment-intent.d.ts +29 -0
  42. package/dist/stripe/internal/payment-intent.js +8 -0
  43. package/dist/stripe/server/Charge.d.ts +5 -3
  44. package/dist/stripe/server/Charge.js +17 -21
  45. package/dist/stripe/server/Methods.d.ts +6 -4
  46. package/dist/stripe/server/Methods.js +42 -14
  47. package/dist/stripe/server/internal/analytics.d.ts +6 -0
  48. package/dist/stripe/server/internal/analytics.js +16 -0
  49. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  50. package/dist/stripe/server/internal/html.gen.js +1 -1
  51. package/dist/stripe/server/internal/record-payment.d.ts +1 -0
  52. package/dist/stripe/server/internal/record-payment.js +7 -5
  53. package/dist/tempo/client/Charge.d.ts +8 -2
  54. package/dist/tempo/client/Charge.js +19 -11
  55. package/dist/tempo/client/Subscription.js +6 -2
  56. package/dist/tempo/internal/fee-payer.d.ts +2 -0
  57. package/dist/tempo/internal/fee-payer.js +4 -0
  58. package/dist/tempo/server/Subscription.js +1 -0
  59. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  60. package/dist/tempo/server/internal/html.gen.js +1 -1
  61. package/dist/tempo/session/client/CredentialState.d.ts +2 -0
  62. package/dist/tempo/session/client/CredentialState.js +9 -3
  63. package/dist/tempo/session/client/Session.d.ts +6 -0
  64. package/dist/tempo/session/client/Session.js +4 -1
  65. package/dist/tempo/session/client/SessionManager.d.ts +11 -0
  66. package/dist/tempo/session/client/SessionManager.js +23 -2
  67. package/dist/tempo/session/precompile/Chain.js +3 -0
  68. package/dist/tempo/subscription/KeyAuthorization.d.ts +7 -1
  69. package/dist/tempo/subscription/KeyAuthorization.js +21 -3
  70. package/dist/validation/core.d.ts +1 -0
  71. package/dist/validation/core.js +7 -2
  72. package/dist/viem/Client.d.ts +4 -0
  73. package/dist/viem/Client.js +13 -0
  74. package/dist/x402/client/Exact.d.ts +2 -2
  75. package/dist/x402/client/Exact.js +1 -1
  76. package/dist/x402/server/EvmCharge.d.ts +2 -0
  77. package/dist/x402/server/EvmCharge.js +34 -26
  78. package/package.json +1 -1
@@ -113,6 +113,8 @@ export type ResolveChallengeContextParameters = {
113
113
  challenge: Challenge.Challenge;
114
114
  /** Optional local escrow override. */
115
115
  escrowOverride?: Address | undefined;
116
+ /** Permitted payment chains, checked before resolution when known and again before signing. */
117
+ allowedChainIds?: readonly number[] | undefined;
116
118
  /** Resolves the viem client for the challenge chain. */
117
119
  getClient(parameters: {
118
120
  chainId?: number | undefined;
@@ -1,5 +1,6 @@
1
1
  import { isAddress, parseUnits, } from 'viem';
2
2
  import * as Constants from '../../../Constants.js';
3
+ import { assertAllowedChainId, assertChainId } from '../../../viem/Client.js';
3
4
  import * as z from '../../../zod.js';
4
5
  import * as AutoSwap from '../../internal/auto-swap.js';
5
6
  import * as Chain from '../precompile/Chain.js';
@@ -126,10 +127,15 @@ function readSuggestedDeposit(value) {
126
127
  }
127
128
  /** Resolves raw challenge fields into the typed data required by client credential planning. */
128
129
  export async function resolveChallengeContext(parameters) {
129
- const { allowCustomEscrow, challenge, escrowOverride, getClient } = parameters;
130
+ const { allowCustomEscrow, challenge, escrowOverride, allowedChainIds, getClient } = parameters;
130
131
  const methodDetails = readMethodDetails(challenge);
131
- const client = await getClient({ chainId: methodDetails.chainId });
132
- const chainId = methodDetails.chainId ?? client.chain?.id;
132
+ if (methodDetails.chainId !== undefined || allowedChainIds?.length === 0)
133
+ assertAllowedChainId(allowedChainIds, methodDetails.chainId);
134
+ const requestedChainId = methodDetails.chainId ?? (allowedChainIds?.length === 1 ? allowedChainIds[0] : undefined);
135
+ const client = await getClient({ chainId: requestedChainId });
136
+ assertChainId(client, requestedChainId);
137
+ const chainId = requestedChainId ?? client.chain?.id;
138
+ assertAllowedChainId(allowedChainIds, chainId);
133
139
  if (!chainId)
134
140
  throw new Error('No chainId configured for TIP-1034 session challenge.');
135
141
  const escrow = resolveEscrow(challenge, escrowOverride, allowCustomEscrow);
@@ -137,6 +137,12 @@ export declare namespace session {
137
137
  decimals?: number | undefined;
138
138
  /** Exact TIP20EscrowChannel address pin. Takes precedence over `allowCustomEscrow`. */
139
139
  escrow?: Address | undefined;
140
+ /**
141
+ * Chains permitted for session credentials. Omitted allows any chain; empty rejects all.
142
+ * A single entry supplies the chain when omitted by the challenge. Otherwise the
143
+ * challenge or resolved client must select an allowed chain.
144
+ */
145
+ allowedChainIds?: readonly number[] | undefined;
140
146
  /** Maximum channel deposit in human-readable units. Caps server-suggested opens and automatic top-ups. */
141
147
  maxDeposit?: string | undefined;
142
148
  /**
@@ -77,7 +77,7 @@ function acknowledgesOpen(outcome, challenge, opened) {
77
77
  * channel descriptor).
78
78
  */
79
79
  export function session(parameters = {}) {
80
- const { account, allowCustomEscrow = false, autoSwap: autoSwapParameter, channelStore, decimals = defaults.decimals, escrow: escrowOverride, getClient: getClientParameter, maxDeposit: maxDepositParameter, topUpAmount: topUpAmountParameter, onChannelUpdate, resolveAccount, } = parameters;
80
+ const { account, allowCustomEscrow = false, autoSwap: autoSwapParameter, channelStore, decimals = defaults.decimals, escrow: escrowOverride, allowedChainIds, getClient: getClientParameter, maxDeposit: maxDepositParameter, topUpAmount: topUpAmountParameter, onChannelUpdate, resolveAccount, } = parameters;
81
81
  const getClient = Client.getResolver({
82
82
  chain: tempo_chain,
83
83
  getClient: getClientParameter,
@@ -120,6 +120,7 @@ export function session(parameters = {}) {
120
120
  allowCustomEscrow,
121
121
  challenge,
122
122
  escrowOverride,
123
+ allowedChainIds,
123
124
  getClient,
124
125
  });
125
126
  const attempt = MethodResponse.getAttempt(parameters);
@@ -237,6 +238,7 @@ export function session(parameters = {}) {
237
238
  allowCustomEscrow,
238
239
  challenge,
239
240
  escrowOverride,
241
+ allowedChainIds,
240
242
  getClient,
241
243
  });
242
244
  const channel = await store.get(resolved.key);
@@ -278,6 +280,7 @@ export function session(parameters = {}) {
278
280
  allowCustomEscrow,
279
281
  challenge,
280
282
  escrowOverride,
283
+ allowedChainIds,
281
284
  getClient,
282
285
  })).key;
283
286
  let channel = await store.get(channelKey);
@@ -3,6 +3,7 @@ import { type Address } from 'viem';
3
3
  import * as Account from '../../../viem/Account.js';
4
4
  import * as Client from '../../../viem/Client.js';
5
5
  import { type ChannelStore } from '../client/ChannelStore.js';
6
+ import { type SessionContext } from '../client/CredentialState.js';
6
7
  import { session as sessionPlugin } from '../client/Session.js';
7
8
  import type { SessionReceipt } from '../precompile/Protocol.js';
8
9
  import { deserializeSnapshot as deserializeSessionSnapshot, serializeSnapshot as serializeSessionSnapshot } from '../Snapshot.js';
@@ -48,6 +49,8 @@ export type PaymentResponse = Response & {
48
49
  /** Local cumulative voucher authorization in raw token units. */
49
50
  cumulative: bigint;
50
51
  };
52
+ /** @internal Merges manager-wide context without allowing it to replace an operation's fields. */
53
+ export declare function mergeSessionCredentialContext(credentialContext: unknown, operationContext: SessionContext): SessionContext;
51
54
  /**
52
55
  * Creates a session manager that handles the full client payment lifecycle:
53
56
  * channel open, incremental vouchers, SSE streaming, and channel close.
@@ -76,8 +79,16 @@ export declare namespace sessionManager {
76
79
  decimals?: number | undefined;
77
80
  /** Exact TIP20EscrowChannel address pin. Takes precedence over `allowCustomEscrow`. */
78
81
  escrow?: Address | undefined;
82
+ /**
83
+ * Chains permitted for sessions, bootstrap proofs, and snapshots. Omitted allows any
84
+ * chain; empty rejects all. A single entry supplies an omitted challenge chain;
85
+ * otherwise the challenge or resolved client must select an allowed chain.
86
+ */
87
+ allowedChainIds?: readonly number[] | undefined;
79
88
  /** Fetch implementation used for HTTP probes, management posts, and paid retries. */
80
89
  fetch?: typeof globalThis.fetch | undefined;
90
+ /** Base context supplied to every session credential created by the manager. */
91
+ credentialContext?: unknown;
81
92
  /** Maximum deposit in human-readable units (e.g. `'10'` for 10 tokens). Converted to raw units via `decimals`. */
82
93
  maxDeposit?: string | undefined;
83
94
  /**
@@ -10,7 +10,7 @@ import * as Client from '../../../viem/Client.js';
10
10
  import { charge as chargePlugin } from '../../client/Charge.js';
11
11
  import * as defaults from '../../internal/defaults.js';
12
12
  import { createChannelStore, entryKey } from '../client/ChannelStore.js';
13
- import { hydrateSessionSnapshot } from '../client/CredentialState.js';
13
+ import { hydrateSessionSnapshot, sessionContextSchema, } from '../client/CredentialState.js';
14
14
  import { session as sessionPlugin } from '../client/Session.js';
15
15
  import * as Channel from '../precompile/Channel.js';
16
16
  import { deserializeSessionReceipt } from '../precompile/Protocol.js';
@@ -60,6 +60,12 @@ function requestInitWithSessionHint(input, init, channelId) {
60
60
  },
61
61
  };
62
62
  }
63
+ /** @internal Merges manager-wide context without allowing it to replace an operation's fields. */
64
+ export function mergeSessionCredentialContext(credentialContext, operationContext) {
65
+ if (credentialContext === undefined)
66
+ return operationContext;
67
+ return { ...sessionContextSchema.parse(credentialContext), ...operationContext };
68
+ }
63
69
  function resolveSessionManagerConfig(parameters) {
64
70
  const decimals = parameters.decimals ?? 6;
65
71
  const WebSocket = parameters.webSocket ??
@@ -83,6 +89,9 @@ function resolveSessionManagerConfig(parameters) {
83
89
  * `channelStore` can persist reusable channels between manager instances.
84
90
  */
85
91
  export function sessionManager(parameters) {
92
+ const clientChainId = parameters.client?.chain?.id;
93
+ if (clientChainId !== undefined || parameters.allowedChainIds?.length === 0)
94
+ Client.assertAllowedChainId(parameters.allowedChainIds, clientChainId);
86
95
  const allowCustomEscrow = parameters.allowCustomEscrow ?? false;
87
96
  const config = resolveSessionManagerConfig(parameters);
88
97
  const getClient = Client.getResolver({
@@ -92,6 +101,9 @@ export function sessionManager(parameters) {
92
101
  });
93
102
  const getAccount = Account.getResolver({ account: parameters.account });
94
103
  const runtime = createSessionManagerRuntime();
104
+ const credentialContext = parameters.credentialContext === undefined
105
+ ? undefined
106
+ : mergeSessionCredentialContext(parameters.credentialContext, {});
95
107
  const receipts = createSessionReceiptCoordinator({
96
108
  getSocketSession: () => runtime.socketSession,
97
109
  });
@@ -197,6 +209,7 @@ export function sessionManager(parameters) {
197
209
  getClient: parameters.client ? () => parameters.client : parameters.getClient,
198
210
  resolveAccount: parameters.resolveAccount,
199
211
  escrow: parameters.escrow,
212
+ allowedChainIds: parameters.allowedChainIds,
200
213
  decimals: config.decimals,
201
214
  maxDeposit: parameters.maxDeposit,
202
215
  topUpAmount: parameters.topUpAmount,
@@ -219,6 +232,7 @@ export function sessionManager(parameters) {
219
232
  });
220
233
  MethodResponse.unregister(method);
221
234
  const chargeMethod = chargePlugin({
235
+ allowedChainIds: parameters.allowedChainIds,
222
236
  account: parameters.account,
223
237
  getClient: parameters.client ? () => parameters.client : parameters.getClient,
224
238
  });
@@ -248,7 +262,10 @@ export function sessionManager(parameters) {
248
262
  },
249
263
  });
250
264
  function createSessionCredential(challenge, context) {
251
- return method.createCredential({ challenge, context });
265
+ return method.createCredential({
266
+ challenge,
267
+ context: mergeSessionCredentialContext(credentialContext, context),
268
+ });
252
269
  }
253
270
  function updateSpentFromReceipt(receipt) {
254
271
  applySessionReceiptToRuntime({
@@ -278,7 +295,9 @@ export function sessionManager(parameters) {
278
295
  if (!header)
279
296
  return undefined;
280
297
  const snapshot = deserializeSessionSnapshot(header);
298
+ Client.assertAllowedChainId(parameters.allowedChainIds, snapshot.chainId);
281
299
  const client = await getClient({ chainId: snapshot.chainId });
300
+ Client.assertChainId(client, snapshot.chainId);
282
301
  const defaultAccount = getAccount(client);
283
302
  const account = (await parameters.resolveAccount?.({
284
303
  account: defaultAccount,
@@ -535,6 +554,8 @@ export function sessionManager(parameters) {
535
554
  use.trackCreates = true;
536
555
  const liveHint = runtime.channel?.opened ? runtime.channel.channelId : undefined;
537
556
  let effectiveInit = requestInitWithSessionHint(input, init, liveHint);
557
+ if (credentialContext)
558
+ effectiveInit = { ...effectiveInit, context: credentialContext };
538
559
  // Stored channels may be stale, so retry once after evicting the resumed entry.
539
560
  let canRetryResumed = !previous.channel?.opened;
540
561
  async function retryWithoutResumed() {
@@ -318,6 +318,7 @@ async function prepareFeePayerCallTransaction(client, parameters) {
318
318
  account,
319
319
  calls: [{ to, data }],
320
320
  feePayer: true,
321
+ nonceKey: 'expiring',
321
322
  ...(feeToken ? { feeToken } : {}),
322
323
  });
323
324
  }
@@ -329,6 +330,8 @@ function sendPrecompileContractCall(client, parameters) {
329
330
  ...(account ? { account } : {}),
330
331
  to,
331
332
  data,
333
+ // Server processes can share a signer without sharing viem's concurrency detection.
334
+ nonceKey: 'expiring',
332
335
  ...(feePayer ? { feePayer } : {}),
333
336
  ...(feeToken ? { feeToken } : {}),
334
337
  });
@@ -1,3 +1,4 @@
1
+ import { Hex } from 'ox';
1
2
  import { KeyAuthorization } from 'ox/tempo';
2
3
  import { type Address } from 'viem';
3
4
  import type * as Methods from '../Methods.js';
@@ -42,6 +43,8 @@ export declare function getSubscriptionRpcAllowedCalls(request: Pick<Subscriptio
42
43
  readonly recipients: readonly [`0x${string}`];
43
44
  }];
44
45
  }];
46
+ /** Decodes the server-issued challenge identifier for use as a signed Tempo witness. */
47
+ export declare function getSubscriptionChallengeWitness(challengeId: string): Hex.Hex;
45
48
  /**
46
49
  * Creates and signs a Tempo key authorization for subscription payments when the account can sign
47
50
  * arbitrary hashes locally.
@@ -53,6 +56,7 @@ export declare function signSubscriptionKeyAuthorization(parameters: {
53
56
  hash: `0x${string}`;
54
57
  }) => Promise<`0x${string}`>) | undefined;
55
58
  };
59
+ challengeId: string;
56
60
  chainId: number;
57
61
  request: Pick<SubscriptionRequest, 'amount' | 'currency' | 'periodCount' | 'periodUnit' | 'recipient' | 'subscriptionExpires'>;
58
62
  }): Promise<{
@@ -96,7 +100,7 @@ export declare function signSubscriptionKeyAuthorization(parameters: {
96
100
  version?: undefined;
97
101
  } | {
98
102
  metadata: {
99
- authenticatorData: import("ox/Hex").Hex;
103
+ authenticatorData: Hex.Hex;
100
104
  clientDataJSON: string;
101
105
  };
102
106
  signature: {
@@ -133,6 +137,7 @@ export declare function signSubscriptionKeyAuthorization(parameters: {
133
137
  readonly recipients: readonly `0x${string}`[];
134
138
  }[];
135
139
  readonly type: "p256" | "secp256k1" | "webAuthn";
140
+ readonly witness: `0x${string}`;
136
141
  } | undefined>;
137
142
  /**
138
143
  * Verifies that a subscription credential contains a key authorization scoped to the requested
@@ -140,6 +145,7 @@ export declare function signSubscriptionKeyAuthorization(parameters: {
140
145
  */
141
146
  export declare function verifySubscriptionKeyAuthorization(parameters: {
142
147
  accessKey?: SubscriptionAccessKey | undefined;
148
+ challengeId: string;
143
149
  chainId: number;
144
150
  payload: SubscriptionCredentialPayload;
145
151
  request: SubscriptionRequest;
@@ -1,3 +1,4 @@
1
+ import { Base64, Hex } from 'ox';
1
2
  import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo';
2
3
  import { isAddress, isAddressEqual } from 'viem';
3
4
  import { VerificationFailedError } from '../../Errors.js';
@@ -104,16 +105,29 @@ export function getSubscriptionRpcAllowedCalls(request) {
104
105
  },
105
106
  ];
106
107
  }
108
+ /** Decodes the server-issued challenge identifier for use as a signed Tempo witness. */
109
+ export function getSubscriptionChallengeWitness(challengeId) {
110
+ try {
111
+ const witness = Hex.fromBytes(Base64.toBytes(challengeId));
112
+ if (Hex.size(witness) !== 32)
113
+ throw new Error('invalid challenge id size');
114
+ return witness;
115
+ }
116
+ catch {
117
+ throw new VerificationFailedError({ reason: 'challenge id must encode 32 bytes' });
118
+ }
119
+ }
107
120
  /**
108
121
  * Creates and signs a Tempo key authorization for subscription payments when the account can sign
109
122
  * arbitrary hashes locally.
110
123
  */
111
124
  export async function signSubscriptionKeyAuthorization(parameters) {
112
- const { accessKey, account, chainId, request } = parameters;
125
+ const { accessKey, account, challengeId, chainId, request } = parameters;
113
126
  if (typeof account.sign !== 'function')
114
127
  return undefined;
115
128
  const authorization = createUnsignedAuthorization({
116
129
  accessKey,
130
+ challengeId,
117
131
  chainId,
118
132
  request,
119
133
  });
@@ -135,7 +149,7 @@ export async function signSubscriptionKeyAuthorization(parameters) {
135
149
  * token, recipient, amount, period, expiry, chain, and server-issued access key.
136
150
  */
137
151
  export function verifySubscriptionKeyAuthorization(parameters) {
138
- const { accessKey, chainId, payload, request } = parameters;
152
+ const { accessKey, challengeId, chainId, payload, request } = parameters;
139
153
  if (payload.type !== 'keyAuthorization') {
140
154
  throw new VerificationFailedError({ reason: 'invalid keyAuthorization payload' });
141
155
  }
@@ -146,6 +160,9 @@ export function verifySubscriptionKeyAuthorization(parameters) {
146
160
  authorization,
147
161
  chainId,
148
162
  });
163
+ if (authorization.witness?.toLowerCase() !== getSubscriptionChallengeWitness(challengeId)) {
164
+ throw new VerificationFailedError({ reason: 'keyAuthorization challenge mismatch' });
165
+ }
149
166
  assertAuthorizationExpiry(authorization, request);
150
167
  assertAuthorizationLimit(getSingleTokenLimit(authorization), request);
151
168
  assertAuthorizationScopes(authorization.scopes, request);
@@ -159,7 +176,7 @@ export function verifySubscriptionKeyAuthorization(parameters) {
159
176
  };
160
177
  }
161
178
  function createUnsignedAuthorization(parameters) {
162
- const { accessKey, chainId, request } = parameters;
179
+ const { accessKey, challengeId, chainId, request } = parameters;
163
180
  return KeyAuthorization.from({
164
181
  address: normalizeAddress(accessKey.accessKeyAddress, 'accessKeyAddress'),
165
182
  chainId: BigInt(chainId),
@@ -173,6 +190,7 @@ function createUnsignedAuthorization(parameters) {
173
190
  ],
174
191
  scopes: getSubscriptionScopes(request),
175
192
  type: accessKey.keyType,
193
+ witness: getSubscriptionChallengeWitness(challengeId),
176
194
  });
177
195
  }
178
196
  function deserializeAuthorization(signature) {
@@ -37,6 +37,7 @@ export type ValidateResult = {
37
37
  passed: number;
38
38
  failed: number;
39
39
  warnings: number;
40
+ suggested: number;
40
41
  skipped: number;
41
42
  };
42
43
  suggestions: string[];
@@ -1,6 +1,6 @@
1
1
  import * as viemChains from 'viem/chains';
2
2
  import { validateChallenge, validateErrorHandling } from '../cli/validate/challenge.js';
3
- import { buildUrl, extractEndpointsFromDiscovery, extractRequestBodyFromDiscovery, fetchDiscoveryDoc, } from '../cli/validate/discovery.js';
3
+ import { buildUrl, extractEndpointsFromDiscovery, extractRequestBodyFromDiscovery, fetchDiscoveryDoc, validateLlmsDoc, } from '../cli/validate/discovery.js';
4
4
  import { isValidIntegerAmount, parseEndpointArg, resolveBodyForEndpoint, } from '../cli/validate/helpers.js';
5
5
  import { missingDiscoverySuggestion } from '../cli/validate/messages.js';
6
6
  import { validatePaymentFlow } from '../cli/validate/payment.js';
@@ -123,7 +123,7 @@ export async function validate(options) {
123
123
  break;
124
124
  }
125
125
  }
126
- const summary = { passed: 0, failed: 0, warnings: 0, skipped: 0 };
126
+ const summary = { passed: 0, failed: 0, warnings: 0, suggested: 0, skipped: 0 };
127
127
  if (discovery) {
128
128
  for (const r of discovery.checks) {
129
129
  if (r.severity === 'pass')
@@ -132,6 +132,8 @@ export async function validate(options) {
132
132
  summary.failed++;
133
133
  else if (r.severity === 'warn')
134
134
  summary.warnings++;
135
+ else if (r.severity === 'suggested')
136
+ summary.suggested++;
135
137
  else if (r.severity === 'skip')
136
138
  summary.skipped++;
137
139
  }
@@ -144,6 +146,8 @@ export async function validate(options) {
144
146
  summary.failed++;
145
147
  else if (r.severity === 'warn')
146
148
  summary.warnings++;
149
+ else if (r.severity === 'suggested')
150
+ summary.suggested++;
147
151
  else if (r.severity === 'skip')
148
152
  summary.skipped++;
149
153
  }
@@ -189,6 +193,7 @@ async function runDiscovery(baseUrl, options) {
189
193
  let discoveryDoc = null;
190
194
  let found = false;
191
195
  let valid = false;
196
+ checks.push(await validateLlmsDoc(baseUrl));
192
197
  // Try the user's path first, then root, then /api.
193
198
  const origin = new URL(baseUrl).origin;
194
199
  let candidates;
@@ -25,4 +25,8 @@ export declare namespace getResolver {
25
25
  }) => MaybePromise<Client>) | undefined;
26
26
  };
27
27
  }
28
+ /** Rejects a resolved client whose known chain conflicts with the requested payment chain. */
29
+ export declare function assertChainId(client: Client, chainId: number | undefined): void;
30
+ /** Rejects disallowed or unknown payment chains when an allowlist is configured. */
31
+ export declare function assertAllowedChainId(allowedChainIds: readonly number[] | undefined, chainId: number | undefined): void;
28
32
  //# sourceMappingURL=Client.d.ts.map
@@ -50,6 +50,8 @@ export function getResolver(parameters) {
50
50
  return Object.assign({}, resolvedClient, {
51
51
  chain: {
52
52
  ...chain,
53
+ // Serializer defaults must not replace an explicitly requested network.
54
+ ...(params.chainId !== undefined ? { id: params.chainId } : {}),
53
55
  ...resolvedClient.chain,
54
56
  formatters: resolvedClient.chain?.formatters ?? chain.formatters,
55
57
  prepareTransactionRequest: resolvedClient.chain?.prepareTransactionRequest ?? chain.prepareTransactionRequest,
@@ -76,4 +78,15 @@ export function getResolver(parameters) {
76
78
  });
77
79
  };
78
80
  }
81
+ /** Rejects a resolved client whose known chain conflicts with the requested payment chain. */
82
+ export function assertChainId(client, chainId) {
83
+ if (chainId !== undefined && client.chain?.id !== undefined && client.chain.id !== chainId)
84
+ throw new Error(`Chain ID mismatch: expected ${chainId}, got ${client.chain.id}.`);
85
+ }
86
+ /** Rejects disallowed or unknown payment chains when an allowlist is configured. */
87
+ export function assertAllowedChainId(allowedChainIds, chainId) {
88
+ if (allowedChainIds !== undefined &&
89
+ (chainId === undefined || !allowedChainIds.includes(chainId)))
90
+ throw new Error(`Chain ID not allowed: ${chainId ?? 'unknown'}.`);
91
+ }
79
92
  //# sourceMappingURL=Client.js.map
@@ -28,8 +28,8 @@ export type Signer = Account & {
28
28
  signTypedData?: (parameters: any) => Promise<`0x${string}`>;
29
29
  };
30
30
  export type Config = {
31
- /** Account used to sign exact EVM payment payloads. */
32
- account: Account;
31
+ /** Account used to sign exact EVM payment payloads, unless supplied in credential context. */
32
+ account?: Account | undefined;
33
33
  /** Optional token decimals used to parse `maxAmount` when currency metadata is not provided. */
34
34
  decimals?: number | undefined;
35
35
  /** Optional maximum display-unit amount the client is willing to pay. */
@@ -8,7 +8,7 @@ import * as Types from '../Types.js';
8
8
  */
9
9
  export async function createCredential(parameters) {
10
10
  const account = (parameters.context?.account ?? parameters.config.account);
11
- if (!account.signTypedData)
11
+ if (!account?.signTypedData)
12
12
  throw new Error('x402 exact requires a typed-data signer.');
13
13
  const request = parameters.challenge.request;
14
14
  const accepted = Types.toPaymentRequirements(request);
@@ -52,6 +52,8 @@ export declare function resolveOptions(parameters: {
52
52
  export declare function createPath(config: ResolvedOptions): Path;
53
53
  /** Settles a verified EVM authorization through an x402 facilitator. */
54
54
  export declare function settleWithFacilitator(parameters: ResolvedOptions): SettleWithFacilitator;
55
+ /** Checks an EVM authorization with the facilitator without settling it. */
56
+ export declare function verifyWithFacilitator(parameters: ResolvedOptions, authorization: Parameters<SettleWithFacilitator>[0]): Promise<void>;
55
57
  export type SettleWithFacilitator = (parameters: {
56
58
  credential: Credential.Credential<Types.AuthorizationPayload>;
57
59
  payload: Types.AuthorizationPayload;
@@ -174,34 +174,11 @@ export function createPath(config) {
174
174
  }
175
175
  /** Settles a verified EVM authorization through an x402 facilitator. */
176
176
  export function settleWithFacilitator(parameters) {
177
- const { facilitator, maxTimeoutSeconds } = parameters;
177
+ const { facilitator } = parameters;
178
178
  if (!facilitator)
179
179
  throw new Error('EVM authorization x402 requires `facilitator`.');
180
- return async ({ payload, request }) => {
181
- const paymentRequirements = toPaymentRequirements(request, {
182
- ...parameters,
183
- maxTimeoutSeconds,
184
- });
185
- const paymentPayload = {
186
- accepted: paymentRequirements,
187
- payload: {
188
- authorization: {
189
- from: payload.from,
190
- nonce: payload.nonce,
191
- to: payload.to,
192
- validAfter: payload.validAfter,
193
- validBefore: payload.validBefore,
194
- value: payload.value,
195
- },
196
- signature: payload.signature,
197
- },
198
- x402Version: 2,
199
- };
200
- const verified = await facilitator.verify(paymentPayload, paymentRequirements);
201
- if (!verified.isValid)
202
- throw new VerificationFailedError({
203
- reason: verified.invalidMessage ?? verified.invalidReason ?? 'EVM facilitator verify failed',
204
- });
180
+ return async (authorization) => {
181
+ const { paymentPayload, paymentRequirements } = facilitatorPayment(parameters, authorization);
205
182
  const settled = await facilitator.settle(paymentPayload, paymentRequirements);
206
183
  if (!settled.success)
207
184
  throw new VerificationFailedError({
@@ -212,6 +189,37 @@ export function settleWithFacilitator(parameters) {
212
189
  };
213
190
  };
214
191
  }
192
+ /** Checks an EVM authorization with the facilitator without settling it. */
193
+ export async function verifyWithFacilitator(parameters, authorization) {
194
+ const { facilitator } = parameters;
195
+ if (!facilitator)
196
+ throw new Error('EVM authorization x402 requires `facilitator`.');
197
+ const { paymentPayload, paymentRequirements } = facilitatorPayment(parameters, authorization);
198
+ const verified = await facilitator.verify(paymentPayload, paymentRequirements);
199
+ if (!verified.isValid)
200
+ throw new VerificationFailedError({
201
+ reason: verified.invalidMessage ?? verified.invalidReason ?? 'EVM facilitator verify failed',
202
+ });
203
+ }
204
+ function facilitatorPayment(parameters, { payload, request }) {
205
+ const paymentRequirements = toPaymentRequirements(request, parameters);
206
+ const paymentPayload = {
207
+ accepted: paymentRequirements,
208
+ payload: {
209
+ authorization: {
210
+ from: payload.from,
211
+ nonce: payload.nonce,
212
+ to: payload.to,
213
+ validAfter: payload.validAfter,
214
+ validBefore: payload.validBefore,
215
+ value: payload.value,
216
+ },
217
+ signature: payload.signature,
218
+ },
219
+ x402Version: 2,
220
+ };
221
+ return { paymentPayload, paymentRequirements };
222
+ }
215
223
  /** Returns whether a credential was converted from an x402 payment payload. */
216
224
  export function isCredential(credential) {
217
225
  return credential[x402Credential] === true;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mppx",
3
3
  "type": "module",
4
- "version": "0.9.2",
4
+ "version": "0.10.0",
5
5
  "main": "./dist/index.js",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/wevm/mppx#readme",