mppx 0.11.0 → 0.13.1

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 (126) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +22 -1
  3. package/dist/Method.d.ts +7 -1
  4. package/dist/Method.js +7 -2
  5. package/dist/Receipt.d.ts +2 -0
  6. package/dist/Receipt.js +2 -0
  7. package/dist/cli/agent.d.ts +3 -0
  8. package/dist/cli/agent.js +20 -0
  9. package/dist/cli/cli.d.ts +9 -1
  10. package/dist/cli/cli.js +134 -136
  11. package/dist/cli/plugins/index.js +12 -14
  12. package/dist/cli/plugins/tempo.js +9 -2
  13. package/dist/cli/utils.d.ts +11 -1
  14. package/dist/cli/utils.js +15 -2
  15. package/dist/cli/validate/challenge.d.ts +2 -0
  16. package/dist/cli/validate/challenge.js +19 -4
  17. package/dist/cli/validate/discovery.d.ts +1 -0
  18. package/dist/cli/validate/discovery.js +10 -1
  19. package/dist/cli/validate/payment.js +63 -24
  20. package/dist/client/Methods.d.ts +0 -2
  21. package/dist/client/Methods.js +0 -2
  22. package/dist/client/Mppx.js +1 -0
  23. package/dist/client/index.d.ts +1 -1
  24. package/dist/client/index.js +1 -1
  25. package/dist/client/internal/Fetch.d.ts +2 -0
  26. package/dist/client/internal/Fetch.js +8 -2
  27. package/dist/client/internal/MethodChallenge.d.ts +1 -0
  28. package/dist/evm/Types.js +1 -1
  29. package/dist/evm/server/Charge.js +5 -0
  30. package/dist/evm/server/Methods.d.ts +164 -3
  31. package/dist/evm/server/Methods.js +50 -2
  32. package/dist/integrations/x402/Evm.js +4 -3
  33. package/dist/internal/HeaderCodec.d.ts +2 -2
  34. package/dist/internal/version.d.ts +2 -2
  35. package/dist/internal/version.js +1 -1
  36. package/dist/mcp/client/McpClient.js +4 -7
  37. package/dist/middlewares/elysia.d.ts +11 -8
  38. package/dist/middlewares/elysia.js +67 -14
  39. package/dist/middlewares/express.js +13 -6
  40. package/dist/middlewares/hono.js +4 -4
  41. package/dist/middlewares/nextjs.js +9 -5
  42. package/dist/proxy/internal/Headers.js +65 -0
  43. package/dist/proxy/services/stripe.js +1 -0
  44. package/dist/server/Mppx.d.ts +18 -2
  45. package/dist/server/Mppx.js +204 -92
  46. package/dist/server/NodeListener.js +71 -6
  47. package/dist/server/Request.d.ts +11 -1
  48. package/dist/server/Request.js +132 -13
  49. package/dist/server/Transport.d.ts +14 -1
  50. package/dist/server/Transport.js +1 -0
  51. package/dist/server/internal/html/compose.main.gen.d.ts +1 -1
  52. package/dist/server/internal/html/serviceWorker.gen.d.ts +1 -1
  53. package/dist/stripe/server/Methods.d.ts +10 -3
  54. package/dist/stripe/server/Methods.js +19 -6
  55. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  56. package/dist/stripe/server/internal/html.gen.js +1 -1
  57. package/dist/tempo/Methods.d.ts +1 -0
  58. package/dist/tempo/Methods.js +1 -0
  59. package/dist/tempo/client/Charge.js +17 -13
  60. package/dist/tempo/client/Methods.d.ts +1 -5
  61. package/dist/tempo/client/Methods.js +10 -5
  62. package/dist/tempo/client/ResolveAccount.d.ts +3 -0
  63. package/dist/tempo/client/index.d.ts +0 -3
  64. package/dist/tempo/client/index.js +0 -2
  65. package/dist/tempo/internal/currencies.d.ts +19 -0
  66. package/dist/tempo/internal/currencies.js +41 -0
  67. package/dist/tempo/internal/defaults.d.ts +5 -3
  68. package/dist/tempo/internal/defaults.js +6 -2
  69. package/dist/tempo/internal/fee-payer.d.ts +11 -7
  70. package/dist/tempo/internal/fee-payer.js +30 -11
  71. package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
  72. package/dist/tempo/internal/machine-token-charge.js +2 -1
  73. package/dist/tempo/legacy/index.d.ts +1 -3
  74. package/dist/tempo/legacy/index.js +1 -3
  75. package/dist/tempo/server/Charge.d.ts +12 -1
  76. package/dist/tempo/server/Charge.js +109 -78
  77. package/dist/tempo/server/Methods.d.ts +5048 -40
  78. package/dist/tempo/server/Methods.js +104 -13
  79. package/dist/tempo/server/Subscription.d.ts +1 -1
  80. package/dist/tempo/server/Subscription.js +31 -4
  81. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  82. package/dist/tempo/server/internal/html.gen.js +1 -1
  83. package/dist/tempo/server/internal/transport.js +28 -11
  84. package/dist/tempo/session/client/ChannelOps.js +5 -0
  85. package/dist/tempo/session/client/Session.d.ts +1 -0
  86. package/dist/tempo/session/client/Session.js +7 -4
  87. package/dist/tempo/session/client/SessionManager.js +3 -0
  88. package/dist/tempo/session/client/Transports.d.ts +8 -1
  89. package/dist/tempo/session/client/Transports.js +12 -3
  90. package/dist/tempo/session/precompile/Chain.js +16 -1
  91. package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
  92. package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
  93. package/dist/tempo/session/precompile/Voucher.js +48 -0
  94. package/dist/tempo/session/server/ChannelStore.d.ts +22 -2
  95. package/dist/tempo/session/server/ChannelStore.js +37 -7
  96. package/dist/tempo/session/server/CredentialVerification.d.ts +6 -1
  97. package/dist/tempo/session/server/CredentialVerification.js +56 -19
  98. package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
  99. package/dist/tempo/session/server/MeteredStream.js +85 -29
  100. package/dist/tempo/session/server/RequestState.d.ts +6 -0
  101. package/dist/tempo/session/server/RequestState.js +11 -4
  102. package/dist/tempo/session/server/Session.d.ts +2 -1
  103. package/dist/tempo/session/server/Session.js +10 -2
  104. package/dist/tempo/session/server/Settlement.d.ts +23 -0
  105. package/dist/tempo/session/server/Settlement.js +105 -25
  106. package/dist/tempo/session/server/Sse.d.ts +3 -1
  107. package/dist/tempo/session/server/Sse.js +105 -32
  108. package/dist/tempo/session/server/Transports.d.ts +24 -19
  109. package/dist/tempo/session/server/Transports.js +169 -58
  110. package/dist/tempo/session/server/Ws.d.ts +5 -1
  111. package/dist/tempo/session/server/Ws.js +75 -20
  112. package/dist/tempo/subscription/KeyAuthorization.d.ts +70 -8
  113. package/dist/tempo/subscription/Store.d.ts +2 -0
  114. package/dist/tempo/subscription/Store.js +3 -0
  115. package/dist/validation/core.js +2 -1
  116. package/dist/x402/server/EvmCharge.d.ts +3 -1
  117. package/dist/x402/server/EvmCharge.js +14 -1
  118. package/package.json +8 -8
  119. package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
  120. package/dist/tempo/legacy/client/ChannelOps.js +0 -168
  121. package/dist/tempo/legacy/client/Session.d.ts +0 -185
  122. package/dist/tempo/legacy/client/Session.js +0 -279
  123. package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
  124. package/dist/tempo/legacy/client/SessionManager.js +0 -706
  125. package/dist/tempo/legacy/client/index.d.ts +0 -7
  126. package/dist/tempo/legacy/client/index.js +0 -5
