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