@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.
Files changed (62) hide show
  1. package/README.md +24 -24
  2. package/dist/base-command.js +28 -8
  3. package/dist/base-command.js.map +1 -1
  4. package/dist/commands/config/set.js +28 -38
  5. package/dist/commands/config/set.js.map +1 -1
  6. package/dist/commands/config/show.js +29 -12
  7. package/dist/commands/config/show.js.map +1 -1
  8. package/dist/commands/config/write.js +288 -0
  9. package/dist/commands/config/write.js.map +1 -0
  10. package/dist/commands/disconnect.js +42 -54
  11. package/dist/commands/disconnect.js.map +1 -1
  12. package/dist/commands/mcp/index.js +62 -102
  13. package/dist/commands/mcp/index.js.map +1 -1
  14. package/dist/commands/rpc/call.js +218 -92
  15. package/dist/commands/rpc/call.js.map +1 -1
  16. package/dist/commands/session/revoke.js +727 -0
  17. package/dist/commands/session/revoke.js.map +1 -0
  18. package/dist/commands/session/setup.js +960 -0
  19. package/dist/commands/session/setup.js.map +1 -0
  20. package/dist/commands/session/status.js +206 -0
  21. package/dist/commands/session/status.js.map +1 -0
  22. package/dist/commands/version.js +28 -8
  23. package/dist/commands/version.js.map +1 -1
  24. package/dist/index.js +42 -67
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/bridge-singleton.js +47 -61
  27. package/dist/lib/bridge-singleton.js.map +1 -1
  28. package/dist/lib/config.js +17 -13
  29. package/dist/lib/config.js.map +1 -1
  30. package/dist/lib/crypto.js +3 -15
  31. package/dist/lib/crypto.js.map +1 -1
  32. package/dist/lib/keystore.js +64 -0
  33. package/dist/lib/keystore.js.map +1 -0
  34. package/dist/lib/output.js +1 -16
  35. package/dist/lib/output.js.map +1 -1
  36. package/dist/lib/paths.js +3 -1
  37. package/dist/lib/paths.js.map +1 -1
  38. package/dist/lib/session-bridge.js +168 -0
  39. package/dist/lib/session-bridge.js.map +1 -0
  40. package/dist/lib/session-config.js +52 -0
  41. package/dist/lib/session-config.js.map +1 -0
  42. package/dist/lib/validation.js +62 -27
  43. package/dist/lib/validation.js.map +1 -1
  44. package/dist/lib/ws-bridge.js +15 -46
  45. package/dist/lib/ws-bridge.js.map +1 -1
  46. package/dist/mcp/handlers/config.js +28 -29
  47. package/dist/mcp/handlers/config.js.map +1 -1
  48. package/dist/mcp/handlers/daemon.js +41 -51
  49. package/dist/mcp/handlers/daemon.js.map +1 -1
  50. package/dist/mcp/handlers/resources.js +1 -3
  51. package/dist/mcp/handlers/resources.js.map +1 -1
  52. package/dist/mcp/handlers/rpc.js +63 -79
  53. package/dist/mcp/handlers/rpc.js.map +1 -1
  54. package/dist/mcp/helpers.js.map +1 -1
  55. package/dist/mcp/server.js +61 -99
  56. package/dist/mcp/server.js.map +1 -1
  57. package/dist/mcp/tools.js +2 -4
  58. package/dist/mcp/tools.js.map +1 -1
  59. package/oclif.manifest.json +304 -4
  60. package/package.json +19 -1
  61. package/dist/lib/session-store.js +0 -80
  62. package/dist/lib/session-store.js.map +0 -1
@@ -15,12 +15,10 @@ var rpcMethodSchema = {
15
15
  params: z.any().optional().describe(
16
16
  "Method parameters \u2014 structure varies by method. Read the jaw://api-reference/{method} resource for the expected format."
17
17
  ),
18
- chainId: z.number().optional().describe(
19
- "Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia"
20
- )
18
+ chainId: z.number().optional().describe("Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia")
21
19
  };
