@jaw.id/cli 0.1.26 → 0.2.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 (72) hide show
  1. package/dist/base-command.js +3 -1
  2. package/dist/base-command.js.map +1 -1
  3. package/dist/commands/config/set.js +140 -12
  4. package/dist/commands/config/set.js.map +1 -1
  5. package/dist/commands/config/show.js +3 -1
  6. package/dist/commands/config/show.js.map +1 -1
  7. package/dist/commands/config/write.js +6 -4
  8. package/dist/commands/config/write.js.map +1 -1
  9. package/dist/commands/disconnect.js +24 -10
  10. package/dist/commands/disconnect.js.map +1 -1
  11. package/dist/commands/mcp/index.js +2426 -94
  12. package/dist/commands/mcp/index.js.map +1 -1
  13. package/dist/commands/rpc/call.js +197 -45
  14. package/dist/commands/rpc/call.js.map +1 -1
  15. package/dist/commands/session/add.js +1547 -0
  16. package/dist/commands/session/add.js.map +1 -0
  17. package/dist/commands/session/revoke.js +181 -54
  18. package/dist/commands/session/revoke.js.map +1 -1
  19. package/dist/commands/session/setup.js +516 -65
  20. package/dist/commands/session/setup.js.map +1 -1
  21. package/dist/commands/session/status.js +315 -6
  22. package/dist/commands/session/status.js.map +1 -1
  23. package/dist/commands/version.js +3 -1
  24. package/dist/commands/version.js.map +1 -1
  25. package/dist/commands/x402/log.js +344 -0
  26. package/dist/commands/x402/log.js.map +1 -0
  27. package/dist/commands/x402/pay.js +2122 -0
  28. package/dist/commands/x402/pay.js.map +1 -0
  29. package/dist/commands/x402/status.js +1047 -0
  30. package/dist/commands/x402/status.js.map +1 -0
  31. package/dist/index.js +41 -14
  32. package/dist/index.js.map +1 -1
  33. package/dist/lib/bridge-singleton.js +41 -14
  34. package/dist/lib/bridge-singleton.js.map +1 -1
  35. package/dist/lib/config.js +26 -3
  36. package/dist/lib/config.js.map +1 -1
  37. package/dist/lib/keystore.js +13 -2
  38. package/dist/lib/keystore.js.map +1 -1
  39. package/dist/lib/paths.js +3 -1
  40. package/dist/lib/paths.js.map +1 -1
  41. package/dist/lib/payment-lock.js +121 -0
  42. package/dist/lib/payment-lock.js.map +1 -0
  43. package/dist/lib/session-bridge.js +148 -24
  44. package/dist/lib/session-bridge.js.map +1 -1
  45. package/dist/lib/session-config.js +78 -11
  46. package/dist/lib/session-config.js.map +1 -1
  47. package/dist/lib/terminal.js +22 -0
  48. package/dist/lib/terminal.js.map +1 -0
  49. package/dist/lib/validation.js +3 -3
  50. package/dist/lib/validation.js.map +1 -1
  51. package/dist/lib/ws-bridge.js +22 -10
  52. package/dist/lib/ws-bridge.js.map +1 -1
  53. package/dist/mcp/handlers/config.js +73 -6
  54. package/dist/mcp/handlers/config.js.map +1 -1
  55. package/dist/mcp/handlers/daemon.js +43 -12
  56. package/dist/mcp/handlers/daemon.js.map +1 -1
  57. package/dist/mcp/handlers/resources.js +119 -0
  58. package/dist/mcp/handlers/resources.js.map +1 -1
  59. package/dist/mcp/handlers/rpc.js +269 -60
  60. package/dist/mcp/handlers/rpc.js.map +1 -1
  61. package/dist/mcp/helpers.js +50 -3
  62. package/dist/mcp/helpers.js.map +1 -1
  63. package/dist/mcp/server.js +2426 -94
  64. package/dist/mcp/server.js.map +1 -1
  65. package/dist/mcp/tools.js +43 -3
  66. package/dist/mcp/tools.js.map +1 -1
  67. package/dist/x402/log-view.js +160 -0
  68. package/dist/x402/log-view.js.map +1 -0
  69. package/dist/x402/status-report.js +90 -0
  70. package/dist/x402/status-report.js.map +1 -0
  71. package/oclif.manifest.json +405 -11
  72. package/package.json +5 -2
