@jaw.id/cli 0.0.2 → 0.0.6
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 +472 -218
- 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 +17 -2
- package/dist/mcp/handlers/config.js.map +1 -1
- package/dist/mcp/handlers/daemon.js +446 -46
- package/dist/mcp/handlers/daemon.js.map +1 -1
- package/dist/mcp/handlers/rpc.js +429 -204
- package/dist/mcp/handlers/rpc.js.map +1 -1
- package/dist/mcp/server.js +472 -218
- package/dist/mcp/server.js.map +1 -1
- package/oclif.manifest.json +70 -2
- package/package.json +5 -1
- package/dist/lib/ws-daemon.js +0 -344
- package/dist/lib/ws-daemon.js.map +0 -1
|
@@ -2,10 +2,8 @@ import { Command, Flags } from '@oclif/core';
|
|
|
2
2
|
import * as fs2 from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
|
-
import '
|
|
6
|
-
import
|
|
7
|
-
import 'crypto';
|
|
8
|
-
import 'ws';
|
|
5
|
+
import * as crypto from 'crypto';
|
|
6
|
+
import WebSocket from 'ws';
|
|
9
7
|
|
|
10
8
|
// src/base-command.ts
|
|
11
9
|
var JAW_DIR = path.join(os.homedir(), ".jaw");
|
|
@@ -13,8 +11,7 @@ var PATHS = {
|
|
|
13
11
|
root: JAW_DIR,
|
|
14
12
|
config: path.join(JAW_DIR, "config.json"),
|
|
15
13
|
session: path.join(JAW_DIR, "session.json"),
|
|
16
|
-
|
|
17
|
-
daemonLog: path.join(JAW_DIR, "daemon.log")
|
|
14
|
+
relay: path.join(JAW_DIR, "relay.json")
|
|
18
15
|
};
|
|
19
16
|
|
|
20
17
|
// src/lib/config.ts
|
|
@@ -124,58 +121,464 @@ var BaseCommand = class extends Command {
|
|
|
124
121
|
}
|
|
125
122
|
};
|
|
126
123
|
|
|
127
|
-
// src/lib/
|
|
128
|
-
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
124
|
+
// src/lib/crypto.ts
|
|
125
|
+
var subtle = globalThis.crypto.subtle;
|
|
126
|
+
async function deriveSharedSecret(privateKey, peerPublicKey) {
|
|
127
|
+
return subtle.deriveKey(
|
|
128
|
+
{ name: "ECDH", public: peerPublicKey },
|
|
129
|
+
privateKey,
|
|
130
|
+
{ name: "AES-GCM", length: 256 },
|
|
131
|
+
false,
|
|
132
|
+
["encrypt", "decrypt"]
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
async function encryptMessage(sharedSecret, payload) {
|
|
136
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
137
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
138
|
+
const cipherBuf = await subtle.encrypt(
|
|
139
|
+
{ name: "AES-GCM", iv },
|
|
140
|
+
sharedSecret,
|
|
141
|
+
plaintext
|
|
142
|
+
);
|
|
143
|
+
return {
|
|
144
|
+
iv: bufferToBase64(iv),
|
|
145
|
+
ciphertext: bufferToBase64(new Uint8Array(cipherBuf))
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function decryptMessage(sharedSecret, envelope) {
|
|
149
|
+
const iv = Buffer.from(envelope.iv, "base64");
|
|
150
|
+
const ciphertext = Buffer.from(envelope.ciphertext, "base64");
|
|
151
|
+
const plainBuf = await subtle.decrypt(
|
|
152
|
+
{ name: "AES-GCM", iv },
|
|
153
|
+
sharedSecret,
|
|
154
|
+
ciphertext
|
|
155
|
+
);
|
|
156
|
+
return JSON.parse(new TextDecoder().decode(plainBuf));
|
|
157
|
+
}
|
|
158
|
+
async function importKeyFromHex(type, hex) {
|
|
159
|
+
const format = type === "private" ? "pkcs8" : "spki";
|
|
160
|
+
return subtle.importKey(
|
|
161
|
+
format,
|
|
162
|
+
Buffer.from(hexToBytes(hex)),
|
|
163
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
164
|
+
true,
|
|
165
|
+
type === "private" ? ["deriveKey"] : []
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
function hexToBytes(hex) {
|
|
169
|
+
if (hex.length % 2 !== 0) throw new Error("Invalid hex: odd length");
|
|
170
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
171
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
172
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
173
|
+
}
|
|
174
|
+
return bytes;
|
|
175
|
+
}
|
|
176
|
+
function bufferToBase64(buf) {
|
|
177
|
+
return Buffer.from(buf).toString("base64");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/lib/ws-bridge.ts
|
|
181
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
182
|
+
var MAX_MESSAGE_BYTES = 5 * 1024 * 1024;
|
|
183
|
+
var BROWSER_REOPEN_COOLDOWN_MS = 5e3;
|
|
184
|
+
var MAX_RECONNECT_ATTEMPTS = 3;
|
|
185
|
+
var RECONNECT_BASE_DELAY_MS = 1e3;
|
|
186
|
+
var WSBridge = class {
|
|
187
|
+
relayUrl;
|
|
188
|
+
session;
|
|
189
|
+
timeout;
|
|
190
|
+
config;
|
|
191
|
+
privateKeyHex;
|
|
192
|
+
publicKeyHex;
|
|
193
|
+
peerPublicKeyHex;
|
|
194
|
+
sharedSecret = null;
|
|
195
|
+
ws = null;
|
|
196
|
+
disposed = false;
|
|
197
|
+
// Auto-reopen browser state
|
|
198
|
+
onBrowserNeeded;
|
|
199
|
+
onPeerKeyChanged;
|
|
200
|
+
lastBrowserOpenTime = 0;
|
|
201
|
+
// Reconnection state
|
|
202
|
+
reconnectAttempts = 0;
|
|
203
|
+
/** Updated after key exchange — caller should persist this. */
|
|
204
|
+
get peerPublicKey() {
|
|
205
|
+
return this.peerPublicKeyHex;
|
|
206
|
+
}
|
|
207
|
+
constructor(options) {
|
|
208
|
+
this.relayUrl = options.relayUrl;
|
|
209
|
+
this.session = options.session;
|
|
210
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
211
|
+
this.config = options.config;
|
|
212
|
+
this.privateKeyHex = options.privateKeyHex;
|
|
213
|
+
this.publicKeyHex = options.publicKeyHex;
|
|
214
|
+
this.peerPublicKeyHex = options.peerPublicKeyHex;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Connect to the relay and wait for the browser to be ready.
|
|
218
|
+
*
|
|
219
|
+
* @param onBrowserNeeded — called when the relay reports no browser connected.
|
|
220
|
+
* @param onPeerKeyChanged — called when a key_exchange updates the peer key.
|
|
221
|
+
*/
|
|
222
|
+
async connect(onBrowserNeeded, onPeerKeyChanged) {
|
|
223
|
+
this.onBrowserNeeded = onBrowserNeeded;
|
|
224
|
+
this.onPeerKeyChanged = onPeerKeyChanged;
|
|
225
|
+
if (this.peerPublicKeyHex) {
|
|
226
|
+
await this.deriveSecret();
|
|
227
|
+
}
|
|
228
|
+
return this.connectInternal(onBrowserNeeded, onPeerKeyChanged);
|
|
229
|
+
}
|
|
230
|
+
async connectInternal(onBrowserNeeded, onPeerKeyChanged) {
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
const url = `${this.relayUrl}?session=${encodeURIComponent(this.session)}&role=cli`;
|
|
233
|
+
const ws = new WebSocket(url);
|
|
234
|
+
let browserOpened = false;
|
|
235
|
+
let resolved = false;
|
|
236
|
+
let expectingKeyExchange = !this.peerPublicKeyHex;
|
|
237
|
+
const timer = setTimeout(() => {
|
|
238
|
+
ws.close();
|
|
239
|
+
reject(
|
|
240
|
+
new Error(
|
|
241
|
+
"Browser did not connect in time.\nRun `jaw disconnect` then try again."
|
|
242
|
+
)
|
|
243
|
+
);
|
|
244
|
+
}, 3e4);
|
|
245
|
+
const sendEncryptedInit = async () => {
|
|
246
|
+
if (!this.sharedSecret) return;
|
|
247
|
+
const envelope = await encryptMessage(this.sharedSecret, {
|
|
248
|
+
type: "init",
|
|
249
|
+
apiKey: this.config.apiKey,
|
|
250
|
+
chainId: this.config.chainId,
|
|
251
|
+
ens: this.config.ens,
|
|
252
|
+
paymasterUrl: this.config.paymasterUrl
|
|
253
|
+
});
|
|
254
|
+
this.sendRaw(ws, JSON.stringify({ type: "encrypted", ...envelope }));
|
|
255
|
+
};
|
|
256
|
+
const waitForReady = () => {
|
|
257
|
+
const readyTimer = setTimeout(() => {
|
|
258
|
+
ws.close();
|
|
259
|
+
reject(new Error("Browser SDK did not become ready in time."));
|
|
260
|
+
}, 15e3);
|
|
261
|
+
const onMsg = async (data) => {
|
|
262
|
+
const msg = safeParse(data);
|
|
263
|
+
if (!msg) return;
|
|
264
|
+
if (msg.type === "encrypted" && this.sharedSecret) {
|
|
265
|
+
try {
|
|
266
|
+
const inner = await decryptMessage(
|
|
267
|
+
this.sharedSecret,
|
|
268
|
+
msg
|
|
269
|
+
);
|
|
270
|
+
if (inner.type === "ready") {
|
|
271
|
+
clearTimeout(readyTimer);
|
|
272
|
+
ws.off("message", onMsg);
|
|
273
|
+
this.reconnectAttempts = 0;
|
|
274
|
+
resolve();
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
ws.on("message", onMsg);
|
|
281
|
+
};
|
|
282
|
+
const onBrowserReady = async () => {
|
|
283
|
+
if (resolved) return;
|
|
284
|
+
resolved = true;
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
waitForReady();
|
|
287
|
+
await sendEncryptedInit();
|
|
288
|
+
};
|
|
289
|
+
ws.on("open", () => {
|
|
290
|
+
this.ws = ws;
|
|
291
|
+
});
|
|
292
|
+
ws.on("message", async (data) => {
|
|
293
|
+
const msg = safeParse(data);
|
|
294
|
+
if (!msg) return;
|
|
295
|
+
if (msg.type === "status") {
|
|
296
|
+
if (msg.browserConnected) {
|
|
297
|
+
if (this.sharedSecret) {
|
|
298
|
+
await onBrowserReady();
|
|
299
|
+
} else {
|
|
300
|
+
expectingKeyExchange = true;
|
|
301
|
+
}
|
|
302
|
+
} else if (!browserOpened && onBrowserNeeded) {
|
|
303
|
+
browserOpened = true;
|
|
304
|
+
expectingKeyExchange = true;
|
|
305
|
+
onBrowserNeeded().catch(() => {
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
} else if (msg.type === "browser_connected") {
|
|
309
|
+
expectingKeyExchange = true;
|
|
310
|
+
} else if (msg.type === "browser_disconnected") {
|
|
311
|
+
this.handleBrowserDisconnect();
|
|
312
|
+
} else if (msg.type === "key_exchange" && expectingKeyExchange) {
|
|
313
|
+
expectingKeyExchange = false;
|
|
314
|
+
const peerKey = msg.publicKey;
|
|
315
|
+
this.peerPublicKeyHex = peerKey;
|
|
316
|
+
await this.deriveSecret();
|
|
317
|
+
onPeerKeyChanged?.(peerKey);
|
|
318
|
+
await onBrowserReady();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
ws.on("error", (err) => {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
reject(err);
|
|
324
|
+
});
|
|
325
|
+
ws.on("close", () => {
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
if (!this.disposed) {
|
|
328
|
+
this.handleRelayDisconnect();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Send an encrypted RPC request through the relay to the browser SDK.
|
|
335
|
+
*/
|
|
336
|
+
async request(method, params) {
|
|
337
|
+
const ws = this.ws;
|
|
338
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
339
|
+
throw new Error("Not connected to relay");
|
|
340
|
+
}
|
|
341
|
+
if (!this.sharedSecret) {
|
|
342
|
+
throw new Error("No shared secret \u2014 key exchange not completed");
|
|
343
|
+
}
|
|
344
|
+
const id = crypto.randomUUID();
|
|
345
|
+
const envelope = await encryptMessage(this.sharedSecret, {
|
|
346
|
+
type: "rpc_request",
|
|
347
|
+
id,
|
|
348
|
+
method,
|
|
349
|
+
params
|
|
350
|
+
});
|
|
351
|
+
const serialized = JSON.stringify({ type: "encrypted", ...envelope });
|
|
352
|
+
assertMessageSize(serialized, method);
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
const timer = setTimeout(() => {
|
|
355
|
+
reject(
|
|
356
|
+
new Error(
|
|
357
|
+
`Request timed out after ${this.timeout / 1e3}s. Did you complete the action in the browser?`
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
this.close();
|
|
361
|
+
}, this.timeout);
|
|
362
|
+
const onMessage = async (data) => {
|
|
363
|
+
const msg = safeParse(data);
|
|
364
|
+
if (!msg || msg.type !== "encrypted" || !this.sharedSecret) return;
|
|
365
|
+
try {
|
|
366
|
+
const inner = await decryptMessage(
|
|
367
|
+
this.sharedSecret,
|
|
368
|
+
msg
|
|
369
|
+
);
|
|
370
|
+
if (inner.type === "rpc_response" && inner.id === id) {
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
ws.off("message", onMessage);
|
|
373
|
+
if (inner.success) {
|
|
374
|
+
resolve(inner.data);
|
|
375
|
+
} else {
|
|
376
|
+
const err = inner.error;
|
|
377
|
+
reject(
|
|
378
|
+
new Error(
|
|
379
|
+
err ? `[${err.code}] ${err.message}` : "Request failed"
|
|
380
|
+
)
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
ws.on("message", onMessage);
|
|
388
|
+
this.sendRaw(ws, serialized);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
isOpen() {
|
|
392
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
393
|
+
}
|
|
394
|
+
async shutdown() {
|
|
395
|
+
this.disposed = true;
|
|
396
|
+
if (this.ws?.readyState === WebSocket.OPEN && this.sharedSecret) {
|
|
397
|
+
try {
|
|
398
|
+
const envelope = await encryptMessage(this.sharedSecret, {
|
|
399
|
+
type: "shutdown"
|
|
400
|
+
});
|
|
401
|
+
this.sendRaw(
|
|
402
|
+
this.ws,
|
|
403
|
+
JSON.stringify({ type: "encrypted", ...envelope })
|
|
404
|
+
);
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
this.close();
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Connect to relay and send shutdown directly — no init/ready handshake.
|
|
412
|
+
* Used by `jaw disconnect` when we just need to tell the browser to close.
|
|
413
|
+
*/
|
|
414
|
+
async connectAndShutdown() {
|
|
415
|
+
if (!this.peerPublicKeyHex) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this.disposed = true;
|
|
419
|
+
await this.deriveSecret();
|
|
420
|
+
return new Promise((resolve) => {
|
|
421
|
+
const url = `${this.relayUrl}?session=${encodeURIComponent(this.session)}&role=cli`;
|
|
422
|
+
const ws = new WebSocket(url);
|
|
423
|
+
const timer = setTimeout(() => {
|
|
424
|
+
try {
|
|
425
|
+
ws.close();
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
resolve();
|
|
429
|
+
}, 3e3);
|
|
430
|
+
ws.on("open", async () => {
|
|
431
|
+
this.ws = ws;
|
|
432
|
+
try {
|
|
433
|
+
await this.shutdown();
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
resolve();
|
|
438
|
+
});
|
|
439
|
+
ws.on("error", () => {
|
|
440
|
+
clearTimeout(timer);
|
|
441
|
+
resolve();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
close() {
|
|
446
|
+
this.disposed = true;
|
|
447
|
+
if (this.ws) {
|
|
448
|
+
try {
|
|
449
|
+
this.ws.close();
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
this.ws = null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Auto-reopen browser when browser_disconnected is received from relay.
|
|
457
|
+
* Respects a cooldown to prevent rapid re-opening.
|
|
458
|
+
*/
|
|
459
|
+
handleBrowserDisconnect() {
|
|
460
|
+
if (this.disposed || !this.onBrowserNeeded) return;
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
if (now - this.lastBrowserOpenTime < BROWSER_REOPEN_COOLDOWN_MS) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.lastBrowserOpenTime = now;
|
|
466
|
+
this.sharedSecret = null;
|
|
467
|
+
this.peerPublicKeyHex = null;
|
|
468
|
+
this.onBrowserNeeded().catch(() => {
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Attempt to reconnect to the relay with exponential backoff
|
|
473
|
+
* when the WebSocket connection drops unexpectedly.
|
|
474
|
+
*/
|
|
475
|
+
handleRelayDisconnect() {
|
|
476
|
+
if (this.disposed) return;
|
|
477
|
+
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
|
|
478
|
+
const delay = RECONNECT_BASE_DELAY_MS * Math.pow(2, this.reconnectAttempts);
|
|
479
|
+
this.reconnectAttempts++;
|
|
480
|
+
setTimeout(() => {
|
|
481
|
+
if (this.disposed) return;
|
|
482
|
+
this.connectInternal(this.onBrowserNeeded, this.onPeerKeyChanged).catch(
|
|
483
|
+
() => {
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
}, delay);
|
|
487
|
+
}
|
|
488
|
+
/** Send a raw string over the WebSocket, enforcing message size limits. */
|
|
489
|
+
sendRaw(ws, data) {
|
|
490
|
+
ws.send(data);
|
|
135
491
|
}
|
|
492
|
+
async deriveSecret() {
|
|
493
|
+
if (!this.peerPublicKeyHex) return;
|
|
494
|
+
const privateKey = await importKeyFromHex("private", this.privateKeyHex);
|
|
495
|
+
const peerPublicKey = await importKeyFromHex(
|
|
496
|
+
"public",
|
|
497
|
+
this.peerPublicKeyHex
|
|
498
|
+
);
|
|
499
|
+
this.sharedSecret = await deriveSharedSecret(privateKey, peerPublicKey);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
function assertMessageSize(serialized, method) {
|
|
503
|
+
const byteLength = Buffer.byteLength(serialized, "utf-8");
|
|
504
|
+
if (byteLength > MAX_MESSAGE_BYTES) {
|
|
505
|
+
const sizeMB = (byteLength / (1024 * 1024)).toFixed(2);
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Message for ${method} is too large (${sizeMB} MB, limit ${MAX_MESSAGE_BYTES / (1024 * 1024)} MB). Try reducing the number of calls in your batch.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function safeParse(data) {
|
|
136
512
|
try {
|
|
137
|
-
|
|
138
|
-
encoding: "utf-8",
|
|
139
|
-
timeout: 3e3
|
|
140
|
-
}).trim();
|
|
141
|
-
return cmd.includes("ws-daemon");
|
|
513
|
+
return JSON.parse(data.toString());
|
|
142
514
|
} catch {
|
|
143
|
-
return
|
|
515
|
+
return null;
|
|
144
516
|
}
|
|
145
517
|
}
|
|
146
|
-
|
|
518
|
+
|
|
519
|
+
// src/lib/bridge-singleton.ts
|
|
520
|
+
function loadRelaySession() {
|
|
147
521
|
try {
|
|
148
|
-
if (!fs2.existsSync(PATHS.
|
|
149
|
-
const raw = fs2.readFileSync(PATHS.
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
152
|
-
try {
|
|
153
|
-
fs2.unlinkSync(PATHS.bridge);
|
|
154
|
-
} catch {
|
|
155
|
-
}
|
|
522
|
+
if (!fs2.existsSync(PATHS.relay)) return null;
|
|
523
|
+
const raw = fs2.readFileSync(PATHS.relay, "utf-8");
|
|
524
|
+
const parsed = JSON.parse(raw);
|
|
525
|
+
if (!parsed.session || !parsed.relayUrl || !parsed.privateKey || !parsed.publicKey) {
|
|
156
526
|
return null;
|
|
157
527
|
}
|
|
158
|
-
return
|
|
528
|
+
return parsed;
|
|
159
529
|
} catch {
|
|
160
530
|
return null;
|
|
161
531
|
}
|
|
162
532
|
}
|
|
533
|
+
function deleteRelaySession() {
|
|
534
|
+
try {
|
|
535
|
+
if (fs2.existsSync(PATHS.relay)) fs2.unlinkSync(PATHS.relay);
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
}
|
|
163
539
|
async function shutdownDaemon() {
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
540
|
+
const session = loadRelaySession();
|
|
541
|
+
if (!session) return;
|
|
166
542
|
try {
|
|
167
|
-
|
|
543
|
+
const bridge = new WSBridge({
|
|
544
|
+
relayUrl: session.relayUrl,
|
|
545
|
+
session: session.session,
|
|
546
|
+
timeout: 5e3,
|
|
547
|
+
config: { apiKey: "", chainId: 1 },
|
|
548
|
+
privateKeyHex: session.privateKey,
|
|
549
|
+
publicKeyHex: session.publicKey,
|
|
550
|
+
peerPublicKeyHex: session.peerPublicKey
|
|
551
|
+
});
|
|
552
|
+
await bridge.connectAndShutdown();
|
|
168
553
|
} catch {
|
|
169
554
|
}
|
|
555
|
+
deleteRelaySession();
|
|
556
|
+
const legacyBridge = PATHS.root + "/bridge.json";
|
|
557
|
+
const legacyLog = PATHS.root + "/daemon.log";
|
|
558
|
+
const legacyLock = PATHS.root + "/daemon.lock";
|
|
170
559
|
try {
|
|
171
|
-
if (fs2.existsSync(
|
|
560
|
+
if (fs2.existsSync(legacyBridge)) {
|
|
561
|
+
const info = JSON.parse(fs2.readFileSync(legacyBridge, "utf-8"));
|
|
562
|
+
if (info.pid && Number.isInteger(info.pid) && info.pid > 0) {
|
|
563
|
+
try {
|
|
564
|
+
process.kill(info.pid, "SIGTERM");
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
172
569
|
} catch {
|
|
173
570
|
}
|
|
571
|
+
for (const f of [legacyBridge, legacyLog, legacyLock]) {
|
|
572
|
+
try {
|
|
573
|
+
if (fs2.existsSync(f)) fs2.unlinkSync(f);
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
174
577
|
}
|
|
175
578
|
|
|
176
579
|
// src/commands/disconnect.ts
|
|
177
580
|
var Disconnect = class _Disconnect extends BaseCommand {
|
|
178
|
-
static description = "
|
|
581
|
+
static description = "Close the relay session and browser tab.";
|
|
179
582
|
static examples = ["<%= config.bin %> disconnect"];
|
|
180
583
|
static flags = {
|
|
181
584
|
...BaseCommand.baseFlags
|
|
@@ -186,7 +589,7 @@ var Disconnect = class _Disconnect extends BaseCommand {
|
|
|
186
589
|
if (flags.output === "json") {
|
|
187
590
|
this.outputResult({ success: true }, "json");
|
|
188
591
|
} else {
|
|
189
|
-
this.log("
|
|
592
|
+
this.log("Relay session closed.");
|
|
190
593
|
}
|
|
191
594
|
}
|
|
192
595
|
};
|