@jaw.id/cli 0.0.7 → 0.1.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/README.md +24 -24
- package/dist/base-command.js +28 -8
- package/dist/base-command.js.map +1 -1
- package/dist/commands/config/set.js +28 -38
- package/dist/commands/config/set.js.map +1 -1
- package/dist/commands/config/show.js +29 -12
- package/dist/commands/config/show.js.map +1 -1
- package/dist/commands/config/write.js +288 -0
- package/dist/commands/config/write.js.map +1 -0
- package/dist/commands/disconnect.js +42 -54
- package/dist/commands/disconnect.js.map +1 -1
- package/dist/commands/mcp/index.js +62 -102
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/rpc/call.js +218 -92
- package/dist/commands/rpc/call.js.map +1 -1
- package/dist/commands/session/revoke.js +727 -0
- package/dist/commands/session/revoke.js.map +1 -0
- package/dist/commands/session/setup.js +960 -0
- package/dist/commands/session/setup.js.map +1 -0
- package/dist/commands/session/status.js +206 -0
- package/dist/commands/session/status.js.map +1 -0
- package/dist/commands/version.js +28 -8
- package/dist/commands/version.js.map +1 -1
- package/dist/index.js +42 -67
- package/dist/index.js.map +1 -1
- package/dist/lib/bridge-singleton.js +47 -61
- package/dist/lib/bridge-singleton.js.map +1 -1
- package/dist/lib/config.js +17 -13
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/crypto.js +3 -15
- package/dist/lib/crypto.js.map +1 -1
- package/dist/lib/keystore.js +64 -0
- package/dist/lib/keystore.js.map +1 -0
- package/dist/lib/output.js +1 -16
- package/dist/lib/output.js.map +1 -1
- package/dist/lib/paths.js +3 -1
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/session-bridge.js +168 -0
- package/dist/lib/session-bridge.js.map +1 -0
- package/dist/lib/session-config.js +52 -0
- package/dist/lib/session-config.js.map +1 -0
- package/dist/lib/validation.js +62 -27
- package/dist/lib/validation.js.map +1 -1
- package/dist/lib/ws-bridge.js +15 -46
- package/dist/lib/ws-bridge.js.map +1 -1
- package/dist/mcp/handlers/config.js +28 -29
- package/dist/mcp/handlers/config.js.map +1 -1
- package/dist/mcp/handlers/daemon.js +41 -51
- package/dist/mcp/handlers/daemon.js.map +1 -1
- package/dist/mcp/handlers/resources.js +1 -3
- package/dist/mcp/handlers/resources.js.map +1 -1
- package/dist/mcp/handlers/rpc.js +63 -79
- package/dist/mcp/handlers/rpc.js.map +1 -1
- package/dist/mcp/helpers.js.map +1 -1
- package/dist/mcp/server.js +61 -99
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +2 -4
- package/dist/mcp/tools.js.map +1 -1
- package/oclif.manifest.json +304 -4
- package/package.json +19 -1
- package/dist/lib/session-store.js +0 -80
- package/dist/lib/session-store.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Flags, Args } from '@oclif/core';
|
|
2
|
-
import * as
|
|
2
|
+
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import * as crypto from 'crypto';
|
|
@@ -11,7 +11,9 @@ var PATHS = {
|
|
|
11
11
|
root: JAW_DIR,
|
|
12
12
|
config: path.join(JAW_DIR, "config.json"),
|
|
13
13
|
session: path.join(JAW_DIR, "session.json"),
|
|
14
|
-
relay: path.join(JAW_DIR, "relay.json")
|
|
14
|
+
relay: path.join(JAW_DIR, "relay.json"),
|
|
15
|
+
keystore: path.join(JAW_DIR, "keystore.json"),
|
|
16
|
+
sessionConfig: path.join(JAW_DIR, "session-config.json")
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
// src/lib/validation.ts
|
|
@@ -39,22 +41,39 @@ function isValidRelayUrl(url) {
|
|
|
39
41
|
|
|
40
42
|
// src/lib/config.ts
|
|
41
43
|
function ensureDir(dir) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
45
|
+
fs.chmodSync(dir, 448);
|
|
46
|
+
}
|
|
47
|
+
function migrateConfig(config) {
|
|
48
|
+
if (config.paymasterUrl && !config.paymasters) {
|
|
49
|
+
const chainId = config.defaultChain ?? 1;
|
|
50
|
+
config.paymasters = { [chainId]: { url: config.paymasterUrl } };
|
|
51
|
+
delete config.paymasterUrl;
|
|
52
|
+
saveConfig(config);
|
|
53
|
+
}
|
|
54
|
+
return config;
|
|
44
55
|
}
|
|
45
56
|
function loadConfig() {
|
|
46
|
-
if (!
|
|
57
|
+
if (!fs.existsSync(PATHS.config)) {
|
|
47
58
|
return {};
|
|
48
59
|
}
|
|
49
|
-
const raw =
|
|
60
|
+
const raw = fs.readFileSync(PATHS.config, "utf-8");
|
|
50
61
|
try {
|
|
51
|
-
|
|
62
|
+
const config = JSON.parse(raw);
|
|
63
|
+
return migrateConfig(config);
|
|
52
64
|
} catch {
|
|
53
65
|
throw new Error(
|
|
54
66
|
`Config file at ${PATHS.config} is not valid JSON. Run \`jaw config set apiKey=<key>\` to reset it.`
|
|
55
67
|
);
|
|
56
68
|
}
|
|
57
69
|
}
|
|
70
|
+
function saveConfig(config) {
|
|
71
|
+
ensureDir(PATHS.root);
|
|
72
|
+
fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
|
|
73
|
+
encoding: "utf-8",
|
|
74
|
+
mode: 384
|
|
75
|
+
});
|
|
76
|
+
}
|
|
58
77
|
|
|
59
78
|
// src/lib/output.ts
|
|
60
79
|
function formatOutput(data, format) {
|
|
@@ -127,18 +146,14 @@ var BaseCommand = class extends Command {
|
|
|
127
146
|
resolveApiKey(flags) {
|
|
128
147
|
const apiKey = flags["api-key"] ?? loadConfig().apiKey;
|
|
129
148
|
if (!apiKey) {
|
|
130
|
-
this.error(
|
|
131
|
-
"API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`"
|
|
132
|
-
);
|
|
149
|
+
this.error("API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`");
|
|
133
150
|
}
|
|
134
151
|
return apiKey;
|
|
135
152
|
}
|
|
136
153
|
resolveChainId(flags) {
|
|
137
154
|
const chainId = flags.chain ?? loadConfig().defaultChain;
|
|
138
155
|
if (!chainId) {
|
|
139
|
-
this.error(
|
|
140
|
-
"Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`"
|
|
141
|
-
);
|
|
156
|
+
this.error("Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`");
|
|
142
157
|
}
|
|
143
158
|
return chainId;
|
|
144
159
|
}
|
|
@@ -151,11 +166,7 @@ var BaseCommand = class extends Command {
|
|
|
151
166
|
// src/lib/crypto.ts
|
|
152
167
|
var subtle = globalThis.crypto.subtle;
|
|
153
168
|
async function generateKeyPair() {
|
|
154
|
-
return subtle.generateKey(
|
|
155
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
156
|
-
true,
|
|
157
|
-
["deriveKey"]
|
|
158
|
-
);
|
|
169
|
+
return subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey"]);
|
|
159
170
|
}
|
|
160
171
|
async function deriveSharedSecret(privateKey, peerPublicKey) {
|
|
161
172
|
return subtle.deriveKey(
|
|
@@ -169,11 +180,7 @@ async function deriveSharedSecret(privateKey, peerPublicKey) {
|
|
|
169
180
|
async function encryptMessage(sharedSecret, payload) {
|
|
170
181
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
171
182
|
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
172
|
-
const cipherBuf = await subtle.encrypt(
|
|
173
|
-
{ name: "AES-GCM", iv },
|
|
174
|
-
sharedSecret,
|
|
175
|
-
plaintext
|
|
176
|
-
);
|
|
183
|
+
const cipherBuf = await subtle.encrypt({ name: "AES-GCM", iv }, sharedSecret, plaintext);
|
|
177
184
|
return {
|
|
178
185
|
iv: bufferToBase64(iv),
|
|
179
186
|
ciphertext: bufferToBase64(new Uint8Array(cipherBuf))
|
|
@@ -182,11 +189,7 @@ async function encryptMessage(sharedSecret, payload) {
|
|
|
182
189
|
async function decryptMessage(sharedSecret, envelope) {
|
|
183
190
|
const iv = Buffer.from(envelope.iv, "base64");
|
|
184
191
|
const ciphertext = Buffer.from(envelope.ciphertext, "base64");
|
|
185
|
-
const plainBuf = await subtle.decrypt(
|
|
186
|
-
{ name: "AES-GCM", iv },
|
|
187
|
-
sharedSecret,
|
|
188
|
-
ciphertext
|
|
189
|
-
);
|
|
192
|
+
const plainBuf = await subtle.decrypt({ name: "AES-GCM", iv }, sharedSecret, ciphertext);
|
|
190
193
|
return JSON.parse(new TextDecoder().decode(plainBuf));
|
|
191
194
|
}
|
|
192
195
|
async function exportKeyToHex(type, key) {
|
|
@@ -278,11 +281,7 @@ var WSBridge = class {
|
|
|
278
281
|
let expectingKeyExchange = !this.peerPublicKeyHex;
|
|
279
282
|
const timer = setTimeout(() => {
|
|
280
283
|
ws.close();
|
|
281
|
-
reject(
|
|
282
|
-
new Error(
|
|
283
|
-
"Browser did not connect in time.\nRun `jaw disconnect` then try again."
|
|
284
|
-
)
|
|
285
|
-
);
|
|
284
|
+
reject(new Error("Browser did not connect in time.\nRun `jaw disconnect` then try again."));
|
|
286
285
|
}, 3e4);
|
|
287
286
|
const sendEncryptedInit = async () => {
|
|
288
287
|
if (!this.sharedSecret) return;
|
|
@@ -305,10 +304,7 @@ var WSBridge = class {
|
|
|
305
304
|
if (!msg) return;
|
|
306
305
|
if (msg.type === "encrypted" && this.sharedSecret) {
|
|
307
306
|
try {
|
|
308
|
-
const inner = await decryptMessage(
|
|
309
|
-
this.sharedSecret,
|
|
310
|
-
msg
|
|
311
|
-
);
|
|
307
|
+
const inner = await decryptMessage(this.sharedSecret, msg);
|
|
312
308
|
if (inner.type === "ready") {
|
|
313
309
|
clearTimeout(readyTimer);
|
|
314
310
|
ws.off("message", onMsg);
|
|
@@ -346,6 +342,10 @@ var WSBridge = class {
|
|
|
346
342
|
expectingKeyExchange = true;
|
|
347
343
|
onBrowserNeeded().catch(() => {
|
|
348
344
|
});
|
|
345
|
+
} else if (!onBrowserNeeded) {
|
|
346
|
+
clearTimeout(timer);
|
|
347
|
+
ws.close();
|
|
348
|
+
reject(new Error("Browser not connected \u2014 relay session is stale."));
|
|
349
349
|
}
|
|
350
350
|
} else if (msg.type === "browser_connected") {
|
|
351
351
|
expectingKeyExchange = true;
|
|
@@ -395,9 +395,7 @@ var WSBridge = class {
|
|
|
395
395
|
return new Promise((resolve, reject) => {
|
|
396
396
|
const timer = setTimeout(() => {
|
|
397
397
|
reject(
|
|
398
|
-
new Error(
|
|
399
|
-
`Request timed out after ${this.timeout / 1e3}s. Did you complete the action in the browser?`
|
|
400
|
-
)
|
|
398
|
+
new Error(`Request timed out after ${this.timeout / 1e3}s. Did you complete the action in the browser?`)
|
|
401
399
|
);
|
|
402
400
|
this.close();
|
|
403
401
|
}, this.timeout);
|
|
@@ -405,10 +403,7 @@ var WSBridge = class {
|
|
|
405
403
|
const msg = safeParse(data);
|
|
406
404
|
if (!msg || msg.type !== "encrypted" || !this.sharedSecret) return;
|
|
407
405
|
try {
|
|
408
|
-
const inner = await decryptMessage(
|
|
409
|
-
this.sharedSecret,
|
|
410
|
-
msg
|
|
411
|
-
);
|
|
406
|
+
const inner = await decryptMessage(this.sharedSecret, msg);
|
|
412
407
|
if (inner.type === "rpc_response" && inner.id === id) {
|
|
413
408
|
clearTimeout(timer);
|
|
414
409
|
ws.off("message", onMessage);
|
|
@@ -416,11 +411,7 @@ var WSBridge = class {
|
|
|
416
411
|
resolve(inner.data);
|
|
417
412
|
} else {
|
|
418
413
|
const err = inner.error;
|
|
419
|
-
reject(
|
|
420
|
-
new Error(
|
|
421
|
-
err ? `[${err.code}] ${err.message}` : "Request failed"
|
|
422
|
-
)
|
|
423
|
-
);
|
|
414
|
+
reject(new Error(err ? `[${err.code}] ${err.message}` : "Request failed"));
|
|
424
415
|
}
|
|
425
416
|
}
|
|
426
417
|
} catch {
|
|
@@ -430,9 +421,6 @@ var WSBridge = class {
|
|
|
430
421
|
this.sendRaw(ws, serialized);
|
|
431
422
|
});
|
|
432
423
|
}
|
|
433
|
-
isOpen() {
|
|
434
|
-
return this.ws?.readyState === WebSocket.OPEN;
|
|
435
|
-
}
|
|
436
424
|
async shutdown() {
|
|
437
425
|
this.disposed = true;
|
|
438
426
|
if (this.ws?.readyState === WebSocket.OPEN && this.sharedSecret) {
|
|
@@ -440,10 +428,7 @@ var WSBridge = class {
|
|
|
440
428
|
const envelope = await encryptMessage(this.sharedSecret, {
|
|
441
429
|
type: "shutdown"
|
|
442
430
|
});
|
|
443
|
-
this.sendRaw(
|
|
444
|
-
this.ws,
|
|
445
|
-
JSON.stringify({ type: "encrypted", ...envelope })
|
|
446
|
-
);
|
|
431
|
+
this.sendRaw(this.ws, JSON.stringify({ type: "encrypted", ...envelope }));
|
|
447
432
|
} catch {
|
|
448
433
|
}
|
|
449
434
|
}
|
|
@@ -521,10 +506,8 @@ var WSBridge = class {
|
|
|
521
506
|
this.reconnectAttempts++;
|
|
522
507
|
setTimeout(() => {
|
|
523
508
|
if (this.disposed) return;
|
|
524
|
-
this.connectInternal(this.onBrowserNeeded, this.onPeerKeyChanged).catch(
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
);
|
|
509
|
+
this.connectInternal(this.onBrowserNeeded, this.onPeerKeyChanged).catch(() => {
|
|
510
|
+
});
|
|
528
511
|
}, delay);
|
|
529
512
|
}
|
|
530
513
|
/** Send a raw string over the WebSocket, enforcing message size limits. */
|
|
@@ -534,10 +517,7 @@ var WSBridge = class {
|
|
|
534
517
|
async deriveSecret() {
|
|
535
518
|
if (!this.peerPublicKeyHex) return;
|
|
536
519
|
const privateKey = await importKeyFromHex("private", this.privateKeyHex);
|
|
537
|
-
const peerPublicKey = await importKeyFromHex(
|
|
538
|
-
"public",
|
|
539
|
-
this.peerPublicKeyHex
|
|
540
|
-
);
|
|
520
|
+
const peerPublicKey = await importKeyFromHex("public", this.peerPublicKeyHex);
|
|
541
521
|
this.sharedSecret = await deriveSharedSecret(privateKey, peerPublicKey);
|
|
542
522
|
}
|
|
543
523
|
};
|
|
@@ -563,8 +543,8 @@ var DEFAULT_KEYS_URL = "https://keys.jaw.id";
|
|
|
563
543
|
var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
|
|
564
544
|
function loadRelaySession() {
|
|
565
545
|
try {
|
|
566
|
-
if (!
|
|
567
|
-
const raw =
|
|
546
|
+
if (!fs.existsSync(PATHS.relay)) return null;
|
|
547
|
+
const raw = fs.readFileSync(PATHS.relay, "utf-8");
|
|
568
548
|
const parsed = JSON.parse(raw);
|
|
569
549
|
if (!parsed.session || !parsed.relayUrl || !parsed.privateKey || !parsed.publicKey) {
|
|
570
550
|
return null;
|
|
@@ -576,14 +556,14 @@ function loadRelaySession() {
|
|
|
576
556
|
}
|
|
577
557
|
function saveRelaySession(info) {
|
|
578
558
|
ensureDir(PATHS.root);
|
|
579
|
-
|
|
559
|
+
fs.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
|
|
580
560
|
encoding: "utf-8",
|
|
581
561
|
mode: 384
|
|
582
562
|
});
|
|
583
563
|
}
|
|
584
564
|
function deleteRelaySession() {
|
|
585
565
|
try {
|
|
586
|
-
if (
|
|
566
|
+
if (fs.existsSync(PATHS.relay)) fs.unlinkSync(PATHS.relay);
|
|
587
567
|
} catch {
|
|
588
568
|
}
|
|
589
569
|
}
|
|
@@ -599,17 +579,19 @@ async function getBridge(options) {
|
|
|
599
579
|
throw new Error(`Untrusted relayUrl: ${relayUrl}. Must be wss://*.jaw.id or ws://localhost.`);
|
|
600
580
|
}
|
|
601
581
|
let relaySession = loadRelaySession();
|
|
602
|
-
if (relaySession && relaySession.relayUrl === relayUrl) {
|
|
582
|
+
if (relaySession && relaySession.relayUrl === relayUrl && relaySession.peerPublicKey) {
|
|
603
583
|
try {
|
|
604
|
-
return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl);
|
|
584
|
+
return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl, false);
|
|
605
585
|
} catch {
|
|
606
586
|
deleteRelaySession();
|
|
607
587
|
relaySession = null;
|
|
608
588
|
}
|
|
589
|
+
} else if (relaySession) {
|
|
590
|
+
deleteRelaySession();
|
|
609
591
|
}
|
|
610
592
|
const session = await createNewSession(relayUrl);
|
|
611
593
|
saveRelaySession(session);
|
|
612
|
-
return await connectBridge(session, options, chainId, keysUrl, relayUrl);
|
|
594
|
+
return await connectBridge(session, options, chainId, keysUrl, relayUrl, true);
|
|
613
595
|
}
|
|
614
596
|
async function createNewSession(relayUrl) {
|
|
615
597
|
const kp = await generateKeyPair();
|
|
@@ -624,7 +606,7 @@ async function createNewSession(relayUrl) {
|
|
|
624
606
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
625
607
|
};
|
|
626
608
|
}
|
|
627
|
-
async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl) {
|
|
609
|
+
async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl, openBrowser) {
|
|
628
610
|
const config = loadConfig();
|
|
629
611
|
const bridge = new WSBridge({
|
|
630
612
|
relayUrl,
|
|
@@ -634,19 +616,19 @@ async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl)
|
|
|
634
616
|
apiKey: options.apiKey,
|
|
635
617
|
chainId,
|
|
636
618
|
ens: options.ens ?? config.ens,
|
|
637
|
-
paymasterUrl: options.paymasterUrl ?? config.
|
|
619
|
+
paymasterUrl: options.paymasterUrl ?? config.paymasters?.[chainId]?.url
|
|
638
620
|
},
|
|
639
621
|
privateKeyHex: relaySession.privateKey,
|
|
640
622
|
publicKeyHex: relaySession.publicKey,
|
|
641
623
|
peerPublicKeyHex: relaySession.peerPublicKey
|
|
642
624
|
});
|
|
643
625
|
await bridge.connect(
|
|
644
|
-
// onBrowserNeeded
|
|
645
|
-
async () => {
|
|
626
|
+
// onBrowserNeeded — only open a browser for new sessions
|
|
627
|
+
openBrowser ? async () => {
|
|
646
628
|
const bridgeUrl = buildBridgeUrl(keysUrl, relaySession.session, relayUrl, relaySession.publicKey);
|
|
647
629
|
const { default: open } = await import('open');
|
|
648
630
|
await open(bridgeUrl);
|
|
649
|
-
},
|
|
631
|
+
} : void 0,
|
|
650
632
|
// onPeerKeyChanged
|
|
651
633
|
(newPeerKey) => {
|
|
652
634
|
relaySession.peerPublicKey = newPeerKey;
|
|
@@ -682,16 +664,141 @@ var BROWSER_REQUIRED_METHODS = /* @__PURE__ */ new Set([
|
|
|
682
664
|
function requiresBrowser(method) {
|
|
683
665
|
return BROWSER_REQUIRED_METHODS.has(method);
|
|
684
666
|
}
|
|
667
|
+
var SESSION_SUPPORTED_METHODS = /* @__PURE__ */ new Set([
|
|
668
|
+
"eth_requestAccounts",
|
|
669
|
+
"eth_accounts",
|
|
670
|
+
"wallet_sendCalls",
|
|
671
|
+
"wallet_getCallsStatus",
|
|
672
|
+
"personal_sign",
|
|
673
|
+
"eth_signTypedData_v4"
|
|
674
|
+
]);
|
|
675
|
+
function supportsSessionMode(method) {
|
|
676
|
+
return SESSION_SUPPORTED_METHODS.has(method);
|
|
677
|
+
}
|
|
678
|
+
function loadSessionKey() {
|
|
679
|
+
if (!fs.existsSync(PATHS.keystore)) {
|
|
680
|
+
throw new Error("No session configured. Run `jaw session setup` first.");
|
|
681
|
+
}
|
|
682
|
+
const contents = fs.readFileSync(PATHS.keystore, "utf-8");
|
|
683
|
+
let parsed;
|
|
684
|
+
try {
|
|
685
|
+
parsed = JSON.parse(contents);
|
|
686
|
+
} catch {
|
|
687
|
+
throw new Error(`Keystore at ${PATHS.keystore} is corrupted. Run \`jaw session setup\` to recreate it.`);
|
|
688
|
+
}
|
|
689
|
+
return parsed.privateKey;
|
|
690
|
+
}
|
|
691
|
+
function loadSessionConfig() {
|
|
692
|
+
if (!fs.existsSync(PATHS.sessionConfig)) {
|
|
693
|
+
throw new Error("No session configured. Run `jaw session setup` first.");
|
|
694
|
+
}
|
|
695
|
+
const raw = fs.readFileSync(PATHS.sessionConfig, "utf-8");
|
|
696
|
+
try {
|
|
697
|
+
return JSON.parse(raw);
|
|
698
|
+
} catch {
|
|
699
|
+
throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \`jaw session setup\` to recreate it.`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/lib/session-bridge.ts
|
|
704
|
+
var SessionBridge = class {
|
|
705
|
+
options;
|
|
706
|
+
session = null;
|
|
707
|
+
constructor(options) {
|
|
708
|
+
this.options = { ...options };
|
|
709
|
+
if (!this.options.paymasterUrl) {
|
|
710
|
+
const config = loadConfig();
|
|
711
|
+
const pm = config.paymasters?.[this.options.chainId];
|
|
712
|
+
if (pm) {
|
|
713
|
+
this.options.paymasterUrl = pm.url;
|
|
714
|
+
this.options.paymasterContext = pm.context;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async getSession() {
|
|
719
|
+
if (this.session) {
|
|
720
|
+
this.checkExpiry(this.session.config);
|
|
721
|
+
return this.session;
|
|
722
|
+
}
|
|
723
|
+
const config = loadSessionConfig();
|
|
724
|
+
this.checkExpiry(config);
|
|
725
|
+
if (config.chainId !== this.options.chainId) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`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.`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
let privateKeyHex = loadSessionKey();
|
|
731
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
732
|
+
const localAccount = privateKeyToAccount(privateKeyHex);
|
|
733
|
+
privateKeyHex = null;
|
|
734
|
+
const { Account } = await import('@jaw.id/core');
|
|
735
|
+
const account = await Account.fromLocalAccount(
|
|
736
|
+
{
|
|
737
|
+
chainId: this.options.chainId,
|
|
738
|
+
apiKey: this.options.apiKey,
|
|
739
|
+
paymasterUrl: this.options.paymasterUrl,
|
|
740
|
+
paymasterContext: this.options.paymasterContext
|
|
741
|
+
},
|
|
742
|
+
localAccount
|
|
743
|
+
);
|
|
744
|
+
this.session = { account, config };
|
|
745
|
+
return this.session;
|
|
746
|
+
}
|
|
747
|
+
checkExpiry(config) {
|
|
748
|
+
if (config.expiry <= Date.now() / 1e3) {
|
|
749
|
+
const expiryDate = new Date(config.expiry * 1e3).toISOString();
|
|
750
|
+
throw new Error(`Session expired on ${expiryDate}. Run \`jaw session setup\` to create a new session.`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async request(method, params) {
|
|
754
|
+
const { account, config } = await this.getSession();
|
|
755
|
+
switch (method) {
|
|
756
|
+
case "eth_requestAccounts":
|
|
757
|
+
case "eth_accounts":
|
|
758
|
+
return [config.sessionAddress];
|
|
759
|
+
case "wallet_sendCalls": {
|
|
760
|
+
const payload = Array.isArray(params) ? params[0] : params;
|
|
761
|
+
const { calls } = payload;
|
|
762
|
+
return account.sendCalls(calls, {
|
|
763
|
+
permissionId: config.permissionId
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
case "wallet_getCallsStatus": {
|
|
767
|
+
const batchId = Array.isArray(params) ? params[0] : params;
|
|
768
|
+
return account.getCallStatus(batchId);
|
|
769
|
+
}
|
|
770
|
+
case "personal_sign": {
|
|
771
|
+
const message = Array.isArray(params) ? params[0] : params;
|
|
772
|
+
return account.signMessage(message);
|
|
773
|
+
}
|
|
774
|
+
case "eth_signTypedData_v4": {
|
|
775
|
+
const asArray = Array.isArray(params) ? params : [params];
|
|
776
|
+
const raw = asArray.length > 1 ? asArray[1] : asArray[0];
|
|
777
|
+
const typedData = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
778
|
+
return account.signTypedData(typedData);
|
|
779
|
+
}
|
|
780
|
+
case "wallet_grantPermissions":
|
|
781
|
+
throw new Error("Requires browser \u2014 run `jaw session setup`.");
|
|
782
|
+
case "wallet_revokePermissions":
|
|
783
|
+
throw new Error("Requires browser \u2014 run `jaw session revoke`.");
|
|
784
|
+
default:
|
|
785
|
+
throw new Error(`Method ${method} is not supported in auto mode.`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
close() {
|
|
789
|
+
}
|
|
790
|
+
};
|
|
685
791
|
|
|
686
792
|
// src/commands/rpc/call.ts
|
|
687
793
|
var RpcCall = class _RpcCall extends BaseCommand {
|
|
688
|
-
static description = "Execute any JAW.id RPC method via the browser bridge.";
|
|
794
|
+
static description = "Execute any JAW.id RPC method via the browser bridge or local session key.";
|
|
689
795
|
static examples = [
|
|
690
796
|
`<%= config.bin %> rpc call wallet_sendCalls '{"calls":[{"to":"0x...","value":"0x0"}]}'`,
|
|
691
797
|
`<%= config.bin %> rpc call personal_sign '"Hello World"'`,
|
|
692
798
|
"<%= config.bin %> rpc call wallet_getAssets",
|
|
693
799
|
"<%= config.bin %> rpc call eth_requestAccounts",
|
|
694
|
-
`<%= config.bin %> rpc call wallet_getCallsStatus '"0x..."'
|
|
800
|
+
`<%= config.bin %> rpc call wallet_getCallsStatus '"0x..."'`,
|
|
801
|
+
`<%= config.bin %> rpc call wallet_sendCalls '{"calls":[...]}' --session`
|
|
695
802
|
];
|
|
696
803
|
static args = {
|
|
697
804
|
method: Args.string({
|
|
@@ -709,6 +816,12 @@ var RpcCall = class _RpcCall extends BaseCommand {
|
|
|
709
816
|
char: "t",
|
|
710
817
|
description: "Request timeout in seconds",
|
|
711
818
|
default: 120
|
|
819
|
+
}),
|
|
820
|
+
session: Flags.boolean({
|
|
821
|
+
char: "s",
|
|
822
|
+
description: "Use local session key (auto mode)",
|
|
823
|
+
default: false,
|
|
824
|
+
env: "JAW_SESSION"
|
|
712
825
|
})
|
|
713
826
|
};
|
|
714
827
|
async run() {
|
|
@@ -725,21 +838,34 @@ var RpcCall = class _RpcCall extends BaseCommand {
|
|
|
725
838
|
const format = flags.output;
|
|
726
839
|
const config = loadConfig();
|
|
727
840
|
const apiKey = this.resolveApiKey(flags);
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
timeout: flags.timeout * 1e3
|
|
735
|
-
});
|
|
736
|
-
if (!flags.quiet) {
|
|
737
|
-
if (requiresBrowser(method)) {
|
|
738
|
-
this.log(
|
|
739
|
-
`Sending ${method}... Check your browser to approve the request.`
|
|
841
|
+
const chainId = flags.chain ?? config.defaultChain ?? 1;
|
|
842
|
+
let bridge;
|
|
843
|
+
if (flags.session) {
|
|
844
|
+
if (!supportsSessionMode(method)) {
|
|
845
|
+
this.error(
|
|
846
|
+
`Method ${method} is not supported in session mode. Use without --session to route through the browser bridge.`
|
|
740
847
|
);
|
|
741
|
-
}
|
|
742
|
-
|
|
848
|
+
}
|
|
849
|
+
bridge = new SessionBridge({ apiKey, chainId });
|
|
850
|
+
if (!flags.quiet) {
|
|
851
|
+
this.log(`Sending ${method} (session mode)...`);
|
|
852
|
+
}
|
|
853
|
+
} else {
|
|
854
|
+
const pm = config.paymasters?.[chainId];
|
|
855
|
+
bridge = await getBridge({
|
|
856
|
+
keysUrl: config.keysUrl,
|
|
857
|
+
apiKey,
|
|
858
|
+
chainId,
|
|
859
|
+
ens: config.ens,
|
|
860
|
+
paymasterUrl: pm?.url,
|
|
861
|
+
timeout: flags.timeout * 1e3
|
|
862
|
+
});
|
|
863
|
+
if (!flags.quiet) {
|
|
864
|
+
if (requiresBrowser(method)) {
|
|
865
|
+
this.log(`Sending ${method}... Check your browser to approve the request.`);
|
|
866
|
+
} else {
|
|
867
|
+
this.log(`Sending ${method}...`);
|
|
868
|
+
}
|
|
743
869
|
}
|
|
744
870
|
}
|
|
745
871
|
try {
|