22
20
  var configSetSchema = {
23
- key: z.enum(["apiKey", "defaultChain", "keysUrl", "paymasterUrl", "ens", "relayUrl"]).describe("Config key"),
21
+ key: z.enum(["apiKey", "defaultChain", "keysUrl", "ens", "relayUrl"]).describe("Config key"),
24
22
  value: z.string().describe("Config value")
25
23
  };
26
24
 
@@ -51,7 +49,9 @@ var PATHS = {
51
49
  root: JAW_DIR,
52
50
  config: path.join(JAW_DIR, "config.json"),
53
51
  session: path.join(JAW_DIR, "session.json"),
54
- relay: path.join(JAW_DIR, "relay.json")
52
+ relay: path.join(JAW_DIR, "relay.json"),
53
+ keystore: path.join(JAW_DIR, "keystore.json"),
54
+ sessionConfig: path.join(JAW_DIR, "session-config.json")
55
55
  };
56
56
 
57
57
  // src/lib/validation.ts
@@ -82,13 +82,23 @@ function ensureDir(dir) {
82
82
  fs2.mkdirSync(dir, { recursive: true, mode: 448 });
83
83
  fs2.chmodSync(dir, 448);
84
84
  }
85
+ function migrateConfig(config) {
86
+ if (config.paymasterUrl && !config.paymasters) {
87
+ const chainId = config.defaultChain ?? 1;
88
+ config.paymasters = { [chainId]: { url: config.paymasterUrl } };
89
+ delete config.paymasterUrl;
90
+ saveConfig(config);
91
+ }
92
+ return config;
93
+ }
85
94
  function loadConfig() {
86
95
  if (!fs2.existsSync(PATHS.config)) {
87
96
  return {};
88
97
  }
89
98
  const raw = fs2.readFileSync(PATHS.config, "utf-8");
90
99
  try {
91
- return JSON.parse(raw);
100
+ const config = JSON.parse(raw);
101
+ return migrateConfig(config);
92
102
  } catch {
93
103
  throw new Error(
94
104
  `Config file at ${PATHS.config} is not valid JSON. Run \`jaw config set apiKey=<key>\` to reset it.`
@@ -110,14 +120,10 @@ function redactConfig(config) {
110
120
  }
111
121
  function setConfigValue(key, value) {
112
122
  if (key === "keysUrl" && typeof value === "string" && !isValidKeysUrl(value)) {
113
- throw new Error(
114
- `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`
115
- );
123
+ throw new Error(`Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`);
116
124
  }
117
125
  if (key === "relayUrl" && typeof value === "string" && !isValidRelayUrl(value)) {
118
- throw new Error(
119
- `Untrusted relayUrl: ${value}. Must be wss://*.jaw.id or ws://localhost.`
120
- );
126
+ throw new Error(`Untrusted relayUrl: ${value}. Must be wss://*.jaw.id or ws://localhost.`);
121
127
  }
122
128
  const config = loadConfig();
123
129
  const updated = { ...config, [key]: value };
@@ -127,11 +133,7 @@ function setConfigValue(key, value) {
127
133
  // src/lib/crypto.ts
128
134
  var subtle = globalThis.crypto.subtle;
129
135
  async function generateKeyPair() {
130
- return subtle.generateKey(
131
- { name: "ECDH", namedCurve: "P-256" },
132
- true,
133
- ["deriveKey"]
134
- );
136
+ return subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey"]);
135
137
  }
136
138
  async function deriveSharedSecret(privateKey, peerPublicKey) {
137
139
  return subtle.deriveKey(
@@ -145,11 +147,7 @@ async function deriveSharedSecret(privateKey, peerPublicKey) {
145
147
  async function encryptMessage(sharedSecret, payload) {
146
148
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
147
149
  const plaintext = new TextEncoder().encode(JSON.stringify(payload));
148
- const cipherBuf = await subtle.encrypt(
149
- { name: "AES-GCM", iv },
150
- sharedSecret,
151
- plaintext
152
- );
150
+ const cipherBuf = await subtle.encrypt({ name: "AES-GCM", iv }, sharedSecret, plaintext);
153
151
  return {
154
152
  iv: bufferToBase64(iv),
155
153
  ciphertext: bufferToBase64(new Uint8Array(cipherBuf))
@@ -158,11 +156,7 @@ async function encryptMessage(sharedSecret, payload) {
158
156
  async function decryptMessage(sharedSecret, envelope) {
159
157
  const iv = Buffer.from(envelope.iv, "base64");
160
158
  const ciphertext = Buffer.from(envelope.ciphertext, "base64");
161
- const plainBuf = await subtle.decrypt(
162
- { name: "AES-GCM", iv },
163
- sharedSecret,
164
- ciphertext
165
- );
159
+ const plainBuf = await subtle.decrypt({ name: "AES-GCM", iv }, sharedSecret, ciphertext);
166
160
  return JSON.parse(new TextDecoder().decode(plainBuf));
167
161
  }
168
162
  async function exportKeyToHex(type, key) {
@@ -254,11 +248,7 @@ var WSBridge = class {
254
248
  let expectingKeyExchange = !this.peerPublicKeyHex;
255
249
  const timer = setTimeout(() => {
256
250
  ws.close();
257
- reject(
258
- new Error(
259
- "Browser did not connect in time.\nRun `jaw disconnect` then try again."
260
- )
261
- );
251
+ reject(new Error("Browser did not connect in time.\nRun `jaw disconnect` then try again."));
262
252
  }, 3e4);
263
253
  const sendEncryptedInit = async () => {
264
254
  if (!this.sharedSecret) return;
@@ -281,10 +271,7 @@ var WSBridge = class {
281
271
  if (!msg) return;
282
272
  if (msg.type === "encrypted" && this.sharedSecret) {
283
273
  try {
284
- const inner = await decryptMessage(
285
- this.sharedSecret,
286
- msg
287
- );
274
+ const inner = await decryptMessage(this.sharedSecret, msg);
288
275
  if (inner.type === "ready") {
289
276
  clearTimeout(readyTimer);
290
277
  ws.off("message", onMsg);
@@ -322,6 +309,10 @@ var WSBridge = class {
322
309
  expectingKeyExchange = true;
323
310
  onBrowserNeeded().catch(() => {
324
311
  });
312
+ } else if (!onBrowserNeeded) {
313
+ clearTimeout(timer);
314
+ ws.close();
315
+ reject(new Error("Browser not connected \u2014 relay session is stale."));
325
316
  }
326
317
  } else if (msg.type === "browser_connected") {
327
318
  expectingKeyExchange = true;
@@ -371,9 +362,7 @@ var WSBridge = class {
371
362
  return new Promise((resolve, reject) => {
372
363
  const timer = setTimeout(() => {
373
364
  reject(
374
- new Error(
375
- `Request timed out after ${this.timeout / 1e3}s. Did you complete the action in the browser?`
376
- )
365
+ new Error(`Request timed out after ${this.timeout / 1e3}s. Did you complete the action in the browser?`)
377
366
  );
378
367
  this.close();
379
368
  }, this.timeout);
@@ -381,10 +370,7 @@ var WSBridge = class {
381
370
  const msg = safeParse(data);
382
371
  if (!msg || msg.type !== "encrypted" || !this.sharedSecret) return;
383
372
  try {
384
- const inner = await decryptMessage(
385
- this.sharedSecret,
386
- msg
387
- );
373
+ const inner = await decryptMessage(this.sharedSecret, msg);
388
374
  if (inner.type === "rpc_response" && inner.id === id) {
389
375
  clearTimeout(timer);
390
376
  ws.off("message", onMessage);
@@ -392,11 +378,7 @@ var WSBridge = class {
392
378
  resolve(inner.data);
393
379
  } else {
394
380
  const err = inner.error;
395
- reject(
396
- new Error(
397
- err ? `[${err.code}] ${err.message}` : "Request failed"
398
- )
399
- );
381
+ reject(new Error(err ? `[${err.code}] ${err.message}` : "Request failed"));
400
382
  }
401
383
  }
402
384
  } catch {
@@ -406,9 +388,6 @@ var WSBridge = class {
406
388
  this.sendRaw(ws, serialized);
407
389
  });
408
390
  }
409
- isOpen() {
410
- return this.ws?.readyState === WebSocket.OPEN;
411
- }
412
391
  async shutdown() {
413
392
  this.disposed = true;
414
393
  if (this.ws?.readyState === WebSocket.OPEN && this.sharedSecret) {
@@ -416,10 +395,7 @@ var WSBridge = class {
416
395
  const envelope = await encryptMessage(this.sharedSecret, {
417
396
  type: "shutdown"
418
397
  });
419
- this.sendRaw(
420
- this.ws,
421
- JSON.stringify({ type: "encrypted", ...envelope })
422
- );
398
+ this.sendRaw(this.ws, JSON.stringify({ type: "encrypted", ...envelope }));
423
399
  } catch {
424
400
  }
425
401
  }
@@ -497,10 +473,8 @@ var WSBridge = class {
497
473
  this.reconnectAttempts++;
498
474
  setTimeout(() => {
499
475
  if (this.disposed) return;
500
- this.connectInternal(this.onBrowserNeeded, this.onPeerKeyChanged).catch(
501
- () => {
502
- }
503
- );
476
+ this.connectInternal(this.onBrowserNeeded, this.onPeerKeyChanged).catch(() => {
477
+ });
504
478
  }, delay);
505
479
  }
506
480
  /** Send a raw string over the WebSocket, enforcing message size limits. */
@@ -510,10 +484,7 @@ var WSBridge = class {
510
484
  async deriveSecret() {
511
485
  if (!this.peerPublicKeyHex) return;
512
486
  const privateKey = await importKeyFromHex("private", this.privateKeyHex);
513
- const peerPublicKey = await importKeyFromHex(
514
- "public",
515
- this.peerPublicKeyHex
516
- );
487
+ const peerPublicKey = await importKeyFromHex("public", this.peerPublicKeyHex);
517
488
  this.sharedSecret = await deriveSharedSecret(privateKey, peerPublicKey);
518
489
  }
519
490
  };
@@ -575,17 +546,19 @@ async function getBridge(options) {
575
546
  throw new Error(`Untrusted relayUrl: ${relayUrl}. Must be wss://*.jaw.id or ws://localhost.`);
576
547
  }
577
548
  let relaySession = loadRelaySession();
578
- if (relaySession && relaySession.relayUrl === relayUrl) {
549
+ if (relaySession && relaySession.relayUrl === relayUrl && relaySession.peerPublicKey) {
579
550
  try {
580
- return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl);
551
+ return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl, false);
581
552
  } catch {
582
553
  deleteRelaySession();
583
554
  relaySession = null;
584
555
  }
556
+ } else if (relaySession) {
557
+ deleteRelaySession();
585
558
  }
586
559
  const session = await createNewSession(relayUrl);
587
560
  saveRelaySession(session);
588
- return await connectBridge(session, options, chainId, keysUrl, relayUrl);
561
+ return await connectBridge(session, options, chainId, keysUrl, relayUrl, true);
589
562
  }
590
563
  async function createNewSession(relayUrl) {
591
564
  const kp = await generateKeyPair();
@@ -600,7 +573,7 @@ async function createNewSession(relayUrl) {
600
573
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
601
574
  };
602
575
  }
603
- async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl) {
576
+ async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl, openBrowser) {
604
577
  const config = loadConfig();
605
578
  const bridge = new WSBridge({
606
579
  relayUrl,
@@ -610,19 +583,19 @@ async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl)
610
583
  apiKey: options.apiKey,
611
584
  chainId,
612
585
  ens: options.ens ?? config.ens,
613
- paymasterUrl: options.paymasterUrl ?? config.paymasterUrl
586
+ paymasterUrl: options.paymasterUrl ?? config.paymasters?.[chainId]?.url
614
587
  },
615
588
  privateKeyHex: relaySession.privateKey,
616
589
  publicKeyHex: relaySession.publicKey,
617
590
  peerPublicKeyHex: relaySession.peerPublicKey
618
591
  });
619
592
  await bridge.connect(
620
- // onBrowserNeeded
621
- async () => {
593
+ // onBrowserNeeded — only open a browser for new sessions
594
+ openBrowser ? async () => {
622
595
  const bridgeUrl = buildBridgeUrl(keysUrl, relaySession.session, relayUrl, relaySession.publicKey);
623
596
  const { default: open } = await import('open');
624
597
  await open(bridgeUrl);
625
- },
598
+ } : void 0,
626
599
  // onPeerKeyChanged
627
600
  (newPeerKey) => {
628
601
  relaySession.peerPublicKey = newPeerKey;
@@ -682,9 +655,7 @@ async function shutdownDaemon() {
682
655
  function resolveApiKey() {
683
656
  const apiKey = process.env["JAW_API_KEY"] ?? loadConfig().apiKey;
684
657
  if (!apiKey) {
685
- throw new Error(
686
- "API key required. Set JAW_API_KEY env var or run: jaw config set apiKey <key>"
687
- );
658
+ throw new Error("API key required. Set JAW_API_KEY env var or run: jaw config set apiKey <key>");
688
659
  }
689
660
  return apiKey;
690
661
  }
@@ -696,12 +667,14 @@ function registerRpcTool(server) {
696
667
  async (params) => {
697
668
  const config = loadConfig();
698
669
  const apiKey = resolveApiKey();
670
+ const chainId = params.chainId ?? config.defaultChain ?? 1;
671
+ const pm = config.paymasters?.[chainId];
699
672
  const bridge = await getBridge({
700
673
  keysUrl: config.keysUrl,
701
674
  apiKey,
702
- chainId: params.chainId ?? config.defaultChain,
675
+ chainId,
703
676
  ens: config.ens,
704
- paymasterUrl: config.paymasterUrl
677
+ paymasterUrl: pm?.url
705
678
  });
706
679
  try {
707
680
  const result = await bridge.request(params.method, params.params);
@@ -717,26 +690,19 @@ function registerRpcTool(server) {
717
690
 
718
691
  // src/mcp/handlers/config.ts
719
692
  function registerConfigTools(server) {
720
- server.tool(
721
- "jaw_config_show",
722
- "Show current CLI configuration (API key redacted).",
723
- {},
724
- async () => {
725
- try {
726
- const config = redactConfig(loadConfig());
727
- return {
728
- content: [
729
- { type: "text", text: JSON.stringify(config) }
730
- ]
731
- };
732
- } catch (err) {
733
- return mcpError(err);
734
- }
693
+ server.tool("jaw_config_show", "Show current CLI configuration (API key redacted).", {}, async () => {
694
+ try {
695
+ const config = redactConfig(loadConfig());
696
+ return {
697
+ content: [{ type: "text", text: JSON.stringify(config) }]
698
+ };
699
+ } catch (err) {
700
+ return mcpError(err);
735
701
  }
736
- );
702
+ });
737
703
  server.tool(
738
704
  "jaw_config_set",
739
- "Set a CLI configuration value (apiKey, defaultChain, keysUrl, paymasterUrl, ens, relayUrl).",
705
+ "Set a CLI configuration value (apiKey, defaultChain, keysUrl, ens, relayUrl).",
740
706
  configSetSchema,
741
707
  async (params) => {
742
708
  try {
@@ -786,9 +752,7 @@ function registerDaemonTools(server) {
786
752
  config
787
753
  };
788
754
  return {
789
- content: [
790
- { type: "text", text: JSON.stringify(status, null, 2) }
791
- ]
755
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }]
792
756
  };
793
757
  } catch (err) {
794
758
  return mcpError(err);
@@ -853,9 +817,7 @@ function registerResources(server) {
853
817
  async (uri, variables) => {
854
818
  const method = String(variables.method);
855
819
  if (!/^[\w_]+$/.test(method)) {
856
- throw new Error(
857
- `Invalid method name: "${method}". Expected an RPC method like wallet_sendCalls.`
858
- );
820
+ throw new Error(`Invalid method name: "${method}". Expected an RPC method like wallet_sendCalls.`);
859
821
  }
860
822
  return {
861
823
  contents: [