@jaw.id/cli 0.0.8 → 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/dist/base-command.js +26 -2
- package/dist/base-command.js.map +1 -1
- package/dist/commands/config/set.js +19 -7
- package/dist/commands/config/set.js.map +1 -1
- package/dist/commands/config/show.js +26 -2
- 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 +29 -5
- package/dist/commands/disconnect.js.map +1 -1
- package/dist/commands/mcp/index.js +34 -17
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/rpc/call.js +205 -37
- 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 +26 -2
- package/dist/commands/version.js.map +1 -1
- package/dist/index.js +28 -13
- package/dist/index.js.map +1 -1
- package/dist/lib/bridge-singleton.js +35 -13
- package/dist/lib/bridge-singleton.js.map +1 -1
- package/dist/lib/config.js +15 -7
- package/dist/lib/config.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 -12
- 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 +4 -3
- package/dist/lib/ws-bridge.js.map +1 -1
- package/dist/mcp/handlers/config.js +16 -4
- package/dist/mcp/handlers/config.js.map +1 -1
- package/dist/mcp/handlers/daemon.js +29 -5
- package/dist/mcp/handlers/daemon.js.map +1 -1
- package/dist/mcp/handlers/rpc.js +49 -25
- package/dist/mcp/handlers/rpc.js.map +1 -1
- package/dist/mcp/server.js +34 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +1 -1
- package/dist/mcp/tools.js.map +1 -1
- package/oclif.manifest.json +304 -4
- package/package.json +18 -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) {
|
|
@@ -323,6 +342,10 @@ var WSBridge = class {
|
|
|
323
342
|
expectingKeyExchange = true;
|
|
324
343
|
onBrowserNeeded().catch(() => {
|
|
325
344
|
});
|
|
345
|
+
} else if (!onBrowserNeeded) {
|
|
346
|
+
clearTimeout(timer);
|
|
347
|
+
ws.close();
|
|
348
|
+
reject(new Error("Browser not connected \u2014 relay session is stale."));
|
|
326
349
|
}
|
|
327
350
|
} else if (msg.type === "browser_connected") {
|
|
328
351
|
expectingKeyExchange = true;
|
|
@@ -398,9 +421,6 @@ var WSBridge = class {
|
|
|
398
421
|
this.sendRaw(ws, serialized);
|
|
399
422
|
});
|
|
400
423
|
}
|
|
401
|
-
isOpen() {
|
|
402
|
-
return this.ws?.readyState === WebSocket.OPEN;
|
|
403
|
-
}
|
|
404
424
|
async shutdown() {
|
|
405
425
|
this.disposed = true;
|
|
406
426
|
if (this.ws?.readyState === WebSocket.OPEN && this.sharedSecret) {
|
|
@@ -523,8 +543,8 @@ var DEFAULT_KEYS_URL = "https://keys.jaw.id";
|
|
|
523
543
|
var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
|
|
524
544
|
function loadRelaySession() {
|
|
525
545
|
try {
|
|
526
|
-
if (!
|
|
527
|
-
const raw =
|
|
546
|
+
if (!fs.existsSync(PATHS.relay)) return null;
|
|
547
|
+
const raw = fs.readFileSync(PATHS.relay, "utf-8");
|
|
528
548
|
const parsed = JSON.parse(raw);
|
|
529
549
|
if (!parsed.session || !parsed.relayUrl || !parsed.privateKey || !parsed.publicKey) {
|
|
530
550
|
return null;
|
|
@@ -536,14 +556,14 @@ function loadRelaySession() {
|
|
|
536
556
|
}
|
|
537
557
|
function saveRelaySession(info) {
|
|
538
558
|
ensureDir(PATHS.root);
|
|
539
|
-
|
|
559
|
+
fs.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
|
|
540
560
|
encoding: "utf-8",
|
|
541
561
|
mode: 384
|
|
542
562
|
});
|
|
543
563
|
}
|
|
544
564
|
function deleteRelaySession() {
|
|
545
565
|
try {
|
|
546
|
-
if (
|
|
566
|
+
if (fs.existsSync(PATHS.relay)) fs.unlinkSync(PATHS.relay);
|
|
547
567
|
} catch {
|
|
548
568
|
}
|
|
549
569
|
}
|
|
@@ -559,17 +579,19 @@ async function getBridge(options) {
|
|
|
559
579
|
throw new Error(`Untrusted relayUrl: ${relayUrl}. Must be wss://*.jaw.id or ws://localhost.`);
|
|
560
580
|
}
|
|
561
581
|
let relaySession = loadRelaySession();
|
|
562
|
-
if (relaySession && relaySession.relayUrl === relayUrl) {
|
|
582
|
+
if (relaySession && relaySession.relayUrl === relayUrl && relaySession.peerPublicKey) {
|
|
563
583
|
try {
|
|
564
|
-
return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl);
|
|
584
|
+
return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl, false);
|
|
565
585
|
} catch {
|
|
566
586
|
deleteRelaySession();
|
|
567
587
|
relaySession = null;
|
|
568
588
|
}
|
|
589
|
+
} else if (relaySession) {
|
|
590
|
+
deleteRelaySession();
|
|
569
591
|
}
|
|
570
592
|
const session = await createNewSession(relayUrl);
|
|
571
593
|
saveRelaySession(session);
|
|
572
|
-
return await connectBridge(session, options, chainId, keysUrl, relayUrl);
|
|
594
|
+
return await connectBridge(session, options, chainId, keysUrl, relayUrl, true);
|
|
573
595
|
}
|
|
574
596
|
async function createNewSession(relayUrl) {
|
|
575
597
|
const kp = await generateKeyPair();
|
|
@@ -584,7 +606,7 @@ async function createNewSession(relayUrl) {
|
|
|
584
606
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
585
607
|
};
|
|
586
608
|
}
|
|
587
|
-
async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl) {
|
|
609
|
+
async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl, openBrowser) {
|
|
588
610
|
const config = loadConfig();
|
|
589
611
|
const bridge = new WSBridge({
|
|
590
612
|
relayUrl,
|
|
@@ -594,19 +616,19 @@ async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl)
|
|
|
594
616
|
apiKey: options.apiKey,
|
|
595
617
|
chainId,
|
|
596
618
|
ens: options.ens ?? config.ens,
|
|
597
|
-
paymasterUrl: options.paymasterUrl ?? config.
|
|
619
|
+
paymasterUrl: options.paymasterUrl ?? config.paymasters?.[chainId]?.url
|
|
598
620
|
},
|
|
599
621
|
privateKeyHex: relaySession.privateKey,
|
|
600
622
|
publicKeyHex: relaySession.publicKey,
|
|
601
623
|
peerPublicKeyHex: relaySession.peerPublicKey
|
|
602
624
|
});
|
|
603
625
|
await bridge.connect(
|
|
604
|
-
// onBrowserNeeded
|
|
605
|
-
async () => {
|
|
626
|
+
// onBrowserNeeded — only open a browser for new sessions
|
|
627
|
+
openBrowser ? async () => {
|
|
606
628
|
const bridgeUrl = buildBridgeUrl(keysUrl, relaySession.session, relayUrl, relaySession.publicKey);
|
|
607
629
|
const { default: open } = await import('open');
|
|
608
630
|
await open(bridgeUrl);
|
|
609
|
-
},
|
|
631
|
+
} : void 0,
|
|
610
632
|
// onPeerKeyChanged
|
|
611
633
|
(newPeerKey) => {
|
|
612
634
|
relaySession.peerPublicKey = newPeerKey;
|
|
@@ -642,16 +664,141 @@ var BROWSER_REQUIRED_METHODS = /* @__PURE__ */ new Set([
|
|
|
642
664
|
function requiresBrowser(method) {
|
|
643
665
|
return BROWSER_REQUIRED_METHODS.has(method);
|
|
644
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
|
+
};
|
|
645
791
|
|
|
646
792
|
// src/commands/rpc/call.ts
|
|
647
793
|
var RpcCall = class _RpcCall extends BaseCommand {
|
|
648
|
-
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.";
|
|
649
795
|
static examples = [
|
|
650
796
|
`<%= config.bin %> rpc call wallet_sendCalls '{"calls":[{"to":"0x...","value":"0x0"}]}'`,
|
|
651
797
|
`<%= config.bin %> rpc call personal_sign '"Hello World"'`,
|
|
652
798
|
"<%= config.bin %> rpc call wallet_getAssets",
|
|
653
799
|
"<%= config.bin %> rpc call eth_requestAccounts",
|
|
654
|
-
`<%= config.bin %> rpc call wallet_getCallsStatus '"0x..."'
|
|
800
|
+
`<%= config.bin %> rpc call wallet_getCallsStatus '"0x..."'`,
|
|
801
|
+
`<%= config.bin %> rpc call wallet_sendCalls '{"calls":[...]}' --session`
|
|
655
802
|
];
|
|
656
803
|
static args = {
|
|
657
804
|
method: Args.string({
|
|
@@ -669,6 +816,12 @@ var RpcCall = class _RpcCall extends BaseCommand {
|
|
|
669
816
|
char: "t",
|
|
670
817
|
description: "Request timeout in seconds",
|
|
671
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"
|
|
672
825
|
})
|
|
673
826
|
};
|
|
674
827
|
async run() {
|
|
@@ -685,19 +838,34 @@ var RpcCall = class _RpcCall extends BaseCommand {
|
|
|
685
838
|
const format = flags.output;
|
|
686
839
|
const config = loadConfig();
|
|
687
840
|
const apiKey = this.resolveApiKey(flags);
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if (
|
|
698
|
-
this.log(`Sending ${method}
|
|
699
|
-
}
|
|
700
|
-
|
|
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.`
|
|
847
|
+
);
|
|
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
|
+
}
|
|
701
869
|
}
|
|
702
870
|
}
|
|
703
871
|
try {
|