@jaw.id/cli 0.0.3 → 0.0.7
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 +1 -2
- package/dist/base-command.js.map +1 -1
- package/dist/commands/config/set.js +19 -3
- package/dist/commands/config/set.js.map +1 -1
- package/dist/commands/config/show.js +1 -2
- package/dist/commands/config/show.js.map +1 -1
- package/dist/commands/disconnect.js +439 -36
- package/dist/commands/disconnect.js.map +1 -1
- package/dist/commands/mcp/index.js +486 -251
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/rpc/call.js +429 -204
- package/dist/commands/rpc/call.js.map +1 -1
- package/dist/commands/version.js +134 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/index.js +455 -203
- package/dist/index.js.map +1 -1
- package/dist/lib/bridge-singleton.js +457 -205
- package/dist/lib/bridge-singleton.js.map +1 -1
- package/dist/lib/config.js +17 -2
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/crypto.js +74 -0
- package/dist/lib/crypto.js.map +1 -0
- package/dist/lib/paths.js +1 -2
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/session-store.js +1 -2
- package/dist/lib/session-store.js.map +1 -1
- package/dist/lib/validation.js +12 -1
- package/dist/lib/validation.js.map +1 -1
- package/dist/lib/ws-bridge.js +321 -59
- package/dist/lib/ws-bridge.js.map +1 -1
- package/dist/mcp/handlers/config.js +19 -4
- package/dist/mcp/handlers/config.js.map +1 -1
- package/dist/mcp/handlers/daemon.js +447 -72
- package/dist/mcp/handlers/daemon.js.map +1 -1
- package/dist/mcp/handlers/rpc.js +442 -235
- package/dist/mcp/handlers/rpc.js.map +1 -1
- package/dist/mcp/server.js +486 -251
- 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 +70 -2
- package/package.json +5 -1
- package/dist/lib/ws-daemon.js +0 -382
- package/dist/lib/ws-daemon.js.map +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -2,11 +2,9 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import * as fs2 from 'fs';
|
|
5
|
+
import * as crypto from 'crypto';
|
|
5
6
|
import * as path from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
import { spawn, execSync } from 'child_process';
|
|
8
7
|
import * as os from 'os';
|
|
9
|
-
import * as crypto from 'crypto';
|
|
10
8
|
import WebSocket from 'ws';
|
|
11
9
|
|
|
12
10
|
// src/mcp/server.ts
|
|
@@ -22,7 +20,7 @@ var rpcMethodSchema = {
|
|
|
22
20
|
)
|
|
23
21
|
};
|
|
24
22
|
var configSetSchema = {
|
|
25
|
-
key: z.enum(["apiKey", "defaultChain", "keysUrl", "paymasterUrl", "ens"]).describe("Config key"),
|
|
23
|
+
key: z.enum(["apiKey", "defaultChain", "keysUrl", "paymasterUrl", "ens", "relayUrl"]).describe("Config key"),
|
|
26
24
|
value: z.string().describe("Config value")
|
|
27
25
|
};
|
|
28
26
|
|
|
@@ -53,8 +51,7 @@ var PATHS = {
|
|
|
53
51
|
root: JAW_DIR,
|
|
54
52
|
config: path.join(JAW_DIR, "config.json"),
|
|
55
53
|
session: path.join(JAW_DIR, "session.json"),
|
|
56
|
-
|
|
57
|
-
daemonLog: path.join(JAW_DIR, "daemon.log")
|
|
54
|
+
relay: path.join(JAW_DIR, "relay.json")
|
|
58
55
|
};
|
|
59
56
|
|
|
60
57
|
// src/lib/validation.ts
|
|
@@ -68,6 +65,17 @@ function isValidKeysUrl(url) {
|
|
|
68
65
|
return false;
|
|
69
66
|
}
|
|
70
67
|
}
|
|
68
|
+
function isValidRelayUrl(url) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = new URL(url);
|
|
71
|
+
const isTrustedHost = parsed.hostname.endsWith(".jaw.id") || parsed.hostname === "jaw.id" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
72
|
+
const isSecure = parsed.protocol === "wss:" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
73
|
+
const isWebSocket = parsed.protocol === "wss:" || parsed.protocol === "ws:";
|
|
74
|
+
return isTrustedHost && isSecure && isWebSocket;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
71
79
|
|
|
72
80
|
// src/lib/config.ts
|
|
73
81
|
function ensureDir(dir) {
|
|
@@ -106,53 +114,226 @@ function setConfigValue(key, value) {
|
|
|
106
114
|
`Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`
|
|
107
115
|
);
|
|
108
116
|
}
|
|
117
|
+
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
|
+
);
|
|
121
|
+
}
|
|
109
122
|
const config = loadConfig();
|
|
110
123
|
const updated = { ...config, [key]: value };
|
|
111
124
|
saveConfig(updated);
|
|
112
125
|
}
|
|
126
|
+
|
|
127
|
+
// src/lib/crypto.ts
|
|
128
|
+
var subtle = globalThis.crypto.subtle;
|
|
129
|
+
async function generateKeyPair() {
|
|
130
|
+
return subtle.generateKey(
|
|
131
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
132
|
+
true,
|
|
133
|
+
["deriveKey"]
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
async function deriveSharedSecret(privateKey, peerPublicKey) {
|
|
137
|
+
return subtle.deriveKey(
|
|
138
|
+
{ name: "ECDH", public: peerPublicKey },
|
|
139
|
+
privateKey,
|
|
140
|
+
{ name: "AES-GCM", length: 256 },
|
|
141
|
+
false,
|
|
142
|
+
["encrypt", "decrypt"]
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
async function encryptMessage(sharedSecret, payload) {
|
|
146
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
147
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
148
|
+
const cipherBuf = await subtle.encrypt(
|
|
149
|
+
{ name: "AES-GCM", iv },
|
|
150
|
+
sharedSecret,
|
|
151
|
+
plaintext
|
|
152
|
+
);
|
|
153
|
+
return {
|
|
154
|
+
iv: bufferToBase64(iv),
|
|
155
|
+
ciphertext: bufferToBase64(new Uint8Array(cipherBuf))
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function decryptMessage(sharedSecret, envelope) {
|
|
159
|
+
const iv = Buffer.from(envelope.iv, "base64");
|
|
160
|
+
const ciphertext = Buffer.from(envelope.ciphertext, "base64");
|
|
161
|
+
const plainBuf = await subtle.decrypt(
|
|
162
|
+
{ name: "AES-GCM", iv },
|
|
163
|
+
sharedSecret,
|
|
164
|
+
ciphertext
|
|
165
|
+
);
|
|
166
|
+
return JSON.parse(new TextDecoder().decode(plainBuf));
|
|
167
|
+
}
|
|
168
|
+
async function exportKeyToHex(type, key) {
|
|
169
|
+
const format = type === "private" ? "pkcs8" : "spki";
|
|
170
|
+
const buf = await subtle.exportKey(format, key);
|
|
171
|
+
return bytesToHex(new Uint8Array(buf));
|
|
172
|
+
}
|
|
173
|
+
async function importKeyFromHex(type, hex) {
|
|
174
|
+
const format = type === "private" ? "pkcs8" : "spki";
|
|
175
|
+
return subtle.importKey(
|
|
176
|
+
format,
|
|
177
|
+
Buffer.from(hexToBytes(hex)),
|
|
178
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
179
|
+
true,
|
|
180
|
+
type === "private" ? ["deriveKey"] : []
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
function bytesToHex(bytes) {
|
|
184
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
185
|
+
}
|
|
186
|
+
function hexToBytes(hex) {
|
|
187
|
+
if (hex.length % 2 !== 0) throw new Error("Invalid hex: odd length");
|
|
188
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
189
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
190
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
191
|
+
}
|
|
192
|
+
return bytes;
|
|
193
|
+
}
|
|
194
|
+
function bufferToBase64(buf) {
|
|
195
|
+
return Buffer.from(buf).toString("base64");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/lib/ws-bridge.ts
|
|
113
199
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
200
|
+
var MAX_MESSAGE_BYTES = 5 * 1024 * 1024;
|
|
201
|
+
var BROWSER_REOPEN_COOLDOWN_MS = 5e3;
|
|
202
|
+
var MAX_RECONNECT_ATTEMPTS = 3;
|
|
203
|
+
var RECONNECT_BASE_DELAY_MS = 1e3;
|
|
114
204
|
var WSBridge = class {
|
|
115
|
-
|
|
116
|
-
|
|
205
|
+
relayUrl;
|
|
206
|
+
session;
|
|
117
207
|
timeout;
|
|
208
|
+
config;
|
|
209
|
+
privateKeyHex;
|
|
210
|
+
publicKeyHex;
|
|
211
|
+
peerPublicKeyHex;
|
|
212
|
+
sharedSecret = null;
|
|
118
213
|
ws = null;
|
|
214
|
+
disposed = false;
|
|
215
|
+
// Auto-reopen browser state
|
|
216
|
+
onBrowserNeeded;
|
|
217
|
+
onPeerKeyChanged;
|
|
218
|
+
lastBrowserOpenTime = 0;
|
|
219
|
+
// Reconnection state
|
|
220
|
+
reconnectAttempts = 0;
|
|
221
|
+
/** Updated after key exchange — caller should persist this. */
|
|
222
|
+
get peerPublicKey() {
|
|
223
|
+
return this.peerPublicKeyHex;
|
|
224
|
+
}
|
|
119
225
|
constructor(options) {
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
226
|
+
this.relayUrl = options.relayUrl;
|
|
227
|
+
this.session = options.session;
|
|
122
228
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
229
|
+
this.config = options.config;
|
|
230
|
+
this.privateKeyHex = options.privateKeyHex;
|
|
231
|
+
this.publicKeyHex = options.publicKeyHex;
|
|
232
|
+
this.peerPublicKeyHex = options.peerPublicKeyHex;
|
|
123
233
|
}
|
|
124
234
|
/**
|
|
125
|
-
* Connect to the
|
|
235
|
+
* Connect to the relay and wait for the browser to be ready.
|
|
236
|
+
*
|
|
237
|
+
* @param onBrowserNeeded — called when the relay reports no browser connected.
|
|
238
|
+
* @param onPeerKeyChanged — called when a key_exchange updates the peer key.
|
|
126
239
|
*/
|
|
127
|
-
async connect() {
|
|
240
|
+
async connect(onBrowserNeeded, onPeerKeyChanged) {
|
|
241
|
+
this.onBrowserNeeded = onBrowserNeeded;
|
|
242
|
+
this.onPeerKeyChanged = onPeerKeyChanged;
|
|
243
|
+
if (this.peerPublicKeyHex) {
|
|
244
|
+
await this.deriveSecret();
|
|
245
|
+
}
|
|
246
|
+
return this.connectInternal(onBrowserNeeded, onPeerKeyChanged);
|
|
247
|
+
}
|
|
248
|
+
async connectInternal(onBrowserNeeded, onPeerKeyChanged) {
|
|
128
249
|
return new Promise((resolve, reject) => {
|
|
129
|
-
const url =
|
|
250
|
+
const url = `${this.relayUrl}?session=${encodeURIComponent(this.session)}&role=cli`;
|
|
130
251
|
const ws = new WebSocket(url);
|
|
252
|
+
let browserOpened = false;
|
|
253
|
+
let resolved = false;
|
|
254
|
+
let expectingKeyExchange = !this.peerPublicKeyHex;
|
|
131
255
|
const timer = setTimeout(() => {
|
|
132
256
|
ws.close();
|
|
133
257
|
reject(
|
|
134
258
|
new Error(
|
|
135
|
-
"Browser
|
|
259
|
+
"Browser did not connect in time.\nRun `jaw disconnect` then try again."
|
|
136
260
|
)
|
|
137
261
|
);
|
|
138
262
|
}, 3e4);
|
|
139
|
-
|
|
263
|
+
const sendEncryptedInit = async () => {
|
|
264
|
+
if (!this.sharedSecret) return;
|
|
265
|
+
const envelope = await encryptMessage(this.sharedSecret, {
|
|
266
|
+
type: "init",
|
|
267
|
+
apiKey: this.config.apiKey,
|
|
268
|
+
chainId: this.config.chainId,
|
|
269
|
+
ens: this.config.ens,
|
|
270
|
+
paymasterUrl: this.config.paymasterUrl
|
|
271
|
+
});
|
|
272
|
+
this.sendRaw(ws, JSON.stringify({ type: "encrypted", ...envelope }));
|
|
273
|
+
};
|
|
274
|
+
const waitForReady = () => {
|
|
275
|
+
const readyTimer = setTimeout(() => {
|
|
276
|
+
ws.close();
|
|
277
|
+
reject(new Error("Browser SDK did not become ready in time."));
|
|
278
|
+
}, 15e3);
|
|
279
|
+
const onMsg = async (data) => {
|
|
280
|
+
const msg = safeParse(data);
|
|
281
|
+
if (!msg) return;
|
|
282
|
+
if (msg.type === "encrypted" && this.sharedSecret) {
|
|
283
|
+
try {
|
|
284
|
+
const inner = await decryptMessage(
|
|
285
|
+
this.sharedSecret,
|
|
286
|
+
msg
|
|
287
|
+
);
|
|
288
|
+
if (inner.type === "ready") {
|
|
289
|
+
clearTimeout(readyTimer);
|
|
290
|
+
ws.off("message", onMsg);
|
|
291
|
+
this.reconnectAttempts = 0;
|
|
292
|
+
resolve();
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
ws.on("message", onMsg);
|
|
299
|
+
};
|
|
300
|
+
const onBrowserReady = async () => {
|
|
301
|
+
if (resolved) return;
|
|
302
|
+
resolved = true;
|
|
140
303
|
clearTimeout(timer);
|
|
304
|
+
waitForReady();
|
|
305
|
+
await sendEncryptedInit();
|
|
306
|
+
};
|
|
307
|
+
ws.on("open", () => {
|
|
141
308
|
this.ws = ws;
|
|
142
309
|
});
|
|
143
|
-
ws.on("message", (data) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
310
|
+
ws.on("message", async (data) => {
|
|
311
|
+
const msg = safeParse(data);
|
|
312
|
+
if (!msg) return;
|
|
313
|
+
if (msg.type === "status") {
|
|
314
|
+
if (msg.browserConnected) {
|
|
315
|
+
if (this.sharedSecret) {
|
|
316
|
+
await onBrowserReady();
|
|
317
|
+
} else {
|
|
318
|
+
expectingKeyExchange = true;
|
|
319
|
+
}
|
|
320
|
+
} else if (!browserOpened && onBrowserNeeded) {
|
|
321
|
+
browserOpened = true;
|
|
322
|
+
expectingKeyExchange = true;
|
|
323
|
+
onBrowserNeeded().catch(() => {
|
|
324
|
+
});
|
|
325
|
+
}
|
|
153
326
|
} else if (msg.type === "browser_connected") {
|
|
154
|
-
|
|
155
|
-
|
|
327
|
+
expectingKeyExchange = true;
|
|
328
|
+
} else if (msg.type === "browser_disconnected") {
|
|
329
|
+
this.handleBrowserDisconnect();
|
|
330
|
+
} else if (msg.type === "key_exchange" && expectingKeyExchange) {
|
|
331
|
+
expectingKeyExchange = false;
|
|
332
|
+
const peerKey = msg.publicKey;
|
|
333
|
+
this.peerPublicKeyHex = peerKey;
|
|
334
|
+
await this.deriveSecret();
|
|
335
|
+
onPeerKeyChanged?.(peerKey);
|
|
336
|
+
await onBrowserReady();
|
|
156
337
|
}
|
|
157
338
|
});
|
|
158
339
|
ws.on("error", (err) => {
|
|
@@ -161,18 +342,32 @@ var WSBridge = class {
|
|
|
161
342
|
});
|
|
162
343
|
ws.on("close", () => {
|
|
163
344
|
clearTimeout(timer);
|
|
345
|
+
if (!this.disposed) {
|
|
346
|
+
this.handleRelayDisconnect();
|
|
347
|
+
}
|
|
164
348
|
});
|
|
165
349
|
});
|
|
166
350
|
}
|
|
167
351
|
/**
|
|
168
|
-
* Send an RPC request through the
|
|
352
|
+
* Send an encrypted RPC request through the relay to the browser SDK.
|
|
169
353
|
*/
|
|
170
354
|
async request(method, params) {
|
|
171
355
|
const ws = this.ws;
|
|
172
356
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
173
|
-
throw new Error("Not connected to
|
|
357
|
+
throw new Error("Not connected to relay");
|
|
358
|
+
}
|
|
359
|
+
if (!this.sharedSecret) {
|
|
360
|
+
throw new Error("No shared secret \u2014 key exchange not completed");
|
|
174
361
|
}
|
|
175
362
|
const id = crypto.randomUUID();
|
|
363
|
+
const envelope = await encryptMessage(this.sharedSecret, {
|
|
364
|
+
type: "rpc_request",
|
|
365
|
+
id,
|
|
366
|
+
method,
|
|
367
|
+
params
|
|
368
|
+
});
|
|
369
|
+
const serialized = JSON.stringify({ type: "encrypted", ...envelope });
|
|
370
|
+
assertMessageSize(serialized, method);
|
|
176
371
|
return new Promise((resolve, reject) => {
|
|
177
372
|
const timer = setTimeout(() => {
|
|
178
373
|
reject(
|
|
@@ -182,58 +377,91 @@ var WSBridge = class {
|
|
|
182
377
|
);
|
|
183
378
|
this.close();
|
|
184
379
|
}, this.timeout);
|
|
185
|
-
const onMessage = (data) => {
|
|
186
|
-
|
|
380
|
+
const onMessage = async (data) => {
|
|
381
|
+
const msg = safeParse(data);
|
|
382
|
+
if (!msg || msg.type !== "encrypted" || !this.sharedSecret) return;
|
|
187
383
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
384
|
+
const inner = await decryptMessage(
|
|
385
|
+
this.sharedSecret,
|
|
386
|
+
msg
|
|
387
|
+
);
|
|
388
|
+
if (inner.type === "rpc_response" && inner.id === id) {
|
|
389
|
+
clearTimeout(timer);
|
|
390
|
+
ws.off("message", onMessage);
|
|
391
|
+
if (inner.success) {
|
|
392
|
+
resolve(inner.data);
|
|
393
|
+
} else {
|
|
394
|
+
const err = inner.error;
|
|
395
|
+
reject(
|
|
396
|
+
new Error(
|
|
397
|
+
err ? `[${err.code}] ${err.message}` : "Request failed"
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
204
401
|
}
|
|
402
|
+
} catch {
|
|
205
403
|
}
|
|
206
404
|
};
|
|
207
405
|
ws.on("message", onMessage);
|
|
208
|
-
|
|
209
|
-
JSON.stringify({
|
|
210
|
-
id,
|
|
211
|
-
type: "rpc_request",
|
|
212
|
-
method,
|
|
213
|
-
params
|
|
214
|
-
})
|
|
215
|
-
);
|
|
406
|
+
this.sendRaw(ws, serialized);
|
|
216
407
|
});
|
|
217
408
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Check if the WebSocket connection is open.
|
|
220
|
-
*/
|
|
221
409
|
isOpen() {
|
|
222
410
|
return this.ws?.readyState === WebSocket.OPEN;
|
|
223
411
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
412
|
+
async shutdown() {
|
|
413
|
+
this.disposed = true;
|
|
414
|
+
if (this.ws?.readyState === WebSocket.OPEN && this.sharedSecret) {
|
|
415
|
+
try {
|
|
416
|
+
const envelope = await encryptMessage(this.sharedSecret, {
|
|
417
|
+
type: "shutdown"
|
|
418
|
+
});
|
|
419
|
+
this.sendRaw(
|
|
420
|
+
this.ws,
|
|
421
|
+
JSON.stringify({ type: "encrypted", ...envelope })
|
|
422
|
+
);
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
230
425
|
}
|
|
231
426
|
this.close();
|
|
232
427
|
}
|
|
233
428
|
/**
|
|
234
|
-
*
|
|
429
|
+
* Connect to relay and send shutdown directly — no init/ready handshake.
|
|
430
|
+
* Used by `jaw disconnect` when we just need to tell the browser to close.
|
|
235
431
|
*/
|
|
432
|
+
async connectAndShutdown() {
|
|
433
|
+
if (!this.peerPublicKeyHex) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
this.disposed = true;
|
|
437
|
+
await this.deriveSecret();
|
|
438
|
+
return new Promise((resolve) => {
|
|
439
|
+
const url = `${this.relayUrl}?session=${encodeURIComponent(this.session)}&role=cli`;
|
|
440
|
+
const ws = new WebSocket(url);
|
|
441
|
+
const timer = setTimeout(() => {
|
|
442
|
+
try {
|
|
443
|
+
ws.close();
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
resolve();
|
|
447
|
+
}, 3e3);
|
|
448
|
+
ws.on("open", async () => {
|
|
449
|
+
this.ws = ws;
|
|
450
|
+
try {
|
|
451
|
+
await this.shutdown();
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
clearTimeout(timer);
|
|
455
|
+
resolve();
|
|
456
|
+
});
|
|
457
|
+
ws.on("error", () => {
|
|
458
|
+
clearTimeout(timer);
|
|
459
|
+
resolve();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
236
463
|
close() {
|
|
464
|
+
this.disposed = true;
|
|
237
465
|
if (this.ws) {
|
|
238
466
|
try {
|
|
239
467
|
this.ws.close();
|
|
@@ -242,182 +470,211 @@ var WSBridge = class {
|
|
|
242
470
|
this.ws = null;
|
|
243
471
|
}
|
|
244
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Auto-reopen browser when browser_disconnected is received from relay.
|
|
475
|
+
* Respects a cooldown to prevent rapid re-opening.
|
|
476
|
+
*/
|
|
477
|
+
handleBrowserDisconnect() {
|
|
478
|
+
if (this.disposed || !this.onBrowserNeeded) return;
|
|
479
|
+
const now = Date.now();
|
|
480
|
+
if (now - this.lastBrowserOpenTime < BROWSER_REOPEN_COOLDOWN_MS) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
this.lastBrowserOpenTime = now;
|
|
484
|
+
this.sharedSecret = null;
|
|
485
|
+
this.peerPublicKeyHex = null;
|
|
486
|
+
this.onBrowserNeeded().catch(() => {
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Attempt to reconnect to the relay with exponential backoff
|
|
491
|
+
* when the WebSocket connection drops unexpectedly.
|
|
492
|
+
*/
|
|
493
|
+
handleRelayDisconnect() {
|
|
494
|
+
if (this.disposed) return;
|
|
495
|
+
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
|
|
496
|
+
const delay = RECONNECT_BASE_DELAY_MS * Math.pow(2, this.reconnectAttempts);
|
|
497
|
+
this.reconnectAttempts++;
|
|
498
|
+
setTimeout(() => {
|
|
499
|
+
if (this.disposed) return;
|
|
500
|
+
this.connectInternal(this.onBrowserNeeded, this.onPeerKeyChanged).catch(
|
|
501
|
+
() => {
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
}, delay);
|
|
505
|
+
}
|
|
506
|
+
/** Send a raw string over the WebSocket, enforcing message size limits. */
|
|
507
|
+
sendRaw(ws, data) {
|
|
508
|
+
ws.send(data);
|
|
509
|
+
}
|
|
510
|
+
async deriveSecret() {
|
|
511
|
+
if (!this.peerPublicKeyHex) return;
|
|
512
|
+
const privateKey = await importKeyFromHex("private", this.privateKeyHex);
|
|
513
|
+
const peerPublicKey = await importKeyFromHex(
|
|
514
|
+
"public",
|
|
515
|
+
this.peerPublicKeyHex
|
|
516
|
+
);
|
|
517
|
+
this.sharedSecret = await deriveSharedSecret(privateKey, peerPublicKey);
|
|
518
|
+
}
|
|
245
519
|
};
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
dir = path.dirname(dir);
|
|
254
|
-
}
|
|
255
|
-
throw new Error("Cannot find ws-daemon.js in dist tree");
|
|
256
|
-
}
|
|
257
|
-
var JAW_KEYS_URL = "https://keys.jaw.id";
|
|
258
|
-
var LOCK_PATH = path.join(PATHS.root, "daemon.lock");
|
|
259
|
-
function isDaemonProcess(pid) {
|
|
260
|
-
if (!Number.isInteger(pid) || pid <= 0 || pid > 4194304) return false;
|
|
261
|
-
try {
|
|
262
|
-
process.kill(pid, 0);
|
|
263
|
-
} catch {
|
|
264
|
-
return false;
|
|
520
|
+
function assertMessageSize(serialized, method) {
|
|
521
|
+
const byteLength = Buffer.byteLength(serialized, "utf-8");
|
|
522
|
+
if (byteLength > MAX_MESSAGE_BYTES) {
|
|
523
|
+
const sizeMB = (byteLength / (1024 * 1024)).toFixed(2);
|
|
524
|
+
throw new Error(
|
|
525
|
+
`Message for ${method} is too large (${sizeMB} MB, limit ${MAX_MESSAGE_BYTES / (1024 * 1024)} MB). Try reducing the number of calls in your batch.`
|
|
526
|
+
);
|
|
265
527
|
}
|
|
528
|
+
}
|
|
529
|
+
function safeParse(data) {
|
|
266
530
|
try {
|
|
267
|
-
|
|
268
|
-
encoding: "utf-8",
|
|
269
|
-
timeout: 3e3
|
|
270
|
-
}).trim();
|
|
271
|
-
return cmd.includes("ws-daemon");
|
|
531
|
+
return JSON.parse(data.toString());
|
|
272
532
|
} catch {
|
|
273
|
-
return
|
|
533
|
+
return null;
|
|
274
534
|
}
|
|
275
535
|
}
|
|
276
|
-
|
|
536
|
+
|
|
537
|
+
// src/lib/bridge-singleton.ts
|
|
538
|
+
var DEFAULT_KEYS_URL = "https://keys.jaw.id";
|
|
539
|
+
var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
|
|
540
|
+
function loadRelaySession() {
|
|
277
541
|
try {
|
|
278
|
-
if (!fs2.existsSync(PATHS.
|
|
279
|
-
const raw = fs2.readFileSync(PATHS.
|
|
280
|
-
const
|
|
281
|
-
if (!
|
|
282
|
-
try {
|
|
283
|
-
fs2.unlinkSync(PATHS.bridge);
|
|
284
|
-
} catch {
|
|
285
|
-
}
|
|
542
|
+
if (!fs2.existsSync(PATHS.relay)) return null;
|
|
543
|
+
const raw = fs2.readFileSync(PATHS.relay, "utf-8");
|
|
544
|
+
const parsed = JSON.parse(raw);
|
|
545
|
+
if (!parsed.session || !parsed.relayUrl || !parsed.privateKey || !parsed.publicKey) {
|
|
286
546
|
return null;
|
|
287
547
|
}
|
|
288
|
-
return
|
|
548
|
+
return parsed;
|
|
289
549
|
} catch {
|
|
290
550
|
return null;
|
|
291
551
|
}
|
|
292
552
|
}
|
|
553
|
+
function saveRelaySession(info) {
|
|
554
|
+
ensureDir(PATHS.root);
|
|
555
|
+
fs2.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
|
|
556
|
+
encoding: "utf-8",
|
|
557
|
+
mode: 384
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
function deleteRelaySession() {
|
|
561
|
+
try {
|
|
562
|
+
if (fs2.existsSync(PATHS.relay)) fs2.unlinkSync(PATHS.relay);
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
}
|
|
293
566
|
async function getBridge(options) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
567
|
+
const config = loadConfig();
|
|
568
|
+
const keysUrl = options.keysUrl ?? config.keysUrl ?? DEFAULT_KEYS_URL;
|
|
569
|
+
const relayUrl = options.relayUrl ?? config.relayUrl ?? DEFAULT_RELAY_URL;
|
|
570
|
+
const chainId = options.chainId ?? config.defaultChain ?? 1;
|
|
571
|
+
if (!isValidKeysUrl(keysUrl)) {
|
|
572
|
+
throw new Error(`Untrusted keysUrl: ${keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.`);
|
|
573
|
+
}
|
|
574
|
+
if (!isValidRelayUrl(relayUrl)) {
|
|
575
|
+
throw new Error(`Untrusted relayUrl: ${relayUrl}. Must be wss://*.jaw.id or ws://localhost.`);
|
|
297
576
|
}
|
|
577
|
+
let relaySession = loadRelaySession();
|
|
578
|
+
if (relaySession && relaySession.relayUrl === relayUrl) {
|
|
579
|
+
try {
|
|
580
|
+
return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl);
|
|
581
|
+
} catch {
|
|
582
|
+
deleteRelaySession();
|
|
583
|
+
relaySession = null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const session = await createNewSession(relayUrl);
|
|
587
|
+
saveRelaySession(session);
|
|
588
|
+
return await connectBridge(session, options, chainId, keysUrl, relayUrl);
|
|
589
|
+
}
|
|
590
|
+
async function createNewSession(relayUrl) {
|
|
591
|
+
const kp = await generateKeyPair();
|
|
592
|
+
const privateKey = await exportKeyToHex("private", kp.privateKey);
|
|
593
|
+
const publicKey = await exportKeyToHex("public", kp.publicKey);
|
|
594
|
+
return {
|
|
595
|
+
session: crypto.randomUUID(),
|
|
596
|
+
relayUrl,
|
|
597
|
+
privateKey,
|
|
598
|
+
publicKey,
|
|
599
|
+
peerPublicKey: null,
|
|
600
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl) {
|
|
604
|
+
const config = loadConfig();
|
|
298
605
|
const bridge = new WSBridge({
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
timeout: options.timeout
|
|
606
|
+
relayUrl,
|
|
607
|
+
session: relaySession.session,
|
|
608
|
+
timeout: options.timeout,
|
|
609
|
+
config: {
|
|
610
|
+
apiKey: options.apiKey,
|
|
611
|
+
chainId,
|
|
612
|
+
ens: options.ens ?? config.ens,
|
|
613
|
+
paymasterUrl: options.paymasterUrl ?? config.paymasterUrl
|
|
614
|
+
},
|
|
615
|
+
privateKeyHex: relaySession.privateKey,
|
|
616
|
+
publicKeyHex: relaySession.publicKey,
|
|
617
|
+
peerPublicKeyHex: relaySession.peerPublicKey
|
|
302
618
|
});
|
|
303
|
-
await bridge.connect(
|
|
619
|
+
await bridge.connect(
|
|
620
|
+
// onBrowserNeeded
|
|
621
|
+
async () => {
|
|
622
|
+
const bridgeUrl = buildBridgeUrl(keysUrl, relaySession.session, relayUrl, relaySession.publicKey);
|
|
623
|
+
const { default: open } = await import('open');
|
|
624
|
+
await open(bridgeUrl);
|
|
625
|
+
},
|
|
626
|
+
// onPeerKeyChanged
|
|
627
|
+
(newPeerKey) => {
|
|
628
|
+
relaySession.peerPublicKey = newPeerKey;
|
|
629
|
+
saveRelaySession(relaySession);
|
|
630
|
+
}
|
|
631
|
+
);
|
|
304
632
|
return bridge;
|
|
305
633
|
}
|
|
634
|
+
function buildBridgeUrl(keysUrl, session, relayUrl, cliPublicKeyHex) {
|
|
635
|
+
const url = new URL("/cli-bridge", keysUrl);
|
|
636
|
+
url.searchParams.set("session", session);
|
|
637
|
+
url.searchParams.set("relay", relayUrl);
|
|
638
|
+
url.hash = `pk=${cliPublicKeyHex}`;
|
|
639
|
+
return url.toString();
|
|
640
|
+
}
|
|
306
641
|
async function shutdownDaemon() {
|
|
307
|
-
const
|
|
308
|
-
if (!
|
|
642
|
+
const session = loadRelaySession();
|
|
643
|
+
if (!session) return;
|
|
309
644
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
645
|
+
const bridge = new WSBridge({
|
|
646
|
+
relayUrl: session.relayUrl,
|
|
647
|
+
session: session.session,
|
|
648
|
+
timeout: 5e3,
|
|
649
|
+
config: { apiKey: "", chainId: 1 },
|
|
650
|
+
privateKeyHex: session.privateKey,
|
|
651
|
+
publicKeyHex: session.publicKey,
|
|
652
|
+
peerPublicKeyHex: session.peerPublicKey
|
|
653
|
+
});
|
|
654
|
+
await bridge.connectAndShutdown();
|
|
315
655
|
} catch {
|
|
316
656
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
657
|
+
deleteRelaySession();
|
|
658
|
+
const legacyBridge = PATHS.root + "/bridge.json";
|
|
659
|
+
const legacyLog = PATHS.root + "/daemon.log";
|
|
660
|
+
const legacyLock = PATHS.root + "/daemon.lock";
|
|
320
661
|
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
} catch (err) {
|
|
325
|
-
if (err.code === "EEXIST") {
|
|
326
|
-
try {
|
|
327
|
-
const lockPid = parseInt(fs2.readFileSync(LOCK_PATH, "utf-8").trim(), 10);
|
|
328
|
-
if (Number.isInteger(lockPid) && lockPid > 0) {
|
|
329
|
-
try {
|
|
330
|
-
process.kill(lockPid, 0);
|
|
331
|
-
return null;
|
|
332
|
-
} catch {
|
|
333
|
-
try {
|
|
334
|
-
fs2.unlinkSync(LOCK_PATH);
|
|
335
|
-
} catch {
|
|
336
|
-
}
|
|
337
|
-
return acquireLock();
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
} catch {
|
|
662
|
+
if (fs2.existsSync(legacyBridge)) {
|
|
663
|
+
const info = JSON.parse(fs2.readFileSync(legacyBridge, "utf-8"));
|
|
664
|
+
if (info.pid && Number.isInteger(info.pid) && info.pid > 0) {
|
|
341
665
|
try {
|
|
342
|
-
|
|
666
|
+
process.kill(info.pid, "SIGTERM");
|
|
343
667
|
} catch {
|
|
344
668
|
}
|
|
345
669
|
}
|
|
346
|
-
return null;
|
|
347
670
|
}
|
|
348
|
-
throw err;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
function releaseLock(fd) {
|
|
352
|
-
try {
|
|
353
|
-
fs2.closeSync(fd);
|
|
354
671
|
} catch {
|
|
355
672
|
}
|
|
356
|
-
|
|
357
|
-
fs2.unlinkSync(LOCK_PATH);
|
|
358
|
-
} catch {
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
async function spawnDaemon(options) {
|
|
362
|
-
ensureDir(PATHS.root);
|
|
363
|
-
const lockFd = acquireLock();
|
|
364
|
-
if (lockFd === null) {
|
|
365
|
-
const deadline = Date.now() + 15e3;
|
|
366
|
-
while (Date.now() < deadline) {
|
|
367
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
368
|
-
const info = loadBridgeInfo();
|
|
369
|
-
if (info) return info;
|
|
370
|
-
}
|
|
371
|
-
throw new Error(
|
|
372
|
-
"Another process is starting the daemon. Timed out waiting for it."
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
try {
|
|
376
|
-
const existing = loadBridgeInfo();
|
|
377
|
-
if (existing) return existing;
|
|
378
|
-
const config = loadConfig();
|
|
379
|
-
const keysUrl = options.keysUrl ?? config.keysUrl ?? JAW_KEYS_URL;
|
|
380
|
-
if (!isValidKeysUrl(keysUrl)) {
|
|
381
|
-
throw new Error(
|
|
382
|
-
`Untrusted keysUrl: ${keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.`
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
const daemonArgs = {
|
|
386
|
-
keysUrl,
|
|
387
|
-
chainId: options.chainId ?? config.defaultChain ?? 1,
|
|
388
|
-
ens: options.ens ?? config.ens,
|
|
389
|
-
paymasterUrl: options.paymasterUrl ?? config.paymasterUrl,
|
|
390
|
-
timeout: options.timeout ?? 12e4
|
|
391
|
-
};
|
|
392
|
-
const daemonScript = path.join(findDistDir(), "lib", "ws-daemon.js");
|
|
673
|
+
for (const f of [legacyBridge, legacyLog, legacyLock]) {
|
|
393
674
|
try {
|
|
394
|
-
if (fs2.existsSync(
|
|
675
|
+
if (fs2.existsSync(f)) fs2.unlinkSync(f);
|
|
395
676
|
} catch {
|
|
396
677
|
}
|
|
397
|
-
const logFd = fs2.openSync(PATHS.daemonLog, "w", 384);
|
|
398
|
-
const child = spawn(
|
|
399
|
-
process.execPath,
|
|
400
|
-
[daemonScript, JSON.stringify(daemonArgs)],
|
|
401
|
-
{
|
|
402
|
-
detached: true,
|
|
403
|
-
stdio: ["ignore", logFd, logFd],
|
|
404
|
-
// Pass API key via env var instead of process args to avoid ps aux exposure
|
|
405
|
-
env: { ...process.env, JAW_DAEMON_API_KEY: options.apiKey }
|
|
406
|
-
}
|
|
407
|
-
);
|
|
408
|
-
child.unref();
|
|
409
|
-
fs2.closeSync(logFd);
|
|
410
|
-
const deadline = Date.now() + 15e3;
|
|
411
|
-
while (Date.now() < deadline) {
|
|
412
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
413
|
-
const info = loadBridgeInfo();
|
|
414
|
-
if (info) return info;
|
|
415
|
-
}
|
|
416
|
-
throw new Error(
|
|
417
|
-
`Daemon failed to start within 15s. Check ${PATHS.daemonLog} for details.`
|
|
418
|
-
);
|
|
419
|
-
} finally {
|
|
420
|
-
releaseLock(lockFd);
|
|
421
678
|
}
|
|
422
679
|
}
|
|
423
680
|
|
|
@@ -431,46 +688,28 @@ function resolveApiKey() {
|
|
|
431
688
|
}
|
|
432
689
|
return apiKey;
|
|
433
690
|
}
|
|
434
|
-
var cachedBridge = null;
|
|
435
|
-
async function getOrCreateBridge(chainId) {
|
|
436
|
-
if (cachedBridge && cachedBridge.isOpen()) {
|
|
437
|
-
return cachedBridge;
|
|
438
|
-
}
|
|
439
|
-
const config = loadConfig();
|
|
440
|
-
const apiKey = resolveApiKey();
|
|
441
|
-
cachedBridge = await getBridge({
|
|
442
|
-
keysUrl: config.keysUrl,
|
|
443
|
-
apiKey,
|
|
444
|
-
chainId: chainId ?? config.defaultChain,
|
|
445
|
-
ens: config.ens,
|
|
446
|
-
paymasterUrl: config.paymasterUrl
|
|
447
|
-
});
|
|
448
|
-
return cachedBridge;
|
|
449
|
-
}
|
|
450
|
-
function closeCachedBridge() {
|
|
451
|
-
if (cachedBridge) {
|
|
452
|
-
cachedBridge.close();
|
|
453
|
-
cachedBridge = null;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
function isBridgeCached() {
|
|
457
|
-
return cachedBridge !== null && cachedBridge.isOpen();
|
|
458
|
-
}
|
|
459
691
|
function registerRpcTool(server) {
|
|
460
692
|
server.tool(
|
|
461
693
|
"jaw_rpc",
|
|
462
694
|
"Execute any JAW.id wallet RPC method via the browser bridge. Supports transactions, signing, permissions, and queries. Methods that require signing will open the browser for passkey authentication. IMPORTANT: Read the jaw://api-reference resource for the full list of methods, and jaw://api-reference/{method} for detailed parameter formats and examples.",
|
|
463
695
|
rpcMethodSchema,
|
|
464
696
|
async (params) => {
|
|
697
|
+
const config = loadConfig();
|
|
698
|
+
const apiKey = resolveApiKey();
|
|
699
|
+
const bridge = await getBridge({
|
|
700
|
+
keysUrl: config.keysUrl,
|
|
701
|
+
apiKey,
|
|
702
|
+
chainId: params.chainId ?? config.defaultChain,
|
|
703
|
+
ens: config.ens,
|
|
704
|
+
paymasterUrl: config.paymasterUrl
|
|
705
|
+
});
|
|
465
706
|
try {
|
|
466
|
-
const bridge = await getOrCreateBridge(params.chainId);
|
|
467
707
|
const result = await bridge.request(params.method, params.params);
|
|
468
708
|
return mcpResult(result);
|
|
469
709
|
} catch (err) {
|
|
470
|
-
if (cachedBridge && !cachedBridge.isOpen()) {
|
|
471
|
-
cachedBridge = null;
|
|
472
|
-
}
|
|
473
710
|
return mcpError(err);
|
|
711
|
+
} finally {
|
|
712
|
+
bridge.close();
|
|
474
713
|
}
|
|
475
714
|
}
|
|
476
715
|
);
|
|
@@ -497,7 +736,7 @@ function registerConfigTools(server) {
|
|
|
497
736
|
);
|
|
498
737
|
server.tool(
|
|
499
738
|
"jaw_config_set",
|
|
500
|
-
"Set a CLI configuration value (apiKey, defaultChain, keysUrl, paymasterUrl, ens).",
|
|
739
|
+
"Set a CLI configuration value (apiKey, defaultChain, keysUrl, paymasterUrl, ens, relayUrl).",
|
|
501
740
|
configSetSchema,
|
|
502
741
|
async (params) => {
|
|
503
742
|
try {
|
|
@@ -527,26 +766,23 @@ function registerConfigTools(server) {
|
|
|
527
766
|
function registerDaemonTools(server) {
|
|
528
767
|
server.tool(
|
|
529
768
|
"jaw_status",
|
|
530
|
-
"Check the current status of the JAW.id bridge \u2014 whether
|
|
769
|
+
"Check the current status of the JAW.id relay bridge \u2014 whether a relay session exists, the bridge connection is active, and what configuration is in use.",
|
|
531
770
|
{},
|
|
532
771
|
async () => {
|
|
533
772
|
try {
|
|
534
|
-
let
|
|
535
|
-
let daemonPid = null;
|
|
773
|
+
let relaySession = false;
|
|
536
774
|
try {
|
|
537
|
-
if (fs2.existsSync(PATHS.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
process.kill(info.pid, 0);
|
|
541
|
-
daemonRunning = true;
|
|
775
|
+
if (fs2.existsSync(PATHS.relay)) {
|
|
776
|
+
JSON.parse(fs2.readFileSync(PATHS.relay, "utf-8"));
|
|
777
|
+
relaySession = true;
|
|
542
778
|
}
|
|
543
779
|
} catch {
|
|
544
|
-
|
|
780
|
+
relaySession = false;
|
|
545
781
|
}
|
|
546
782
|
const config = redactConfig(loadConfig());
|
|
547
783
|
const status = {
|
|
548
|
-
|
|
549
|
-
bridgeConnection:
|
|
784
|
+
relay: relaySession ? { session: true } : { session: false },
|
|
785
|
+
bridgeConnection: "disconnected",
|
|
550
786
|
config
|
|
551
787
|
};
|
|
552
788
|
return {
|
|
@@ -561,17 +797,16 @@ function registerDaemonTools(server) {
|
|
|
561
797
|
);
|
|
562
798
|
server.tool(
|
|
563
799
|
"jaw_disconnect",
|
|
564
|
-
"
|
|
800
|
+
"Close the relay session and browser tab. Call this when you are done making wallet requests to clean up resources.",
|
|
565
801
|
{},
|
|
566
802
|
async () => {
|
|
567
803
|
try {
|
|
568
|
-
closeCachedBridge();
|
|
569
804
|
await shutdownDaemon();
|
|
570
805
|
return {
|
|
571
806
|
content: [
|
|
572
807
|
{
|
|
573
808
|
type: "text",
|
|
574
|
-
text: "
|
|
809
|
+
text: "Relay session closed and browser tab dismissed."
|
|
575
810
|
}
|
|
576
811
|
]
|
|
577
812
|
};
|