@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.
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +4 -0
- package/dist/base-command.js +3 -1
- package/dist/base-command.js.map +1 -1
- package/dist/commands/config/set.js +140 -12
- package/dist/commands/config/set.js.map +1 -1
- package/dist/commands/config/show.js +3 -1
- package/dist/commands/config/show.js.map +1 -1
- package/dist/commands/config/write.js +6 -4
- package/dist/commands/config/write.js.map +1 -1
- package/dist/commands/disconnect.js +24 -10
- package/dist/commands/disconnect.js.map +1 -1
- package/dist/commands/mcp/index.js +2426 -94
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/rpc/call.js +197 -45
- package/dist/commands/rpc/call.js.map +1 -1
- package/dist/commands/session/add.js +1547 -0
- package/dist/commands/session/add.js.map +1 -0
- package/dist/commands/session/revoke.js +181 -54
- package/dist/commands/session/revoke.js.map +1 -1
- package/dist/commands/session/setup.js +516 -65
- package/dist/commands/session/setup.js.map +1 -1
- package/dist/commands/session/status.js +315 -6
- package/dist/commands/session/status.js.map +1 -1
- package/dist/commands/version.js +3 -1
- package/dist/commands/version.js.map +1 -1
- package/dist/commands/x402/log.js +344 -0
- package/dist/commands/x402/log.js.map +1 -0
- package/dist/commands/x402/pay.js +2122 -0
- package/dist/commands/x402/pay.js.map +1 -0
- package/dist/commands/x402/status.js +1047 -0
- package/dist/commands/x402/status.js.map +1 -0
- package/dist/index.js +41 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/bridge-singleton.js +41 -14
- package/dist/lib/bridge-singleton.js.map +1 -1
- package/dist/lib/config.js +26 -3
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/keystore.js +13 -2
- package/dist/lib/keystore.js.map +1 -1
- package/dist/lib/paths.js +3 -1
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/payment-lock.js +121 -0
- package/dist/lib/payment-lock.js.map +1 -0
- package/dist/lib/session-bridge.js +148 -24
- package/dist/lib/session-bridge.js.map +1 -1
- package/dist/lib/session-config.js +78 -11
- package/dist/lib/session-config.js.map +1 -1
- package/dist/lib/terminal.js +22 -0
- package/dist/lib/terminal.js.map +1 -0
- package/dist/lib/validation.js +3 -3
- package/dist/lib/validation.js.map +1 -1
- package/dist/lib/ws-bridge.js +22 -10
- package/dist/lib/ws-bridge.js.map +1 -1
- package/dist/mcp/handlers/config.js +73 -6
- package/dist/mcp/handlers/config.js.map +1 -1
- package/dist/mcp/handlers/daemon.js +43 -12
- package/dist/mcp/handlers/daemon.js.map +1 -1
- package/dist/mcp/handlers/resources.js +119 -0
- package/dist/mcp/handlers/resources.js.map +1 -1
- package/dist/mcp/handlers/rpc.js +269 -60
- package/dist/mcp/handlers/rpc.js.map +1 -1
- package/dist/mcp/helpers.js +50 -3
- package/dist/mcp/helpers.js.map +1 -1
- package/dist/mcp/server.js +2426 -94
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +43 -3
- package/dist/mcp/tools.js.map +1 -1
- package/dist/x402/log-view.js +160 -0
- package/dist/x402/log-view.js.map +1 -0
- package/dist/x402/status-report.js +90 -0
- package/dist/x402/status-report.js.map +1 -0
- package/oclif.manifest.json +398 -4
- package/package.json +8 -3
package/dist/mcp/handlers/rpc.js
CHANGED
|
@@ -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
|
|
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: ${
|
|
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:
|
|
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(
|
|
236
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
683
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
case "eth_signTypedData_v4":
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
|
744
|
-
var
|
|
745
|
-
var
|
|
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
|
|
748
|
-
function
|
|
958
|
+
const recentSends = [];
|
|
959
|
+
function assertUnderSendLimit() {
|
|
749
960
|
const now = Date.now();
|
|
750
|
-
while (
|
|
751
|
-
if (
|
|
752
|
-
throw new Error("Autonomous
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
777
|
-
|
|
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 {
|