@@ -12,7 +12,7 @@ import * as Constants from '../../Constants.js';
12
12
  import * as Credential from '../../Credential.js';
13
13
  import { tempo as tempoMethods } from '../../tempo/client/index.js';
14
14
  import { tip20ChannelEscrow } from '../../tempo/session/precompile/Protocol.js';
15
- import { signVoucher } from '../../tempo/session/precompile/Voucher.js';
15
+ import { signCloseAuthorization, signVoucher } from '../../tempo/session/precompile/Voucher.js';
16
16
  import { createDefaultStore, createKeychain, resolveAccountName } from '../account.js';
17
17
  import { fetchTokenInfo, fmtBalance, isTempoAccount, isTestnet, link, pc, resolveChain, resolveRpcUrl, } from '../utils.js';
18
18
  import { createPlugin } from './plugin.js';
@@ -227,14 +227,21 @@ export function tempo() {
227
227
  const descriptor = _session?.descriptor;
228
228
  if (!descriptor)
229
229
  throw new Error('session descriptor not available');
230
+ const voucher = { channelId: channelId, cumulativeAmount };
231
+ const signature = await signVoucher(client, account, voucher, escrowContract, chainId);
230
232
  return Credential.serialize({
231
233
  challenge,
232
234
  payload: {
233
235
  action,
234
236
  channelId,
237
+ ...(action === 'close'
238
+ ? {
239
+ closeSignature: await signCloseAuthorization(client, account, voucher, escrowContract, chainId),
240
+ }
241
+ : {}),
235
242
  descriptor,
236
243
  cumulativeAmount: cumulativeAmount.toString(),
237
- signature: await signVoucher(client, account, { channelId: channelId, cumulativeAmount }, escrowContract, chainId),
244
+ signature,
238
245
  },
239
246
  source: `did:pkh:eip155:${chainId}:${account.address}`,
240
247
  });
@@ -96,7 +96,17 @@ export declare const usdc: Address;
96
96
  export declare const mainnetTokens: readonly [`0x${string}`, `0x${string}`];
97
97
  export declare const testnetTokens: readonly ['0x20c0000000000000000000000000000000000000', '0x20c0000000000000000000000000000000000001', '0x20c0000000000000000000000000000000000002', '0x20c0000000000000000000000000000000000003'];
98
98
  export declare function isTestnet(chain: Chain): boolean;
99
- export declare function fetchTokenInfo(client: ReturnType<typeof createClient>, token: Address, account: Address): Promise<{
99
+ /**
100
+ * Fetches a Tempo token balance and display metadata.
101
+ *
102
+ * By default, unavailable decimals fall back to six for display compatibility.
103
+ * Set `requireDecimals` to throw instead when trusted on-chain decimals cannot
104
+ * be resolved.
105
+ */
106
+ export declare function fetchTokenInfo(client: ReturnType<typeof createClient>, token: Address, account: Address, options?: {
107
+ /** Throws instead of using six decimals when token metadata is unavailable. */
108
+ requireDecimals?: boolean;
109
+ }): Promise<{
100
110
  balance: bigint;
101
111
  decimals: number;
102
112
  symbol: string;
package/dist/cli/utils.js CHANGED
@@ -259,14 +259,25 @@ function toBaseUnitAmount(value) {
259
259
  export function isTestnet(chain) {
260
260
  return chain.id !== tempoMainnet.id;
261
261
  }
262
- export async function fetchTokenInfo(client, token, account) {
262
+ /**
263
+ * Fetches a Tempo token balance and display metadata.
264
+ *
265
+ * By default, unavailable decimals fall back to six for display compatibility.
266
+ * Set `requireDecimals` to throw instead when trusted on-chain decimals cannot
267
+ * be resolved.
268
+ */
269
+ export async function fetchTokenInfo(client, token, account, options = {}) {
263
270
  const { Actions } = await import('viem/tempo');
264
271
  const [balance, metadata] = await Promise.all([
265
272
  Actions.token
266
273
  .getBalance(client, { account, token })
267
274
  .then(toBaseUnitAmount)
268
275
  .catch(() => 0n),
269
- Actions.token.getMetadata(client, { token }).catch(() => ({ symbol: token })),
276
+ Actions.token.getMetadata(client, { token }).catch((error) => {
277
+ if (options.requireDecimals)
278
+ throw error;
279
+ return { symbol: token };
280
+ }),
270
281
  ]);
271
282
  const knownSymbols = {
272
283
  [pathUsd]: 'PathUSD',
@@ -274,6 +285,8 @@ export async function fetchTokenInfo(client, token, account) {
274
285
  };
275
286
  const symbol = knownSymbols[token] ?? metadata.symbol;
276
287
  const decimals = 'decimals' in metadata && typeof metadata.decimals === 'number' ? metadata.decimals : 6;
288
+ if (options.requireDecimals && !('decimals' in metadata && typeof metadata.decimals === 'number'))
289
+ throw new Error('Token decimals are unavailable.');
277
290
  return { balance, symbol, decimals, token };
278
291
  }
279
292
  export async function fetchBalanceLines(address, opts) {
@@ -11,6 +11,8 @@ export declare function validateChallenge(baseUrl: string, endpoint: EndpointSpe
11
11
  resolvedBody?: string | undefined;
12
12
  challenges?: Challenge.Challenge[] | undefined;
13
13
  }>;
14
+ /** Validates that challenge realms cover the server hostname. */
15
+ export declare function validateRealmMatchesHost(challenges: Challenge.Challenge[], serverHost: string, results: CheckResult[]): void;
14
16
  export declare function validateErrorHandling(baseUrl: string, endpoint: EndpointSpec, options?: {
15
17
  body?: string | undefined;
16
18
  query?: string[] | undefined;
@@ -15,7 +15,14 @@ function detectTestnet(challenge) {
15
15
  }
16
16
  export async function validateChallenge(baseUrl, endpoint, verbose, options) {
17
17
  const results = [];
18
- const url = buildUrl(baseUrl, endpoint, options?.query);
18
+ let url;
19
+ try {
20
+ url = buildUrl(baseUrl, endpoint, options?.query);
21
+ }
22
+ catch (error) {
23
+ results.push(fail('Request URL valid', error.message));
24
+ return { results };
25
+ }
19
26
  const fetchHeaders = parseHeaders(options?.extraHeaders);
20
27
  let fetchBody;
21
28
  // If the user explicitly provided a body, include it on the first request
@@ -167,11 +174,19 @@ function validateExpiration(challenges, results) {
167
174
  const soonest = Math.min(...challenges.map((ch) => new Date(ch.expires).getTime() - now.getTime()));
168
175
  results.push(check('Challenge expires in the future', `${Math.round(soonest / 60000)}m from now`));
169
176
  }
170
- function validateRealmMatchesHost(challenges, serverHost, results) {
177
+ /** Validates that challenge realms cover the server hostname. */
178
+ export function validateRealmMatchesHost(challenges, serverHost, results) {
179
+ const normalizeHostname = (value) => value.toLowerCase().replace(/\.$/, '');
180
+ const normalizedServerHost = normalizeHostname(serverHost);
171
181
  const realms = [...new Set(challenges.map((ch) => ch.realm ?? ''))];
172
- const badRealms = realms.filter((r) => r && r !== serverHost && !serverHost.endsWith(`.${r}`));
182
+ const badRealms = realms.filter((realm) => {
183
+ const normalizedRealm = normalizeHostname(realm);
184
+ return (!normalizedRealm ||
185
+ (normalizedRealm !== normalizedServerHost &&
186
+ !normalizedServerHost.endsWith(`.${normalizedRealm}`)));
187
+ });
173
188
  if (badRealms.length > 0) {
174
- results.push(warn('Realm matches server hostname', `realm="${badRealms[0]}" vs host="${serverHost}"`, 'Set the realm to your production hostname (or base domain) in the challenge.'));
189
+ results.push(fail('Realm matches server hostname', `realm="${badRealms[0]}" vs host="${serverHost}"`, 'Set the realm to your production hostname (or base domain) in the challenge.'));
175
190
  }
176
191
  else {
177
192
  results.push(check('Realm matches server hostname'));
@@ -9,5 +9,6 @@ export declare function fetchDiscoveryDoc(baseUrl: string): Promise<{
9
9
  export declare function validateLlmsDoc(baseUrl: string): Promise<CheckResult>;
10
10
  export declare function extractEndpointsFromDiscovery(doc: Record<string, unknown>): EndpointSpec[];
11
11
  export declare function extractRequestBodyFromDiscovery(doc: Record<string, unknown>, endpoint: EndpointSpec): string | undefined;
12
+ /** Builds an endpoint URL and rejects paths that could escape the discovery document's origin. */
12
13
  export declare function buildUrl(baseUrl: string, endpoint: EndpointSpec, query?: string[]): string;
13
14
  //# sourceMappingURL=discovery.d.ts.map
@@ -117,14 +117,23 @@ export function extractRequestBodyFromDiscovery(doc, endpoint) {
117
117
  }
118
118
  return undefined;
119
119
  }
120
+ /** Builds an endpoint URL and rejects paths that could escape the discovery document's origin. */
120
121
  export function buildUrl(baseUrl, endpoint, query) {
122
+ if (!endpoint.path.startsWith('/') ||
123
+ endpoint.path.startsWith('//') ||
124
+ endpoint.path.includes('\\'))
125
+ throw new Error(`OpenAPI path must be an origin-relative path: ${endpoint.path}`);
121
126
  let path = endpoint.path;
122
127
  if (endpoint.parameters) {
123
128
  path = substitutePathParams(path, endpoint.parameters);
124
129
  }
125
130
  // Strip leading slash so URL resolves relative to baseUrl's path, not root.
126
131
  const relativePath = path.startsWith('/') ? path.slice(1) : path;
127
- let url = new URL(relativePath, baseUrl.replace(/\/?$/, '/')).href;
132
+ const base = new URL(baseUrl.replace(/\/?$/, '/'));
133
+ const resolved = new URL(relativePath, base);
134
+ if (resolved.origin !== base.origin)
135
+ throw new Error(`OpenAPI path must be an origin-relative path: ${endpoint.path}`);
136
+ let url = resolved.href;
128
137
  if (query) {
129
138
  const u = new URL(url);
130
139
  for (const q of query) {
@@ -1,5 +1,5 @@
1
1
  import * as childProcess from 'node:child_process';
2
- import { createClient, erc20Abi, http } from 'viem';
2
+ import { createClient, erc20Abi, http, isAddress, zeroAddress, } from 'viem';
3
3
  import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
4
4
  import { readContract, waitForTransactionReceipt } from 'viem/actions';
5
5
  import * as viemChains from 'viem/chains';
@@ -15,6 +15,7 @@ import { isTempoSessionChallenge } from '../../tempo/session/client/Transports.j
15
15
  import { resolveAccount, resolveAccountName } from '../account.js';
16
16
  import { assertSamePaymentRequest, flattenConfigMethods, loadConfig, preparePayment, resolvePlugin, } from '../internal.js';
17
17
  import { fetchTokenInfo, confirm, pc } from '../utils.js';
18
+ import { validateRealmMatchesHost } from './challenge.js';
18
19
  import { buildUrl } from './discovery.js';
19
20
  import { check, fail, fetchWithTimeout, formatBytes, isValidIntegerAmount, parseHeaders, skip, warn, } from './helpers.js';
20
21
  async function provisionAndPayTestnet(challenge, verbose, silent) {
@@ -24,6 +25,16 @@ async function provisionAndPayTestnet(challenge, verbose, silent) {
24
25
  const key = generatePrivateKey();
25
26
  const account = privateKeyToAccount(key);
26
27
  const client = createClient({ chain: tempoModerato, transport: http() });
28
+ if (isTempoSessionChallenge(challenge)) {
29
+ const { chainId, operator } = challenge.request.methodDetails ?? {};
30
+ if (chainId === tempoModerato.id &&
31
+ typeof operator === 'string' &&
32
+ isAddress(operator) &&
33
+ operator !== zeroAddress) {
34
+ const hashes = await Actions.faucet.fund(client, { account: operator });
35
+ await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })));
36
+ }
37
+ }
27
38
  const hashes = await Actions.faucet.fund(client, { account });
28
39
  await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })));
29
40
  if (!silent)
@@ -85,16 +96,19 @@ function resolveStripeKey(verbose) {
85
96
  }
86
97
  async function fetchEvmTokenInfo(chain, token, account) {
87
98
  const client = createClient({ chain, transport: http() });
88
- const [balance, symbol] = await Promise.all([
89
- readContract(client, {
90
- address: token,
91
- abi: erc20Abi,
92
- functionName: 'balanceOf',
93
- args: [account],
94
- }),
99
+ const [balance, decimals, symbol] = await Promise.all([
100
+ account
101
+ ? readContract(client, {
102
+ address: token,
103
+ abi: erc20Abi,
104
+ functionName: 'balanceOf',
105
+ args: [account],
106
+ })
107
+ : 0n,
108
+ readContract(client, { address: token, abi: erc20Abi, functionName: 'decimals' }),
95
109
  readContract(client, { address: token, abi: erc20Abi, functionName: 'symbol' }).catch(() => undefined),
96
110
  ]);
97
- return { balance, symbol: symbol ?? undefined };
111
+ return { balance, decimals, symbol: symbol ?? undefined };
98
112
  }
99
113
  function resolveEvmChain(chainId) {
100
114
  const all = Object.values(viemChains);
@@ -152,6 +166,14 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
152
166
  results.push(fail('Payment: parse challenge', 'No Payment challenges in response'));
153
167
  return results;
154
168
  }
169
+ const realmResults = [];
170
+ validateRealmMatchesHost(challenges, new URL(url).hostname, realmResults);
171
+ const realmFailures = realmResults.filter((result) => result.severity === 'fail');
172
+ if (realmFailures.length > 0) {
173
+ results.push(...realmFailures);
174
+ options.onResults?.(realmFailures);
175
+ return results;
176
+ }
155
177
  const loaded = await loadConfig().catch(() => undefined);
156
178
  // Testnet Tempo: always use ephemeral wallet (zero-setup, free money)
157
179
  const tempoTestnetChallenge = challenges.find((ch) => {
@@ -166,18 +188,25 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
166
188
  if (tempoTestnetChallenge) {
167
189
  const provisioned = await provisionAndPayTestnet(tempoTestnetChallenge, verbose, options.silent);
168
190
  if (provisioned) {
169
- const fakeResp = new Response(null, {
170
- status: 402,
171
- headers: {
172
- [Constants.Headers.wwwAuthenticate]: Challenge.serialize(tempoTestnetChallenge),
173
- },
174
- });
175
191
  try {
176
- const mppx = Mppx.create({ methods: provisioned.methods, polyfill: false });
177
- const credentialContext = await preparePayment(tempoTestnetChallenge, loaded?.config.extensions);
178
- const cred = await mppx.createCredential(fakeResp, credentialContext);
179
- results.push(check('Payment: submitted', 'ephemeral testnet wallet'));
180
- await sendAndValidateResponse(results, url, endpoint, cred, fetchHeaders, fetchBody, verbose, tempoModerato, Challenge.credentialHeader(tempoTestnetChallenge));
192
+ const mppx = Mppx.create({
193
+ methods: provisioned.methods,
194
+ polyfill: false,
195
+ async onChallenge(challenge, { createCredential }) {
196
+ assertSamePaymentRequest(tempoTestnetChallenge, challenge);
197
+ const context = await preparePayment(challenge, loaded?.config.extensions);
198
+ const credential = await createCredential(context);
199
+ results.push(check('Payment: submitted', 'ephemeral testnet wallet'));
200
+ return credential;
201
+ },
202
+ fetch: (input, init) => fetchWithTimeout(input, init ?? {}, 30_000),
203
+ });
204
+ const response = await mppx.fetch(url, {
205
+ method: endpoint.method,
206
+ headers: fetchHeaders,
207
+ body: fetchBody ?? null,
208
+ });
209
+ await validatePaymentResponse(results, response, verbose, tempoModerato);
181
210
  }
182
211
  catch (error) {
183
212
  results.push(fail('Payment: create credential', error.message));
@@ -257,7 +286,7 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
257
286
  const requiredAmount = isValidIntegerAmount(request.amount)
258
287
  ? BigInt(request.amount)
259
288
  : undefined;
260
- const decimals = methodDetails?.decimals ?? request.decimals ?? 6;
289
+ let decimals = methodDetails?.decimals ?? request.decimals ?? 6;
261
290
  const currency = request.currency;
262
291
  const { plugin, method: directMethod } = resolvePlugin(challenge, loaded?.config);
263
292
  if (!plugin && !directMethod) {
@@ -289,22 +318,30 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
289
318
  if (chainId)
290
319
  paymentChain = resolveEvmChain(chainId);
291
320
  }
321
+ if (requiredAmount && currency && !paymentChain) {
322
+ results.push(skip(tag, 'unable to resolve payment chain and verify token decimals'));
323
+ return;
324
+ }
292
325
  let tokenSymbol;
293
- if (walletAddress && requiredAmount && currency && paymentChain) {
326
+ if (requiredAmount && currency && paymentChain) {
294
327
  try {
295
328
  let balance;
296
329
  if (challenge.method === Constants.Methods.tempo) {
297
330
  const client = createClient({ chain: paymentChain, transport: http() });
298
- const info = await fetchTokenInfo(client, currency, walletAddress);
331
+ const info = await fetchTokenInfo(client, currency, (walletAddress ?? zeroAddress), {
332
+ requireDecimals: true,
333
+ });
299
334
  balance = info.balance;
335
+ decimals = info.decimals;
300
336
  tokenSymbol = info.symbol;
301
337
  }
302
338
  else {
303
339
  const info = await fetchEvmTokenInfo(paymentChain, currency, walletAddress);
304
340
  balance = info.balance;
341
+ decimals = info.decimals;
305
342
  tokenSymbol = info.symbol;
306
343
  }
307
- if (balance < requiredAmount) {
344
+ if (walletAddress && balance < requiredAmount) {
308
345
  const requiredDisplay = formatAmount(requiredAmount, decimals);
309
346
  const balanceDisplay = formatAmount(balance, decimals);
310
347
  const symbol = tokenSymbol ?? 'tokens';
@@ -315,6 +352,8 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
315
352
  catch (e) {
316
353
  if (verbose)
317
354
  console.log(pc.dim(` Balance check skipped: ${e.message}`));
355
+ results.push(skip(tag, 'unable to verify token balance and decimals from the payment chain'));
356
+ return;
318
357
  }
319
358
  }
320
359
  // Prompt
@@ -4,6 +4,4 @@ export { subscription } from '../tempo/client/Subscription.js';
4
4
  export { session, tempo } from '../tempo/client/index.js';
5
5
  export { session as sessionMethod } from '../tempo/session/client/Session.js';
6
6
  export { sessionManager } from '../tempo/session/client/SessionManager.js';
7
- export { session as sessionLegacy } from '../tempo/legacy/client/index.js';
8
- export { sessionManager as sessionLegacyManager } from '../tempo/legacy/client/index.js';
9
7
  //# sourceMappingURL=Methods.d.ts.map
@@ -4,6 +4,4 @@ export { subscription } from '../tempo/client/Subscription.js';
4
4
  export { session, tempo } from '../tempo/client/index.js';
5
5
  export { session as sessionMethod } from '../tempo/session/client/Session.js';
6
6
  export { sessionManager } from '../tempo/session/client/SessionManager.js';
7
- export { session as sessionLegacy } from '../tempo/legacy/client/index.js';
8
- export { sessionManager as sessionLegacyManager } from '../tempo/legacy/client/index.js';
9
7
  //# sourceMappingURL=Methods.js.map
@@ -181,6 +181,7 @@ export function create(config) {
181
181
  context,
182
182
  fetch: attestedFetch,
183
183
  input: preparedHttp.replayRequest,
184
+ signal: preparedHttp.replayRequest.signal,
184
185
  });
185
186
  return payment.createCredential(context);
186
187
  });
@@ -1,7 +1,7 @@
1
1
  export * as Constants from '../Constants.js';
2
2
  export * as Expires from '../Expires.js';
3
3
  export * as Fetch from './internal/Fetch.js';
4
- export { evm, session, sessionManager, sessionMethod, sessionLegacy, sessionLegacyManager, stripe, tempo, } from './Methods.js';
4
+ export { evm, session, sessionManager, sessionMethod, stripe, tempo } from './Methods.js';
5
5
  export { createChannelStore, createJsonChannelStore, entryKey, type ChannelStore, type JsonChannelKv, } from '../tempo/session/client/ChannelStore.js';
6
6
  export type { ChargeContext } from '../tempo/client/Charge.js';
7
7
  export type { ResolveAccount, ResolveAccountCall, ResolveAccountInfo, ResolveAccountOperation, } from '../tempo/client/ResolveAccount.js';
@@ -1,7 +1,7 @@
1
1
  export * as Constants from '../Constants.js';
2
2
  export * as Expires from '../Expires.js';
3
3
  export * as Fetch from './internal/Fetch.js';
4
- export { evm, session, sessionManager, sessionMethod, sessionLegacy, sessionLegacyManager, stripe, tempo, } from './Methods.js';
4
+ export { evm, session, sessionManager, sessionMethod, stripe, tempo } from './Methods.js';
5
5
  export { createChannelStore, createJsonChannelStore, entryKey, } from '../tempo/session/client/ChannelStore.js';
6
6
  export * as Mppx from './Mppx.js';
7
7
  export * as Transport from './Transport.js';
@@ -171,6 +171,8 @@ export declare function createEventDispatcher<methods extends readonly Method.An
171
171
  /** @internal */
172
172
  export declare function unwrapFetch(fetch: typeof globalThis.fetch): typeof globalThis.fetch;
173
173
  /** @internal */
174
+ export declare function resolveRequestSignal(input: RequestInfo | URL, init: RequestInit | undefined): AbortSignal | undefined;
175
+ /** @internal */
174
176
  export declare function validateCredentialHeaderValue(credential: string): void;
175
177
  /** @internal */
176
178
  /** @internal */
@@ -122,6 +122,7 @@ export function from(config) {
122
122
  context: credentialContext,
123
123
  fetch: baseFetch,
124
124
  input: paymentInput,
125
+ signal: resolveRequestSignal(input, init),
125
126
  }),
126
127
  };
127
128
  const promise = attempt
@@ -576,8 +577,13 @@ function cloneRequestInput(input) {
576
577
  return input;
577
578
  }
578
579
  }
579
- function resolveRequestSignal(input, init) {
580
- return init?.signal ?? (input instanceof Request ? input.signal : undefined);
580
+ /** @internal */
581
+ export function resolveRequestSignal(input, init) {
582
+ if (init?.signal === null)
583
+ return undefined;
584
+ if (init?.signal !== undefined)
585
+ return init.signal;
586
+ return input instanceof Request ? input.signal : undefined;
581
587
  }
582
588
  /** @internal */
583
589
  function isWrappedFetch(fetch) {
@@ -7,6 +7,7 @@ export type HandlerParameters = {
7
7
  context?: unknown;
8
8
  fetch: typeof globalThis.fetch;
9
9
  input: RequestInfo | URL;
10
+ signal?: AbortSignal | undefined;
10
11
  };
11
12
  /** Internal client-method challenge hook. */
12
13
  export type Handler = (parameters: HandlerParameters) => MaybePromise<void>;
package/dist/evm/Types.js CHANGED
@@ -87,7 +87,7 @@ export function authorizationDomain(parameters) {
87
87
  }
88
88
  /** Computes the Payment-auth EVM challenge hash used as the EIP-3009 nonce. */
89
89
  export function challengeHash(challenge) {
90
- return Hash.keccak256(Bytes.fromString(`${challenge.id}${challenge.realm}`), {
90
+ return Hash.keccak256(Bytes.fromString(JSON.stringify([challenge.id, challenge.realm])), {
91
91
  as: 'Hex',
92
92
  });
93
93
  }
@@ -171,6 +171,7 @@ function createPaths(config) {
171
171
  function createMppPath() {
172
172
  const transport = ServerTransport.http();
173
173
  return {
174
+ credentialHeaders: transport.credentialHeaders ?? [],
174
175
  bindCredential: (options) => transport.bindCredential?.(options) ?? options.credential,
175
176
  captureRequest: transport.captureRequest,
176
177
  getCredential: transport.getCredential,
@@ -181,10 +182,14 @@ function createMppPath() {
181
182
  function httpTransport(paths) {
182
183
  return ServerTransport.from({
183
184
  name: 'evm-http',
185
+ credentialHeaders: [
186
+ ...new Set([...paths.mpp.credentialHeaders, ...paths.x402.credentialHeaders]),
187
+ ],
184
188
  captureRequest: paths.mpp.captureRequest,
185
189
  getCredential(input) {
186
190
  return paths.mpp.getCredential(input) ?? paths.x402.getCredential(input);
187
191
  },
192
+ matchCredential: paths.x402.matchCredential,
188
193
  bindCredential(options) {
189
194
  if (X402.isPendingCredential(options.credential))
190
195
  return paths.x402.bindCredential(options);
@@ -2,8 +2,158 @@ import type { NoExtraKeys } from '../../internal/types.js';
2
2
  import * as Assets from '../Assets.js';
3
3
  import * as Chains from '../Chains.js';
4
4
  import { charge as charge_ } from './Charge.js';
5
- /** Creates EVM server methods from shared charge parameters. */
6
- export declare function evm<const parameters extends evm.Parameters>(parameters: NoExtraKeys<parameters, evm.Parameters>): readonly [import("../../Method.js").Server<{
5
+ /**
6
+ * Creates ordered EVM charge offers from shared parameters.
7
+ *
8
+ * Pass `currencies: [token]` for one asset or an ordered list for multiple assets.
9
+ * Viem token definitions resolve against `chainId`; known assets carry their own
10
+ * network. Unsupported chain entries are skipped and duplicate chain/address
11
+ * pairs are removed. An empty resolved list throws.
12
+ *
13
+ * Token definitions need explicit EIP-3009 domain metadata. Use
14
+ * `evm.assets.fromToken(token, { chainId, transfer })` for per-token domains.
15
+ * A token registry entry alone does not establish EIP-3009 support.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { evm } from 'mppx/server'
20
+ *
21
+ * evm({
22
+ * currencies: [evm.assets.base.USDC],
23
+ * recipient: '0x742d35Cc6634c0532925a3b844bC9e7595F8fE00',
24
+ * settle: async ({ payload }) => settleAuthorization(payload),
25
+ * })
26
+ * ```
27
+ */
28
+ export declare function evm<const parameters extends evm.Parameters>(parameters: NoExtraKeys<parameters, evm.Parameters>): [import("../../Method.js").Server<{
29
+ readonly name: "evm";
30
+ readonly intent: "charge";
31
+ readonly schema: {
32
+ credential: {
33
+ payload: import("zod/mini").ZodMiniObject<{
34
+ from: import("zod/mini").ZodMiniString<string>;
35
+ nonce: import("zod/mini").ZodMiniString<string>;
36
+ signature: import("zod/mini").ZodMiniString<string>;
37
+ to: import("zod/mini").ZodMiniString<string>;
38
+ type: import("zod/mini").ZodMiniLiteral<"authorization">;
39
+ validAfter: import("zod/mini").ZodMiniString<string>;
40
+ validBefore: import("zod/mini").ZodMiniString<string>;
41
+ value: import("zod/mini").ZodMiniString<string>;
42
+ }, import("zod/v4/core").$strip>;
43
+ };
44
+ request: import("zod/mini").ZodMiniPipe<import("zod/mini").ZodMiniObject<{
45
+ amount: import("zod/mini").ZodMiniString<string>;
46
+ chainId: import("zod/mini").ZodMiniNumber<number>;
47
+ currency: import("zod/mini").ZodMiniString<string>;
48
+ credentialTypes: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniEnum<{
49
+ authorization: "authorization";
50
+ }>>>;
51
+ decimals: import("zod/mini").ZodMiniNumber<number>;
52
+ description: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
53
+ externalId: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
54
+ permit2Address: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
55
+ recipient: import("zod/mini").ZodMiniString<string>;
56
+ splits: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniObject<{
57
+ amount: import("zod/mini").ZodMiniString<string>;
58
+ recipient: import("zod/mini").ZodMiniString<string>;
59
+ }, import("zod/v4/core").$strip>>>;
60
+ }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniTransform<{
61
+ description?: string | undefined;
62
+ externalId?: string | undefined;
63
+ amount: string;
64
+ currency: `0x${string}`;
65
+ methodDetails: {
66
+ chainId: number;
67
+ credentialTypes: string[];
68
+ decimals: number;
69
+ permit2Address?: `0x${string}` | undefined;
70
+ splits?: {
71
+ amount: string;
72
+ recipient: string;
73
+ }[] | undefined;
74
+ };
75
+ recipient: `0x${string}`;
76
+ }, {
77
+ amount: string;
78
+ chainId: number;
79
+ currency: string;
80
+ credentialTypes?: "authorization"[] | undefined;
81
+ decimals: number;
82
+ description?: string | undefined;
83
+ externalId?: string | undefined;
84
+ permit2Address?: string | undefined;
85
+ recipient: string;
86
+ splits?: {
87
+ amount: string;
88
+ recipient: string;
89
+ }[] | undefined;
90
+ }>>;
91
+ };
92
+ }, charge_.Defaults, import("../../server/Transport.js").Http, {}, string | undefined>, ...import("../../Method.js").Server<{
93
+ readonly name: "evm";
94
+ readonly intent: "charge";
95
+ readonly schema: {
96
+ credential: {
97
+ payload: import("zod/mini").ZodMiniObject<{
98
+ from: import("zod/mini").ZodMiniString<string>;
99
+ nonce: import("zod/mini").ZodMiniString<string>;
100
+ signature: import("zod/mini").ZodMiniString<string>;
101
+ to: import("zod/mini").ZodMiniString<string>;
102
+ type: import("zod/mini").ZodMiniLiteral<"authorization">;
103
+ validAfter: import("zod/mini").ZodMiniString<string>;
104
+ validBefore: import("zod/mini").ZodMiniString<string>;
105
+ value: import("zod/mini").ZodMiniString<string>;
106
+ }, import("zod/v4/core").$strip>;
107
+ };
108
+ request: import("zod/mini").ZodMiniPipe<import("zod/mini").ZodMiniObject<{
109
+ amount: import("zod/mini").ZodMiniString<string>;
110
+ chainId: import("zod/mini").ZodMiniNumber<number>;
111
+ currency: import("zod/mini").ZodMiniString<string>;
112
+ credentialTypes: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniEnum<{
113
+ authorization: "authorization";
114
+ }>>>;
115
+ decimals: import("zod/mini").ZodMiniNumber<number>;
116
+ description: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
117
+ externalId: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
118
+ permit2Address: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
119
+ recipient: import("zod/mini").ZodMiniString<string>;
120
+ splits: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniObject<{
121
+ amount: import("zod/mini").ZodMiniString<string>;
122
+ recipient: import("zod/mini").ZodMiniString<string>;
123
+ }, import("zod/v4/core").$strip>>>;
124
+ }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniTransform<{
125
+ description?: string | undefined;
126
+ externalId?: string | undefined;
127
+ amount: string;
128
+ currency: `0x${string}`;
129
+ methodDetails: {
130
+ chainId: number;
131
+ credentialTypes: string[];
132
+ decimals: number;
133
+ permit2Address?: `0x${string}` | undefined;
134
+ splits?: {
135
+ amount: string;
136
+ recipient: string;
137
+ }[] | undefined;
138
+ };
139
+ recipient: `0x${string}`;
140
+ }, {
141
+ amount: string;
142
+ chainId: number;
143
+ currency: string;
144
+ credentialTypes?: "authorization"[] | undefined;
145
+ decimals: number;
146
+ description?: string | undefined;
147
+ externalId?: string | undefined;
148
+ permit2Address?: string | undefined;
149
+ recipient: string;
150
+ splits?: {
151
+ amount: string;
152
+ recipient: string;
153
+ }[] | undefined;
154
+ }>>;
155
+ };
156
+ }, charge_.Defaults, import("../../server/Transport.js").Http, {}, string | undefined>[]] | readonly [import("../../Method.js").Server<{
7
157
  readonly name: "evm";
8
158
  readonly intent: "charge";
9
159
  readonly schema: {
@@ -69,7 +219,18 @@ export declare function evm<const parameters extends evm.Parameters>(parameters:
69
219
  };
70
220
  }, charge_.Defaults, import("../../server/Transport.js").Http, {}, string | undefined>];
71
221
  export declare namespace evm {
72
- type Parameters = charge_.Parameters;
222
+ /** Accepted EVM currency metadata, including chain-indexed definitions from `viem/tokens`. */
223
+ type Currency = Assets.Currency;
224
+ /** Shared EVM settlement parameters and ordered accepted currencies. */
225
+ type Parameters = Omit<charge_.Parameters, 'currency'> & ({
226
+ /** Ordered accepted currencies. A one-element list accepts a single asset. */
227
+ currencies: readonly Currency[];
228
+ currency?: undefined;
229
+ } | {
230
+ /** @deprecated Use `currencies: [currency]` instead. */
231
+ currency: Currency;
232
+ currencies?: undefined;
233
+ });
73
234
  /** Creates an EVM `charge` server method. */
74
235
  const charge: typeof charge_;
75
236
  /** Known EVM asset metadata for public config. */