@jaw.id/cli 0.1.25 → 0.2.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 (75) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +4 -0
  4. package/dist/base-command.js +3 -1
  5. package/dist/base-command.js.map +1 -1
  6. package/dist/commands/config/set.js +140 -12
  7. package/dist/commands/config/set.js.map +1 -1
  8. package/dist/commands/config/show.js +3 -1
  9. package/dist/commands/config/show.js.map +1 -1
  10. package/dist/commands/config/write.js +6 -4
  11. package/dist/commands/config/write.js.map +1 -1
  12. package/dist/commands/disconnect.js +24 -10
  13. package/dist/commands/disconnect.js.map +1 -1
  14. package/dist/commands/mcp/index.js +2426 -94
  15. package/dist/commands/mcp/index.js.map +1 -1
  16. package/dist/commands/rpc/call.js +197 -45
  17. package/dist/commands/rpc/call.js.map +1 -1
  18. package/dist/commands/session/add.js +1547 -0
  19. package/dist/commands/session/add.js.map +1 -0
  20. package/dist/commands/session/revoke.js +181 -54
  21. package/dist/commands/session/revoke.js.map +1 -1
  22. package/dist/commands/session/setup.js +516 -65
  23. package/dist/commands/session/setup.js.map +1 -1
  24. package/dist/commands/session/status.js +315 -6
  25. package/dist/commands/session/status.js.map +1 -1
  26. package/dist/commands/version.js +3 -1
  27. package/dist/commands/version.js.map +1 -1
  28. package/dist/commands/x402/log.js +344 -0
  29. package/dist/commands/x402/log.js.map +1 -0
  30. package/dist/commands/x402/pay.js +2122 -0
  31. package/dist/commands/x402/pay.js.map +1 -0
  32. package/dist/commands/x402/status.js +1047 -0
  33. package/dist/commands/x402/status.js.map +1 -0
  34. package/dist/index.js +41 -14
  35. package/dist/index.js.map +1 -1
  36. package/dist/lib/bridge-singleton.js +41 -14
  37. package/dist/lib/bridge-singleton.js.map +1 -1
  38. package/dist/lib/config.js +26 -3
  39. package/dist/lib/config.js.map +1 -1
  40. package/dist/lib/keystore.js +13 -2
  41. package/dist/lib/keystore.js.map +1 -1
  42. package/dist/lib/paths.js +3 -1
  43. package/dist/lib/paths.js.map +1 -1
  44. package/dist/lib/payment-lock.js +121 -0
  45. package/dist/lib/payment-lock.js.map +1 -0
  46. package/dist/lib/session-bridge.js +148 -24
  47. package/dist/lib/session-bridge.js.map +1 -1
  48. package/dist/lib/session-config.js +78 -11
  49. package/dist/lib/session-config.js.map +1 -1
  50. package/dist/lib/terminal.js +22 -0
  51. package/dist/lib/terminal.js.map +1 -0
  52. package/dist/lib/validation.js +3 -3
  53. package/dist/lib/validation.js.map +1 -1
  54. package/dist/lib/ws-bridge.js +22 -10
  55. package/dist/lib/ws-bridge.js.map +1 -1
  56. package/dist/mcp/handlers/config.js +73 -6
  57. package/dist/mcp/handlers/config.js.map +1 -1
  58. package/dist/mcp/handlers/daemon.js +43 -12
  59. package/dist/mcp/handlers/daemon.js.map +1 -1
  60. package/dist/mcp/handlers/resources.js +119 -0
  61. package/dist/mcp/handlers/resources.js.map +1 -1
  62. package/dist/mcp/handlers/rpc.js +269 -60
  63. package/dist/mcp/handlers/rpc.js.map +1 -1
  64. package/dist/mcp/helpers.js +50 -3
  65. package/dist/mcp/helpers.js.map +1 -1
  66. package/dist/mcp/server.js +2426 -94
  67. package/dist/mcp/server.js.map +1 -1
  68. package/dist/mcp/tools.js +43 -3
  69. package/dist/mcp/tools.js.map +1 -1
  70. package/dist/x402/log-view.js +160 -0
  71. package/dist/x402/log-view.js.map +1 -0
  72. package/dist/x402/status-report.js +90 -0
  73. package/dist/x402/status-report.js.map +1 -0
  74. package/oclif.manifest.json +398 -4
  75. package/package.json +8 -3
@@ -1,9 +1,11 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import * as fs from 'fs';
2
+ import * as fs4 from 'fs';
3
3
  import * as path from 'path';
4
4
  import * as os from 'os';
5
5
  import * as crypto from 'crypto';
6
6
  import WebSocket from 'ws';
7
+ import { parseUnits, formatUnits, erc20Abi, createPublicClient, http } from 'viem';
8
+ import { polygonAmoy, polygon, baseSepolia, base } from 'viem/chains';
7
9
 
8
10
  // src/commands/session/setup.ts
9
11
  var JAW_DIR = path.join(os.homedir(), ".jaw");
@@ -13,13 +15,15 @@ var PATHS = {
13
15
  session: path.join(JAW_DIR, "session.json"),
14
16
  relay: path.join(JAW_DIR, "relay.json"),
15
17
  keystore: path.join(JAW_DIR, "keystore.json"),
16
- sessionConfig: path.join(JAW_DIR, "session-config.json")
18
+ sessionConfig: path.join(JAW_DIR, "session-config.json"),
19
+ x402Log: path.join(JAW_DIR, "x402-log.jsonl"),
20
+ paymentLock: path.join(JAW_DIR, "x402-payment.lock")
17
21
  };
18
22
 
19
23
  // src/lib/validation.ts
20
24
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
21
25
  var SELECTOR_RE = /^0x[0-9a-fA-F]{8}$/;
22
- var HEX_RE = /^0x[0-9a-fA-F]+$/;
26
+ var ALLOWANCE_RE = /^(0x[0-9a-fA-F]+|[0-9]+)$/;
23
27
  var VALID_SPEND_UNITS = /* @__PURE__ */ new Set(["minute", "hour", "day", "week", "month", "year", "forever"]);
