agentwallet-mcp 1.13.11 → 1.13.12

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/build/index.js CHANGED
@@ -16,7 +16,7 @@ import { assertPublicUrl, safeFetch, finalUrlOf } from './ssrf-guard.js';
16
16
  import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, assetLabel, autopayAssetAllowed, autopayEnvCap, effectiveAutopayCap, maxAuthWindowSeconds, parseDecimalPins, approvalRefusal, authorizationResource, resolveNetworkChainId, normalizeRawAmount, parseAutopayAssets, assetSupportsExact, approvalResourceUrl, } from './x402-payment.js';
17
17
  import { createHash } from 'node:crypto';
18
18
  import { isLocalMode, getLocalAddress, localWalletRecord, localSend, localNativeBalance, localTokenBalance, localSignMessage, resolveRpcUrl, } from './local-wallet.js';
19
- import { localSignAuthorization, localEthCall, localGetCode, localSignPermit2Upto } from './local-wallet.js';
19
+ import { localSignAuthorization, localEthCall, localGetCode, localSignPermit2Upto, probeTokenDecimals } from './local-wallet.js';
20
20
  import { WRAPPED_NATIVE } from './wrapped-native.js';
21
21
  import { buildUptoAuthorization, uptoPayload, permit2AllowanceCalldata, permit2ApproveCalldata, decodeUint, permit2Address, } from './x402-permit2.js';
22
22
  import { assessTokenRisk } from './token-risk.js';
@@ -381,21 +381,29 @@ async function resolveTrustedDecimals(chainId, token) {
381
381
  throw new Error(`x402 blocked: cannot verify decimals for SPL mint ${token} on chain ${chainId}. ` +
382
382
  `Unknown mints are refused because the payment cap cannot be checked without trusted decimals.`);
383
383
  }
384
- // decimals() selector
384
+ // decimals() selector. In local mode this is the SAME probe and cache the
385
+ // local cap uses: two caches let a node answer 6 to one and 18 to the other
386
+ // and move 1000x the cap (round 5).
385
387
  let onChain = null;
