@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.
Files changed (56) hide show
  1. package/dist/base-command.js +26 -2
  2. package/dist/base-command.js.map +1 -1
  3. package/dist/commands/config/set.js +19 -7
  4. package/dist/commands/config/set.js.map +1 -1
  5. package/dist/commands/config/show.js +26 -2
  6. package/dist/commands/config/show.js.map +1 -1
  7. package/dist/commands/config/write.js +288 -0
  8. package/dist/commands/config/write.js.map +1 -0
  9. package/dist/commands/disconnect.js +29 -5
  10. package/dist/commands/disconnect.js.map +1 -1
  11. package/dist/commands/mcp/index.js +34 -17
  12. package/dist/commands/mcp/index.js.map +1 -1
  13. package/dist/commands/rpc/call.js +205 -37
  14. package/dist/commands/rpc/call.js.map +1 -1
  15. package/dist/commands/session/revoke.js +727 -0
  16. package/dist/commands/session/revoke.js.map +1 -0
  17. package/dist/commands/session/setup.js +960 -0
  18. package/dist/commands/session/setup.js.map +1 -0
  19. package/dist/commands/session/status.js +206 -0
  20. package/dist/commands/session/status.js.map +1 -0
  21. package/dist/commands/version.js +26 -2
  22. package/dist/commands/version.js.map +1 -1
  23. package/dist/index.js +28 -13
  24. package/dist/index.js.map +1 -1
  25. package/dist/lib/bridge-singleton.js +35 -13
  26. package/dist/lib/bridge-singleton.js.map +1 -1
  27. package/dist/lib/config.js +15 -7
  28. package/dist/lib/config.js.map +1 -1
  29. package/dist/lib/keystore.js +64 -0
  30. package/dist/lib/keystore.js.map +1 -0
  31. package/dist/lib/output.js +1 -12
  32. package/dist/lib/output.js.map +1 -1
  33. package/dist/lib/paths.js +3 -1
  34. package/dist/lib/paths.js.map +1 -1
  35. package/dist/lib/session-bridge.js +168 -0
  36. package/dist/lib/session-bridge.js.map +1 -0
  37. package/dist/lib/session-config.js +52 -0
  38. package/dist/lib/session-config.js.map +1 -0
  39. package/dist/lib/validation.js +62 -27
  40. package/dist/lib/validation.js.map +1 -1
  41. package/dist/lib/ws-bridge.js +4 -3
  42. package/dist/lib/ws-bridge.js.map +1 -1
  43. package/dist/mcp/handlers/config.js +16 -4
  44. package/dist/mcp/handlers/config.js.map +1 -1
  45. package/dist/mcp/handlers/daemon.js +29 -5
  46. package/dist/mcp/handlers/daemon.js.map +1 -1
  47. package/dist/mcp/handlers/rpc.js +49 -25
  48. package/dist/mcp/handlers/rpc.js.map +1 -1
  49. package/dist/mcp/server.js +34 -17
  50. package/dist/mcp/server.js.map +1 -1
  51. package/dist/mcp/tools.js +1 -1
  52. package/dist/mcp/tools.js.map +1 -1
  53. package/oclif.manifest.json +304 -4
  54. package/package.json +18 -1
  55. package/dist/lib/session-store.js +0 -80
  56. 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 fs2 from 'fs';
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
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
43
- fs2.chmodSync(dir, 448);
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 (!fs2.existsSync(PATHS.config)) {
57
+ if (!fs.existsSync(PATHS.config)) {
47
58
  return {};
48
59
  }
49
- const raw = fs2.readFileSync(PATHS.config, "utf-8");
60
+ const raw = fs.readFileSync(PATHS.config, "utf-8");
50
61
  try {
51
- return JSON.parse(raw);
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 (!fs2.existsSync(PATHS.relay)) return null;
527
- const raw = fs2.readFileSync(PATHS.relay, "utf-8");
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
- fs2.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
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 (fs2.existsSync(PATHS.relay)) fs2.unlinkSync(PATHS.relay);
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.paymasterUrl
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 bridge = await getBridge({
689
- keysUrl: config.keysUrl,
690
- apiKey,
691
- chainId: flags.chain ?? config.defaultChain,
692
- ens: config.ens,
693
- paymasterUrl: config.paymasterUrl,
694
- timeout: flags.timeout * 1e3
695
- });
696
- if (!flags.quiet) {
697
- if (requiresBrowser(method)) {
698
- this.log(`Sending ${method}... Check your browser to approve the request.`);
699
- } else {
700
- this.log(`Sending ${method}...`);
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 {