24
28
  function parsePermissionsConfig(raw) {
25
29
  const errors = [];
@@ -64,8 +68,8 @@ function parsePermissionsConfig(raw) {
64
68
  if (typeof s.token !== "string" || !ADDRESS_RE.test(s.token)) {
65
69
  errors.push(`spends.${i}.token: Must be a valid 0x address (40 hex chars)`);
66
70
  }
67
- if (typeof s.allowance !== "string" || !HEX_RE.test(s.allowance)) {
68
- errors.push(`spends.${i}.allowance: Must be a non-empty 0x hex value`);
71
+ if (typeof s.allowance !== "string" || !ALLOWANCE_RE.test(s.allowance)) {
72
+ errors.push(`spends.${i}.allowance: Must be a decimal or 0x hex integer`);
69
73
  }
70
74
  if (typeof s.unit !== "string" || !VALID_SPEND_UNITS.has(s.unit)) {
71
75
  errors.push(`spends.${i}.unit: Must be one of: ${[...VALID_SPEND_UNITS].join(", ")}`);
@@ -106,8 +110,8 @@ function isValidRelayUrl(url) {
106
110
 
107
111
  // src/lib/config.ts
108
112
  function ensureDir(dir) {
109
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
110
- fs.chmodSync(dir, 448);
113
+ fs4.mkdirSync(dir, { recursive: true, mode: 448 });
114
+ fs4.chmodSync(dir, 448);
111
115
  }
112
116
  function migrateConfig(config) {
113
117
  if (config.paymasterUrl && !config.paymasters) {
@@ -119,10 +123,10 @@ function migrateConfig(config) {
119
123
  return config;
120
124
  }
121
125
  function loadConfig() {
122
- if (!fs.existsSync(PATHS.config)) {
126
+ if (!fs4.existsSync(PATHS.config)) {
123
127
  return {};
124
128
  }
125
- const raw = fs.readFileSync(PATHS.config, "utf-8");
129
+ const raw = fs4.readFileSync(PATHS.config, "utf-8");
126
130
  try {
127
131
  const config = JSON.parse(raw);
128
132
  return migrateConfig(config);
@@ -134,7 +138,7 @@ function loadConfig() {
134
138
  }
135
139
  function saveConfig(config) {
136
140
  ensureDir(PATHS.root);
137
- fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
141
+ fs4.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
138
142
  encoding: "utf-8",
139
143
  mode: 384
140
144
  });
@@ -288,7 +292,18 @@ function bufferToBase64(buf) {
288
292
  }
289
293
 
290
294
  // src/lib/ws-bridge.ts
295
+ function buildInitPayload(config) {
296
+ return {
297
+ type: "init",
298
+ apiKey: config.apiKey,
299
+ chainId: config.chainId,
300
+ ens: config.ens,
301
+ paymasterUrl: config.paymasterUrl,
302
+ ...config.paymasterUrl && config.paymasterContext ? { paymasterContext: config.paymasterContext } : {}
303
+ };
304
+ }
291
305
  var DEFAULT_TIMEOUT_MS = 12e4;
306
+ var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
292
307
  var MAX_MESSAGE_BYTES = 5 * 1024 * 1024;
293
308
  var BROWSER_REOPEN_COOLDOWN_MS = 5e3;
294
309
  var MAX_RECONNECT_ATTEMPTS = 3;
@@ -297,6 +312,7 @@ var WSBridge = class {
297
312
  relayUrl;
298
313
  session;
299
314
  timeout;
315
+ connectTimeout;
300
316
  config;
301
317
  privateKeyHex;
302
318
  publicKeyHex;
@@ -318,6 +334,7 @@ var WSBridge = class {
318
334
  this.relayUrl = options.relayUrl;
319
335
  this.session = options.session;
320
336
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
337
+ this.connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
321
338
  this.config = options.config;
322
339
  this.privateKeyHex = options.privateKeyHex;
323
340
  this.publicKeyHex = options.publicKeyHex;
@@ -346,17 +363,16 @@ var WSBridge = class {
346
363
  let expectingKeyExchange = !this.peerPublicKeyHex;
347
364
  const timer = setTimeout(() => {
348
365
  ws.close();
349
- reject(new Error("Browser did not connect in time.\nRun `jaw disconnect` then try again."));
350
- }, 3e4);
366
+ reject(
367
+ new Error(
368
+ `Browser did not connect within ${Math.round(this.connectTimeout / 1e3)}s.
369
+ Run \`jaw disconnect\` then try again, or raise JAW_BRIDGE_TIMEOUT_MS.`
370
+ )
371
+ );
372
+ }, this.connectTimeout);
351
373
  const sendEncryptedInit = async () => {
352
374
  if (!this.sharedSecret) return;
353
- const envelope = await encryptMessage(this.sharedSecret, {
354
- type: "init",
355
- apiKey: this.config.apiKey,
356
- chainId: this.config.chainId,
357
- ens: this.config.ens,
358
- paymasterUrl: this.config.paymasterUrl
359
- });
375
+ const envelope = await encryptMessage(this.sharedSecret, buildInitPayload(this.config));
360
376
  this.sendRaw(ws, JSON.stringify({ type: "encrypted", ...envelope }));
361
377
  };
362
378
  const waitForReady = () => {
@@ -604,8 +620,8 @@ function safeParse(data) {
604
620
  }
605
621
  function loadRelaySession() {
606
622
  try {
607
- if (!fs.existsSync(PATHS.relay)) return null;
608
- const raw = fs.readFileSync(PATHS.relay, "utf-8");
623
+ if (!fs4.existsSync(PATHS.relay)) return null;
624
+ const raw = fs4.readFileSync(PATHS.relay, "utf-8");
609
625
  const parsed = JSON.parse(raw);
610
626
  if (!parsed.session || !parsed.relayUrl || !parsed.privateKey || !parsed.publicKey) {
611
627
  return null;
@@ -617,14 +633,14 @@ function loadRelaySession() {
617
633
  }
618
634
  function saveRelaySession(info) {
619
635
  ensureDir(PATHS.root);
620
- fs.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
636
+ fs4.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
621
637
  encoding: "utf-8",
622
638
  mode: 384
623
639
  });
624
640
  }
625
641
  function deleteRelaySession() {
626
642
  try {
627
- if (fs.existsSync(PATHS.relay)) fs.unlinkSync(PATHS.relay);
643
+ if (fs4.existsSync(PATHS.relay)) fs4.unlinkSync(PATHS.relay);
628
644
  } catch {
629
645
  }
630
646
  }
@@ -634,6 +650,10 @@ var DEFAULT_KEYS_URL = "https://keys.jaw.id";
634
650
  var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
635
651
  async function getBridge(options) {
636
652
  const config = loadConfig();
653
+ const envTimeout = Number(process.env["JAW_BRIDGE_TIMEOUT_MS"]);
654
+ const fromEnv = Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0;
655
+ const timeout = options.timeout ?? fromEnv;
656
+ const connectTimeout = options.connectTimeout ?? fromEnv;
637
657
  const keysUrl = options.keysUrl ?? config.keysUrl ?? DEFAULT_KEYS_URL;
638
658
  const relayUrl = options.relayUrl ?? config.relayUrl ?? DEFAULT_RELAY_URL;
639
659
  const chainId = options.chainId ?? config.defaultChain ?? 1;
@@ -646,7 +666,7 @@ async function getBridge(options) {
646
666
  let relaySession = loadRelaySession();
647
667
  if (relaySession && relaySession.relayUrl === relayUrl && relaySession.peerPublicKey) {
648
668
  try {
649
- return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl, false);
669
+ return await connectBridge({ ...options, timeout }, relaySession, chainId, keysUrl, relayUrl, false);
650
670
  } catch {
651
671
  deleteRelaySession();
652
672
  relaySession = null;
@@ -656,7 +676,7 @@ async function getBridge(options) {
656
676
  }
657
677
  const session = await createNewSession(relayUrl);
658
678
  saveRelaySession(session);
659
- return await connectBridge(session, options, chainId, keysUrl, relayUrl, true);
679
+ return await connectBridge({ ...options, timeout, connectTimeout }, session, chainId, keysUrl, relayUrl, true);
660
680
  }
661
681
  async function createNewSession(relayUrl) {
662
682
  const kp = await generateKeyPair();
@@ -671,17 +691,20 @@ async function createNewSession(relayUrl) {
671
691
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
672
692
  };
673
693
  }
674
- async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl, openBrowser) {
694
+ async function connectBridge(options, relaySession, chainId, keysUrl, relayUrl, openBrowser) {
675
695
  const config = loadConfig();
696
+ const paymaster = config.paymasters?.[chainId];
676
697
  const bridge = new WSBridge({
677
698
  relayUrl,
678
699
  session: relaySession.session,
679
700
  timeout: options.timeout,
701
+ connectTimeout: options.connectTimeout,
680
702
  config: {
681
703
  apiKey: options.apiKey,
682
704
  chainId,
683
705
  ens: options.ens ?? config.ens,
684
- paymasterUrl: options.paymasterUrl ?? config.paymasters?.[chainId]?.url
706
+ paymasterUrl: paymaster?.url,
707
+ paymasterContext: paymaster?.context
685
708
  },
686
709
  privateKeyHex: relaySession.privateKey,
687
710
  publicKeyHex: relaySession.publicKey,
@@ -691,6 +714,12 @@ async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl,
691
714
  // onBrowserNeeded — only open a browser for new sessions
692
715
  openBrowser ? async () => {
693
716
  const bridgeUrl = buildBridgeUrl(keysUrl, relaySession.session, relayUrl, relaySession.publicKey);
717
+ if (process.env["JAW_NO_BROWSER"]) {
718
+ process.stderr.write(`Open this URL to approve:
719
+ ${bridgeUrl}
720
+ `);
721
+ return;
722
+ }
694
723
  const { default: open } = await import('open');
695
724
  await open(bridgeUrl);
696
725
  } : void 0,
@@ -721,17 +750,17 @@ function saveKeystore(privateKeyHex, address) {
721
750
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
722
751
  };
723
752
  ensureDir(PATHS.root);
724
- fs.writeFileSync(PATHS.keystore, JSON.stringify(keystore, null, 2) + "\n", {
753
+ fs4.writeFileSync(PATHS.keystore, JSON.stringify(keystore, null, 2) + "\n", {
725
754
  encoding: "utf-8",
726
755
  mode: 384
727
756
  });
728
- fs.chmodSync(PATHS.keystore, 384);
757
+ fs4.chmodSync(PATHS.keystore, 384);
729
758
  }
730
759
  function loadSessionKey() {
731
- if (!fs.existsSync(PATHS.keystore)) {
760
+ if (!fs4.existsSync(PATHS.keystore)) {
732
761
  throw new Error("No session configured. Run `jaw session setup` first.");
733
762
  }
734
- const contents = fs.readFileSync(PATHS.keystore, "utf-8");
763
+ const contents = fs4.readFileSync(PATHS.keystore, "utf-8");
735
764
  let parsed;
736
765
  try {
737
766
  parsed = JSON.parse(contents);
@@ -740,37 +769,370 @@ function loadSessionKey() {
740
769
  }
741
770
  return parsed.privateKey;
742
771
  }
772
+ function tryLoadKeystoreAddress() {
773
+ try {
774
+ if (!fs4.existsSync(PATHS.keystore)) return null;
775
+ const parsed = JSON.parse(fs4.readFileSync(PATHS.keystore, "utf-8"));
776
+ return parsed.address ?? null;
777
+ } catch {
778
+ return null;
779
+ }
780
+ }
743
781
  function keystoreExists() {
744
- return fs.existsSync(PATHS.keystore);
782
+ return fs4.existsSync(PATHS.keystore);
783
+ }
784
+ var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
785
+ var SELECTOR_RE2 = /^0x[0-9a-fA-F]{8}$/;
786
+ var HEX_RE = /^0x[0-9a-fA-F]+$/;
787
+ var ALLOWANCE_RE2 = /^(0x[0-9a-fA-F]+|[0-9]+)$/;
788
+ var SPEND_UNITS = /* @__PURE__ */ new Set(["minute", "hour", "day", "week", "month", "year", "forever"]);
789
+ function isPositiveInt(value) {
790
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
791
+ }
792
+ function parseGrantedPermission(raw) {
793
+ if (typeof raw !== "object" || raw === null) return void 0;
794
+ const r = raw;
795
+ const { account, spender, salt } = r;
796
+ if (typeof account !== "string" || !ADDRESS_RE2.test(account)) return void 0;
797
+ if (typeof spender !== "string" || !ADDRESS_RE2.test(spender)) return void 0;
798
+ if (typeof salt !== "string" || !HEX_RE.test(salt)) return void 0;
799
+ if (!isPositiveInt(r.start) || !isPositiveInt(r.end)) return void 0;
800
+ if (!Array.isArray(r.calls) || r.calls.length === 0) return void 0;
801
+ const calls = [];
802
+ for (const entry of r.calls) {
803
+ if (typeof entry !== "object" || entry === null) return void 0;
804
+ const { target, selector } = entry;
805
+ if (typeof target !== "string" || !ADDRESS_RE2.test(target)) return void 0;
806
+ if (typeof selector !== "string" || !SELECTOR_RE2.test(selector)) return void 0;
807
+ calls.push({ target, selector });
808
+ }
809
+ if (!Array.isArray(r.spends)) return void 0;
810
+ const spends = [];
811
+ for (const entry of r.spends) {
812
+ if (typeof entry !== "object" || entry === null) return void 0;
813
+ const { token, allowance, unit, multiplier } = entry;
814
+ if (typeof token !== "string" || !ADDRESS_RE2.test(token)) return void 0;
815
+ if (typeof allowance !== "string" || !ALLOWANCE_RE2.test(allowance)) return void 0;
816
+ if (typeof unit !== "string" || !SPEND_UNITS.has(unit)) return void 0;
817
+ if (!isPositiveInt(multiplier) || multiplier > 65535) return void 0;
818
+ spends.push({ token, allowance, unit, multiplier });
819
+ }
820
+ return { account, spender, start: r.start, end: r.end, salt, calls, spends };
821
+ }
822
+ function liveOrphans(orphans, now = Date.now() / 1e3) {
823
+ return (orphans ?? []).filter((orphan) => orphan.expiry > now);
745
824
  }
746
825
  function saveSessionConfig(input) {
747
- const config = {
748
- ...input,
749
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
750
- };
826
+ writeSessionConfig({ ...input, createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString() });
827
+ }
828
+ function writeSessionConfig(config) {
751
829
  ensureDir(PATHS.root);
752
- fs.writeFileSync(PATHS.sessionConfig, JSON.stringify(config, null, 2) + "\n", {
753
- encoding: "utf-8",
754
- mode: 384
755
- });
756
- fs.chmodSync(PATHS.sessionConfig, 384);
830
+ const temp = `${PATHS.sessionConfig}.${process.pid}.tmp`;
831
+ fs4.writeFileSync(temp, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
832
+ fs4.chmodSync(temp, 384);
833
+ fs4.renameSync(temp, PATHS.sessionConfig);
757
834
  }
758
835
  function loadSessionConfig() {
759
- if (!fs.existsSync(PATHS.sessionConfig)) {
836
+ if (!fs4.existsSync(PATHS.sessionConfig)) {
760
837
  throw new Error("No session configured. Run `jaw session setup` first.");
761
838
  }
762
- const raw = fs.readFileSync(PATHS.sessionConfig, "utf-8");
839
+ const raw = fs4.readFileSync(PATHS.sessionConfig, "utf-8");
763
840
  try {
764
841
  return JSON.parse(raw);
765
842
  } catch {
766
843
  throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \`jaw session setup\` to recreate it.`);
767
844
  }
768
845
  }
846
+ function tryLoadSessionConfig() {
847
+ try {
848
+ return loadSessionConfig();
849
+ } catch {
850
+ return null;
851
+ }
852
+ }
853
+
854
+ // src/x402/asset-registry.ts
855
+ var USDC_BY_NETWORK = {
856
+ "eip155:8453": {
857
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
858
+ chainId: 8453,
859
+ wireNetwork: "eip155:8453",
860
+ usdcName: "USD Coin",
861
+ usdcVersion: "2",
862
+ decimals: 6
863
+ },
864
+ "eip155:84532": {
865
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
866
+ chainId: 84532,
867
+ wireNetwork: "eip155:84532",
868
+ usdcName: "USDC",
869
+ usdcVersion: "2",
870
+ decimals: 6
871
+ },
872
+ "eip155:137": {
873
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
874
+ chainId: 137,
875
+ wireNetwork: "eip155:137",
876
+ usdcName: "USD Coin",
877
+ usdcVersion: "2",
878
+ decimals: 6
879
+ },
880
+ "eip155:80002": {
881
+ address: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
882
+ chainId: 80002,
883
+ wireNetwork: "eip155:80002",
884
+ usdcName: "USDC",
885
+ usdcVersion: "2",
886
+ decimals: 6
887
+ }
888
+ };
889
+ function usdcForNetwork(network) {
890
+ return Object.hasOwn(USDC_BY_NETWORK, network) ? USDC_BY_NETWORK[network] : void 0;
891
+ }
892
+
893
+ // src/x402/grant-preset.ts
894
+ var TRANSFER_SIGNATURE = "transfer(address,uint256)";
895
+ var MAX_ALLOWANCE = 2n ** 160n - 1n;
896
+ var LIMIT_PERIODS = ["minute", "hour", "day", "week", "month", "year", "forever"];
897
+ var DEFAULT_X402_LIMIT = "10/day";
898
+ function parseLimit(input) {
899
+ const trimmed = input.trim();
900
+ if (!trimmed) throw new Error("Limit is empty. Use --limit <amount>/<period>, for example 10/day.");
901
+ const [rawAmount, rawPeriod = "day", ...rest] = trimmed.split("/");
902
+ if (rest.length > 0) {
903
+ throw new Error(`Invalid limit "${input}". Expected <amount>/<period>, for example 10/day.`);
904
+ }
905
+ const amount = rawAmount.trim();
906
+ if (!/^\d+(\.\d+)?$/.test(amount)) {
907
+ throw new Error(`Invalid limit amount "${rawAmount}". Expected a positive number, for example 10 or 2.5.`);
908
+ }
909
+ const period = rawPeriod.trim().toLowerCase();
910
+ if (!LIMIT_PERIODS.includes(period)) {
911
+ throw new Error(`Invalid limit period "${rawPeriod}". Expected one of: ${LIMIT_PERIODS.join(", ")}.`);
912
+ }
913
+ return { amount, period };
914
+ }
915
+ function buildX402Permissions(chainId, limit = DEFAULT_X402_LIMIT) {
916
+ const usdc = Object.values(USDC_BY_NETWORK).find((asset) => asset.chainId === chainId);
917
+ if (!usdc) {
918
+ const supported = Object.values(USDC_BY_NETWORK).map((a) => a.chainId).sort((a, b) => a - b).join(", ");
919
+ throw new Error(`No USDC configured for chain ${chainId}. x402 payments are supported on: ${supported}.`);
920
+ }
921
+ const { amount, period } = parseLimit(limit);
922
+ let allowance;
923
+ try {
924
+ allowance = parseUnits(amount, usdc.decimals);
925
+ } catch {
926
+ throw new Error(`Invalid limit amount "${amount}" for a token with ${usdc.decimals} decimals.`);
927
+ }
928
+ if (allowance <= 0n) {
929
+ throw new Error(`Limit "${limit}" resolves to zero, which would refuse every payment.`);
930
+ }
931
+ if (allowance > MAX_ALLOWANCE) {
932
+ throw new Error(`Limit "${limit}" is larger than a spend allowance can hold (max ${MAX_ALLOWANCE}).`);
933
+ }
934
+ return {
935
+ calls: [{ target: usdc.address, functionSignature: TRANSFER_SIGNATURE }],
936
+ spends: [{ token: usdc.address, allowance: allowance.toString(), unit: period, multiplier: 1 }]
937
+ };
938
+ }
939
+ function describeX402Grant(limit = DEFAULT_X402_LIMIT) {
940
+ const { amount, period } = parseLimit(limit);
941
+ const per = period === "forever" ? "in total" : `per ${period}`;
942
+ return `${amount} USDC ${per}, transfers only`;
943
+ }
944
+ var JAW_RPC_URL = "https://api.justaname.id/proxy/v1/rpc";
945
+ var CHAINS = {
946
+ [base.id]: base,
947
+ [baseSepolia.id]: baseSepolia,
948
+ [polygon.id]: polygon,
949
+ [polygonAmoy.id]: polygonAmoy
950
+ };
951
+ for (const chainId of Object.values(USDC_BY_NETWORK).map((a) => a.chainId)) {
952
+ if (!CHAINS[chainId]) {
953
+ throw new Error(
954
+ `x402 balance: USDC registry has chain ${chainId} but no viem chain is mapped for it in balance.ts`
955
+ );
956
+ }
957
+ }
958
+ var clients = /* @__PURE__ */ new Map();
959
+ function rpcTransport(chainId, apiKey) {
960
+ if (!apiKey) return http();
961
+ return http(`${JAW_RPC_URL}?chainId=${chainId}&api-key=${apiKey}`);
962
+ }
963
+ function publicClientFor(chainId) {
964
+ const chain = CHAINS[chainId];
965
+ if (!chain) throw new Error(`x402: no viem chain configured for chainId ${chainId}`);
966
+ const apiKey = loadConfig().apiKey;
967
+ const key = `${chainId}:${apiKey ?? ""}`;
968
+ let client = clients.get(key);
969
+ if (!client) {
970
+ client = createPublicClient({ chain, transport: rpcTransport(chainId, apiKey) });
971
+ clients.set(key, client);
972
+ }
973
+ return client;
974
+ }
975
+ var readOnChain = (asset, owner) => publicClientFor(asset.chainId).readContract({
976
+ address: asset.address,
977
+ abi: erc20Abi,
978
+ functionName: "balanceOf",
979
+ args: [owner]
980
+ });
981
+ async function usdcBalance(network, owner, read = readOnChain) {
982
+ const asset = usdcForNetwork(network);
983
+ if (!asset) throw new Error(`Unsupported x402 network: ${network}`);
984
+ const raw = await read(asset, owner);
985
+ return { network, asset: asset.address, raw: raw.toString(), formatted: formatUnits(raw, asset.decimals) };
986
+ }
987
+
988
+ // src/x402/gas-reserve.ts
989
+ function gasReserve(asset) {
990
+ return 10n ** BigInt(asset.decimals) / 10n;
991
+ }
992
+ function firstOperationCost(asset) {
993
+ return 10n ** BigInt(asset.decimals) / 100n;
994
+ }
995
+
996
+ // src/x402/amount.ts
997
+ function parseBigInt(value) {
998
+ if (value === void 0 || value === null || value === "") return null;
999
+ try {
1000
+ return BigInt(value);
1001
+ } catch {
1002
+ return null;
1003
+ }
1004
+ }
1005
+
1006
+ // src/lib/terminal.ts
1007
+ var INVISIBLE_AND_BIDI = /[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g;
1008
+ var LINE_CONTROLS = /[\u0000-\u001F\u007F-\u009F]/g;
1009
+ var REPLACEMENT = "\uFFFD";
1010
+ var DEFAULT_LINE_LENGTH = 200;
1011
+ function bound(text, maxLength) {
1012
+ if (text.length <= maxLength) return text;
1013
+ return `${text.slice(0, maxLength)}\u2026 (${text.length - maxLength} more characters)`;
1014
+ }
1015
+ function sanitizeLine(value, maxLength = DEFAULT_LINE_LENGTH) {
1016
+ const text = typeof value === "string" ? value : String(value);
1017
+ return bound(text.replace(LINE_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT), maxLength);
1018
+ }
1019
+
1020
+ // src/x402/status-report.ts
1021
+ function formatUsdc(base2, decimals) {
1022
+ if (base2 === void 0) return "unlimited";
1023
+ const value = parseBigInt(base2);
1024
+ if (value === null) return `${sanitizeLine(base2, 32)} (invalid)`;
1025
+ const scale = 10n ** BigInt(decimals);
1026
+ const whole = value / scale;
1027
+ const frac = (value % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
1028
+ return `${whole}${frac ? `.${frac}` : ""} USDC`;
1029
+ }
1030
+
1031
+ // src/x402/funded-owner.ts
1032
+ async function whyOwnerCannotFundSession(check) {
1033
+ const asset = usdcForNetwork(`eip155:${check.chainId}`);
1034
+ if (!asset) return null;
1035
+ const accounts = await check.request("eth_requestAccounts");
1036
+ const owner = accounts?.[0];
1037
+ if (!owner) return null;
1038
+ const read = check.readBalance ?? (async (network, address) => BigInt((await usdcBalance(network, address)).raw));
1039
+ let held;
1040
+ try {
1041
+ held = await read(asset.wireNetwork, owner);
1042
+ } catch {
1043
+ return null;
1044
+ }
1045
+ const needed = gasReserve(asset);
1046
+ if (held >= needed) return null;
1047
+ return `${owner} holds ${formatUsdc(held.toString(), asset.decimals)} on chain ${check.chainId}, and setting up a session needs at least ${formatUsdc(needed.toString(), asset.decimals)} there. That much rides along in the grant so the session can pay for its own first transaction. Fund the account and run this again.`;
1048
+ }
1049
+ async function whySpenderCannotPay(check) {
1050
+ const asset = usdcForNetwork(`eip155:${check.chainId}`);
1051
+ if (!asset) return null;
1052
+ const read = check.readBalance ?? (async (network, address) => BigInt((await usdcBalance(network, address)).raw));
1053
+ let held;
1054
+ let timer;
1055
+ try {
1056
+ const expired = new Promise((_, reject) => {
1057
+ timer = setTimeout(() => reject(new Error("timed out")), check.timeoutMs ?? 5e3);
1058
+ });
1059
+ held = await Promise.race([read(asset.wireNetwork, check.spender), expired]);
1060
+ } catch {
1061
+ return null;
1062
+ } finally {
1063
+ clearTimeout(timer);
1064
+ }
1065
+ const needed = firstOperationCost(asset);
1066
+ if (held >= needed) return null;
1067
+ return `${check.spender} holds ${formatUsdc(held.toString(), asset.decimals)} on chain ${check.chainId}, which is not enough to pay for its first operation. The session pays its own gas, and the grant asked this wallet to send it a little along with the permission. Nothing usable arrived: the wallet may not implement that yet, or it could not price the transfer, or the granted allowance was too small to cover one. Send ${formatUsdc(needed.toString(), asset.decimals)} to that address and the session works from there.`;
1068
+ }
1069
+
1070
+ // src/x402/period.ts
1071
+ function describeSpendPeriod(unit, multiplier) {
1072
+ const n = Math.max(1, Math.floor(multiplier ?? 1));
1073
+ return n === 1 ? unit : `${n} ${unit}s`;
1074
+ }
1075
+ function periodLengthSeconds(unit, multiplier, bound2) {
1076
+ const lengths = {
1077
+ minute: 60,
1078
+ hour: 3600,
1079
+ day: 86400,
1080
+ week: 604800,
1081
+ month: (bound2 === "min" ? 28 : 31) * 86400,
1082
+ year: (bound2 === "min" ? 365 : 366) * 86400,
1083
+ forever: Number.POSITIVE_INFINITY
1084
+ };
1085
+ if (!Object.hasOwn(lengths, unit)) return null;
1086
+ return lengths[unit] * Math.max(1, Math.floor(multiplier));
1087
+ }
1088
+
1089
+ // src/x402/grant-ceiling.ts
1090
+ function whyGrantExceedsCeiling(permissions, chainId, ceiling) {
1091
+ if (!ceiling) return null;
1092
+ let parsed;
1093
+ try {
1094
+ parsed = parseLimit(ceiling);
1095
+ } catch {
1096
+ return `The grant ceiling in your config is not a valid limit: ${ceiling}. Fix it with \`jaw config set grantCeiling <amount>/<period>\`, or remove it.`;
1097
+ }
1098
+ const usdc = Object.values(USDC_BY_NETWORK).find((asset) => asset.chainId === chainId);
1099
+ if (!usdc) {
1100
+ return (permissions.spends ?? []).length > 0 ? `The grant ceiling is set to ${ceiling}, and chain ${chainId} has no USDC in the registry to measure a spend against it. Grant on a supported chain, or remove the ceiling with \`jaw config set grantCeiling ""\`.` : null;
1101
+ }
1102
+ const maxAllowance = parseUnits(parsed.amount, usdc.decimals);
1103
+ const ceilingSeconds = periodLengthSeconds(parsed.period, 1, "max");
1104
+ if (ceilingSeconds === null) return null;
1105
+ for (const spend of permissions.spends ?? []) {
1106
+ if (spend.token.toLowerCase() !== usdc.address.toLowerCase()) {
1107
+ return `The grant ceiling is set to ${ceiling}, and this permission spends ${spend.token}, which cannot be measured against it. Grant USDC, or remove the ceiling with \`jaw config set grantCeiling ""\`.`;
1108
+ }
1109
+ let allowance;
1110
+ try {
1111
+ allowance = BigInt(spend.allowance);
1112
+ } catch {
1113
+ return `This permission asks for an allowance that cannot be read: ${spend.allowance}.`;
1114
+ }
1115
+ if (allowance > maxAllowance) {
1116
+ return `This grant asks for ${formatUsdc(allowance.toString(), usdc.decimals)} per period, over the ${ceiling} ceiling set on this machine. Lower it, or raise the ceiling with \`jaw config set grantCeiling <amount>/<period>\`.`;
1117
+ }
1118
+ const grantSeconds = periodLengthSeconds(spend.unit, spend.multiplier ?? 1, "min");
1119
+ if (grantSeconds === null) {
1120
+ return `This permission uses a spend period this CLI does not recognise: ${spend.unit}.`;
1121
+ }
1122
+ const sameUnit = spend.unit === parsed.period;
1123
+ if (!sameUnit && grantSeconds < ceilingSeconds) {
1124
+ return `This grant resets its allowance every ${describeSpendPeriod(spend.unit, spend.multiplier ?? 1)}, which is more often than the ${ceiling} ceiling set on this machine allows. A shorter period is more money over the same time, even at the same allowance.`;
1125
+ }
1126
+ }
1127
+ return null;
1128
+ }
769
1129
 
770
1130
  // src/commands/session/setup.ts
771
1131
  var SessionSetup = class _SessionSetup extends BaseCommand {
772
1132
  static description = "Generate a session key and grant scoped on-chain permissions (one-time browser approval).";
773
1133
  static examples = [
1134
+ "<%= config.bin %> session setup --chain 8453 --x402",
1135
+ "<%= config.bin %> session setup --chain 8453 --x402 --limit 25/day --expiry 14",
774
1136
  "<%= config.bin %> session setup --chain 84532",
775
1137
  `<%= config.bin %> session setup --permissions '{"calls":[...]}' --expiry 14`,
776
1138
  "<%= config.bin %> session setup --permissions ./permissions.json"
@@ -778,7 +1140,18 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
778
1140
  static flags = {
779
1141
  ...BaseCommand.baseFlags,
780
1142
  permissions: Flags.string({
781
- description: "Permission scope (inline JSON or file path). Overrides config.permissions."
1143
+ description: "Permission scope (inline JSON or file path). Overrides config.permissions.",
1144
+ exclusive: ["x402"]
1145
+ }),
1146
+ x402: Flags.boolean({
1147
+ description: "Grant exactly what x402 payments need on this chain: a USDC transfer capped per period. Builds the permission from the asset registry so the USDC address and function signature do not have to be written by hand. Tune the cap with --limit.",
1148
+ default: false,
1149
+ exclusive: ["permissions"]
1150
+ }),
1151
+ limit: Flags.string({
1152
+ description: `Spend cap for --x402, as <amount>/<period> (default ${DEFAULT_X402_LIMIT}). Examples: 25/day, 2.5/week, 100/month.`
1153
+ // No `dependsOn: ['x402']`: a boolean flag with a default always reads as
1154
+ // provided, so oclif would never fire it. Checked in run() instead.
782
1155
  }),
783
1156
  expiry: Flags.integer({
784
1157
  description: "Permission expiry in days. Overrides config.sessionExpiry."
@@ -790,21 +1163,46 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
790
1163
  const format = flags.output;
791
1164
  const apiKey = this.resolveApiKey(flags);
792
1165
  const chainId = this.resolveChainId(flags);
1166
+ if (flags.limit && !flags.x402) {
1167
+ this.error("--limit only applies to --x402. Re-run with --x402, or set the cap inside --permissions.");
1168
+ }
1169
+ const resolvedPermissions = this.resolvePermissions(flags.permissions, config.permissions, {
1170
+ x402: flags.x402,
1171
+ limit: flags.limit,
1172
+ chainId
1173
+ });
1174
+ const overCeiling = whyGrantExceedsCeiling(resolvedPermissions, chainId, config.grantCeiling);
1175
+ if (overCeiling) this.error(overCeiling);
793
1176
  let reuseKey = null;
794
1177
  let oldPermissionRevoked = false;
1178
+ let orphaned = [];
795
1179
  if (keystoreExists()) {
796
- const existing = loadSessionConfig();
797
- const isActive = existing.expiry > Date.now() / 1e3;
1180
+ const existing = tryLoadSessionConfig();
1181
+ const isActive = existing !== null && existing.expiry > Date.now() / 1e3;
1182
+ orphaned = liveOrphans(existing?.orphanedPermissions);
798
1183
  if (!flags.yes && !process.stdin.isTTY) {
799
1184
  this.error(
800
- "Existing session found, but stdin is not a terminal (piped, redirected, or running in CI). Re-run with --yes to overwrite the existing session non-interactively."
1185
+ "Existing session key found, but stdin is not a terminal (piped, redirected, or running in CI). Re-run with --yes to overwrite the existing session non-interactively."
801
1186
  );
802
1187
  }
803
1188
  if (!flags.yes) {
804
1189
  const readline = await import('readline');
805
1190
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
806
1191
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
807
- if (isActive) {
1192
+ if (!existing) {
1193
+ const orphanAddress = tryLoadKeystoreAddress();
1194
+ this.log("Session key found, but no session config alongside it.\n");
1195
+ if (orphanAddress) {
1196
+ this.log(` Key address: ${orphanAddress}`);
1197
+ }
1198
+ this.log(
1199
+ "\nIf that key still holds a live on-chain permission it cannot be revoked\nautomatically, because the permission id lived in the missing config.\nReusing the key keeps a single key in play instead of leaving two.\n"
1200
+ );
1201
+ const reuseAnswer = await ask("Reuse existing session key? (Y/n) ");
1202
+ if (reuseAnswer.toLowerCase() !== "n") {
1203
+ reuseKey = loadSessionKey();
1204
+ }
1205
+ } else if (isActive) {
808
1206
  const remaining = Math.floor((existing.expiry - Date.now() / 1e3) / 86400);
809
1207
  this.log("Active session found:\n");
810
1208
  this.log(` Session address: ${existing.sessionAddress}`);
@@ -818,13 +1216,11 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
818
1216
  const revokeAnswer = await ask("Revoke old permission on-chain first? (Y/n) ");
819
1217
  if (revokeAnswer.toLowerCase() !== "n") {
820
1218
  this.log("Opening browser to revoke old permission...");
821
- const pm = config.paymasters?.[existing.chainId];
822
1219
  const revokeBridge = await getBridge({
823
1220
  keysUrl: config.keysUrl,
824
1221
  apiKey,
825
1222
  chainId: existing.chainId,
826
- ens: config.ens,
827
- paymasterUrl: pm?.url
1223
+ ens: config.ens
828
1224
  });
829
1225
  try {
830
1226
  await revokeBridge.request("wallet_revokePermissions", [{ id: existing.permissionId }]);
@@ -833,6 +1229,11 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
833
1229
  }
834
1230
  oldPermissionRevoked = true;
835
1231
  this.log("Old permission revoked.");
1232
+ } else {
1233
+ orphaned = [orphanOf(existing), ...orphaned];
1234
+ this.log(
1235
+ "Keeping the old permission. It stays live until it expires, and the new session will not name it, so `jaw session revoke` will revoke both."
1236
+ );
836
1237
  }
837
1238
  const reuseAnswer = await ask("Reuse existing session key? (Y/n) ");
838
1239
  if (reuseAnswer.toLowerCase() !== "n") {
@@ -847,14 +1248,20 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
847
1248
  }
848
1249
  }
849
1250
  rl.close();
1251
+ } else if (!existing) {
1252
+ const orphanAddress = tryLoadKeystoreAddress();
1253
+ this.logToStderr(
1254
+ `Warning: session key${orphanAddress ? ` ${orphanAddress}` : ""} has no session config. Generating a new key; any permission the old one still holds cannot be revoked automatically because the permission id is unknown.`
1255
+ );
850
1256
  } else if (isActive) {
1257
+ orphaned = [orphanOf(existing), ...orphaned];
851
1258
  this.logToStderr(
852
- `Warning: overwriting active session without revoking. Old permission ${existing.permissionId} on chain ${existing.chainId} remains live until ${new Date(existing.expiry * 1e3).toISOString()}.`
1259
+ `Warning: overwriting active session without revoking. Old permission ${existing.permissionId} on chain ${existing.chainId} remains live until ${new Date(existing.expiry * 1e3).toISOString()}. Recorded on the new session, so \`jaw session revoke\` will revoke it too.`
853
1260
  );
854
1261
  }
855
1262
  }
856
1263
  try {
857
- const permissions = this.resolvePermissions(flags.permissions, config.permissions);
1264
+ const permissions = resolvedPermissions;
858
1265
  const expiryDays = flags.expiry ?? config.sessionExpiry ?? 7;
859
1266
  const expiryTimestamp = Math.floor(Date.now() / 1e3) + expiryDays * 86400;
860
1267
  const privateKeyHex = reuseKey ?? generateSessionKey();
@@ -862,6 +1269,7 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
862
1269
  const localAccount = privateKeyToAccount(privateKeyHex);
863
1270
  const { Account } = await import('@jaw.id/core');
864
1271
  const pm = config.paymasters?.[chainId];
1272
+ const mode = "eip7702";
865
1273
  const account = await Account.fromLocalAccount(
866
1274
  {
867
1275
  chainId,
@@ -869,52 +1277,84 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
869
1277
  paymasterUrl: pm?.url,
870
1278
  paymasterContext: pm?.context
871
1279
  },
872
- localAccount
1280
+ localAccount,
1281
+ { eip7702: true }
873
1282
  );
874
1283
  const sessionAddress = account.address;
875
1284
  if (!flags.quiet) {
1285
+ if (flags.x402) {
1286
+ this.log(
1287
+ `Connect with an account that holds USDC on chain ${chainId}.
1288
+ Payments pull from it through the permission, and the grant carries 0.1 USDC
1289
+ to the session so it can pay for its own first transaction.
1290
+ `
1291
+ );
1292
+ }
876
1293
  this.log("Opening browser to approve permissions...");
877
1294
  }
878
1295
  const bridge = await getBridge({
879
1296
  keysUrl: config.keysUrl,
880
1297
  apiKey,
881
1298
  chainId,
882
- ens: config.ens,
883
- paymasterUrl: pm?.url
1299
+ ens: config.ens
884
1300
  });
885
- let grantResponse;
1301
+ let granted;
886
1302
  try {
887
- grantResponse = await bridge.request("wallet_grantPermissions", [
1303
+ if (flags.x402) {
1304
+ const blocked = await whyOwnerCannotFundSession({ chainId, request: (m, p) => bridge.request(m, p) });
1305
+ if (blocked) this.error(blocked);
1306
+ }
1307
+ granted = await bridge.request("wallet_grantPermissions", [
888
1308
  {
889
1309
  spender: sessionAddress,
890
1310
  expiry: expiryTimestamp,
891
1311
  permissions,
892
- chainId
1312
+ chainId,
1313
+ // The session account sends every op the permission authorises, and
1314
+ // the ERC-20 paymaster charges the sender, so its first one has
1315
+ // nothing to be charged. The wallet rides a small transfer along in
1316
+ // this same transaction; it decides the amount.
1317
+ capabilities: { prefundSpender: true }
893
1318
  }
894
1319
  ]);
895
1320
  } finally {
896
1321
  bridge.close();
897
1322
  }
1323
+ const grantResponse = granted;
1324
+ const permission = parseGrantedPermission(granted);
898
1325
  saveKeystore(privateKeyHex, sessionAddress);
899
1326
  saveSessionConfig({
900
1327
  ownerAddress: grantResponse.account,
901
1328
  sessionAddress,
902
1329
  permissionId: grantResponse.permissionId,
903
1330
  chainId,
904
- expiry: expiryTimestamp
1331
+ expiry: expiryTimestamp,
1332
+ mode,
1333
+ ...permission ? { permission } : {},
1334
+ ...orphaned.length > 0 ? { orphanedPermissions: orphaned } : {}
905
1335
  });
1336
+ if (flags.x402) {
1337
+ const unfunded = await whySpenderCannotPay({ chainId, spender: sessionAddress });
1338
+ if (unfunded) this.logToStderr(`
1339
+ Warning: ${unfunded}`);
1340
+ }
906
1341
  const summary = {
907
1342
  ownerAddress: grantResponse.account,
908
1343
  sessionAddress,
909
1344
  permissionId: grantResponse.permissionId,
910
- expiry: expiryTimestamp
1345
+ expiry: expiryTimestamp,
1346
+ mode
911
1347
  };
912
1348
  if (flags.quiet) {
913
1349
  this.outputResult(summary, format);
914
1350
  } else {
915
1351
  this.log("\nSession created successfully.\n");
916
1352
  this.log(` Session address: ${sessionAddress}`);
1353
+ this.log(" (the session key EOA, and the x402 payer)");
917
1354
  this.log(` Owner address: ${grantResponse.account}`);
1355
+ if (flags.x402) {
1356
+ this.log(` (payments pull from here, capped at ${describeX402Grant(flags.limit)})`);
1357
+ }
918
1358
  this.log(` Permission ID: ${grantResponse.permissionId}`);
919
1359
  this.log(` Chain: ${chainId}`);
920
1360
  this.log(` Expires: ${new Date(expiryTimestamp * 1e3).toISOString()} (${expiryDays} days)`);
@@ -929,9 +1369,15 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
929
1369
  throw error;
930
1370
  }
931
1371
  }
932
- resolvePermissions(flagValue, configValue) {
1372
+ resolvePermissions(flagValue, configValue, preset) {
933
1373
  let raw;
934
- if (flagValue) {
1374
+ if (preset.x402) {
1375
+ try {
1376
+ raw = buildX402Permissions(preset.chainId, preset.limit);
1377
+ } catch (err) {
1378
+ this.error(err instanceof Error ? err.message : String(err));
1379
+ }
1380
+ } else if (flagValue) {
935
1381
  if (flagValue.trimStart().startsWith("{")) {
936
1382
  try {
937
1383
  raw = JSON.parse(flagValue);
@@ -939,7 +1385,7 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
939
1385
  this.error(`--permissions is not valid JSON: ${flagValue}`);
940
1386
  }
941
1387
  } else {
942
- const content = fs.readFileSync(flagValue, "utf-8");
1388
+ const content = fs4.readFileSync(flagValue, "utf-8");
943
1389
  try {
944
1390
  raw = JSON.parse(content);
945
1391
  } catch {
@@ -949,11 +1395,16 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
949
1395
  } else if (configValue) {
950
1396
  raw = configValue;
951
1397
  } else {
952
- this.error('Permissions required. Set via --permissions flag or add "permissions" to ~/.jaw/config.json');
1398
+ this.error(
1399
+ 'Permissions required. For x402 payments run `jaw session setup --x402` and the scope is built for you. Otherwise pass --permissions or add "permissions" to ~/.jaw/config.json.'
1400
+ );
953
1401
  }
954
1402
  return parsePermissionsConfig(raw);
955
1403
  }
956
1404
  };
1405
+ function orphanOf(session) {
1406
+ return { id: session.permissionId, chainId: session.chainId, expiry: session.expiry };
1407
+ }
957
1408
 
958
1409
  export { SessionSetup as default };
959
1410
  //# sourceMappingURL=setup.js.map