@lanonasis/oauth-client 1.2.1 → 1.2.3

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.
@@ -0,0 +1,793 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/browser.ts
31
+ var browser_exports = {};
32
+ __export(browser_exports, {
33
+ ApiKeyStorageWeb: () => ApiKeyStorageWeb,
34
+ BaseOAuthFlow: () => BaseOAuthFlow,
35
+ DesktopOAuthFlow: () => DesktopOAuthFlow,
36
+ MCPClient: () => MCPClient,
37
+ TokenStorageWeb: () => TokenStorageWeb
38
+ });
39
+ module.exports = __toCommonJS(browser_exports);
40
+
41
+ // src/flows/base-flow.ts
42
+ var import_cross_fetch = __toESM(require("cross-fetch"), 1);
43
+ var BaseOAuthFlow = class {
44
+ constructor(config) {
45
+ this.clientId = config.clientId;
46
+ this.authBaseUrl = config.authBaseUrl || "https://auth.lanonasis.com";
47
+ this.scope = config.scope || "memories:read memories:write memories:delete profile";
48
+ }
49
+ async makeTokenRequest(body) {
50
+ const response = await (0, import_cross_fetch.default)(`${this.authBaseUrl}/oauth/token`, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify(body)
54
+ });
55
+ const data = await response.json();
56
+ if (!response.ok) {
57
+ throw new Error(data.error_description || "Token request failed");
58
+ }
59
+ return data;
60
+ }
61
+ generateState() {
62
+ const array = new Uint8Array(32);
63
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
64
+ crypto.getRandomValues(array);
65
+ } else {
66
+ throw new Error("Secure random generation is not available");
67
+ }
68
+ return this.base64URLEncode(array);
69
+ }
70
+ base64URLEncode(buffer) {
71
+ const bytes = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
72
+ let binary = "";
73
+ bytes.forEach((byte) => {
74
+ binary += String.fromCharCode(byte);
75
+ });
76
+ const base64 = typeof btoa !== "undefined" ? btoa(binary) : Buffer.from(bytes).toString("base64");
77
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
78
+ }
79
+ async refreshToken(refreshToken) {
80
+ return this.makeTokenRequest({
81
+ grant_type: "refresh_token",
82
+ refresh_token: refreshToken,
83
+ client_id: this.clientId
84
+ });
85
+ }
86
+ async revokeToken(token, tokenType = "access_token") {
87
+ const response = await (0, import_cross_fetch.default)(`${this.authBaseUrl}/oauth/revoke`, {
88
+ method: "POST",
89
+ headers: { "Content-Type": "application/json" },
90
+ body: JSON.stringify({
91
+ token,
92
+ token_type_hint: tokenType,
93
+ client_id: this.clientId
94
+ })
95
+ });
96
+ if (!response.ok) {
97
+ throw new Error("Failed to revoke token");
98
+ }
99
+ }
100
+ };
101
+
102
+ // src/flows/desktop-flow.ts
103
+ var DesktopOAuthFlow = class extends BaseOAuthFlow {
104
+ constructor(config) {
105
+ super({
106
+ ...config,
107
+ clientId: config.clientId || "lanonasis-mcp-desktop"
108
+ });
109
+ this.authWindow = null;
110
+ this.redirectUri = config.redirectUri || "lanonasis://oauth/callback";
111
+ }
112
+ async authenticate() {
113
+ const pkce = await this.generatePKCEChallenge();
114
+ const state = this.generateState();
115
+ const authUrl = this.buildAuthorizationUrl(pkce.codeChallenge, state);
116
+ const authCode = await this.openAuthWindow(authUrl, state);
117
+ return await this.exchangeCodeForToken(authCode, pkce.codeVerifier);
118
+ }
119
+ async generatePKCEChallenge() {
120
+ const codeVerifier = this.generateCodeVerifier();
121
+ const codeChallenge = await this.generateCodeChallenge(codeVerifier);
122
+ return { codeVerifier, codeChallenge };
123
+ }
124
+ generateCodeVerifier() {
125
+ const array = new Uint8Array(32);
126
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
127
+ crypto.getRandomValues(array);
128
+ } else {
129
+ throw new Error("Secure random generation is not available in this environment");
130
+ }
131
+ return this.base64URLEncode(array);
132
+ }
133
+ async generateCodeChallenge(verifier) {
134
+ const subtle = typeof crypto !== "undefined" ? crypto.subtle : void 0;
135
+ if (!subtle) {
136
+ throw new Error("Web Crypto is required to generate PKCE code challenge");
137
+ }
138
+ const encoder = new TextEncoder();
139
+ const data = encoder.encode(verifier);
140
+ const hash = await subtle.digest("SHA-256", data);
141
+ return this.base64URLEncode(hash);
142
+ }
143
+ buildAuthorizationUrl(codeChallenge, state) {
144
+ const params = new URLSearchParams({
145
+ client_id: this.clientId,
146
+ response_type: "code",
147
+ redirect_uri: this.redirectUri,
148
+ scope: this.scope,
149
+ code_challenge: codeChallenge,
150
+ code_challenge_method: "S256",
151
+ state
152
+ });
153
+ return `${this.authBaseUrl}/oauth/authorize?${params}`;
154
+ }
155
+ async openAuthWindow(authUrl, expectedState) {
156
+ return new Promise((resolve, reject) => {
157
+ if (typeof window !== "undefined") {
158
+ this.openBrowserWindow(authUrl, expectedState, resolve, reject);
159
+ } else {
160
+ this.openElectronWindow(authUrl, expectedState, resolve, reject);
161
+ }
162
+ });
163
+ }
164
+ openBrowserWindow(authUrl, expectedState, resolve, reject) {
165
+ const width = 500;
166
+ const height = 700;
167
+ const left = (window.screen.width - width) / 2;
168
+ const top = (window.screen.height - height) / 2;
169
+ this.authWindow = window.open(
170
+ authUrl,
171
+ "Lan Onasis Login",
172
+ `width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no`
173
+ );
174
+ if (!this.authWindow) {
175
+ reject(new Error("Failed to open authentication window"));
176
+ return;
177
+ }
178
+ const checkInterval = setInterval(() => {
179
+ try {
180
+ if (!this.authWindow || this.authWindow.closed) {
181
+ clearInterval(checkInterval);
182
+ reject(new Error("Authentication window was closed"));
183
+ return;
184
+ }
185
+ const currentUrl = this.authWindow.location.href;
186
+ if (currentUrl.startsWith(this.redirectUri)) {
187
+ clearInterval(checkInterval);
188
+ this.authWindow.close();
189
+ const url = new URL(currentUrl);
190
+ const code = url.searchParams.get("code");
191
+ const state = url.searchParams.get("state");
192
+ const error = url.searchParams.get("error");
193
+ if (error) {
194
+ reject(new Error(url.searchParams.get("error_description") || error));
195
+ } else if (state !== expectedState) {
196
+ reject(new Error("State mismatch - possible CSRF attack"));
197
+ } else if (code) {
198
+ resolve(code);
199
+ } else {
200
+ reject(new Error("No authorization code received"));
201
+ }
202
+ }
203
+ } catch (e) {
204
+ }
205
+ }, 500);
206
+ }
207
+ openElectronWindow(authUrl, expectedState, resolve, reject) {
208
+ const { BrowserWindow } = require("electron");
209
+ const authWindow = new BrowserWindow({
210
+ width: 500,
211
+ height: 700,
212
+ webPreferences: {
213
+ nodeIntegration: false,
214
+ contextIsolation: true
215
+ }
216
+ });
217
+ authWindow.loadURL(authUrl);
218
+ authWindow.webContents.on("will-redirect", (event, url) => {
219
+ if (url.startsWith(this.redirectUri)) {
220
+ event.preventDefault();
221
+ authWindow.close();
222
+ const callbackUrl = new URL(url);
223
+ const code = callbackUrl.searchParams.get("code");
224
+ const state = callbackUrl.searchParams.get("state");
225
+ const error = callbackUrl.searchParams.get("error");
226
+ if (error) {
227
+ reject(new Error(callbackUrl.searchParams.get("error_description") || error));
228
+ } else if (state !== expectedState) {
229
+ reject(new Error("State mismatch"));
230
+ } else if (code) {
231
+ resolve(code);
232
+ }
233
+ }
234
+ });
235
+ authWindow.on("closed", () => {
236
+ reject(new Error("Authentication window was closed"));
237
+ });
238
+ }
239
+ async exchangeCodeForToken(code, codeVerifier) {
240
+ return this.makeTokenRequest({
241
+ grant_type: "authorization_code",
242
+ code,
243
+ client_id: this.clientId,
244
+ redirect_uri: this.redirectUri,
245
+ code_verifier: codeVerifier
246
+ });
247
+ }
248
+ };
249
+
250
+ // src/client/mcp-client-browser.ts
251
+ var import_cross_fetch3 = __toESM(require("cross-fetch"), 1);
252
+
253
+ // src/storage/token-storage-web.ts
254
+ var TokenStorageWeb = class {
255
+ constructor() {
256
+ this.storageKey = "lanonasis_mcp_tokens";
257
+ this.webEncryptionKeyStorage = "lanonasis_web_token_enc_key";
258
+ }
259
+ async store(tokens) {
260
+ const tokensWithTimestamp = {
261
+ ...tokens,
262
+ issued_at: Date.now()
263
+ };
264
+ const tokenString = JSON.stringify(tokensWithTimestamp);
265
+ const encrypted = await this.encrypt(tokenString);
266
+ localStorage.setItem(this.storageKey, encrypted);
267
+ }
268
+ async retrieve() {
269
+ const encrypted = localStorage.getItem(this.storageKey);
270
+ if (!encrypted) return null;
271
+ try {
272
+ const tokenString = await this.decrypt(encrypted);
273
+ return JSON.parse(tokenString);
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+ async clear() {
279
+ localStorage.removeItem(this.storageKey);
280
+ }
281
+ isTokenExpired(tokens) {
282
+ if (tokens.token_type === "api-key" || tokens.expires_in === 0) return false;
283
+ if (!tokens.expires_in) return false;
284
+ if (!tokens.issued_at) return true;
285
+ const expiresAt = tokens.issued_at + tokens.expires_in * 1e3;
286
+ return expiresAt - Date.now() < 3e5;
287
+ }
288
+ async encrypt(text) {
289
+ if (typeof window === "undefined" || !window.crypto?.subtle) {
290
+ const encoder2 = new TextEncoder();
291
+ return this.base64Encode(encoder2.encode(text));
292
+ }
293
+ const encoder = new TextEncoder();
294
+ const data = encoder.encode(text);
295
+ const passphrase = await this.getWebEncryptionKey();
296
+ const keyMaterial = await window.crypto.subtle.importKey(
297
+ "raw",
298
+ encoder.encode(passphrase),
299
+ "PBKDF2",
300
+ false,
301
+ ["deriveBits", "deriveKey"]
302
+ );
303
+ const key = await window.crypto.subtle.deriveKey(
304
+ {
305
+ name: "PBKDF2",
306
+ salt: encoder.encode("lanonasis-token-salt"),
307
+ iterations: 1e5,
308
+ hash: "SHA-256"
309
+ },
310
+ keyMaterial,
311
+ { name: "AES-GCM", length: 256 },
312
+ true,
313
+ ["encrypt", "decrypt"]
314
+ );
315
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
316
+ const encrypted = await window.crypto.subtle.encrypt(
317
+ { name: "AES-GCM", iv },
318
+ key,
319
+ data
320
+ );
321
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
322
+ combined.set(iv, 0);
323
+ combined.set(new Uint8Array(encrypted), iv.length);
324
+ return this.base64Encode(combined);
325
+ }
326
+ async decrypt(encrypted) {
327
+ if (typeof window === "undefined" || !window.crypto?.subtle) {
328
+ const decoder2 = new TextDecoder();
329
+ return decoder2.decode(this.base64Decode(encrypted));
330
+ }
331
+ const bytes = this.base64Decode(encrypted);
332
+ const iv = bytes.slice(0, 12);
333
+ const data = bytes.slice(12);
334
+ const encoder = new TextEncoder();
335
+ const decoder = new TextDecoder();
336
+ const passphrase = await this.getWebEncryptionKey();
337
+ const keyMaterial = await window.crypto.subtle.importKey(
338
+ "raw",
339
+ encoder.encode(passphrase),
340
+ "PBKDF2",
341
+ false,
342
+ ["deriveBits", "deriveKey"]
343
+ );
344
+ const key = await window.crypto.subtle.deriveKey(
345
+ {
346
+ name: "PBKDF2",
347
+ salt: encoder.encode("lanonasis-token-salt"),
348
+ iterations: 1e5,
349
+ hash: "SHA-256"
350
+ },
351
+ keyMaterial,
352
+ { name: "AES-GCM", length: 256 },
353
+ true,
354
+ ["encrypt", "decrypt"]
355
+ );
356
+ const decrypted = await window.crypto.subtle.decrypt(
357
+ { name: "AES-GCM", iv },
358
+ key,
359
+ data
360
+ );
361
+ return decoder.decode(decrypted);
362
+ }
363
+ async getWebEncryptionKey() {
364
+ const existing = localStorage.getItem(this.webEncryptionKeyStorage);
365
+ if (existing) return existing;
366
+ const buf = new Uint8Array(32);
367
+ if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
368
+ window.crypto.getRandomValues(buf);
369
+ }
370
+ const raw = Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
371
+ localStorage.setItem(this.webEncryptionKeyStorage, raw);
372
+ return raw;
373
+ }
374
+ base64Encode(bytes) {
375
+ let binary = "";
376
+ bytes.forEach((b) => {
377
+ binary += String.fromCharCode(b);
378
+ });
379
+ return btoa(binary);
380
+ }
381
+ base64Decode(value) {
382
+ const binary = atob(value);
383
+ const bytes = new Uint8Array(binary.length);
384
+ for (let i = 0; i < binary.length; i++) {
385
+ bytes[i] = binary.charCodeAt(i);
386
+ }
387
+ return bytes;
388
+ }
389
+ };
390
+
391
+ // src/flows/apikey-flow.ts
392
+ var import_cross_fetch2 = __toESM(require("cross-fetch"), 1);
393
+ var APIKeyFlow = class extends BaseOAuthFlow {
394
+ constructor(apiKey, authBaseUrl = "https://mcp.lanonasis.com") {
395
+ super({
396
+ clientId: "api-key-client",
397
+ authBaseUrl
398
+ });
399
+ this.apiKey = apiKey;
400
+ }
401
+ /**
402
+ * "Authenticate" by returning the API key as a virtual token
403
+ * The API key will be used directly in request headers
404
+ */
405
+ async authenticate() {
406
+ if (!this.apiKey || !this.apiKey.startsWith("lano_") && !this.apiKey.startsWith("vx_")) {
407
+ throw new Error(
408
+ 'Invalid API key format. Must start with "lano_" or "vx_". Please regenerate your API key from the dashboard.'
409
+ );
410
+ }
411
+ if (this.apiKey.startsWith("vx_")) {
412
+ console.warn(
413
+ '\u26A0\uFE0F DEPRECATION WARNING: API keys with "vx_" prefix are deprecated and will stop working soon. Please regenerate your API key from the dashboard to get a "lano_" prefixed key. Support for "vx_" keys will be removed in a future version.'
414
+ );
415
+ }
416
+ return {
417
+ access_token: this.apiKey,
418
+ token_type: "api-key",
419
+ expires_in: 0,
420
+ // API keys don't expire
421
+ issued_at: Date.now()
422
+ };
423
+ }
424
+ /**
425
+ * API keys don't need refresh
426
+ */
427
+ async refreshToken(refreshToken) {
428
+ throw new Error("API keys do not support token refresh");
429
+ }
430
+ /**
431
+ * Optional: Validate API key by making a test request
432
+ */
433
+ async validateAPIKey() {
434
+ try {
435
+ const response = await (0, import_cross_fetch2.default)(`${this.config.authBaseUrl}/api/v1/health`, {
436
+ headers: {
437
+ "x-api-key": this.apiKey
438
+ }
439
+ });
440
+ return response.ok;
441
+ } catch (error) {
442
+ console.error("API key validation failed:", error);
443
+ return false;
444
+ }
445
+ }
446
+ };
447
+
448
+ // src/client/mcp-client-browser.ts
449
+ var MCPClient = class {
450
+ constructor(config = {}) {
451
+ this.ws = null;
452
+ this.eventSource = null;
453
+ this.accessToken = null;
454
+ this.refreshTimer = null;
455
+ this.config = {
456
+ mcpEndpoint: "wss://mcp.lanonasis.com",
457
+ autoRefresh: true,
458
+ ...config
459
+ };
460
+ this.tokenStorage = config.tokenStorage || new TokenStorageWeb();
461
+ this.authMode = config.apiKey ? "apikey" : "oauth";
462
+ if (this.authMode === "apikey") {
463
+ this.authFlow = new APIKeyFlow(
464
+ config.apiKey,
465
+ config.authBaseUrl || "https://mcp.lanonasis.com"
466
+ );
467
+ } else {
468
+ this.authFlow = new DesktopOAuthFlow(config);
469
+ }
470
+ }
471
+ async connect() {
472
+ try {
473
+ let tokens = await this.tokenStorage.retrieve();
474
+ if (this.authMode === "apikey") {
475
+ if (!tokens) {
476
+ tokens = await this.authenticate();
477
+ }
478
+ this.accessToken = tokens.access_token;
479
+ } else {
480
+ if (!tokens || this.tokenStorage.isTokenExpired(tokens)) {
481
+ if (tokens?.refresh_token) {
482
+ try {
483
+ tokens = await this.authFlow.refreshToken(tokens.refresh_token);
484
+ await this.tokenStorage.store(tokens);
485
+ } catch (error) {
486
+ tokens = await this.authenticate();
487
+ }
488
+ } else {
489
+ tokens = await this.authenticate();
490
+ }
491
+ }
492
+ this.accessToken = tokens.access_token;
493
+ if (this.config.autoRefresh && tokens.expires_in) {
494
+ this.scheduleTokenRefresh(tokens.expires_in);
495
+ }
496
+ }
497
+ await this.establishConnection();
498
+ } catch (error) {
499
+ console.error("MCP connection failed:", error);
500
+ throw error;
501
+ }
502
+ }
503
+ async authenticate() {
504
+ const tokens = await this.authFlow.authenticate();
505
+ await this.tokenStorage.store(tokens);
506
+ return tokens;
507
+ }
508
+ async ensureAccessToken() {
509
+ if (!this.accessToken) {
510
+ await this.connect();
511
+ }
512
+ return this.accessToken;
513
+ }
514
+ scheduleTokenRefresh(expiresIn) {
515
+ const refreshTime = (expiresIn - 300) * 1e3;
516
+ if (this.refreshTimer) {
517
+ clearTimeout(this.refreshTimer);
518
+ }
519
+ this.refreshTimer = setTimeout(async () => {
520
+ try {
521
+ const tokens = await this.tokenStorage.retrieve();
522
+ if (tokens?.refresh_token) {
523
+ const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
524
+ await this.tokenStorage.store(newTokens);
525
+ this.accessToken = newTokens.access_token;
526
+ if (newTokens.expires_in) {
527
+ this.scheduleTokenRefresh(newTokens.expires_in);
528
+ }
529
+ }
530
+ } catch (error) {
531
+ console.error("Token refresh failed:", error);
532
+ await this.connect();
533
+ }
534
+ }, refreshTime);
535
+ }
536
+ async establishConnection() {
537
+ const endpoint = this.config.mcpEndpoint;
538
+ if (endpoint.startsWith("ws://") || endpoint.startsWith("wss://")) {
539
+ await this.connectWebSocket(endpoint);
540
+ } else {
541
+ await this.connectSSE(endpoint);
542
+ }
543
+ }
544
+ async connectWebSocket(endpoint) {
545
+ if (typeof WebSocket !== "undefined") {
546
+ this.ws = new WebSocket(endpoint);
547
+ this.ws.onopen = () => {
548
+ console.log("MCP WebSocket connected");
549
+ this.ws?.send(JSON.stringify({
550
+ type: "auth",
551
+ token: this.accessToken
552
+ }));
553
+ };
554
+ this.ws.onmessage = (event) => {
555
+ this.handleMessage(JSON.parse(event.data));
556
+ };
557
+ this.ws.onerror = (error) => {
558
+ console.error("MCP WebSocket error:", error);
559
+ };
560
+ this.ws.onclose = () => {
561
+ console.log("MCP WebSocket disconnected");
562
+ setTimeout(() => this.reconnect(), 5e3);
563
+ };
564
+ } else {
565
+ throw new Error("WebSocket is not available in this environment");
566
+ }
567
+ }
568
+ async connectSSE(endpoint) {
569
+ const sseUrl = new URL(endpoint);
570
+ if (typeof EventSource !== "undefined") {
571
+ if (this.authMode === "apikey") {
572
+ sseUrl.searchParams.set("api_key", this.accessToken);
573
+ } else {
574
+ sseUrl.searchParams.set("token", this.accessToken);
575
+ }
576
+ this.eventSource = new EventSource(sseUrl.toString());
577
+ this.eventSource.onopen = () => {
578
+ console.log("MCP SSE connected");
579
+ };
580
+ this.eventSource.onmessage = (event) => {
581
+ this.handleMessage(JSON.parse(event.data));
582
+ };
583
+ this.eventSource.onerror = () => {
584
+ console.error("MCP SSE error");
585
+ this.eventSource?.close();
586
+ setTimeout(() => this.reconnect(), 5e3);
587
+ };
588
+ } else {
589
+ throw new Error("EventSource is not available in this environment");
590
+ }
591
+ }
592
+ handleMessage(message) {
593
+ console.log("MCP message:", message);
594
+ }
595
+ async reconnect() {
596
+ try {
597
+ await this.connect();
598
+ } catch (error) {
599
+ console.error("Reconnection failed:", error);
600
+ setTimeout(() => this.reconnect(), 1e4);
601
+ }
602
+ }
603
+ async request(method, params) {
604
+ await this.ensureAccessToken();
605
+ const endpoint = this.config.mcpEndpoint.replace(/^ws/, "http");
606
+ const response = await (0, import_cross_fetch3.default)(`${endpoint}/rpc`, {
607
+ method: "POST",
608
+ headers: {
609
+ "Content-Type": "application/json",
610
+ "Authorization": `Bearer ${this.accessToken}`
611
+ },
612
+ body: JSON.stringify({
613
+ jsonrpc: "2.0",
614
+ id: this.generateId(),
615
+ method,
616
+ params
617
+ })
618
+ });
619
+ const data = await response.json();
620
+ if (data.error) {
621
+ throw new Error(data.error.message || "MCP request failed");
622
+ }
623
+ return data.result;
624
+ }
625
+ disconnect() {
626
+ if (this.ws) {
627
+ this.ws.close();
628
+ this.ws = null;
629
+ }
630
+ if (this.eventSource) {
631
+ this.eventSource.close();
632
+ this.eventSource = null;
633
+ }
634
+ if (this.refreshTimer) {
635
+ clearTimeout(this.refreshTimer);
636
+ this.refreshTimer = null;
637
+ }
638
+ this.accessToken = null;
639
+ }
640
+ async logout() {
641
+ this.disconnect();
642
+ await this.tokenStorage.clear();
643
+ }
644
+ generateId() {
645
+ return Math.random().toString(36).substring(2, 15);
646
+ }
647
+ // Convenience methods for memory operations
648
+ async createMemory(title, content, options) {
649
+ return this.request("memories.create", { title, content, ...options || {} });
650
+ }
651
+ async searchMemories(query, options) {
652
+ return this.request("memories.search", { query, ...options || {} });
653
+ }
654
+ async getMemory(id) {
655
+ return this.request("memories.get", { id });
656
+ }
657
+ async updateMemory(id, updates) {
658
+ return this.request("memories.update", { id, updates });
659
+ }
660
+ async deleteMemory(id) {
661
+ return this.request("memories.delete", { id });
662
+ }
663
+ };
664
+
665
+ // src/storage/api-key-storage-web.ts
666
+ var ApiKeyStorageWeb = class {
667
+ constructor() {
668
+ this.storageKey = "lanonasis_api_key";
669
+ this.webEncryptionKeyStorage = "lanonasis_web_enc_key";
670
+ }
671
+ async store(data) {
672
+ const payload = JSON.stringify({
673
+ ...data,
674
+ createdAt: data.createdAt || (/* @__PURE__ */ new Date()).toISOString()
675
+ });
676
+ const encrypted = await this.encrypt(payload);
677
+ localStorage.setItem(this.storageKey, encrypted);
678
+ }
679
+ async retrieve() {
680
+ const encrypted = localStorage.getItem(this.storageKey);
681
+ if (!encrypted) return null;
682
+ try {
683
+ const decrypted = await this.decrypt(encrypted);
684
+ return JSON.parse(decrypted);
685
+ } catch {
686
+ return null;
687
+ }
688
+ }
689
+ async clear() {
690
+ localStorage.removeItem(this.storageKey);
691
+ }
692
+ async encrypt(text) {
693
+ if (typeof window === "undefined" || !window.crypto?.subtle) {
694
+ const encoder2 = new TextEncoder();
695
+ return this.base64Encode(encoder2.encode(text));
696
+ }
697
+ const encoder = new TextEncoder();
698
+ const data = encoder.encode(text);
699
+ const passphrase = await this.getWebEncryptionKey();
700
+ const keyMaterial = await window.crypto.subtle.importKey(
701
+ "raw",
702
+ encoder.encode(passphrase),
703
+ "PBKDF2",
704
+ false,
705
+ ["deriveBits", "deriveKey"]
706
+ );
707
+ const key = await window.crypto.subtle.deriveKey(
708
+ {
709
+ name: "PBKDF2",
710
+ salt: encoder.encode("lanonasis-api-key-salt"),
711
+ iterations: 1e5,
712
+ hash: "SHA-256"
713
+ },
714
+ keyMaterial,
715
+ { name: "AES-GCM", length: 256 },
716
+ true,
717
+ ["encrypt", "decrypt"]
718
+ );
719
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
720
+ const encrypted = await window.crypto.subtle.encrypt(
721
+ { name: "AES-GCM", iv },
722
+ key,
723
+ data
724
+ );
725
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
726
+ combined.set(iv, 0);
727
+ combined.set(new Uint8Array(encrypted), iv.length);
728
+ return this.base64Encode(combined);
729
+ }
730
+ async decrypt(encrypted) {
731
+ if (typeof window === "undefined" || !window.crypto?.subtle) {
732
+ const decoder2 = new TextDecoder();
733
+ return decoder2.decode(this.base64Decode(encrypted));
734
+ }
735
+ const bytes = this.base64Decode(encrypted);
736
+ const iv = bytes.slice(0, 12);
737
+ const data = bytes.slice(12);
738
+ const encoder = new TextEncoder();
739
+ const decoder = new TextDecoder();
740
+ const passphrase = await this.getWebEncryptionKey();
741
+ const keyMaterial = await window.crypto.subtle.importKey(
742
+ "raw",
743
+ encoder.encode(passphrase),
744
+ "PBKDF2",
745
+ false,
746
+ ["deriveBits", "deriveKey"]
747
+ );
748
+ const key = await window.crypto.subtle.deriveKey(
749
+ {
750
+ name: "PBKDF2",
751
+ salt: encoder.encode("lanonasis-api-key-salt"),
752
+ iterations: 1e5,
753
+ hash: "SHA-256"
754
+ },
755
+ keyMaterial,
756
+ { name: "AES-GCM", length: 256 },
757
+ true,
758
+ ["encrypt", "decrypt"]
759
+ );
760
+ const decrypted = await window.crypto.subtle.decrypt(
761
+ { name: "AES-GCM", iv },
762
+ key,
763
+ data
764
+ );
765
+ return decoder.decode(decrypted);
766
+ }
767
+ async getWebEncryptionKey() {
768
+ const existing = localStorage.getItem(this.webEncryptionKeyStorage);
769
+ if (existing) return existing;
770
+ const buf = new Uint8Array(32);
771
+ if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
772
+ window.crypto.getRandomValues(buf);
773
+ }
774
+ const raw = Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
775
+ localStorage.setItem(this.webEncryptionKeyStorage, raw);
776
+ return raw;
777
+ }
778
+ base64Encode(bytes) {
779
+ let binary = "";
780
+ bytes.forEach((b) => {
781
+ binary += String.fromCharCode(b);
782
+ });
783
+ return btoa(binary);
784
+ }
785
+ base64Decode(value) {
786
+ const binary = atob(value);
787
+ const bytes = new Uint8Array(binary.length);
788
+ for (let i = 0; i < binary.length; i++) {
789
+ bytes[i] = binary.charCodeAt(i);
790
+ }
791
+ return bytes;
792
+ }
793
+ };