386
- try {
387
- const r = (await api('/eth-call', 'POST', {
388
- chain_id: chainId, to: token, data: '0x313ce567',
389
- }, { 'X-AGW-SKIP-X402': 'true' }));
390
- const hex = (r?.result || '').replace(/^0x/, '');
391
- if (hex && /^[0-9a-fA-F]+$/.test(hex)) {
392
- const n = parseInt(hex, 16);
393
- if (Number.isInteger(n) && n >= 0 && n <= 36)
394
- onChain = n;
395
- }
388
+ if (anyLocalMode()) {
389
+ const p = await probeTokenDecimals(chainId, token);
390
+ onChain = typeof p === 'number' ? p : null;
396
391
  }
397
- catch {
398
- onChain = null; // fall through to the refusal below
392
+ else {
393
+ try {
394
+ const r = (await api('/eth-call', 'POST', {
395
+ chain_id: chainId, to: token, data: '0x313ce567',
396
+ }, { 'X-AGW-SKIP-X402': 'true' }));
397
+ const hex = (r?.result || '').replace(/^0x/, '');
398
+ if (hex && /^[0-9a-fA-F]+$/.test(hex)) {
399
+ const n = parseInt(hex, 16);
400
+ if (Number.isInteger(n) && n >= 0 && n <= 36)
401
+ onChain = n;
402
+ }
403
+ }
404
+ catch {
405
+ onChain = null; // fall through to the refusal below
406
+ }
399
407
  }
400
408
  if (onChain === null) {
401
409
  throw new Error(`x402 blocked: could not resolve on-chain decimals for ${token} on chain ${chainId}. ` +
@@ -579,8 +587,43 @@ function redactRpcSecrets(err) {
579
587
  const e = err;
580
588
  // A rejection that is not an Error (web3.js rejects with a raw {InstructionError} object) used to read "[object Object]".
581
589
  let msg = e?.shortMessage ? [e.shortMessage, e.details].filter(Boolean).join(' ') : (e?.message !== undefined ? String(e.message) : (err && typeof err === 'object' ? JSON.stringify(err) : String(err)));
590
+ // Bounded BEFORE the regex: the URL pattern is quadratic on text without "://",
591
+ // and a node's 160 KB error page held the whole server for 14 s (round 5).
592
+ msg = msg.slice(0, RPC_ERROR_MAX_CHARS);
582
593
  msg = msg.split(/\r?\n/).filter(l => !/^\s*(URL|Request body|Request Arguments|Raw Call Arguments)\s*:/i.test(l)).join('\n').trim();
583
- return msg.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s"'<>)]+/gi, (u) => redactUrl(u)); // brackets stay in: IPv6 hosts
594
+ msg = msg.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s"'<>)]+/gi, (u) => redactUrl(u)); // brackets stay in: IPv6 hosts
595
+ // A node that echoes the request path ("The requested URL /v3/<key> was not found")
596
+ // leaks the provider key without a scheme in front of it; every configured RPC
597
+ // URL's path and query are scrubbed as plain text as well (round 5).
598
+ for (const secret of rpcPathSecrets())
599
+ msg = msg.split(secret).join('/…');
600
+ return msg.length > 2000 ? msg.slice(0, 2000) + '…' : msg;
601
+ }
602
+ const RPC_ERROR_MAX_CHARS = 4000;
603
+ let rpcPathSecretsCache = null;
604
+ /** The path+query of every configured RPC URL (and its URL-decoded form), longest first. */
605
+ function rpcPathSecrets() {
606
+ if (rpcPathSecretsCache)
607
+ return rpcPathSecretsCache;
608
+ const out = new Set();
609
+ for (const [k, v] of Object.entries(process.env)) {
610
+ if (!/^AGENTWALLET_(RPC_URL|RPC_\d+|SOLANA_RPC|SOLANA_RPC_\d+)$/.test(k) || !v)
611
+ continue;
612
+ try {
613
+ const u = new URL(v.trim());
614
+ const tail = u.pathname + u.search;
615
+ if (tail.length > 1) {
616
+ out.add(tail);
617
+ try {
618
+ out.add(decodeURIComponent(tail));
619
+ }
620
+ catch { /* keep the raw form */ }
621
+ }
622
+ }
623
+ catch { /* not a URL; nothing to scrub */ }
624
+ }
625
+ rpcPathSecretsCache = [...out].sort((a, b) => b.length - a.length);
626
+ return rpcPathSecretsCache;
584
627
  }
585
628
  /**
586
629
  * Decimals to scale a human amount by. Always the token's own value (registry,
@@ -747,7 +790,7 @@ function resolveChainId(network) {
747
790
  const AddressSchema = z.string().regex(/^(0x[a-fA-F0-9]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})$/, 'Must be a 0x-prefixed 40-hex EVM address or a base58 Solana address');
748
791
  const server = new McpServer({
749
792
  name: 'agentwallet',
750
- version: '1.13.11',
793
+ version: '1.13.12',
751
794
  }, {
752
795
  instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
753
796
  ? 'This server runs in LOCAL (self-custody) mode: the operator supplied the key, signing happens in this process, and the only spend guards are the environment caps (AGENTWALLET_MAX_TX_NATIVE / _TOKEN / _SOL per transaction, AGENTWALLET_MAX_AUTOPAY and AGENTWALLET_AUTOPAY_ASSETS for x402, AGENTWALLET_X402_MAX_TIMEOUT for authorization windows). There is no server-side pause, daily limit or approval email; pause_wallet, unpause_wallet and delete_wallet are refused.'
@@ -1352,8 +1395,10 @@ async function resolveTokenDomain(chainId, asset, extra) {
1352
1395
  }
1353
1396
  return known;
1354
1397
  }
1355
- if (declared)
1356
- return declared;
1398
+ // Outside the registry the contract's own name()/version() come first; the
1399
+ // 402's declared pair is accepted only when the contract does not answer, and
1400
+ // a declared pair that contradicts the contract is refused (round 5: a
1401
+ // declared "Evil Name"/"9" used to be signed over the real domain).
1357
1402
  let name = '', version = '';
1358
1403
  try {
1359
1404
  // The signing decoder (256 bytes, strict UTF-8), not the 64-char display sanitiser
@@ -1362,12 +1407,17 @@ async function resolveTokenDomain(chainId, asset, extra) {
1362
1407
  name = decodeDomainString(await ethCallHex(chainId, asset, '0x06fdde03')); // name()
1363
1408
  version = decodeDomainString(await ethCallHex(chainId, asset, '0x54fd4d50')); // version()
1364
1409
  }
1365
- catch { /* refused below */ }
1366
- if (!name || !version) {
1367
- throw new Error(`x402: cannot determine the EIP-712 domain for token ${asset} on chain ${chainId}: the endpoint sent no ` +
1368
- `extra.name/extra.version and the contract did not answer name()/version(). Refused rather than signed with a guessed domain.`);
1410
+ catch { /* handled below */ }
1411
+ if (name && version) {
1412
+ if (declared && (declared.name !== name || declared.version !== version)) {
1413
+ throw new Error(`x402: the endpoint declared EIP-712 domain "${serverText(declared.name)}"/"${serverText(declared.version)}" for ${asset} on chain ${chainId}, but the contract reports "${serverText(name)}"/"${serverText(version)}"; refusing to sign under a domain the token would reject.`);
1414
+ }
1415
+ return { name, version };
1369
1416
  }
1370
- return { name, version };
1417
+ if (declared)
1418
+ return declared;
1419
+ throw new Error(`x402: cannot determine the EIP-712 domain for token ${asset} on chain ${chainId}: the endpoint sent no ` +
1420
+ `extra.name/extra.version and the contract did not answer name()/version(). Refused rather than signed with a guessed domain.`);
1371
1421
  }
1372
1422
  /** Text an endpoint wrote, bounded so it cannot flood the agent's context. */
1373
1423
  function serverText(s) {
@@ -1821,8 +1871,17 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1821
1871
  const retryServedFrom = finalUrlOf(retryRes, url);
1822
1872
  const retryCrossOrigin = new URL(retryServedFrom).origin !== new URL(url).origin;
1823
1873
  const verdict = settlementVerdict(retryRes.status, settlement, retryCrossOrigin);
1824
- const claimedRaw = typeof settlement?.amount === 'string' && /^\d+$/.test(settlement.amount) ? settlement.amount : null;
1825
- const claimedAboveMax = claimedRaw !== null && BigInt(claimedRaw) > BigInt(rawAmount);
1874
+ // The server's claimed settled amount may be in base units or human units; it is inconsistent if it exceeds the maximum in either reading.
1875
+ const claimedStr = typeof settlement?.amount === 'string' ? settlement.amount.trim() : null;
1876
+ let claimedAboveMax = false;
1877
+ if (claimedStr && /^\d+(\.\d+)?$/.test(claimedStr)) {
1878
+ try {
1879
+ claimedAboveMax = (/^\d+$/.test(claimedStr) && BigInt(claimedStr) > BigInt(rawAmount)) || BigInt(parseUnits(claimedStr, trustedDecimals)) > BigInt(rawAmount);
1880
+ }
1881
+ catch {
1882
+ claimedAboveMax = false;
1883
+ }
1884
+ }
1826
1885
  if (retryCrossOrigin) {
1827
1886
  retryError = `The paid request was redirected to another origin (${new URL(retryServedFrom).origin}); the payment header is never forwarded across origins, so this response did not see the payment.`;
1828
1887
  }
@@ -1841,6 +1900,8 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1841
1900
  payment_made: verdict.payment_made,
1842
1901
  payment_outcome: verdict.outcome,
1843
1902
  authorization_reused: authorizationReused,
1903
+ // A cached authorization is re-sent as signed; if this call's max_payment is lower than it, say so (no new authorization was signed).
1904
+ cached_authorization_above_current_cap: authorizationReused && !isWithinCap(rawAmount, trustedDecimals, effectiveMax),
1844
1905
  retry_error: retryError,
1845
1906
  payment_method: isUpto ? 'permit2-upto-authorization' : (standardExact ? 'eip3009-authorization' : 'onchain-transfer'),
1846
1907
  approval_id: approvalUsed,
@@ -1898,7 +1959,13 @@ tool('check_token_risk', 'Assess an ERC-20 token before approving or swapping it
1898
1959
  'A warning for the caller to weigh, never a block.', {
1899
1960
  token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
1900
1961
  chain_id: z.number().int().describe('Chain ID'),
1901
- }, async ({ token, chain_id }) => jsonResponse({ token, chain_id, risk: await assessTokenRisk(chain_id, token, (to, data) => ethCallHex(chain_id, to, data)) }));
1962
+ }, async ({ token, chain_id }) => {
1963
+ if (process.env.AGENTWALLET_TOKEN_RISK === '0') {
1964
+ // The operator said no third-party lookups; approve_token honoured it, this tool did not (round 5).
1965
+ return jsonResponse({ token, chain_id, risk: { source: 'none', notes: ['The GoPlus lookup is disabled by AGENTWALLET_TOKEN_RISK=0; no third party was contacted.'] } });
1966
+ }
1967
+ return jsonResponse({ token, chain_id, risk: await assessTokenRisk(chain_id, token, (to, data) => ethCallHex(chain_id, to, data)) });
1968
+ });
1902
1969
  // ─── Tool: get_usage ─────────────────────────────────────────────
1903
1970
  tool('get_usage', 'Get the current month\'s usage statistics. ' +
1904
1971
  'Returns operations count, tier info, remaining quota, and fees.', {}, async () => {
@@ -106,6 +106,9 @@ export function resolveSolanaRpc(chainId = 900) {
106
106
  const explicit = (process.env.AGENTWALLET_SOLANA_RPC || '').trim();
107
107
  if (explicit)
108
108
  return explicit;
109
+ if (Object.keys(process.env).some(k => /^AGENTWALLET_SOLANA_RPC_\d+$/.test(k) && (process.env[k] || '').trim())) {
110
+ throw new Error(`No Solana RPC for chain ${chainId}: AGENTWALLET_SOLANA_RPC_<chainId> is set for another cluster, so the public default is not used. Set AGENTWALLET_SOLANA_RPC_${chainId} or AGENTWALLET_SOLANA_RPC.`);
111
+ }
109
112
  const fallback = DEFAULT_SOLANA_RPC[chainId];
110
113
  if (fallback)
111
114
  return fallback;
@@ -90,6 +90,11 @@ export interface LocalSendResult {
90
90
  * differing only in that immutable were blocked (18) and sent (77).
91
91
  */
92
92
  type DecimalsProbe = number | 'unusable' | 'not-a-token' | 'unreachable';
93
+ /**
94
+ * Decimals for `token`: the trusted registry, then the contract's own
95
+ * decimals() (cached on success). See DecimalsProbe for the other outcomes.
96
+ */
97
+ export declare function probeTokenDecimals(chainId: number, token: Address): Promise<DecimalsProbe>;
93
98
  /** An eth_call failure is a revert only when the node says so; every other failure is "could not ask". */
94
99
  export declare function classifyDecimalsError(err: unknown): DecimalsProbe;
95
100
  export declare function permit2AddressFor(chainId: number): string;
@@ -113,6 +113,12 @@ export function resolveRpcUrl(chainId) {
113
113
  const generic = (process.env.AGENTWALLET_RPC_URL || '').trim();
114
114
  if (generic)
115
115
  return assertRpcScheme(generic, 'AGENTWALLET_RPC_URL');
116
+ // An operator who named a node for any chain wants control of all of them:
117
+ // a chain they did not name must not fall through to a public endpoint
118
+ // (a devnet default answered a real simulation in round 5).
119
+ if (Object.keys(process.env).some(k => /^AGENTWALLET_RPC_\d+$/.test(k) && (process.env[k] || '').trim())) {
120
+ throw new Error(`No RPC endpoint for chain ${chainId}: AGENTWALLET_RPC_<chainId> is set for other chains, so the public default is not used. Set AGENTWALLET_RPC_${chainId} or AGENTWALLET_RPC_URL.`);
121
+ }
116
122
  const fallback = DEFAULT_RPC[chainId];
117
123
  if (fallback)
118
124
  return fallback;
@@ -186,6 +192,7 @@ function assertWithinNativeCap(valueWei) {
186
192
  }
187
193
  /* ── Read operations ─────────────────────────────────────────────── */
188
194
  export async function localNativeBalance(chainId) {
195
+ await assertRpcServesChain(chainId); // reads used to skip the check and report another chain's balance (round 5)
189
196
  const { pub } = clients(chainId);
190
197
  const address = getLocalAddress();
191
198
  const wei = await pub.getBalance({ address });
@@ -200,6 +207,7 @@ const ERC20_ABI = parseAbi([
200
207
  'function allowance(address,address) view returns (uint256)',
201
208
  ]);
202
209
  export async function localTokenBalance(chainId, token) {
210
+ await assertRpcServesChain(chainId);
203
211
  const { pub } = clients(chainId);
204
212
  const address = getLocalAddress();
205
213
  const [raw, decimalsRaw, symbol] = await Promise.all([
@@ -252,7 +260,7 @@ const decimalsCache = new Map();
252
260
  * Decimals for `token`: the trusted registry, then the contract's own
253
261
  * decimals() (cached on success). See DecimalsProbe for the other outcomes.
254
262
  */
255
- async function probeTokenDecimals(chainId, token) {
263
+ export async function probeTokenDecimals(chainId, token) {
256
264
  const known = lookupTrustedDecimals(chainId, token);
257
265
  if (typeof known === 'number')
258
266
  return known;
@@ -326,12 +334,19 @@ const PERMIT2_SELECTORS = new Set(['87517c45', '2b67b570', '2a2d80d1', '36c78516
326
334
  and so read as "not a token" to the cap, which let setApprovalForAll to an
327
335
  attacker through uncapped (2026-10-04 round 2, verified on a live
328
336
  collection). Nothing prices an NFT, so these are refused outright. */
329
- const NFT_SELECTORS = new Set(['a22cb465', '42842e0e', 'b88d4fde', 'f242432a', '2eb2c2d6']);
337
+ const NFT_SELECTORS = new Set(['a22cb465', '42842e0e', 'b88d4fde', 'f242432a', '2eb2c2d6', '959b8c3f']); // plus ERC-777 authorizeOperator: an unbounded delegation
330
338
  const CAP_LAYOUTS = {
331
339
  a9059cbb: { amountIndex: 1, what: 'token transfer' }, // transfer(address,uint256)
332
340
  '23b872dd': { amountIndex: 2, what: 'token transfer' }, // transferFrom(address,address,uint256)
333
341
  '095ea7b3': { amountIndex: 1, what: 'approval' }, // approve(address,uint256)
334
342
  '39509351': { amountIndex: 1, what: 'allowance increase' }, // increaseAllowance(address,uint256)
343
+ // ERC-677 and ERC-777 value movers: priced like transfer, so a contract that
344
+ // declines decimals() is evaluated at 0 decimals instead of waved through
345
+ // (transferAndCall(attacker, 1e24) on a not-a-token target, round 5).
346
+ '4000aea0': { amountIndex: 1, what: 'token transfer (transferAndCall)' }, // transferAndCall(address,uint256,bytes)
347
+ cae9ca51: { amountIndex: 1, what: 'approval (approveAndCall)' }, // approveAndCall(address,uint256,bytes)
348
+ '9bd9bbc6': { amountIndex: 1, what: 'token transfer (ERC-777 send)' }, // send(address,uint256,bytes)
349
+ '62ad1b83': { amountIndex: 2, what: 'token transfer (ERC-777 operatorSend)' }, // operatorSend(address,address,uint256,bytes,bytes)
335
350
  '87517c45': { amountIndex: 2, tokenIndex: 0, what: 'Permit2 approval' }, // Permit2.approve(address,address,uint160,uint48)
336
351
  // WETH9 calls, priced only when `to` is the chain's own wrapped-native
337
352
  // contract (wrappedNativeOnly). deposit() carries its amount as msg.value,
@@ -626,12 +641,14 @@ export async function localSignAuthorization(chainId, asset, domain, auth) {
626
641
  }
627
642
  /** Read-only eth_call against the local RPC, same shape as the hosted /eth-call route. */
628
643
  export async function localEthCall(chainId, to, data) {
644
+ await assertRpcServesChain(chainId);
629
645
  const client = createPublicClient({ transport: http(resolveRpcUrl(chainId)) });
630
646
  const r = await client.call({ to, data });
631
647
  return { result: r.data ?? '0x' };
632
648
  }
633
649
  /** Read-only eth_getCode against the local RPC, same shape as the hosted /eth-get-code route. */
634
650
  export async function localGetCode(chainId, address) {
651
+ await assertRpcServesChain(chainId);
635
652
  const client = createPublicClient({ transport: http(resolveRpcUrl(chainId)) });
636
653
  const code = await client.getCode({ address });
637
654
  return { code: code ?? '0x' };
@@ -363,7 +363,8 @@ export function buildFetchedResponse(status, statusText, serverHeaders, body, fi
363
363
  headers.push([k, v]);
364
364
  }
365
365
  headers.push([FINAL_URL_HEADER, finalUrl]);
366
- const out = new Response(body, { status, statusText, headers });
366
+ // A reason phrase with bytes above 0xFF (undici decodes it as UTF-8 with U+FFFD) is not a ByteString and threw (round 5).
367
+ const out = new Response(body, { status, statusText: statusText.replace(/[^\x20-\x7e]/g, ''), headers });
367
368
  finalUrls.set(out, finalUrl);
368
369
  return out;
369
370
  }
@@ -143,7 +143,7 @@ export function autopayAssetAllowed(chainId, asset) {
143
143
  const label = assetLabel(chainId, asset);
144
144
  return {
145
145
  allowed: false, via: 'none',
146
- reason: `x402 blocked: this endpoint wants payment in ${label}${asset ? ` (${asset})` : ''} on chain ${chainId}, which is not a stablecoin ` +
146
+ reason: `x402 blocked: this endpoint wants payment in ${String(label).slice(0, 40)}${asset ? ` (${asset.slice(0, 80)})` : ''} on chain ${chainId}, which is not a stablecoin ` +
147
147
  `AGENTWALLET_MAX_AUTOPAY can price and is not on the operator's AGENTWALLET_AUTOPAY_ASSETS list for chain ${chainId}. ` +
148
148
  `Only the operator can change that list; do not edit the environment or config to add it.`,
149
149
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "agentwallet-mcp",
3
3
  "mcpName": "io.github.hifriendbot/agentwallet",
4
4
  "repository": { "type": "git", "url": "https://github.com/hifriendbot/agentwallet-mcp" },
5
- "version": "1.13.11",
5
+ "version": "1.13.12",
6
6
  "description": "MCP server for AgentWallet: self-custody local signing on EVM and Solana, or hosted wallets. Sign, broadcast, pay x402 invoices, and accept x402 payments.",
7
7
  "type": "module",
8
8
  "main": "build/index.js",
@@ -17,7 +17,7 @@
17
17
  "scripts": {
18
18
  "build": "tsc",
19
19
  "start": "node build/index.js",
20
- "test": "node test/x402-payment.test.mjs && node test/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-cap.test.mjs && node test/decimals-range-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs",
20
+ "test": "node test/x402-payment.test.mjs && node test/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-cap.test.mjs && node test/decimals-range-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs && node test/audit-11312.test.mjs",
21
21
  "prepublishOnly": "npm run build"
22
22
  },
23
23
  "keywords": [