@lanonasis/oauth-client 1.0.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/index.js ADDED
@@ -0,0 +1,1303 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/flows/base-flow.ts
9
+ var BaseOAuthFlow = class {
10
+ constructor(config) {
11
+ this.clientId = config.clientId;
12
+ this.authBaseUrl = config.authBaseUrl || "https://auth.lanonasis.com";
13
+ this.scope = config.scope || "mcp:read mcp:write api_keys:manage";
14
+ }
15
+ async makeTokenRequest(body) {
16
+ const response = await fetch(`${this.authBaseUrl}/oauth/token`, {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify(body)
20
+ });
21
+ const data = await response.json();
22
+ if (!response.ok) {
23
+ throw new Error(data.error_description || "Token request failed");
24
+ }
25
+ return data;
26
+ }
27
+ generateState() {
28
+ const array = new Uint8Array(32);
29
+ if (typeof window !== "undefined" && window.crypto) {
30
+ window.crypto.getRandomValues(array);
31
+ } else {
32
+ const crypto = __require("crypto");
33
+ crypto.randomFillSync(array);
34
+ }
35
+ return this.base64URLEncode(array);
36
+ }
37
+ base64URLEncode(buffer) {
38
+ const bytes = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
39
+ let binary = "";
40
+ bytes.forEach((byte) => {
41
+ binary += String.fromCharCode(byte);
42
+ });
43
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
44
+ }
45
+ async refreshToken(refreshToken) {
46
+ return this.makeTokenRequest({
47
+ grant_type: "refresh_token",
48
+ refresh_token: refreshToken,
49
+ client_id: this.clientId
50
+ });
51
+ }
52
+ async revokeToken(token, tokenType = "access_token") {
53
+ const response = await fetch(`${this.authBaseUrl}/oauth/revoke`, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify({
57
+ token,
58
+ token_type_hint: tokenType,
59
+ client_id: this.clientId
60
+ })
61
+ });
62
+ if (!response.ok) {
63
+ throw new Error("Failed to revoke token");
64
+ }
65
+ }
66
+ };
67
+
68
+ // src/flows/terminal-flow.ts
69
+ import open from "open";
70
+ var TerminalOAuthFlow = class extends BaseOAuthFlow {
71
+ constructor(config) {
72
+ super({
73
+ ...config,
74
+ clientId: config.clientId || "lanonasis-mcp-cli"
75
+ });
76
+ this.pollInterval = 5;
77
+ }
78
+ async authenticate() {
79
+ try {
80
+ const deviceResponse = await this.requestDeviceCode();
81
+ this.displayInstructions(deviceResponse);
82
+ if (deviceResponse.verification_uri_complete) {
83
+ await this.openBrowser(deviceResponse.verification_uri_complete);
84
+ }
85
+ return await this.pollForToken(deviceResponse);
86
+ } catch (error) {
87
+ console.error("Authentication failed:", error);
88
+ throw error;
89
+ }
90
+ }
91
+ async requestDeviceCode() {
92
+ const response = await fetch(`${this.authBaseUrl}/oauth/device`, {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify({
96
+ client_id: this.clientId,
97
+ scope: this.scope
98
+ })
99
+ });
100
+ if (!response.ok) {
101
+ const error = await response.json();
102
+ throw new Error(error.error_description || "Failed to request device code");
103
+ }
104
+ const data = await response.json();
105
+ this.pollInterval = data.interval || 5;
106
+ return data;
107
+ }
108
+ displayInstructions(response) {
109
+ console.log("\n\u{1F510} Lan Onasis Authentication Required\n");
110
+ console.log(`Please visit: ${response.verification_uri}`);
111
+ console.log(`Enter code: ${response.user_code}
112
+ `);
113
+ console.log("Or press Enter to open your browser automatically...");
114
+ }
115
+ async openBrowser(url) {
116
+ try {
117
+ await Promise.race([
118
+ this.waitForEnter(),
119
+ new Promise((resolve) => setTimeout(resolve, 2e3))
120
+ ]);
121
+ console.log("Opening browser...");
122
+ await open(url);
123
+ } catch (error) {
124
+ console.log("Please open the URL manually in your browser.");
125
+ }
126
+ }
127
+ waitForEnter() {
128
+ return new Promise((resolve) => {
129
+ if (process.stdin.isTTY) {
130
+ process.stdin.setRawMode(true);
131
+ process.stdin.resume();
132
+ process.stdin.once("data", () => {
133
+ process.stdin.setRawMode(false);
134
+ process.stdin.pause();
135
+ resolve();
136
+ });
137
+ } else {
138
+ resolve();
139
+ }
140
+ });
141
+ }
142
+ async pollForToken(deviceResponse) {
143
+ const startTime = Date.now();
144
+ const expiresAt = startTime + deviceResponse.expires_in * 1e3;
145
+ console.log("Waiting for authorization...");
146
+ while (Date.now() < expiresAt) {
147
+ await new Promise((resolve) => setTimeout(resolve, this.pollInterval * 1e3));
148
+ try {
149
+ const token = await this.checkDeviceCode(deviceResponse.device_code);
150
+ console.log("\u2705 Authorization successful!\n");
151
+ return token;
152
+ } catch (error) {
153
+ if (error.message === "authorization_pending") {
154
+ process.stdout.write(".");
155
+ } else if (error.message === "slow_down") {
156
+ this.pollInterval += 5;
157
+ } else {
158
+ throw error;
159
+ }
160
+ }
161
+ }
162
+ throw new Error("Authorization timeout - please try again");
163
+ }
164
+ async checkDeviceCode(deviceCode) {
165
+ const response = await fetch(`${this.authBaseUrl}/oauth/token`, {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify({
169
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
170
+ device_code: deviceCode,
171
+ client_id: this.clientId
172
+ })
173
+ });
174
+ const data = await response.json();
175
+ if (!response.ok) {
176
+ throw new Error(data.error || "Token request failed");
177
+ }
178
+ return data;
179
+ }
180
+ };
181
+
182
+ // src/flows/desktop-flow.ts
183
+ var DesktopOAuthFlow = class extends BaseOAuthFlow {
184
+ constructor(config) {
185
+ super({
186
+ ...config,
187
+ clientId: config.clientId || "lanonasis-mcp-desktop"
188
+ });
189
+ this.authWindow = null;
190
+ this.redirectUri = config.redirectUri || "lanonasis://oauth/callback";
191
+ }
192
+ async authenticate() {
193
+ const pkce = await this.generatePKCEChallenge();
194
+ const state = this.generateState();
195
+ const authUrl = this.buildAuthorizationUrl(pkce.codeChallenge, state);
196
+ const authCode = await this.openAuthWindow(authUrl, state);
197
+ return await this.exchangeCodeForToken(authCode, pkce.codeVerifier);
198
+ }
199
+ async generatePKCEChallenge() {
200
+ const codeVerifier = this.generateCodeVerifier();
201
+ const codeChallenge = await this.generateCodeChallenge(codeVerifier);
202
+ return { codeVerifier, codeChallenge };
203
+ }
204
+ generateCodeVerifier() {
205
+ const array = new Uint8Array(32);
206
+ if (typeof window !== "undefined" && window.crypto) {
207
+ window.crypto.getRandomValues(array);
208
+ } else {
209
+ const crypto = __require("crypto");
210
+ crypto.randomFillSync(array);
211
+ }
212
+ return this.base64URLEncode(array);
213
+ }
214
+ async generateCodeChallenge(verifier) {
215
+ if (typeof window !== "undefined" && window.crypto?.subtle) {
216
+ const encoder = new TextEncoder();
217
+ const data = encoder.encode(verifier);
218
+ const hash = await window.crypto.subtle.digest("SHA-256", data);
219
+ return this.base64URLEncode(hash);
220
+ } else {
221
+ const crypto = __require("crypto");
222
+ const hash = crypto.createHash("sha256").update(verifier).digest();
223
+ return this.base64URLEncode(hash);
224
+ }
225
+ }
226
+ buildAuthorizationUrl(codeChallenge, state) {
227
+ const params = new URLSearchParams({
228
+ client_id: this.clientId,
229
+ response_type: "code",
230
+ redirect_uri: this.redirectUri,
231
+ scope: this.scope,
232
+ code_challenge: codeChallenge,
233
+ code_challenge_method: "S256",
234
+ state
235
+ });
236
+ return `${this.authBaseUrl}/oauth/authorize?${params}`;
237
+ }
238
+ async openAuthWindow(authUrl, expectedState) {
239
+ return new Promise((resolve, reject) => {
240
+ if (typeof window !== "undefined") {
241
+ this.openBrowserWindow(authUrl, expectedState, resolve, reject);
242
+ } else {
243
+ this.openElectronWindow(authUrl, expectedState, resolve, reject);
244
+ }
245
+ });
246
+ }
247
+ openBrowserWindow(authUrl, expectedState, resolve, reject) {
248
+ const width = 500;
249
+ const height = 700;
250
+ const left = (window.screen.width - width) / 2;
251
+ const top = (window.screen.height - height) / 2;
252
+ this.authWindow = window.open(
253
+ authUrl,
254
+ "Lan Onasis Login",
255
+ `width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no`
256
+ );
257
+ if (!this.authWindow) {
258
+ reject(new Error("Failed to open authentication window"));
259
+ return;
260
+ }
261
+ const checkInterval = setInterval(() => {
262
+ try {
263
+ if (!this.authWindow || this.authWindow.closed) {
264
+ clearInterval(checkInterval);
265
+ reject(new Error("Authentication window was closed"));
266
+ return;
267
+ }
268
+ const currentUrl = this.authWindow.location.href;
269
+ if (currentUrl.startsWith(this.redirectUri)) {
270
+ clearInterval(checkInterval);
271
+ this.authWindow.close();
272
+ const url = new URL(currentUrl);
273
+ const code = url.searchParams.get("code");
274
+ const state = url.searchParams.get("state");
275
+ const error = url.searchParams.get("error");
276
+ if (error) {
277
+ reject(new Error(url.searchParams.get("error_description") || error));
278
+ } else if (state !== expectedState) {
279
+ reject(new Error("State mismatch - possible CSRF attack"));
280
+ } else if (code) {
281
+ resolve(code);
282
+ } else {
283
+ reject(new Error("No authorization code received"));
284
+ }
285
+ }
286
+ } catch (e) {
287
+ }
288
+ }, 500);
289
+ }
290
+ openElectronWindow(authUrl, expectedState, resolve, reject) {
291
+ const { BrowserWindow } = __require("electron");
292
+ const authWindow = new BrowserWindow({
293
+ width: 500,
294
+ height: 700,
295
+ webPreferences: {
296
+ nodeIntegration: false,
297
+ contextIsolation: true
298
+ }
299
+ });
300
+ authWindow.loadURL(authUrl);
301
+ authWindow.webContents.on("will-redirect", (event, url) => {
302
+ if (url.startsWith(this.redirectUri)) {
303
+ event.preventDefault();
304
+ authWindow.close();
305
+ const callbackUrl = new URL(url);
306
+ const code = callbackUrl.searchParams.get("code");
307
+ const state = callbackUrl.searchParams.get("state");
308
+ const error = callbackUrl.searchParams.get("error");
309
+ if (error) {
310
+ reject(new Error(callbackUrl.searchParams.get("error_description") || error));
311
+ } else if (state !== expectedState) {
312
+ reject(new Error("State mismatch"));
313
+ } else if (code) {
314
+ resolve(code);
315
+ }
316
+ }
317
+ });
318
+ authWindow.on("closed", () => {
319
+ reject(new Error("Authentication window was closed"));
320
+ });
321
+ }
322
+ async exchangeCodeForToken(code, codeVerifier) {
323
+ return this.makeTokenRequest({
324
+ grant_type: "authorization_code",
325
+ code,
326
+ client_id: this.clientId,
327
+ redirect_uri: this.redirectUri,
328
+ code_verifier: codeVerifier
329
+ });
330
+ }
331
+ };
332
+
333
+ // src/storage/token-storage.ts
334
+ var TokenStorage = class {
335
+ constructor() {
336
+ this.storageKey = "lanonasis_mcp_tokens";
337
+ this.webEncryptionKeyStorage = "lanonasis_web_token_enc_key";
338
+ if (this.isNode()) {
339
+ try {
340
+ this.keytar = __require("keytar");
341
+ } catch (e) {
342
+ console.warn("Keytar not available - falling back to file storage");
343
+ }
344
+ }
345
+ }
346
+ async store(tokens) {
347
+ const tokenString = JSON.stringify(tokens);
348
+ if (this.isNode()) {
349
+ if (this.keytar) {
350
+ await this.keytar.setPassword("lanonasis-mcp", "tokens", tokenString);
351
+ } else {
352
+ await this.storeToFile(tokenString);
353
+ }
354
+ } else if (this.isElectron()) {
355
+ await window.electronAPI.secureStore.set(this.storageKey, tokens);
356
+ } else if (this.isMobile()) {
357
+ await window.SecureStorage.set(this.storageKey, tokenString);
358
+ } else {
359
+ const encrypted = await this.encrypt(tokenString);
360
+ localStorage.setItem(this.storageKey, encrypted);
361
+ }
362
+ }
363
+ async retrieve() {
364
+ let tokenString = null;
365
+ try {
366
+ if (this.isNode()) {
367
+ if (this.keytar) {
368
+ tokenString = await this.keytar.getPassword("lanonasis-mcp", "tokens");
369
+ }
370
+ if (!tokenString) {
371
+ tokenString = await this.retrieveFromFile();
372
+ }
373
+ } else if (this.isElectron()) {
374
+ const tokens = await window.electronAPI.secureStore.get(this.storageKey);
375
+ return tokens || null;
376
+ } else if (this.isMobile()) {
377
+ tokenString = await window.SecureStorage.get(this.storageKey);
378
+ } else {
379
+ const encrypted = localStorage.getItem(this.storageKey);
380
+ if (encrypted) {
381
+ tokenString = await this.decrypt(encrypted);
382
+ }
383
+ }
384
+ return tokenString ? JSON.parse(tokenString) : null;
385
+ } catch (error) {
386
+ console.error("Error retrieving tokens:", error);
387
+ return null;
388
+ }
389
+ }
390
+ async clear() {
391
+ if (this.isNode()) {
392
+ if (this.keytar) {
393
+ await this.keytar.deletePassword("lanonasis-mcp", "tokens");
394
+ }
395
+ await this.deleteFile();
396
+ } else if (this.isElectron()) {
397
+ await window.electronAPI.secureStore.delete(this.storageKey);
398
+ } else if (this.isMobile()) {
399
+ await window.SecureStorage.remove(this.storageKey);
400
+ } else {
401
+ localStorage.removeItem(this.storageKey);
402
+ }
403
+ }
404
+ isTokenExpired(tokens) {
405
+ if (!tokens.expires_in) return false;
406
+ const storedAt = this.getStoredAt(tokens);
407
+ if (!storedAt) return true;
408
+ const expiresAt = storedAt + tokens.expires_in * 1e3;
409
+ const now = Date.now();
410
+ return expiresAt - now < 3e5;
411
+ }
412
+ getStoredAt(tokens) {
413
+ return Date.now() - 36e5;
414
+ }
415
+ async storeToFile(tokenString) {
416
+ if (!this.isNode()) return;
417
+ const fs = __require("fs").promises;
418
+ const path = __require("path");
419
+ const os = __require("os");
420
+ const crypto = __require("crypto");
421
+ const configDir = path.join(os.homedir(), ".lanonasis");
422
+ const tokenFile = path.join(configDir, "mcp-tokens.enc");
423
+ await fs.mkdir(configDir, { recursive: true });
424
+ const key = this.getFileEncryptionKey();
425
+ const iv = crypto.randomBytes(16);
426
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
427
+ let encrypted = cipher.update(tokenString, "utf8", "hex");
428
+ encrypted += cipher.final("hex");
429
+ const authTag = cipher.getAuthTag().toString("hex");
430
+ const data = [iv.toString("hex"), authTag, encrypted].join(":");
431
+ await fs.writeFile(tokenFile, data, { mode: 384 });
432
+ }
433
+ async retrieveFromFile() {
434
+ if (!this.isNode()) return null;
435
+ const fs = __require("fs").promises;
436
+ const path = __require("path");
437
+ const os = __require("os");
438
+ const crypto = __require("crypto");
439
+ const tokenFile = path.join(os.homedir(), ".lanonasis", "mcp-tokens.enc");
440
+ try {
441
+ const data = await fs.readFile(tokenFile, "utf8");
442
+ const parts = data.split(":");
443
+ const key = this.getFileEncryptionKey();
444
+ if (parts.length === 3) {
445
+ const [ivHex, authTagHex, encrypted] = parts;
446
+ const iv = Buffer.from(ivHex, "hex");
447
+ const authTag = Buffer.from(authTagHex, "hex");
448
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
449
+ decipher.setAuthTag(authTag);
450
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
451
+ decrypted += decipher.final("utf8");
452
+ return decrypted;
453
+ }
454
+ if (parts.length === 2) {
455
+ const [ivHex, encrypted] = parts;
456
+ const iv = Buffer.from(ivHex, "hex");
457
+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
458
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
459
+ decrypted += decipher.final("utf8");
460
+ return decrypted;
461
+ }
462
+ throw new Error("Invalid encrypted token format");
463
+ } catch (error) {
464
+ return null;
465
+ }
466
+ }
467
+ async deleteFile() {
468
+ if (!this.isNode()) return;
469
+ const fs = __require("fs").promises;
470
+ const path = __require("path");
471
+ const os = __require("os");
472
+ const tokenFile = path.join(os.homedir(), ".lanonasis", "mcp-tokens.enc");
473
+ try {
474
+ await fs.unlink(tokenFile);
475
+ } catch (error) {
476
+ }
477
+ }
478
+ getFileEncryptionKey() {
479
+ const crypto = __require("crypto");
480
+ const os = __require("os");
481
+ const machineId = os.hostname() + os.userInfo().username;
482
+ const salt = "lanonasis-mcp-oauth-2024";
483
+ return crypto.pbkdf2Sync(machineId, salt, 1e5, 32, "sha256");
484
+ }
485
+ async encrypt(text) {
486
+ if (typeof window === "undefined" || !window.crypto?.subtle) {
487
+ const encoder2 = new TextEncoder();
488
+ const data2 = encoder2.encode(text);
489
+ return btoa(String.fromCharCode(...data2));
490
+ }
491
+ const encoder = new TextEncoder();
492
+ const data = encoder.encode(text);
493
+ const passphrase = await this.getWebEncryptionKey();
494
+ const keyMaterial = await window.crypto.subtle.importKey(
495
+ "raw",
496
+ encoder.encode(passphrase),
497
+ "PBKDF2",
498
+ false,
499
+ ["deriveBits", "deriveKey"]
500
+ );
501
+ const key = await window.crypto.subtle.deriveKey(
502
+ {
503
+ name: "PBKDF2",
504
+ salt: encoder.encode("lanonasis-token-salt"),
505
+ iterations: 1e5,
506
+ hash: "SHA-256"
507
+ },
508
+ keyMaterial,
509
+ { name: "AES-GCM", length: 256 },
510
+ true,
511
+ ["encrypt", "decrypt"]
512
+ );
513
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
514
+ const encrypted = await window.crypto.subtle.encrypt(
515
+ { name: "AES-GCM", iv },
516
+ key,
517
+ data
518
+ );
519
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
520
+ combined.set(iv, 0);
521
+ combined.set(new Uint8Array(encrypted), iv.length);
522
+ return btoa(String.fromCharCode(...combined));
523
+ }
524
+ async decrypt(encrypted) {
525
+ if (typeof window === "undefined" || !window.crypto?.subtle) {
526
+ const binary2 = atob(encrypted);
527
+ const bytes2 = new Uint8Array(binary2.length);
528
+ for (let i = 0; i < binary2.length; i++) {
529
+ bytes2[i] = binary2.charCodeAt(i);
530
+ }
531
+ const decoder2 = new TextDecoder();
532
+ return decoder2.decode(bytes2);
533
+ }
534
+ const binary = atob(encrypted);
535
+ const bytes = new Uint8Array(binary.length);
536
+ for (let i = 0; i < binary.length; i++) {
537
+ bytes[i] = binary.charCodeAt(i);
538
+ }
539
+ const iv = bytes.slice(0, 12);
540
+ const data = bytes.slice(12);
541
+ const encoder = new TextEncoder();
542
+ const decoder = new TextDecoder();
543
+ const passphrase = await this.getWebEncryptionKey();
544
+ const keyMaterial = await window.crypto.subtle.importKey(
545
+ "raw",
546
+ encoder.encode(passphrase),
547
+ "PBKDF2",
548
+ false,
549
+ ["deriveBits", "deriveKey"]
550
+ );
551
+ const key = await window.crypto.subtle.deriveKey(
552
+ {
553
+ name: "PBKDF2",
554
+ salt: encoder.encode("lanonasis-token-salt"),
555
+ iterations: 1e5,
556
+ hash: "SHA-256"
557
+ },
558
+ keyMaterial,
559
+ { name: "AES-GCM", length: 256 },
560
+ true,
561
+ ["encrypt", "decrypt"]
562
+ );
563
+ const decrypted = await window.crypto.subtle.decrypt(
564
+ { name: "AES-GCM", iv },
565
+ key,
566
+ data
567
+ );
568
+ return decoder.decode(decrypted);
569
+ }
570
+ isNode() {
571
+ return !!(typeof process !== "undefined" && process.versions && process.versions.node && !this.isElectron());
572
+ }
573
+ isElectron() {
574
+ return typeof window !== "undefined" && window.electronAPI !== void 0;
575
+ }
576
+ isMobile() {
577
+ return typeof window !== "undefined" && window.SecureStorage !== void 0;
578
+ }
579
+ async getWebEncryptionKey() {
580
+ const existing = typeof localStorage !== "undefined" ? localStorage.getItem(this.webEncryptionKeyStorage) : null;
581
+ if (existing) {
582
+ return existing;
583
+ }
584
+ let raw = "";
585
+ if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
586
+ const buf = new Uint8Array(32);
587
+ window.crypto.getRandomValues(buf);
588
+ raw = Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
589
+ } else {
590
+ raw = `${navigator.userAgent}-${Math.random().toString(36).slice(2)}-${Date.now()}`;
591
+ }
592
+ if (typeof localStorage !== "undefined") {
593
+ localStorage.setItem(this.webEncryptionKeyStorage, raw);
594
+ }
595
+ return raw;
596
+ }
597
+ };
598
+
599
+ // src/storage/api-key-storage.ts
600
+ var ApiKeyStorage = class {
601
+ constructor() {
602
+ this.storageKey = "lanonasis_api_key";
603
+ this.legacyConfigKey = "lanonasis_legacy_api_key";
604
+ this.webEncryptionKeyStorage = "lanonasis_web_enc_key";
605
+ this.migrationCompleted = false;
606
+ if (this.isNode()) {
607
+ try {
608
+ this.keytar = __require("keytar");
609
+ } catch (e) {
610
+ console.warn("Keytar not available - falling back to encrypted file storage");
611
+ }
612
+ }
613
+ }
614
+ /**
615
+ * Initialize and migrate from legacy storage if needed
616
+ */
617
+ async initialize() {
618
+ await this.migrateFromLegacyIfNeeded();
619
+ }
620
+ /**
621
+ * Store API key securely
622
+ */
623
+ async store(data) {
624
+ const normalizedKey = await this.normalizeApiKey(data.apiKey);
625
+ const dataWithTimestamp = {
626
+ ...data,
627
+ apiKey: normalizedKey,
628
+ createdAt: data.createdAt || (/* @__PURE__ */ new Date()).toISOString()
629
+ };
630
+ const keyString = JSON.stringify(dataWithTimestamp);
631
+ if (this.isNode()) {
632
+ if (this.keytar) {
633
+ try {
634
+ await this.keytar.setPassword("lanonasis-mcp", this.storageKey, keyString);
635
+ return;
636
+ } catch (error) {
637
+ console.warn("Keytar storage failed, falling back to file:", error);
638
+ }
639
+ }
640
+ await this.storeToFile(keyString);
641
+ } else if (this.isElectron()) {
642
+ await window.electronAPI.secureStore.set(this.storageKey, dataWithTimestamp);
643
+ } else if (this.isMobile()) {
644
+ await window.SecureStorage.set(this.storageKey, keyString);
645
+ } else {
646
+ const encrypted = await this.encrypt(keyString);
647
+ localStorage.setItem(this.storageKey, encrypted);
648
+ }
649
+ }
650
+ /**
651
+ * Retrieve API key from secure storage
652
+ */
653
+ async retrieve() {
654
+ let keyString = null;
655
+ try {
656
+ if (this.isNode()) {
657
+ if (this.keytar) {
658
+ try {
659
+ keyString = await this.keytar.getPassword("lanonasis-mcp", this.storageKey);
660
+ } catch (error) {
661
+ console.warn("Keytar retrieval failed, trying file:", error);
662
+ }
663
+ }
664
+ if (!keyString) {
665
+ keyString = await this.retrieveFromFile();
666
+ }
667
+ } else if (this.isElectron()) {
668
+ const data = await window.electronAPI.secureStore.get(this.storageKey);
669
+ return data || null;
670
+ } else if (this.isMobile()) {
671
+ keyString = await window.SecureStorage.get(this.storageKey);
672
+ } else {
673
+ const encrypted = localStorage.getItem(this.storageKey);
674
+ if (encrypted) {
675
+ keyString = await this.decrypt(encrypted);
676
+ }
677
+ }
678
+ if (!keyString) {
679
+ return null;
680
+ }
681
+ const parsed = JSON.parse(keyString);
682
+ const normalizedKey = await this.normalizeApiKey(parsed.apiKey);
683
+ if (normalizedKey !== parsed.apiKey) {
684
+ await this.store({
685
+ ...parsed,
686
+ apiKey: normalizedKey
687
+ });
688
+ }
689
+ return {
690
+ ...parsed,
691
+ apiKey: normalizedKey
692
+ };
693
+ } catch (error) {
694
+ console.error("Error retrieving API key:", error);
695
+ return null;
696
+ }
697
+ }
698
+ /**
699
+ * Get just the API key string (convenience method)
700
+ */
701
+ async getApiKey() {
702
+ const data = await this.retrieve();
703
+ return data?.apiKey || null;
704
+ }
705
+ /**
706
+ * Check if API key exists
707
+ */
708
+ async hasApiKey() {
709
+ const data = await this.retrieve();
710
+ return data !== null && !!data.apiKey;
711
+ }
712
+ /**
713
+ * Clear API key from storage
714
+ */
715
+ async clear() {
716
+ if (this.isNode()) {
717
+ if (this.keytar) {
718
+ try {
719
+ await this.keytar.deletePassword("lanonasis-mcp", this.storageKey);
720
+ } catch (error) {
721
+ console.warn("Keytar deletion failed:", error);
722
+ }
723
+ }
724
+ await this.deleteFile();
725
+ } else if (this.isElectron()) {
726
+ await window.electronAPI.secureStore.delete(this.storageKey);
727
+ } else if (this.isMobile()) {
728
+ await window.SecureStorage.remove(this.storageKey);
729
+ } else {
730
+ localStorage.removeItem(this.storageKey);
731
+ }
732
+ }
733
+ /**
734
+ * Check if API key is expired
735
+ */
736
+ isExpired(data) {
737
+ if (!data.expiresAt) return false;
738
+ try {
739
+ const expiresAt = new Date(data.expiresAt).getTime();
740
+ const now = Date.now();
741
+ return expiresAt - now < 36e5;
742
+ } catch {
743
+ return false;
744
+ }
745
+ }
746
+ /**
747
+ * Update API key metadata without changing the key itself
748
+ */
749
+ async updateMetadata(metadata) {
750
+ const existing = await this.retrieve();
751
+ if (!existing) {
752
+ throw new Error("No API key found to update");
753
+ }
754
+ await this.store({
755
+ ...existing,
756
+ metadata: {
757
+ ...existing.metadata,
758
+ ...metadata
759
+ }
760
+ });
761
+ }
762
+ /**
763
+ * Migrate from legacy configuration storage
764
+ */
765
+ async migrateFromLegacyIfNeeded() {
766
+ if (this.migrationCompleted) {
767
+ return;
768
+ }
769
+ const hasSecureKey = await this.hasApiKey();
770
+ if (hasSecureKey) {
771
+ this.migrationCompleted = true;
772
+ return;
773
+ }
774
+ let legacyKey = null;
775
+ if (this.isNode()) {
776
+ legacyKey = await this.retrieveLegacyFromFile();
777
+ } else if (!this.isElectron() && !this.isMobile()) {
778
+ legacyKey = localStorage.getItem(this.legacyConfigKey);
779
+ }
780
+ if (legacyKey) {
781
+ try {
782
+ const keyData = typeof legacyKey === "string" ? { apiKey: await this.normalizeApiKey(legacyKey) } : JSON.parse(legacyKey);
783
+ await this.store(keyData);
784
+ console.log("API key migrated to secure storage");
785
+ if (this.isNode()) {
786
+ await this.deleteLegacyFile();
787
+ } else {
788
+ localStorage.removeItem(this.legacyConfigKey);
789
+ }
790
+ } catch (error) {
791
+ console.error("Failed to migrate API key:", error);
792
+ }
793
+ }
794
+ this.migrationCompleted = true;
795
+ }
796
+ // ==================== Node.js File Storage ====================
797
+ async storeToFile(keyString) {
798
+ if (!this.isNode()) return;
799
+ const fs = __require("fs").promises;
800
+ const path = __require("path");
801
+ const os = __require("os");
802
+ const crypto = __require("crypto");
803
+ const configDir = path.join(os.homedir(), ".lanonasis");
804
+ const keyFile = path.join(configDir, "api-key.enc");
805
+ await fs.mkdir(configDir, { recursive: true, mode: 448 });
806
+ const key = this.getFileEncryptionKey();
807
+ const iv = crypto.randomBytes(16);
808
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
809
+ let encrypted = cipher.update(keyString, "utf8", "hex");
810
+ encrypted += cipher.final("hex");
811
+ const authTag = cipher.getAuthTag();
812
+ const data = iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted;
813
+ await fs.writeFile(keyFile, data, { mode: 384 });
814
+ }
815
+ async retrieveFromFile() {
816
+ if (!this.isNode()) return null;
817
+ const fs = __require("fs").promises;
818
+ const path = __require("path");
819
+ const os = __require("os");
820
+ const crypto = __require("crypto");
821
+ const keyFile = path.join(os.homedir(), ".lanonasis", "api-key.enc");
822
+ try {
823
+ const data = await fs.readFile(keyFile, "utf8");
824
+ const [ivHex, authTagHex, encrypted] = data.split(":");
825
+ if (!ivHex || !authTagHex || !encrypted) {
826
+ throw new Error("Invalid encrypted file format");
827
+ }
828
+ const key = this.getFileEncryptionKey();
829
+ const iv = Buffer.from(ivHex, "hex");
830
+ const authTag = Buffer.from(authTagHex, "hex");
831
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
832
+ decipher.setAuthTag(authTag);
833
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
834
+ decrypted += decipher.final("utf8");
835
+ return decrypted;
836
+ } catch (error) {
837
+ return null;
838
+ }
839
+ }
840
+ async deleteFile() {
841
+ if (!this.isNode()) return;
842
+ const fs = __require("fs").promises;
843
+ const path = __require("path");
844
+ const os = __require("os");
845
+ const keyFile = path.join(os.homedir(), ".lanonasis", "api-key.enc");
846
+ try {
847
+ await fs.unlink(keyFile);
848
+ } catch (error) {
849
+ }
850
+ }
851
+ async retrieveLegacyFromFile() {
852
+ if (!this.isNode()) return null;
853
+ const fs = __require("fs").promises;
854
+ const path = __require("path");
855
+ const os = __require("os");
856
+ const legacyFile = path.join(os.homedir(), ".lanonasis", "api-key.txt");
857
+ try {
858
+ return await fs.readFile(legacyFile, "utf8");
859
+ } catch {
860
+ return null;
861
+ }
862
+ }
863
+ async deleteLegacyFile() {
864
+ if (!this.isNode()) return;
865
+ const fs = __require("fs").promises;
866
+ const path = __require("path");
867
+ const os = __require("os");
868
+ const legacyFile = path.join(os.homedir(), ".lanonasis", "api-key.txt");
869
+ try {
870
+ await fs.unlink(legacyFile);
871
+ } catch {
872
+ }
873
+ }
874
+ getFileEncryptionKey() {
875
+ const crypto = __require("crypto");
876
+ const os = __require("os");
877
+ const machineId = os.hostname() + os.userInfo().username;
878
+ const salt = "lanonasis-mcp-api-key-2024";
879
+ return crypto.pbkdf2Sync(machineId, salt, 1e5, 32, "sha256");
880
+ }
881
+ // ==================== Web Encryption ====================
882
+ async encrypt(text) {
883
+ if (typeof window === "undefined" || !window.crypto || !window.crypto.subtle) {
884
+ const encoder = new TextEncoder();
885
+ const data = encoder.encode(text);
886
+ return btoa(String.fromCharCode(...data));
887
+ }
888
+ try {
889
+ const encoder = new TextEncoder();
890
+ const data = encoder.encode(text);
891
+ const passphrase = await this.getWebEncryptionKey();
892
+ const keyMaterial = await window.crypto.subtle.importKey(
893
+ "raw",
894
+ encoder.encode(passphrase),
895
+ "PBKDF2",
896
+ false,
897
+ ["deriveBits", "deriveKey"]
898
+ );
899
+ const key = await window.crypto.subtle.deriveKey(
900
+ {
901
+ name: "PBKDF2",
902
+ salt: encoder.encode("lanonasis-api-key-salt"),
903
+ iterations: 1e5,
904
+ hash: "SHA-256"
905
+ },
906
+ keyMaterial,
907
+ { name: "AES-GCM", length: 256 },
908
+ true,
909
+ ["encrypt", "decrypt"]
910
+ );
911
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
912
+ const encrypted = await window.crypto.subtle.encrypt(
913
+ { name: "AES-GCM", iv },
914
+ key,
915
+ data
916
+ );
917
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
918
+ combined.set(iv, 0);
919
+ combined.set(new Uint8Array(encrypted), iv.length);
920
+ return btoa(String.fromCharCode(...combined));
921
+ } catch (error) {
922
+ console.error("Web encryption failed:", error);
923
+ const encoder = new TextEncoder();
924
+ const data = encoder.encode(text);
925
+ return btoa(String.fromCharCode(...data));
926
+ }
927
+ }
928
+ async decrypt(encrypted) {
929
+ if (typeof window === "undefined" || !window.crypto || !window.crypto.subtle) {
930
+ const binary = atob(encrypted);
931
+ const bytes = new Uint8Array(binary.length);
932
+ for (let i = 0; i < binary.length; i++) {
933
+ bytes[i] = binary.charCodeAt(i);
934
+ }
935
+ const decoder = new TextDecoder();
936
+ return decoder.decode(bytes);
937
+ }
938
+ try {
939
+ const encoder = new TextEncoder();
940
+ const decoder = new TextDecoder();
941
+ const binary = atob(encrypted);
942
+ const bytes = new Uint8Array(binary.length);
943
+ for (let i = 0; i < binary.length; i++) {
944
+ bytes[i] = binary.charCodeAt(i);
945
+ }
946
+ const iv = bytes.slice(0, 12);
947
+ const data = bytes.slice(12);
948
+ const passphrase = await this.getWebEncryptionKey();
949
+ const keyMaterial = await window.crypto.subtle.importKey(
950
+ "raw",
951
+ encoder.encode(passphrase),
952
+ "PBKDF2",
953
+ false,
954
+ ["deriveBits", "deriveKey"]
955
+ );
956
+ const key = await window.crypto.subtle.deriveKey(
957
+ {
958
+ name: "PBKDF2",
959
+ salt: encoder.encode("lanonasis-api-key-salt"),
960
+ iterations: 1e5,
961
+ hash: "SHA-256"
962
+ },
963
+ keyMaterial,
964
+ { name: "AES-GCM", length: 256 },
965
+ true,
966
+ ["encrypt", "decrypt"]
967
+ );
968
+ const decrypted = await window.crypto.subtle.decrypt(
969
+ { name: "AES-GCM", iv },
970
+ key,
971
+ data
972
+ );
973
+ return decoder.decode(decrypted);
974
+ } catch (error) {
975
+ console.error("Web decryption failed:", error);
976
+ const binary = atob(encrypted);
977
+ const bytes = new Uint8Array(binary.length);
978
+ for (let i = 0; i < binary.length; i++) {
979
+ bytes[i] = binary.charCodeAt(i);
980
+ }
981
+ const decoder = new TextDecoder();
982
+ return decoder.decode(bytes);
983
+ }
984
+ }
985
+ async getWebEncryptionKey() {
986
+ const existing = typeof localStorage !== "undefined" ? localStorage.getItem(this.webEncryptionKeyStorage) : null;
987
+ if (existing) {
988
+ return existing;
989
+ }
990
+ let raw = "";
991
+ if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
992
+ const buf = new Uint8Array(32);
993
+ window.crypto.getRandomValues(buf);
994
+ raw = Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
995
+ } else {
996
+ raw = `${navigator.userAgent}-${Math.random().toString(36).slice(2)}-${Date.now()}`;
997
+ }
998
+ if (typeof localStorage !== "undefined") {
999
+ localStorage.setItem(this.webEncryptionKeyStorage, raw);
1000
+ }
1001
+ return raw;
1002
+ }
1003
+ // ==================== Platform Detection ====================
1004
+ isNode() {
1005
+ return typeof process !== "undefined" && !!process.versions && !!process.versions.node && !this.isElectron();
1006
+ }
1007
+ isElectron() {
1008
+ return typeof window !== "undefined" && window.electronAPI !== void 0;
1009
+ }
1010
+ isMobile() {
1011
+ return typeof window !== "undefined" && window.SecureStorage !== void 0;
1012
+ }
1013
+ /**
1014
+ * Normalize API keys to a SHA-256 hex digest.
1015
+ * Accepts pre-hashed input and lowercases it to prevent double hashing.
1016
+ */
1017
+ async normalizeApiKey(apiKey) {
1018
+ const value = apiKey?.trim();
1019
+ if (!value) {
1020
+ throw new Error("API key must be a non-empty string");
1021
+ }
1022
+ if (/^[a-f0-9]{64}$/i.test(value)) {
1023
+ return value.toLowerCase();
1024
+ }
1025
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle) {
1026
+ const encoder = new TextEncoder();
1027
+ const data = encoder.encode(value);
1028
+ const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data);
1029
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
1030
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
1031
+ }
1032
+ const nodeCrypto = await import("crypto");
1033
+ return nodeCrypto.createHash("sha256").update(value).digest("hex");
1034
+ }
1035
+ };
1036
+
1037
+ // src/client/mcp-client.ts
1038
+ var MCPClient = class {
1039
+ constructor(config = {}) {
1040
+ this.ws = null;
1041
+ this.eventSource = null;
1042
+ this.accessToken = null;
1043
+ this.refreshTimer = null;
1044
+ this.config = {
1045
+ mcpEndpoint: "wss://mcp.lanonasis.com",
1046
+ autoRefresh: true,
1047
+ ...config
1048
+ };
1049
+ this.tokenStorage = new TokenStorage();
1050
+ if (this.isTerminal()) {
1051
+ this.authFlow = new TerminalOAuthFlow(config);
1052
+ } else {
1053
+ this.authFlow = new DesktopOAuthFlow(config);
1054
+ }
1055
+ }
1056
+ async connect() {
1057
+ try {
1058
+ let tokens = await this.tokenStorage.retrieve();
1059
+ if (!tokens || this.tokenStorage.isTokenExpired(tokens)) {
1060
+ if (tokens?.refresh_token) {
1061
+ try {
1062
+ tokens = await this.authFlow.refreshToken(tokens.refresh_token);
1063
+ await this.tokenStorage.store(tokens);
1064
+ } catch (error) {
1065
+ tokens = await this.authenticate();
1066
+ }
1067
+ } else {
1068
+ tokens = await this.authenticate();
1069
+ }
1070
+ }
1071
+ this.accessToken = tokens.access_token;
1072
+ if (this.config.autoRefresh && tokens.expires_in) {
1073
+ this.scheduleTokenRefresh(tokens);
1074
+ }
1075
+ await this.establishConnection();
1076
+ } catch (error) {
1077
+ console.error("Failed to connect:", error);
1078
+ throw error;
1079
+ }
1080
+ }
1081
+ async authenticate() {
1082
+ console.log("Authenticating with Lan Onasis...");
1083
+ const tokens = await this.authFlow.authenticate();
1084
+ await this.tokenStorage.store(tokens);
1085
+ return tokens;
1086
+ }
1087
+ scheduleTokenRefresh(tokens) {
1088
+ if (this.refreshTimer) {
1089
+ clearTimeout(this.refreshTimer);
1090
+ }
1091
+ const refreshIn = (tokens.expires_in - 300) * 1e3;
1092
+ this.refreshTimer = setTimeout(async () => {
1093
+ try {
1094
+ if (tokens.refresh_token) {
1095
+ const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
1096
+ await this.tokenStorage.store(newTokens);
1097
+ this.accessToken = newTokens.access_token;
1098
+ this.scheduleTokenRefresh(newTokens);
1099
+ await this.reconnect();
1100
+ }
1101
+ } catch (error) {
1102
+ console.error("Token refresh failed:", error);
1103
+ }
1104
+ }, refreshIn);
1105
+ }
1106
+ async establishConnection() {
1107
+ const endpoint = this.config.mcpEndpoint;
1108
+ if (endpoint.startsWith("wss://")) {
1109
+ await this.connectWebSocket(endpoint);
1110
+ } else if (endpoint.startsWith("https://")) {
1111
+ await this.connectSSE(endpoint);
1112
+ } else {
1113
+ throw new Error("Invalid MCP endpoint - must be wss:// or https://");
1114
+ }
1115
+ }
1116
+ async connectWebSocket(endpoint) {
1117
+ const wsUrl = new URL(endpoint);
1118
+ wsUrl.pathname = "/ws";
1119
+ if (this.accessToken) {
1120
+ wsUrl.searchParams.set("access_token", this.accessToken);
1121
+ }
1122
+ if (typeof WebSocket !== "undefined") {
1123
+ this.ws = new WebSocket(wsUrl.toString());
1124
+ } else {
1125
+ const { default: WS } = await import("ws");
1126
+ this.ws = new WS(wsUrl.toString(), {
1127
+ headers: {
1128
+ "Authorization": `Bearer ${this.accessToken}`
1129
+ }
1130
+ });
1131
+ }
1132
+ return new Promise((resolve, reject) => {
1133
+ if (!this.ws) {
1134
+ reject(new Error("WebSocket not initialized"));
1135
+ return;
1136
+ }
1137
+ this.ws.onopen = () => {
1138
+ console.log("MCP WebSocket connected");
1139
+ resolve();
1140
+ };
1141
+ this.ws.onerror = (error) => {
1142
+ console.error("WebSocket error:", error);
1143
+ reject(error);
1144
+ };
1145
+ this.ws.onclose = (event) => {
1146
+ console.log("WebSocket closed:", event.code, event.reason);
1147
+ if (event.code !== 1008 && event.code !== 4001) {
1148
+ setTimeout(() => this.reconnect(), 5e3);
1149
+ }
1150
+ };
1151
+ this.ws.onmessage = (event) => {
1152
+ this.handleMessage(event.data);
1153
+ };
1154
+ });
1155
+ }
1156
+ async connectSSE(endpoint) {
1157
+ const sseUrl = new URL(endpoint);
1158
+ sseUrl.pathname = "/sse";
1159
+ if (typeof EventSource !== "undefined") {
1160
+ this.eventSource = new EventSource(sseUrl.toString());
1161
+ } else {
1162
+ const EventSourceModule = await import("eventsource");
1163
+ const ES = EventSourceModule.default || EventSourceModule;
1164
+ this.eventSource = new ES(sseUrl.toString(), {
1165
+ headers: {
1166
+ "Authorization": `Bearer ${this.accessToken}`
1167
+ }
1168
+ });
1169
+ }
1170
+ this.eventSource.onopen = () => {
1171
+ console.log("MCP SSE connected");
1172
+ };
1173
+ this.eventSource.onerror = (error) => {
1174
+ console.error("SSE error:", error);
1175
+ setTimeout(() => this.reconnect(), 5e3);
1176
+ };
1177
+ this.eventSource.onmessage = (event) => {
1178
+ this.handleMessage(event.data);
1179
+ };
1180
+ }
1181
+ handleMessage(data) {
1182
+ try {
1183
+ const message = JSON.parse(data);
1184
+ console.log("MCP message:", message);
1185
+ } catch (error) {
1186
+ console.error("Failed to parse message:", error);
1187
+ }
1188
+ }
1189
+ async reconnect() {
1190
+ this.disconnect();
1191
+ await this.establishConnection();
1192
+ }
1193
+ async request(method, params) {
1194
+ if (!this.accessToken) {
1195
+ throw new Error("Not authenticated");
1196
+ }
1197
+ const response = await fetch(`${this.config.mcpEndpoint}/api`, {
1198
+ method: "POST",
1199
+ headers: {
1200
+ "Authorization": `Bearer ${this.accessToken}`,
1201
+ "Content-Type": "application/json"
1202
+ },
1203
+ body: JSON.stringify({
1204
+ jsonrpc: "2.0",
1205
+ id: this.generateId(),
1206
+ method,
1207
+ params
1208
+ })
1209
+ });
1210
+ if (response.status === 401) {
1211
+ const tokens = await this.tokenStorage.retrieve();
1212
+ if (tokens?.refresh_token) {
1213
+ const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
1214
+ await this.tokenStorage.store(newTokens);
1215
+ this.accessToken = newTokens.access_token;
1216
+ return this.request(method, params);
1217
+ } else {
1218
+ await this.connect();
1219
+ return this.request(method, params);
1220
+ }
1221
+ }
1222
+ const result = await response.json();
1223
+ if (result.error) {
1224
+ throw new Error(result.error.message || "Request failed");
1225
+ }
1226
+ return result.result;
1227
+ }
1228
+ disconnect() {
1229
+ if (this.ws) {
1230
+ this.ws.close();
1231
+ this.ws = null;
1232
+ }
1233
+ if (this.eventSource) {
1234
+ this.eventSource.close();
1235
+ this.eventSource = null;
1236
+ }
1237
+ if (this.refreshTimer) {
1238
+ clearTimeout(this.refreshTimer);
1239
+ this.refreshTimer = null;
1240
+ }
1241
+ }
1242
+ async logout() {
1243
+ const tokens = await this.tokenStorage.retrieve();
1244
+ if (tokens) {
1245
+ try {
1246
+ if (tokens.access_token) {
1247
+ await this.authFlow.revokeToken(tokens.access_token, "access_token");
1248
+ }
1249
+ if (tokens.refresh_token) {
1250
+ await this.authFlow.revokeToken(tokens.refresh_token, "refresh_token");
1251
+ }
1252
+ } catch (error) {
1253
+ console.error("Failed to revoke tokens:", error);
1254
+ }
1255
+ }
1256
+ await this.tokenStorage.clear();
1257
+ this.disconnect();
1258
+ this.accessToken = null;
1259
+ }
1260
+ isTerminal() {
1261
+ return !!(typeof process !== "undefined" && process.versions && process.versions.node && !this.isElectron());
1262
+ }
1263
+ isElectron() {
1264
+ return typeof window !== "undefined" && window.electronAPI !== void 0;
1265
+ }
1266
+ generateId() {
1267
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1268
+ }
1269
+ // MCP-specific methods
1270
+ async createMemory(title, content, options) {
1271
+ return this.request("memory/create", {
1272
+ title,
1273
+ content,
1274
+ ...options
1275
+ });
1276
+ }
1277
+ async searchMemories(query, options) {
1278
+ return this.request("memory/search", {
1279
+ query,
1280
+ ...options
1281
+ });
1282
+ }
1283
+ async getMemory(id) {
1284
+ return this.request("memory/get", { id });
1285
+ }
1286
+ async updateMemory(id, updates) {
1287
+ return this.request("memory/update", {
1288
+ id,
1289
+ ...updates
1290
+ });
1291
+ }
1292
+ async deleteMemory(id) {
1293
+ return this.request("memory/delete", { id });
1294
+ }
1295
+ };
1296
+ export {
1297
+ ApiKeyStorage,
1298
+ BaseOAuthFlow,
1299
+ DesktopOAuthFlow,
1300
+ MCPClient,
1301
+ TerminalOAuthFlow,
1302
+ TokenStorage
1303
+ };