@@ -4,6 +4,7 @@ import * as crypto from 'crypto';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
6
6
  import WebSocket from 'ws';
7
+ import { encodeFunctionData, maxUint256, erc20Abi } from 'viem';
7
8
 
8
9
  // src/mcp/tools.ts
9
10
  var rpcMethodSchema = {
@@ -13,15 +14,69 @@ var rpcMethodSchema = {
13
14
  params: z.any().optional().describe(
14
15
  "Method parameters \u2014 structure varies by method. Read the jaw://api-reference/{method} resource for the expected format."
15
16
  ),
16
- chainId: z.number().optional().describe("Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia"),
17
+ chainId: z.number().int().positive().optional().describe("Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia"),
17
18
  session: z.boolean().optional().describe(
18
- "Sign with the local session key instead of opening the browser (requires `jaw session setup`; check jaw_session_status first). Supported methods only: eth_requestAccounts, eth_accounts, wallet_sendCalls, wallet_getCallsStatus, personal_sign, eth_signTypedData_v4. Defaults to the JAW_SESSION env var."
19
+ "Sign with the local session key instead of opening the browser (requires `jaw session setup`; check jaw_session_status first). Supported methods only: eth_requestAccounts, eth_accounts, wallet_sendCalls, wallet_getCallsStatus. personal_sign and eth_signTypedData_v4 are browser only: a signature made by the session key never passes the spend caps or the ledger. Defaults to the JAW_SESSION env var."
19
20
  )
20
21
  };
21
22
  ({
22
23
  key: z.enum(["apiKey", "defaultChain", "keysUrl", "ens", "relayUrl", "sessionExpiry"]).describe("Config key"),
23
24
  value: z.string().describe("Config value")
24
25
  });
26
+ var httpUrl = z.string().url().refine(
27
+ (u) => {
28
+ try {
29
+ const p = new URL(u).protocol;
30
+ return p === "http:" || p === "https:";
31
+ } catch {
32
+ return false;
33
+ }
34
+ },
35
+ { message: "url must be http(s) \u2014 other schemes (file:, data:, javascript:, ftp:) are not fetched" }
36
+ );
37
+ ({
38
+ url: httpUrl.describe("Resource URL to fetch (http/https only). If it answers HTTP 402 (x402), pay and retry."),
39
+ method: z.string().optional().describe("HTTP method (default GET)."),
40
+ headers: z.record(z.string()).optional().describe("Extra request headers."),
41
+ body: z.string().optional().describe("Request body (for POST/PUT/etc.)."),
42
+ maxAmount: z.string().optional().describe(
43
+ "Hard ceiling for THIS call, in the asset base units (e.g. 6-decimals for USDC). If the 402 asks for more, the payment is refused, not made."
44
+ ),
45
+ asset: z.string().optional().describe("Require a specific asset contract address."),
46
+ network: z.string().optional().describe("Require a specific CAIP-2 network, e.g. eip155:8453 (Base).")
47
+ });
48
+ ({
49
+ query: z.string().max(400).optional().describe(
50
+ 'Keyword or natural-language search over the x402 Bazaar catalog of paid services (e.g. "ens resolver", "weather api", "token price"). Required unless `payTo` is set.'
51
+ ),
52
+ network: z.string().optional().describe("CAIP-2 network to prefer when picking the price to show, e.g. eip155:8453 (Base, default)."),
53
+ maxUsdPrice: z.string().optional().describe("Only return services priced at or below this many USD per call."),
54
+ curatedOnly: z.boolean().optional().describe("Only return Coinbase-curated (health-probed) services."),
55
+ limit: z.number().int().min(1).max(20).optional().describe("Maximum results to return (1-20, default 10)."),
56
+ payTo: z.string().optional().describe(
57
+ "Instead of searching, list every service registered by this seller address (0x\u2026). Takes precedence over `query` if both are given."
58
+ )
59
+ });
60
+ ({
61
+ limit: z.number().optional().describe("Return only the most recent N ledger entries (default: all).")
62
+ });
63
+ ({
64
+ network: z.string().optional().describe("CAIP-2 network to check the USDC balance on, e.g. eip155:8453 (Base) or eip155:84532 (Base Sepolia).")
65
+ });
66
+
67
+ // src/lib/errors.ts
68
+ function errorMessage(err) {
69
+ return err instanceof Error ? err.message : String(err);
70
+ }
71
+
72
+ // src/lib/terminal.ts
73
+ var INVISIBLE_AND_BIDI = /[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g;
74
+ var BLOCK_CONTROLS = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g;
75
+ var REPLACEMENT = "\uFFFD";
76
+ function sanitizeBlock(value) {
77
+ const text = typeof value === "string" ? value : String(value);
78
+ return text.replace(BLOCK_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT);
79
+ }
25
80
 
26
81
  // src/mcp/helpers.ts
27
82
  function mcpError(err) {
@@ -30,17 +85,20 @@ function mcpError(err) {
30
85
  content: [
31
86
  {
32
87
  type: "text",
33
- text: `Error: ${err instanceof Error ? err.message : String(err)}`
88
+ text: `Error: ${sanitizeBlock(errorMessage(err))}`
34
89
  }
35
90
  ]
36
91
  };
37
92
  }
93
+ function encode(value) {
94
+ return sanitizeBlock(JSON.stringify(value));
95
+ }
38
96
  function mcpResult(data) {
39
97
  return {
40
98
  content: [
41
99
  {
42
100
  type: "text",
43
- text: JSON.stringify(data)
101
+ text: encode(data)
44
102
  }
45
103
  ]
46
104
  };
@@ -52,7 +110,9 @@ var PATHS = {
52
110
  session: path.join(JAW_DIR, "session.json"),
53
111
  relay: path.join(JAW_DIR, "relay.json"),
54
112
  keystore: path.join(JAW_DIR, "keystore.json"),
55
- sessionConfig: path.join(JAW_DIR, "session-config.json")
113
+ sessionConfig: path.join(JAW_DIR, "session-config.json"),
114
+ x402Log: path.join(JAW_DIR, "x402-log.jsonl"),
115
+ paymentLock: path.join(JAW_DIR, "x402-payment.lock")
56
116
  };
57
117
 
58
118
  // src/lib/validation.ts
@@ -174,7 +234,18 @@ function bufferToBase64(buf) {
174
234
  }
175
235
 
176
236
  // src/lib/ws-bridge.ts
237
+ function buildInitPayload(config) {
238
+ return {
239
+ type: "init",
240
+ apiKey: config.apiKey,
241
+ chainId: config.chainId,
242
+ ens: config.ens,
243
+ paymasterUrl: config.paymasterUrl,
244
+ ...config.paymasterUrl && config.paymasterContext ? { paymasterContext: config.paymasterContext } : {}
245
+ };
246
+ }
177
247
  var DEFAULT_TIMEOUT_MS = 12e4;
248
+ var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
178
249
  var MAX_MESSAGE_BYTES = 5 * 1024 * 1024;
179
250
  var BROWSER_REOPEN_COOLDOWN_MS = 5e3;
180
251
  var MAX_RECONNECT_ATTEMPTS = 3;
@@ -183,6 +254,7 @@ var WSBridge = class {
183
254
  relayUrl;
184
255
  session;
185
256
  timeout;
257
+ connectTimeout;
186
258
  config;
187
259
  privateKeyHex;
188
260
  publicKeyHex;
@@ -204,6 +276,7 @@ var WSBridge = class {
204
276
  this.relayUrl = options.relayUrl;
205
277
  this.session = options.session;
206
278
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
279
+ this.connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
207
280
  this.config = options.config;
208
281
  this.privateKeyHex = options.privateKeyHex;
209
282
  this.publicKeyHex = options.publicKeyHex;
@@ -232,17 +305,16 @@ var WSBridge = class {
232
305
  let expectingKeyExchange = !this.peerPublicKeyHex;
233
306
  const timer = setTimeout(() => {
234
307
  ws.close();
235
- reject(new Error("Browser did not connect in time.\nRun `jaw disconnect` then try again."));
236
- }, 3e4);
308
+ reject(
309
+ new Error(
310
+ `Browser did not connect within ${Math.round(this.connectTimeout / 1e3)}s.
311
+ Run \`jaw disconnect\` then try again, or raise JAW_BRIDGE_TIMEOUT_MS.`
312
+ )
313
+ );
314
+ }, this.connectTimeout);
237
315
  const sendEncryptedInit = async () => {
238
316
  if (!this.sharedSecret) return;
239
- const envelope = await encryptMessage(this.sharedSecret, {
240
- type: "init",
241
- apiKey: this.config.apiKey,
242
- chainId: this.config.chainId,
243
- ens: this.config.ens,
244
- paymasterUrl: this.config.paymasterUrl
245
- });
317
+ const envelope = await encryptMessage(this.sharedSecret, buildInitPayload(this.config));
246
318
  this.sendRaw(ws, JSON.stringify({ type: "encrypted", ...envelope }));
247
319
  };
248
320
  const waitForReady = () => {
@@ -520,6 +592,10 @@ var DEFAULT_KEYS_URL = "https://keys.jaw.id";
520
592
  var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
521
593
  async function getBridge(options) {
522
594
  const config = loadConfig();
595
+ const envTimeout = Number(process.env["JAW_BRIDGE_TIMEOUT_MS"]);
596
+ const fromEnv = Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0;
597
+ const timeout = options.timeout ?? fromEnv;
598
+ const connectTimeout = options.connectTimeout ?? fromEnv;
523
599
  const keysUrl = options.keysUrl ?? config.keysUrl ?? DEFAULT_KEYS_URL;
524
600
  const relayUrl = options.relayUrl ?? config.relayUrl ?? DEFAULT_RELAY_URL;
525
601
  const chainId = options.chainId ?? config.defaultChain ?? 1;
@@ -532,7 +608,7 @@ async function getBridge(options) {
532
608
  let relaySession = loadRelaySession();
533
609
  if (relaySession && relaySession.relayUrl === relayUrl && relaySession.peerPublicKey) {
534
610
  try {
535
- return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl, false);
611
+ return await connectBridge({ ...options, timeout }, relaySession, chainId, keysUrl, relayUrl, false);
536
612
  } catch {
537
613
  deleteRelaySession();
538
614
  relaySession = null;
@@ -542,7 +618,7 @@ async function getBridge(options) {
542
618
  }
543
619
  const session = await createNewSession(relayUrl);
544
620
  saveRelaySession(session);
545
- return await connectBridge(session, options, chainId, keysUrl, relayUrl, true);
621
+ return await connectBridge({ ...options, timeout, connectTimeout }, session, chainId, keysUrl, relayUrl, true);
546
622
  }
547
623
  async function createNewSession(relayUrl) {
548
624
  const kp = await generateKeyPair();
@@ -557,17 +633,20 @@ async function createNewSession(relayUrl) {
557
633
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
558
634
  };
559
635
  }
560
- async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl, openBrowser) {
636
+ async function connectBridge(options, relaySession, chainId, keysUrl, relayUrl, openBrowser) {
561
637
  const config = loadConfig();
638
+ const paymaster = config.paymasters?.[chainId];
562
639
  const bridge = new WSBridge({
563
640
  relayUrl,
564
641
  session: relaySession.session,
565
642
  timeout: options.timeout,
643
+ connectTimeout: options.connectTimeout,
566
644
  config: {
567
645
  apiKey: options.apiKey,
568
646
  chainId,
569
647
  ens: options.ens ?? config.ens,
570
- paymasterUrl: options.paymasterUrl ?? config.paymasters?.[chainId]?.url
648
+ paymasterUrl: paymaster?.url,
649
+ paymasterContext: paymaster?.context
571
650
  },
572
651
  privateKeyHex: relaySession.privateKey,
573
652
  publicKeyHex: relaySession.publicKey,
@@ -577,6 +656,12 @@ async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl,
577
656
  // onBrowserNeeded — only open a browser for new sessions
578
657
  openBrowser ? async () => {
579
658
  const bridgeUrl = buildBridgeUrl(keysUrl, relaySession.session, relayUrl, relaySession.publicKey);
659
+ if (process.env["JAW_NO_BROWSER"]) {
660
+ process.stderr.write(`Open this URL to approve:
661
+ ${bridgeUrl}
662
+ `);
663
+ return;
664
+ }
580
665
  const { default: open } = await import('open');
581
666
  await open(bridgeUrl);
582
667
  } : void 0,
@@ -608,6 +693,9 @@ function loadSessionKey() {
608
693
  }
609
694
  return parsed.privateKey;
610
695
  }
696
+ function isLegacySession(config) {
697
+ return config.mode !== "eip7702";
698
+ }
611
699
  function loadSessionConfig() {
612
700
  if (!fs.existsSync(PATHS.sessionConfig)) {
613
701
  throw new Error("No session configured. Run `jaw session setup` first.");
@@ -619,21 +707,94 @@ function loadSessionConfig() {
619
707
  throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \`jaw session setup\` to recreate it.`);
620
708
  }
621
709
  }
710
+ function tryLoadSessionConfig() {
711
+ try {
712
+ return loadSessionConfig();
713
+ } catch {
714
+ return null;
715
+ }
716
+ }
717
+
718
+ // src/x402/asset-registry.ts
719
+ var USDC_BY_NETWORK = {
720
+ "eip155:8453": {
721
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
722
+ chainId: 8453,
723
+ wireNetwork: "eip155:8453",
724
+ usdcName: "USD Coin",
725
+ usdcVersion: "2",
726
+ decimals: 6
727
+ },
728
+ "eip155:84532": {
729
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
730
+ chainId: 84532,
731
+ wireNetwork: "eip155:84532",
732
+ usdcName: "USDC",
733
+ usdcVersion: "2",
734
+ decimals: 6
735
+ },
736
+ "eip155:137": {
737
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
738
+ chainId: 137,
739
+ wireNetwork: "eip155:137",
740
+ usdcName: "USD Coin",
741
+ usdcVersion: "2",
742
+ decimals: 6
743
+ },
744
+ "eip155:80002": {
745
+ address: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
746
+ chainId: 80002,
747
+ wireNetwork: "eip155:80002",
748
+ usdcName: "USDC",
749
+ usdcVersion: "2",
750
+ decimals: 6
751
+ }
752
+ };
753
+ function usdcForNetwork(network) {
754
+ return Object.hasOwn(USDC_BY_NETWORK, network) ? USDC_BY_NETWORK[network] : void 0;
755
+ }
756
+
757
+ // src/x402/permit2.ts
758
+ var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
622
759
 
623
760
  // src/lib/session-bridge.ts
761
+ var JAW_ERC20_PAYMASTER_URL = "https://api.justaname.id/proxy/v1/rpc/erc20-paymaster";
762
+ function resolvePaymaster(options) {
763
+ if (options.paymasterUrl) {
764
+ return { paymasterUrl: options.paymasterUrl, paymasterContext: options.paymasterContext };
765
+ }
766
+ const configured = loadConfig().paymasters?.[options.chainId];
767
+ if (configured) {
768
+ return { paymasterUrl: configured.url, paymasterContext: configured.context };
769
+ }
770
+ if (!options.apiKey) return {};
771
+ const asset = usdcForNetwork(`eip155:${options.chainId}`);
772
+ if (!asset) {
773
+ console.warn(
774
+ `[jaw] No USDC in the x402 asset registry for chain ${options.chainId}, so no ERC-20 paymaster can be engaged. Gas will come out of the account\u2019s native balance. Set \`paymasters\` in your config to sponsor this chain.`
775
+ );
776
+ return {};
777
+ }
778
+ const url = new URL(JAW_ERC20_PAYMASTER_URL);
779
+ url.searchParams.set("chainId", String(options.chainId));
780
+ url.searchParams.set("api-key", options.apiKey);
781
+ return { paymasterUrl: url.toString(), paymasterContext: { token: asset.address } };
782
+ }
783
+ function explainUnchargeableSender(err, sessionAddress) {
784
+ const message = err instanceof Error ? err.message : String(err);
785
+ if (!message.includes("Could not size the ERC-20 paymaster approval")) return err;
786
+ return new Error(
787
+ `${message}
788
+
789
+ If ${sessionAddress} holds no USDC, that is why: it pays for its own gas and cannot be charged with an empty balance. Send it 0.1 USDC, or run \`jaw session setup\` again.`,
790
+ { cause: err }
791
+ );
792
+ }
624
793
  var SessionBridge = class {
625
794
  options;
626
795
  session = null;
627
796
  constructor(options) {
628
- this.options = { ...options };
629
- if (!this.options.paymasterUrl) {
630
- const config = loadConfig();
631
- const pm = config.paymasters?.[this.options.chainId];
632
- if (pm) {
633
- this.options.paymasterUrl = pm.url;
634
- this.options.paymasterContext = pm.context;
635
- }
636
- }
797
+ this.options = { ...options, ...resolvePaymaster(options) };
637
798
  }
638
799
  async getSession() {
639
800
  if (this.session) {
@@ -642,6 +803,11 @@ var SessionBridge = class {
642
803
  }
643
804
  const config = loadSessionConfig();
644
805
  this.checkExpiry(config);
806
+ if (isLegacySession(config)) {
807
+ throw new Error(
808
+ "This session was created by an older CLI and uses a session address separate from the session key. Run `jaw session setup` to recreate it, which offers to revoke the old permission first. `jaw session status` still shows the old session, and `jaw session revoke` still revokes it."
809
+ );
810
+ }
645
811
  if (config.chainId !== this.options.chainId) {
646
812
  throw new Error(
647
813
  `Session was created for chain ${config.chainId}, but --chain ${this.options.chainId} was requested. Run \`jaw session setup --chain ${this.options.chainId}\` to create a session for that chain.`
@@ -659,8 +825,14 @@ var SessionBridge = class {
659
825
  paymasterUrl: this.options.paymasterUrl,
660
826
  paymasterContext: this.options.paymasterContext
661
827
  },
662
- localAccount
828
+ localAccount,
829
+ { eip7702: true }
663
830
  );
831
+ if (account.address.toLowerCase() !== config.sessionAddress.toLowerCase()) {
832
+ throw new Error(
833
+ `Session key derives ${account.address}, but the stored session address is ${config.sessionAddress}. The keystore and session config are out of sync. Run \`jaw session setup\` to recreate the session.`
834
+ );
835
+ }
664
836
  this.session = { account, config };
665
837
  return this.session;
666
838
  }
@@ -670,6 +842,46 @@ var SessionBridge = class {
670
842
  throw new Error(`Session expired on ${expiryDate}. Run \`jaw session setup\` to create a new session.`);
671
843
  }
672
844
  }
845
+ /**
846
+ * Approve Permit2 to move one of the payer's tokens, and return the batch id.
847
+ *
848
+ * The only call this session sends outside its permission, and the only one
849
+ * that can be: `JustaPermissionManager` checks every call's selector against
850
+ * the grant, and the x402 grant permits `transfer` alone, so an approval
851
+ * routed through the permission reverts before anything else happens. Sent by
852
+ * the session on its own balance it never reaches the manager at all, whose
853
+ * approval revocation and Permit2 lockdown act on the granting account and
854
+ * only within their own execution.
855
+ *
856
+ * Being outside the permission is exactly why it is not a general send. It
857
+ * takes a token and nothing else: the spender is Permit2 and the amount is
858
+ * the maximum, neither reachable by a caller, and the token has to be the
859
+ * registry's USDC for this session's chain. There is no shape of argument
860
+ * that turns this into an arbitrary transfer, which matters because an agent
861
+ * reaches the tools that reach this.
862
+ */
863
+ async approvePermit2(token) {
864
+ const { account, config } = await this.getSession();
865
+ const usdc = usdcForNetwork(`eip155:${config.chainId}`);
866
+ if (!usdc || token.toLowerCase() !== usdc.address.toLowerCase()) {
867
+ throw new Error(
868
+ `Refusing to approve Permit2 for ${token}: only the registry USDC on chain ${config.chainId} is allowed.`
869
+ );
870
+ }
871
+ const data = encodeFunctionData({
872
+ abi: erc20Abi,
873
+ functionName: "approve",
874
+ args: [PERMIT2_ADDRESS, maxUint256]
875
+ });
876
+ try {
877
+ const sent = await account.sendCalls([{ to: usdc.address, data }]);
878
+ const id = typeof sent === "string" ? sent : sent?.id;
879
+ if (!id) throw new Error("approval submitted but no call id was returned");
880
+ return id;
881
+ } catch (err) {
882
+ throw explainUnchargeableSender(err, config.sessionAddress);
883
+ }
884
+ }
673
885
  async request(method, params) {
674
886
  const { account, config } = await this.getSession();
675
887
  switch (method) {
@@ -679,24 +891,25 @@ var SessionBridge = class {
679
891
  case "wallet_sendCalls": {
680
892
  const payload = Array.isArray(params) ? params[0] : params;
681
893
  const { calls } = payload;
682
- return account.sendCalls(calls, {
683
- permissionId: config.permissionId
684
- });
894
+ const sendOptions = { permissionId: config.permissionId };
895
+ try {
896
+ return await account.sendCalls(calls, sendOptions);
897
+ } catch (err) {
898
+ throw explainUnchargeableSender(err, config.sessionAddress);
899
+ }
685
900
  }
686
901
  case "wallet_getCallsStatus": {
687
902
  const batchId = Array.isArray(params) ? params[0] : params;
688
903
  return account.getCallStatus(batchId);
689
904
  }
690
- case "personal_sign": {
691
- const message = Array.isArray(params) ? params[0] : params;
692
- return account.signMessage(message);
693
- }
694
- case "eth_signTypedData_v4": {
695
- const asArray = Array.isArray(params) ? params : [params];
696
- const raw = asArray.length > 1 ? asArray[1] : asArray[0];
697
- const typedData = typeof raw === "string" ? JSON.parse(raw) : raw;
698
- return account.signTypedData(typedData);
699
- }
905
+ // Refused rather than absent, so the reason is on screen instead of a
906
+ // caller reading "not supported in auto mode" and looking for a flag. See
907
+ // `supportsSessionMode` in rpc-classifier.ts for why.
908
+ case "personal_sign":
909
+ case "eth_signTypedData_v4":
910
+ throw new Error(
911
+ `${method} is not available in auto mode: a signature the session makes is not a call, so it never reaches the spend caps or the ledger. Run it through the browser instead.`
912
+ );
700
913
  case "wallet_grantPermissions":
701
914
  throw new Error("Requires browser \u2014 run `jaw session setup`.");
702
915
  case "wallet_revokePermissions":
@@ -714,9 +927,7 @@ var SESSION_SUPPORTED_METHODS = /* @__PURE__ */ new Set([
714
927
  "eth_requestAccounts",
715
928
  "eth_accounts",
716
929
  "wallet_sendCalls",
717
- "wallet_getCallsStatus",
718
- "personal_sign",
719
- "eth_signTypedData_v4"
930
+ "wallet_getCallsStatus"
720
931
  ]);
721
932
  function supportsSessionMode(method) {
722
933
  return SESSION_SUPPORTED_METHODS.has(method);
@@ -740,32 +951,31 @@ function envSessionEnabled() {
740
951
  const value = process.env["JAW_SESSION"]?.toLowerCase();
741
952
  return value === "1" || value === "true";
742
953
  }
743
- var SIGN_RATE_WINDOW_MS = 6e4;
744
- var MAX_SIGNS_PER_WINDOW = 5;
745
- var SESSION_SIGNING_METHODS = ["wallet_sendCalls", "personal_sign", "eth_signTypedData_v4"];
954
+ var SEND_RATE_WINDOW_MS = 6e4;
955
+ var MAX_SENDS_PER_WINDOW = 5;
956
+ var RATE_LIMITED_SESSION_METHODS = ["wallet_sendCalls"];
746
957
  function registerRpcTool(server) {
747
- const recentSigns = [];
748
- function assertUnderSignLimit() {
958
+ const recentSends = [];
959
+ function assertUnderSendLimit() {
749
960
  const now = Date.now();
750
- while (recentSigns.length && now - recentSigns[0] > SIGN_RATE_WINDOW_MS) recentSigns.shift();
751
- if (recentSigns.length >= MAX_SIGNS_PER_WINDOW) {
752
- throw new Error("Autonomous signing rate limit reached, retry shortly or call again with session: false.");
961
+ while (recentSends.length && now - recentSends[0] > SEND_RATE_WINDOW_MS) recentSends.shift();
962
+ if (recentSends.length >= MAX_SENDS_PER_WINDOW) {
963
+ throw new Error("Autonomous send rate limit reached, retry shortly or call again with session: false.");
753
964
  }
754
- recentSigns.push(now);
965
+ recentSends.push(now);
755
966
  }
756
967
  server.registerTool(
757
968
  "jaw_rpc",
758
969
  {
759
- description: "Execute any JAW.id wallet RPC method. Supports transactions, signing, permissions, and queries. By default, methods that require signing open the browser for passkey authentication. Pass session: true to sign autonomously with the local session key instead (requires a session created via `jaw session setup` \u2014 check jaw_session_status). IMPORTANT: Read the jaw://api-reference resource for the full list of methods, and jaw://api-reference/{method} for detailed parameter formats and examples.",
970
+ description: "Execute any JAW.id wallet RPC method. Supports transactions, signing, permissions, and queries. By default, any method that uses the account opens the browser for passkey authentication. Pass session: true to send transactions autonomously with the local session key instead (requires a session created via `jaw session setup` \u2014 check jaw_session_status). Session mode sends, it does not sign: personal_sign and eth_signTypedData_v4 always open the browser, and asking for either with session: true is refused rather than routed. IMPORTANT: Read the jaw://api-reference resource for the full list of methods, and jaw://api-reference/{method} for detailed parameter formats and examples.",
760
971
  inputSchema: rpcMethodSchema
761
972
  },
762
- // @ts-expect-error — MCP SDK deep type inference with z.any() in schema
763
973
  async (params) => {
764
974
  try {
765
975
  const config = loadConfig();
766
976
  const apiKey = resolveApiKey(config);
767
- const chainId = resolveChainId(params.chainId, config);
768
977
  const useSession = params.session ?? envSessionEnabled();
978
+ const chainId = useSession && params.chainId === void 0 ? tryLoadSessionConfig()?.chainId ?? resolveChainId(void 0, config) : resolveChainId(params.chainId, config);
769
979
  let bridge;
770
980
  if (useSession) {
771
981
  if (!supportsSessionMode(params.method)) {
@@ -773,8 +983,8 @@ function registerRpcTool(server) {
773
983
  `Method ${params.method} is not supported in session mode. Call again with session: false to route through the browser bridge.`
774
984
  );
775
985
  }
776
- if (SESSION_SIGNING_METHODS.includes(params.method)) {
777
- assertUnderSignLimit();
986
+ if (RATE_LIMITED_SESSION_METHODS.includes(params.method)) {
987
+ assertUnderSendLimit();
778
988
  }
779
989
  bridge = new SessionBridge({ apiKey, chainId });
780
990
  } else {
@@ -782,8 +992,7 @@ function registerRpcTool(server) {
782
992
  keysUrl: config.keysUrl,
783
993
  apiKey,
784
994
  chainId,
785
- ens: config.ens,
786
- paymasterUrl: config.paymasters?.[chainId]?.url
995
+ ens: config.ens
787
996
  });
788
997
  }
789
998
  try {