@lanonasis/oauth-client 1.2.0 → 1.2.2
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 +11 -5
- package/dist/browser-CUJNgghM.d.cts +246 -0
- package/dist/browser-CUJNgghM.d.ts +246 -0
- package/dist/browser.cjs +1312 -0
- package/dist/browser.d.cts +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.mjs +1286 -0
- package/dist/index.cjs +386 -79
- package/dist/index.d.cts +3 -196
- package/dist/index.d.ts +3 -196
- package/dist/{index.js → index.mjs} +383 -67
- package/package.json +15 -8
package/dist/browser.mjs
ADDED
|
@@ -0,0 +1,1286 @@
|
|
|
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.ts
|
|
218
|
+
import fetch4 from "cross-fetch";
|
|
219
|
+
|
|
220
|
+
// src/storage/token-storage.ts
|
|
221
|
+
var TokenStorage = class {
|
|
222
|
+
constructor() {
|
|
223
|
+
this.storageKey = "lanonasis_mcp_tokens";
|
|
224
|
+
this.webEncryptionKeyStorage = "lanonasis_web_token_enc_key";
|
|
225
|
+
if (this.isNode()) {
|
|
226
|
+
try {
|
|
227
|
+
this.keytar = __require("keytar");
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.warn("Keytar not available - falling back to file storage");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async store(tokens) {
|
|
234
|
+
const tokensWithTimestamp = {
|
|
235
|
+
...tokens,
|
|
236
|
+
issued_at: Date.now()
|
|
237
|
+
};
|
|
238
|
+
const tokenString = JSON.stringify(tokensWithTimestamp);
|
|
239
|
+
if (this.isNode()) {
|
|
240
|
+
if (this.keytar) {
|
|
241
|
+
await this.keytar.setPassword("lanonasis-mcp", "tokens", tokenString);
|
|
242
|
+
} else {
|
|
243
|
+
await this.storeToFile(tokenString);
|
|
244
|
+
}
|
|
245
|
+
} else if (this.isElectron()) {
|
|
246
|
+
await window.electronAPI.secureStore.set(this.storageKey, tokensWithTimestamp);
|
|
247
|
+
} else if (this.isMobile()) {
|
|
248
|
+
await window.SecureStorage.set(this.storageKey, tokenString);
|
|
249
|
+
} else {
|
|
250
|
+
const encrypted = await this.encrypt(tokenString);
|
|
251
|
+
localStorage.setItem(this.storageKey, encrypted);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async retrieve() {
|
|
255
|
+
let tokenString = null;
|
|
256
|
+
try {
|
|
257
|
+
if (this.isNode()) {
|
|
258
|
+
if (this.keytar) {
|
|
259
|
+
tokenString = await this.keytar.getPassword("lanonasis-mcp", "tokens");
|
|
260
|
+
}
|
|
261
|
+
if (!tokenString) {
|
|
262
|
+
tokenString = await this.retrieveFromFile();
|
|
263
|
+
}
|
|
264
|
+
} else if (this.isElectron()) {
|
|
265
|
+
const tokens = await window.electronAPI.secureStore.get(this.storageKey);
|
|
266
|
+
return tokens || null;
|
|
267
|
+
} else if (this.isMobile()) {
|
|
268
|
+
tokenString = await window.SecureStorage.get(this.storageKey);
|
|
269
|
+
} else {
|
|
270
|
+
const encrypted = localStorage.getItem(this.storageKey);
|
|
271
|
+
if (encrypted) {
|
|
272
|
+
tokenString = await this.decrypt(encrypted);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return tokenString ? JSON.parse(tokenString) : null;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error("Error retrieving tokens:", error);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async clear() {
|
|
282
|
+
if (this.isNode()) {
|
|
283
|
+
if (this.keytar) {
|
|
284
|
+
await this.keytar.deletePassword("lanonasis-mcp", "tokens");
|
|
285
|
+
}
|
|
286
|
+
await this.deleteFile();
|
|
287
|
+
} else if (this.isElectron()) {
|
|
288
|
+
await window.electronAPI.secureStore.delete(this.storageKey);
|
|
289
|
+
} else if (this.isMobile()) {
|
|
290
|
+
await window.SecureStorage.remove(this.storageKey);
|
|
291
|
+
} else {
|
|
292
|
+
localStorage.removeItem(this.storageKey);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
isTokenExpired(tokens) {
|
|
296
|
+
if (tokens.token_type === "api-key" || tokens.expires_in === 0) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (!tokens.expires_in) return false;
|
|
300
|
+
if (!tokens.issued_at) {
|
|
301
|
+
console.warn("Token missing issued_at timestamp, treating as expired");
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
const expiresAt = tokens.issued_at + tokens.expires_in * 1e3;
|
|
305
|
+
const now = Date.now();
|
|
306
|
+
return expiresAt - now < 3e5;
|
|
307
|
+
}
|
|
308
|
+
async storeToFile(tokenString) {
|
|
309
|
+
if (!this.isNode()) return;
|
|
310
|
+
const fs = __require("fs").promises;
|
|
311
|
+
const path = __require("path");
|
|
312
|
+
const os = __require("os");
|
|
313
|
+
const crypto2 = __require("crypto");
|
|
314
|
+
const configDir = path.join(os.homedir(), ".lanonasis");
|
|
315
|
+
const tokenFile = path.join(configDir, "mcp-tokens.enc");
|
|
316
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
317
|
+
const key = this.getFileEncryptionKey();
|
|
318
|
+
const iv = crypto2.randomBytes(16);
|
|
319
|
+
const cipher = crypto2.createCipheriv("aes-256-gcm", key, iv);
|
|
320
|
+
let encrypted = cipher.update(tokenString, "utf8", "hex");
|
|
321
|
+
encrypted += cipher.final("hex");
|
|
322
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
323
|
+
const data = [iv.toString("hex"), authTag, encrypted].join(":");
|
|
324
|
+
await fs.writeFile(tokenFile, data, { mode: 384 });
|
|
325
|
+
}
|
|
326
|
+
async retrieveFromFile() {
|
|
327
|
+
if (!this.isNode()) return null;
|
|
328
|
+
const fs = __require("fs").promises;
|
|
329
|
+
const path = __require("path");
|
|
330
|
+
const os = __require("os");
|
|
331
|
+
const crypto2 = __require("crypto");
|
|
332
|
+
const tokenFile = path.join(os.homedir(), ".lanonasis", "mcp-tokens.enc");
|
|
333
|
+
try {
|
|
334
|
+
const data = await fs.readFile(tokenFile, "utf8");
|
|
335
|
+
const parts = data.split(":");
|
|
336
|
+
const key = this.getFileEncryptionKey();
|
|
337
|
+
if (parts.length === 3) {
|
|
338
|
+
const [ivHex, authTagHex, encrypted] = parts;
|
|
339
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
340
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
341
|
+
const decipher = crypto2.createDecipheriv("aes-256-gcm", key, iv);
|
|
342
|
+
decipher.setAuthTag(authTag);
|
|
343
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
344
|
+
decrypted += decipher.final("utf8");
|
|
345
|
+
return decrypted;
|
|
346
|
+
}
|
|
347
|
+
if (parts.length === 2) {
|
|
348
|
+
const [ivHex, encrypted] = parts;
|
|
349
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
350
|
+
const decipher = crypto2.createDecipheriv("aes-256-cbc", key, iv);
|
|
351
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
352
|
+
decrypted += decipher.final("utf8");
|
|
353
|
+
return decrypted;
|
|
354
|
+
}
|
|
355
|
+
throw new Error("Invalid encrypted token format");
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async deleteFile() {
|
|
361
|
+
if (!this.isNode()) return;
|
|
362
|
+
const fs = __require("fs").promises;
|
|
363
|
+
const path = __require("path");
|
|
364
|
+
const os = __require("os");
|
|
365
|
+
const tokenFile = path.join(os.homedir(), ".lanonasis", "mcp-tokens.enc");
|
|
366
|
+
try {
|
|
367
|
+
await fs.unlink(tokenFile);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
getFileEncryptionKey() {
|
|
372
|
+
const crypto2 = __require("crypto");
|
|
373
|
+
const os = __require("os");
|
|
374
|
+
const machineId = os.hostname() + os.userInfo().username;
|
|
375
|
+
const salt = "lanonasis-mcp-oauth-2024";
|
|
376
|
+
return crypto2.pbkdf2Sync(machineId, salt, 1e5, 32, "sha256");
|
|
377
|
+
}
|
|
378
|
+
async encrypt(text) {
|
|
379
|
+
if (typeof window === "undefined" || !window.crypto?.subtle) {
|
|
380
|
+
const encoder2 = new TextEncoder();
|
|
381
|
+
const data2 = encoder2.encode(text);
|
|
382
|
+
return this.base64Encode(data2);
|
|
383
|
+
}
|
|
384
|
+
const encoder = new TextEncoder();
|
|
385
|
+
const data = encoder.encode(text);
|
|
386
|
+
const passphrase = await this.getWebEncryptionKey();
|
|
387
|
+
const keyMaterial = await window.crypto.subtle.importKey(
|
|
388
|
+
"raw",
|
|
389
|
+
encoder.encode(passphrase),
|
|
390
|
+
"PBKDF2",
|
|
391
|
+
false,
|
|
392
|
+
["deriveBits", "deriveKey"]
|
|
393
|
+
);
|
|
394
|
+
const key = await window.crypto.subtle.deriveKey(
|
|
395
|
+
{
|
|
396
|
+
name: "PBKDF2",
|
|
397
|
+
salt: encoder.encode("lanonasis-token-salt"),
|
|
398
|
+
iterations: 1e5,
|
|
399
|
+
hash: "SHA-256"
|
|
400
|
+
},
|
|
401
|
+
keyMaterial,
|
|
402
|
+
{ name: "AES-GCM", length: 256 },
|
|
403
|
+
true,
|
|
404
|
+
["encrypt", "decrypt"]
|
|
405
|
+
);
|
|
406
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
407
|
+
const encrypted = await window.crypto.subtle.encrypt(
|
|
408
|
+
{ name: "AES-GCM", iv },
|
|
409
|
+
key,
|
|
410
|
+
data
|
|
411
|
+
);
|
|
412
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
413
|
+
combined.set(iv, 0);
|
|
414
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
415
|
+
return this.base64Encode(combined);
|
|
416
|
+
}
|
|
417
|
+
async decrypt(encrypted) {
|
|
418
|
+
if (typeof window === "undefined" || !window.crypto?.subtle) {
|
|
419
|
+
const bytes2 = this.base64Decode(encrypted);
|
|
420
|
+
const decoder2 = new TextDecoder();
|
|
421
|
+
return decoder2.decode(bytes2);
|
|
422
|
+
}
|
|
423
|
+
const bytes = this.base64Decode(encrypted);
|
|
424
|
+
const iv = bytes.slice(0, 12);
|
|
425
|
+
const data = bytes.slice(12);
|
|
426
|
+
const encoder = new TextEncoder();
|
|
427
|
+
const decoder = new TextDecoder();
|
|
428
|
+
const passphrase = await this.getWebEncryptionKey();
|
|
429
|
+
const keyMaterial = await window.crypto.subtle.importKey(
|
|
430
|
+
"raw",
|
|
431
|
+
encoder.encode(passphrase),
|
|
432
|
+
"PBKDF2",
|
|
433
|
+
false,
|
|
434
|
+
["deriveBits", "deriveKey"]
|
|
435
|
+
);
|
|
436
|
+
const key = await window.crypto.subtle.deriveKey(
|
|
437
|
+
{
|
|
438
|
+
name: "PBKDF2",
|
|
439
|
+
salt: encoder.encode("lanonasis-token-salt"),
|
|
440
|
+
iterations: 1e5,
|
|
441
|
+
hash: "SHA-256"
|
|
442
|
+
},
|
|
443
|
+
keyMaterial,
|
|
444
|
+
{ name: "AES-GCM", length: 256 },
|
|
445
|
+
true,
|
|
446
|
+
["encrypt", "decrypt"]
|
|
447
|
+
);
|
|
448
|
+
const decrypted = await window.crypto.subtle.decrypt(
|
|
449
|
+
{ name: "AES-GCM", iv },
|
|
450
|
+
key,
|
|
451
|
+
data
|
|
452
|
+
);
|
|
453
|
+
return decoder.decode(decrypted);
|
|
454
|
+
}
|
|
455
|
+
isNode() {
|
|
456
|
+
return !!(typeof process !== "undefined" && process.versions && process.versions.node && !this.isElectron());
|
|
457
|
+
}
|
|
458
|
+
isElectron() {
|
|
459
|
+
return typeof window !== "undefined" && window.electronAPI !== void 0;
|
|
460
|
+
}
|
|
461
|
+
isMobile() {
|
|
462
|
+
return typeof window !== "undefined" && window.SecureStorage !== void 0;
|
|
463
|
+
}
|
|
464
|
+
base64Encode(bytes) {
|
|
465
|
+
if (typeof btoa !== "undefined") {
|
|
466
|
+
let binary = "";
|
|
467
|
+
bytes.forEach((b) => {
|
|
468
|
+
binary += String.fromCharCode(b);
|
|
469
|
+
});
|
|
470
|
+
return btoa(binary);
|
|
471
|
+
}
|
|
472
|
+
if (typeof Buffer !== "undefined") {
|
|
473
|
+
return Buffer.from(bytes).toString("base64");
|
|
474
|
+
}
|
|
475
|
+
throw new Error("No base64 encoder available");
|
|
476
|
+
}
|
|
477
|
+
base64Decode(value) {
|
|
478
|
+
if (typeof atob !== "undefined") {
|
|
479
|
+
const binary = atob(value);
|
|
480
|
+
const bytes = new Uint8Array(binary.length);
|
|
481
|
+
for (let i = 0; i < binary.length; i++) {
|
|
482
|
+
bytes[i] = binary.charCodeAt(i);
|
|
483
|
+
}
|
|
484
|
+
return bytes;
|
|
485
|
+
}
|
|
486
|
+
if (typeof Buffer !== "undefined") {
|
|
487
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
488
|
+
}
|
|
489
|
+
throw new Error("No base64 decoder available");
|
|
490
|
+
}
|
|
491
|
+
async getWebEncryptionKey() {
|
|
492
|
+
const existing = typeof localStorage !== "undefined" ? localStorage.getItem(this.webEncryptionKeyStorage) : null;
|
|
493
|
+
if (existing) {
|
|
494
|
+
return existing;
|
|
495
|
+
}
|
|
496
|
+
let raw = "";
|
|
497
|
+
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
|
498
|
+
const buf = new Uint8Array(32);
|
|
499
|
+
window.crypto.getRandomValues(buf);
|
|
500
|
+
raw = Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
501
|
+
} else {
|
|
502
|
+
const ua = typeof navigator !== "undefined" ? navigator.userAgent : "node";
|
|
503
|
+
raw = `${ua}-${Math.random().toString(36).slice(2)}-${Date.now()}`;
|
|
504
|
+
}
|
|
505
|
+
if (typeof localStorage !== "undefined") {
|
|
506
|
+
localStorage.setItem(this.webEncryptionKeyStorage, raw);
|
|
507
|
+
}
|
|
508
|
+
return raw;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// src/storage/token-storage-web.ts
|
|
513
|
+
var TokenStorageWeb = class {
|
|
514
|
+
constructor() {
|
|
515
|
+
this.storageKey = "lanonasis_mcp_tokens";
|
|
516
|
+
this.webEncryptionKeyStorage = "lanonasis_web_token_enc_key";
|
|
517
|
+
}
|
|
518
|
+
async store(tokens) {
|
|
519
|
+
const tokensWithTimestamp = {
|
|
520
|
+
...tokens,
|
|
521
|
+
issued_at: Date.now()
|
|
522
|
+
};
|
|
523
|
+
const tokenString = JSON.stringify(tokensWithTimestamp);
|
|
524
|
+
const encrypted = await this.encrypt(tokenString);
|
|
525
|
+
localStorage.setItem(this.storageKey, encrypted);
|
|
526
|
+
}
|
|
527
|
+
async retrieve() {
|
|
528
|
+
const encrypted = localStorage.getItem(this.storageKey);
|
|
529
|
+
if (!encrypted) return null;
|
|
530
|
+
try {
|
|
531
|
+
const tokenString = await this.decrypt(encrypted);
|
|
532
|
+
return JSON.parse(tokenString);
|
|
533
|
+
} catch {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async clear() {
|
|
538
|
+
localStorage.removeItem(this.storageKey);
|
|
539
|
+
}
|
|
540
|
+
isTokenExpired(tokens) {
|
|
541
|
+
if (tokens.token_type === "api-key" || tokens.expires_in === 0) return false;
|
|
542
|
+
if (!tokens.expires_in) return false;
|
|
543
|
+
if (!tokens.issued_at) return true;
|
|
544
|
+
const expiresAt = tokens.issued_at + tokens.expires_in * 1e3;
|
|
545
|
+
return expiresAt - Date.now() < 3e5;
|
|
546
|
+
}
|
|
547
|
+
async encrypt(text) {
|
|
548
|
+
if (typeof window === "undefined" || !window.crypto?.subtle) {
|
|
549
|
+
const encoder2 = new TextEncoder();
|
|
550
|
+
return this.base64Encode(encoder2.encode(text));
|
|
551
|
+
}
|
|
552
|
+
const encoder = new TextEncoder();
|
|
553
|
+
const data = encoder.encode(text);
|
|
554
|
+
const passphrase = await this.getWebEncryptionKey();
|
|
555
|
+
const keyMaterial = await window.crypto.subtle.importKey(
|
|
556
|
+
"raw",
|
|
557
|
+
encoder.encode(passphrase),
|
|
558
|
+
"PBKDF2",
|
|
559
|
+
false,
|
|
560
|
+
["deriveBits", "deriveKey"]
|
|
561
|
+
);
|
|
562
|
+
const key = await window.crypto.subtle.deriveKey(
|
|
563
|
+
{
|
|
564
|
+
name: "PBKDF2",
|
|
565
|
+
salt: encoder.encode("lanonasis-token-salt"),
|
|
566
|
+
iterations: 1e5,
|
|
567
|
+
hash: "SHA-256"
|
|
568
|
+
},
|
|
569
|
+
keyMaterial,
|
|
570
|
+
{ name: "AES-GCM", length: 256 },
|
|
571
|
+
true,
|
|
572
|
+
["encrypt", "decrypt"]
|
|
573
|
+
);
|
|
574
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
575
|
+
const encrypted = await window.crypto.subtle.encrypt(
|
|
576
|
+
{ name: "AES-GCM", iv },
|
|
577
|
+
key,
|
|
578
|
+
data
|
|
579
|
+
);
|
|
580
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
581
|
+
combined.set(iv, 0);
|
|
582
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
583
|
+
return this.base64Encode(combined);
|
|
584
|
+
}
|
|
585
|
+
async decrypt(encrypted) {
|
|
586
|
+
if (typeof window === "undefined" || !window.crypto?.subtle) {
|
|
587
|
+
const decoder2 = new TextDecoder();
|
|
588
|
+
return decoder2.decode(this.base64Decode(encrypted));
|
|
589
|
+
}
|
|
590
|
+
const bytes = this.base64Decode(encrypted);
|
|
591
|
+
const iv = bytes.slice(0, 12);
|
|
592
|
+
const data = bytes.slice(12);
|
|
593
|
+
const encoder = new TextEncoder();
|
|
594
|
+
const decoder = new TextDecoder();
|
|
595
|
+
const passphrase = await this.getWebEncryptionKey();
|
|
596
|
+
const keyMaterial = await window.crypto.subtle.importKey(
|
|
597
|
+
"raw",
|
|
598
|
+
encoder.encode(passphrase),
|
|
599
|
+
"PBKDF2",
|
|
600
|
+
false,
|
|
601
|
+
["deriveBits", "deriveKey"]
|
|
602
|
+
);
|
|
603
|
+
const key = await window.crypto.subtle.deriveKey(
|
|
604
|
+
{
|
|
605
|
+
name: "PBKDF2",
|
|
606
|
+
salt: encoder.encode("lanonasis-token-salt"),
|
|
607
|
+
iterations: 1e5,
|
|
608
|
+
hash: "SHA-256"
|
|
609
|
+
},
|
|
610
|
+
keyMaterial,
|
|
611
|
+
{ name: "AES-GCM", length: 256 },
|
|
612
|
+
true,
|
|
613
|
+
["encrypt", "decrypt"]
|
|
614
|
+
);
|
|
615
|
+
const decrypted = await window.crypto.subtle.decrypt(
|
|
616
|
+
{ name: "AES-GCM", iv },
|
|
617
|
+
key,
|
|
618
|
+
data
|
|
619
|
+
);
|
|
620
|
+
return decoder.decode(decrypted);
|
|
621
|
+
}
|
|
622
|
+
async getWebEncryptionKey() {
|
|
623
|
+
const existing = localStorage.getItem(this.webEncryptionKeyStorage);
|
|
624
|
+
if (existing) return existing;
|
|
625
|
+
const buf = new Uint8Array(32);
|
|
626
|
+
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
|
627
|
+
window.crypto.getRandomValues(buf);
|
|
628
|
+
}
|
|
629
|
+
const raw = Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
630
|
+
localStorage.setItem(this.webEncryptionKeyStorage, raw);
|
|
631
|
+
return raw;
|
|
632
|
+
}
|
|
633
|
+
base64Encode(bytes) {
|
|
634
|
+
let binary = "";
|
|
635
|
+
bytes.forEach((b) => {
|
|
636
|
+
binary += String.fromCharCode(b);
|
|
637
|
+
});
|
|
638
|
+
return btoa(binary);
|
|
639
|
+
}
|
|
640
|
+
base64Decode(value) {
|
|
641
|
+
const binary = atob(value);
|
|
642
|
+
const bytes = new Uint8Array(binary.length);
|
|
643
|
+
for (let i = 0; i < binary.length; i++) {
|
|
644
|
+
bytes[i] = binary.charCodeAt(i);
|
|
645
|
+
}
|
|
646
|
+
return bytes;
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// src/flows/terminal-flow.ts
|
|
651
|
+
import fetch2 from "cross-fetch";
|
|
652
|
+
var TerminalOAuthFlow = class extends BaseOAuthFlow {
|
|
653
|
+
constructor(config) {
|
|
654
|
+
super({
|
|
655
|
+
...config,
|
|
656
|
+
clientId: config.clientId || "lanonasis-mcp-cli"
|
|
657
|
+
});
|
|
658
|
+
this.pollInterval = 5;
|
|
659
|
+
}
|
|
660
|
+
async authenticate() {
|
|
661
|
+
try {
|
|
662
|
+
const deviceResponse = await this.requestDeviceCode();
|
|
663
|
+
this.displayInstructions(deviceResponse);
|
|
664
|
+
if (deviceResponse.verification_uri_complete) {
|
|
665
|
+
await this.openBrowser(deviceResponse.verification_uri_complete);
|
|
666
|
+
}
|
|
667
|
+
return await this.pollForToken(deviceResponse);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
console.error("Authentication failed:", error);
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
async requestDeviceCode() {
|
|
674
|
+
const response = await fetch2(`${this.authBaseUrl}/oauth/device`, {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: { "Content-Type": "application/json" },
|
|
677
|
+
body: JSON.stringify({
|
|
678
|
+
client_id: this.clientId,
|
|
679
|
+
scope: this.scope
|
|
680
|
+
})
|
|
681
|
+
});
|
|
682
|
+
if (!response.ok) {
|
|
683
|
+
const error = await response.json();
|
|
684
|
+
throw new Error(error.error_description || "Failed to request device code");
|
|
685
|
+
}
|
|
686
|
+
const data = await response.json();
|
|
687
|
+
this.pollInterval = data.interval || 5;
|
|
688
|
+
return data;
|
|
689
|
+
}
|
|
690
|
+
displayInstructions(response) {
|
|
691
|
+
console.log("\n\u{1F510} Lan Onasis Authentication Required\n");
|
|
692
|
+
console.log(`Please visit: ${response.verification_uri}`);
|
|
693
|
+
console.log(`Enter code: ${response.user_code}
|
|
694
|
+
`);
|
|
695
|
+
console.log("Or press Enter to open your browser automatically...");
|
|
696
|
+
}
|
|
697
|
+
async openBrowser(url) {
|
|
698
|
+
try {
|
|
699
|
+
const { default: open } = await import("open");
|
|
700
|
+
await Promise.race([
|
|
701
|
+
this.waitForEnter(),
|
|
702
|
+
new Promise((resolve) => setTimeout(resolve, 2e3))
|
|
703
|
+
]);
|
|
704
|
+
console.log("Opening browser...");
|
|
705
|
+
await open(url);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.log("Please open the URL manually in your browser.");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
waitForEnter() {
|
|
711
|
+
return new Promise((resolve) => {
|
|
712
|
+
if (process.stdin.isTTY) {
|
|
713
|
+
process.stdin.setRawMode(true);
|
|
714
|
+
process.stdin.resume();
|
|
715
|
+
process.stdin.once("data", () => {
|
|
716
|
+
process.stdin.setRawMode(false);
|
|
717
|
+
process.stdin.pause();
|
|
718
|
+
resolve();
|
|
719
|
+
});
|
|
720
|
+
} else {
|
|
721
|
+
resolve();
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
async pollForToken(deviceResponse) {
|
|
726
|
+
const startTime = Date.now();
|
|
727
|
+
const expiresAt = startTime + deviceResponse.expires_in * 1e3;
|
|
728
|
+
console.log("Waiting for authorization...");
|
|
729
|
+
while (Date.now() < expiresAt) {
|
|
730
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollInterval * 1e3));
|
|
731
|
+
try {
|
|
732
|
+
const token = await this.checkDeviceCode(deviceResponse.device_code);
|
|
733
|
+
console.log("\u2705 Authorization successful!\n");
|
|
734
|
+
return token;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (error.message === "authorization_pending") {
|
|
737
|
+
process.stdout.write(".");
|
|
738
|
+
} else if (error.message === "slow_down") {
|
|
739
|
+
this.pollInterval += 5;
|
|
740
|
+
} else {
|
|
741
|
+
throw error;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
throw new Error("Authorization timeout - please try again");
|
|
746
|
+
}
|
|
747
|
+
async checkDeviceCode(deviceCode) {
|
|
748
|
+
const response = await fetch2(`${this.authBaseUrl}/oauth/token`, {
|
|
749
|
+
method: "POST",
|
|
750
|
+
headers: { "Content-Type": "application/json" },
|
|
751
|
+
body: JSON.stringify({
|
|
752
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
753
|
+
device_code: deviceCode,
|
|
754
|
+
client_id: this.clientId
|
|
755
|
+
})
|
|
756
|
+
});
|
|
757
|
+
const data = await response.json();
|
|
758
|
+
if (!response.ok) {
|
|
759
|
+
throw new Error(data.error || "Token request failed");
|
|
760
|
+
}
|
|
761
|
+
return data;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// src/flows/apikey-flow.ts
|
|
766
|
+
import fetch3 from "cross-fetch";
|
|
767
|
+
var APIKeyFlow = class extends BaseOAuthFlow {
|
|
768
|
+
constructor(apiKey, authBaseUrl = "https://mcp.lanonasis.com") {
|
|
769
|
+
super({
|
|
770
|
+
clientId: "api-key-client",
|
|
771
|
+
authBaseUrl
|
|
772
|
+
});
|
|
773
|
+
this.apiKey = apiKey;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* "Authenticate" by returning the API key as a virtual token
|
|
777
|
+
* The API key will be used directly in request headers
|
|
778
|
+
*/
|
|
779
|
+
async authenticate() {
|
|
780
|
+
if (!this.apiKey || !this.apiKey.startsWith("lano_") && !this.apiKey.startsWith("vx_")) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
'Invalid API key format. Must start with "lano_" or "vx_". Please regenerate your API key from the dashboard.'
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
if (this.apiKey.startsWith("vx_")) {
|
|
786
|
+
console.warn(
|
|
787
|
+
'\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.'
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
access_token: this.apiKey,
|
|
792
|
+
token_type: "api-key",
|
|
793
|
+
expires_in: 0,
|
|
794
|
+
// API keys don't expire
|
|
795
|
+
issued_at: Date.now()
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* API keys don't need refresh
|
|
800
|
+
*/
|
|
801
|
+
async refreshToken(refreshToken) {
|
|
802
|
+
throw new Error("API keys do not support token refresh");
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Optional: Validate API key by making a test request
|
|
806
|
+
*/
|
|
807
|
+
async validateAPIKey() {
|
|
808
|
+
try {
|
|
809
|
+
const response = await fetch3(`${this.config.authBaseUrl}/api/v1/health`, {
|
|
810
|
+
headers: {
|
|
811
|
+
"x-api-key": this.apiKey
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
return response.ok;
|
|
815
|
+
} catch (error) {
|
|
816
|
+
console.error("API key validation failed:", error);
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
// src/client/mcp-client.ts
|
|
823
|
+
var MCPClient = class {
|
|
824
|
+
constructor(config = {}) {
|
|
825
|
+
// ← NEW: Track auth mode
|
|
826
|
+
this.ws = null;
|
|
827
|
+
this.eventSource = null;
|
|
828
|
+
this.accessToken = null;
|
|
829
|
+
this.refreshTimer = null;
|
|
830
|
+
this.config = {
|
|
831
|
+
mcpEndpoint: "wss://mcp.lanonasis.com",
|
|
832
|
+
autoRefresh: true,
|
|
833
|
+
...config
|
|
834
|
+
};
|
|
835
|
+
const defaultStorage = typeof window !== "undefined" ? new TokenStorageWeb() : new TokenStorage();
|
|
836
|
+
this.tokenStorage = config.tokenStorage || defaultStorage;
|
|
837
|
+
this.authMode = config.apiKey ? "apikey" : "oauth";
|
|
838
|
+
if (this.authMode === "apikey") {
|
|
839
|
+
this.authFlow = new APIKeyFlow(
|
|
840
|
+
config.apiKey,
|
|
841
|
+
config.authBaseUrl || "https://mcp.lanonasis.com"
|
|
842
|
+
);
|
|
843
|
+
} else {
|
|
844
|
+
if (this.isTerminal()) {
|
|
845
|
+
this.authFlow = new TerminalOAuthFlow(config);
|
|
846
|
+
} else {
|
|
847
|
+
this.authFlow = new DesktopOAuthFlow(config);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async connect() {
|
|
852
|
+
try {
|
|
853
|
+
let tokens = await this.tokenStorage.retrieve();
|
|
854
|
+
if (this.authMode === "apikey") {
|
|
855
|
+
if (!tokens) {
|
|
856
|
+
tokens = await this.authenticate();
|
|
857
|
+
}
|
|
858
|
+
this.accessToken = tokens.access_token;
|
|
859
|
+
} else {
|
|
860
|
+
if (!tokens || this.tokenStorage.isTokenExpired(tokens)) {
|
|
861
|
+
if (tokens?.refresh_token) {
|
|
862
|
+
try {
|
|
863
|
+
tokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
|
864
|
+
await this.tokenStorage.store(tokens);
|
|
865
|
+
} catch (error) {
|
|
866
|
+
tokens = await this.authenticate();
|
|
867
|
+
}
|
|
868
|
+
} else {
|
|
869
|
+
tokens = await this.authenticate();
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
this.accessToken = tokens.access_token;
|
|
873
|
+
if (this.config.autoRefresh && tokens.expires_in) {
|
|
874
|
+
this.scheduleTokenRefresh(tokens);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
await this.establishConnection();
|
|
878
|
+
} catch (error) {
|
|
879
|
+
console.error("Failed to connect:", error);
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async authenticate() {
|
|
884
|
+
console.log("Authenticating with Lan Onasis...");
|
|
885
|
+
const tokens = await this.authFlow.authenticate();
|
|
886
|
+
await this.tokenStorage.store(tokens);
|
|
887
|
+
return tokens;
|
|
888
|
+
}
|
|
889
|
+
async ensureAccessToken() {
|
|
890
|
+
if (this.accessToken) return;
|
|
891
|
+
const tokens = await this.tokenStorage.retrieve();
|
|
892
|
+
if (!tokens) {
|
|
893
|
+
throw new Error("Not authenticated");
|
|
894
|
+
}
|
|
895
|
+
if (this.authMode === "apikey") {
|
|
896
|
+
this.accessToken = tokens.access_token;
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (this.tokenStorage.isTokenExpired(tokens)) {
|
|
900
|
+
if (tokens.refresh_token) {
|
|
901
|
+
try {
|
|
902
|
+
const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
|
903
|
+
await this.tokenStorage.store(newTokens);
|
|
904
|
+
this.accessToken = newTokens.access_token;
|
|
905
|
+
return;
|
|
906
|
+
} catch (error) {
|
|
907
|
+
console.error("Token refresh failed:", error);
|
|
908
|
+
throw new Error("Token expired and refresh failed");
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
throw new Error("Token expired and no refresh token available");
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
this.accessToken = tokens.access_token;
|
|
915
|
+
}
|
|
916
|
+
scheduleTokenRefresh(tokens) {
|
|
917
|
+
if (this.refreshTimer) {
|
|
918
|
+
clearTimeout(this.refreshTimer);
|
|
919
|
+
}
|
|
920
|
+
const refreshIn = (tokens.expires_in - 300) * 1e3;
|
|
921
|
+
this.refreshTimer = setTimeout(async () => {
|
|
922
|
+
try {
|
|
923
|
+
if (tokens.refresh_token) {
|
|
924
|
+
const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
|
925
|
+
await this.tokenStorage.store(newTokens);
|
|
926
|
+
this.accessToken = newTokens.access_token;
|
|
927
|
+
this.scheduleTokenRefresh(newTokens);
|
|
928
|
+
await this.reconnect();
|
|
929
|
+
}
|
|
930
|
+
} catch (error) {
|
|
931
|
+
console.error("Token refresh failed:", error);
|
|
932
|
+
}
|
|
933
|
+
}, refreshIn);
|
|
934
|
+
}
|
|
935
|
+
async establishConnection() {
|
|
936
|
+
const endpoint = this.config.mcpEndpoint;
|
|
937
|
+
if (endpoint.startsWith("wss://")) {
|
|
938
|
+
await this.connectWebSocket(endpoint);
|
|
939
|
+
} else if (endpoint.startsWith("https://")) {
|
|
940
|
+
await this.connectSSE(endpoint);
|
|
941
|
+
} else {
|
|
942
|
+
throw new Error("Invalid MCP endpoint - must be wss:// or https://");
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async connectWebSocket(endpoint) {
|
|
946
|
+
const wsUrl = new URL(endpoint);
|
|
947
|
+
wsUrl.pathname = "/ws";
|
|
948
|
+
if (this.accessToken) {
|
|
949
|
+
wsUrl.searchParams.set("access_token", this.accessToken);
|
|
950
|
+
}
|
|
951
|
+
if (typeof WebSocket !== "undefined") {
|
|
952
|
+
this.ws = new WebSocket(wsUrl.toString());
|
|
953
|
+
} else {
|
|
954
|
+
const { default: WS } = await import("ws");
|
|
955
|
+
if (this.authMode === "apikey") {
|
|
956
|
+
this.ws = new WS(wsUrl.toString(), {
|
|
957
|
+
headers: {
|
|
958
|
+
"x-api-key": this.accessToken
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
} else {
|
|
962
|
+
this.ws = new WS(wsUrl.toString(), {
|
|
963
|
+
headers: {
|
|
964
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return new Promise((resolve, reject) => {
|
|
970
|
+
if (!this.ws) {
|
|
971
|
+
reject(new Error("WebSocket not initialized"));
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
this.ws.onopen = () => {
|
|
975
|
+
console.log("MCP WebSocket connected");
|
|
976
|
+
resolve();
|
|
977
|
+
};
|
|
978
|
+
this.ws.onerror = (error) => {
|
|
979
|
+
console.error("WebSocket error:", error);
|
|
980
|
+
reject(error);
|
|
981
|
+
};
|
|
982
|
+
this.ws.onclose = (event) => {
|
|
983
|
+
console.log("WebSocket closed:", event.code, event.reason);
|
|
984
|
+
if (event.code !== 1008 && event.code !== 4001) {
|
|
985
|
+
setTimeout(() => this.reconnect(), 5e3);
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
this.ws.onmessage = (event) => {
|
|
989
|
+
this.handleMessage(event.data);
|
|
990
|
+
};
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
async connectSSE(endpoint) {
|
|
994
|
+
const sseUrl = new URL(endpoint);
|
|
995
|
+
sseUrl.pathname = "/sse";
|
|
996
|
+
if (typeof EventSource !== "undefined") {
|
|
997
|
+
this.eventSource = new EventSource(sseUrl.toString());
|
|
998
|
+
} else {
|
|
999
|
+
const EventSourceModule = await import("eventsource");
|
|
1000
|
+
const ES = EventSourceModule.default || EventSourceModule;
|
|
1001
|
+
if (this.authMode === "apikey") {
|
|
1002
|
+
this.eventSource = new ES(sseUrl.toString(), {
|
|
1003
|
+
headers: {
|
|
1004
|
+
"x-api-key": this.accessToken
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
} else {
|
|
1008
|
+
this.eventSource = new ES(sseUrl.toString(), {
|
|
1009
|
+
headers: {
|
|
1010
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
this.eventSource.onopen = () => {
|
|
1016
|
+
console.log("MCP SSE connected");
|
|
1017
|
+
};
|
|
1018
|
+
this.eventSource.onerror = (error) => {
|
|
1019
|
+
console.error("SSE error:", error);
|
|
1020
|
+
setTimeout(() => this.reconnect(), 5e3);
|
|
1021
|
+
};
|
|
1022
|
+
this.eventSource.onmessage = (event) => {
|
|
1023
|
+
this.handleMessage(event.data);
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
handleMessage(data) {
|
|
1027
|
+
try {
|
|
1028
|
+
const message = JSON.parse(data);
|
|
1029
|
+
console.log("MCP message:", message);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
console.error("Failed to parse message:", error);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
async reconnect() {
|
|
1035
|
+
this.disconnect();
|
|
1036
|
+
await this.establishConnection();
|
|
1037
|
+
}
|
|
1038
|
+
async request(method, params) {
|
|
1039
|
+
await this.ensureAccessToken();
|
|
1040
|
+
if (!this.accessToken) {
|
|
1041
|
+
throw new Error("Not authenticated");
|
|
1042
|
+
}
|
|
1043
|
+
const headers = {
|
|
1044
|
+
"Content-Type": "application/json"
|
|
1045
|
+
};
|
|
1046
|
+
if (this.authMode === "apikey") {
|
|
1047
|
+
headers["x-api-key"] = this.accessToken;
|
|
1048
|
+
} else {
|
|
1049
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
1050
|
+
}
|
|
1051
|
+
const response = await fetch4(`${this.config.mcpEndpoint}/api`, {
|
|
1052
|
+
method: "POST",
|
|
1053
|
+
headers,
|
|
1054
|
+
body: JSON.stringify({
|
|
1055
|
+
jsonrpc: "2.0",
|
|
1056
|
+
id: this.generateId(),
|
|
1057
|
+
method,
|
|
1058
|
+
params
|
|
1059
|
+
})
|
|
1060
|
+
});
|
|
1061
|
+
if (response.status === 401) {
|
|
1062
|
+
if (this.authMode === "apikey") {
|
|
1063
|
+
throw new Error("Invalid API key - please check your credentials");
|
|
1064
|
+
}
|
|
1065
|
+
const tokens = await this.tokenStorage.retrieve();
|
|
1066
|
+
if (tokens?.refresh_token) {
|
|
1067
|
+
const newTokens = await this.authFlow.refreshToken(tokens.refresh_token);
|
|
1068
|
+
await this.tokenStorage.store(newTokens);
|
|
1069
|
+
this.accessToken = newTokens.access_token;
|
|
1070
|
+
return this.request(method, params);
|
|
1071
|
+
} else {
|
|
1072
|
+
await this.connect();
|
|
1073
|
+
return this.request(method, params);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const result = await response.json();
|
|
1077
|
+
if (result.error) {
|
|
1078
|
+
throw new Error(result.error.message || "Request failed");
|
|
1079
|
+
}
|
|
1080
|
+
return result.result;
|
|
1081
|
+
}
|
|
1082
|
+
disconnect() {
|
|
1083
|
+
if (this.ws) {
|
|
1084
|
+
this.ws.close();
|
|
1085
|
+
this.ws = null;
|
|
1086
|
+
}
|
|
1087
|
+
if (this.eventSource) {
|
|
1088
|
+
this.eventSource.close();
|
|
1089
|
+
this.eventSource = null;
|
|
1090
|
+
}
|
|
1091
|
+
if (this.refreshTimer) {
|
|
1092
|
+
clearTimeout(this.refreshTimer);
|
|
1093
|
+
this.refreshTimer = null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
async logout() {
|
|
1097
|
+
const tokens = await this.tokenStorage.retrieve();
|
|
1098
|
+
if (tokens) {
|
|
1099
|
+
try {
|
|
1100
|
+
if (tokens.access_token) {
|
|
1101
|
+
await this.authFlow.revokeToken(tokens.access_token, "access_token");
|
|
1102
|
+
}
|
|
1103
|
+
if (tokens.refresh_token) {
|
|
1104
|
+
await this.authFlow.revokeToken(tokens.refresh_token, "refresh_token");
|
|
1105
|
+
}
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
console.error("Failed to revoke tokens:", error);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
await this.tokenStorage.clear();
|
|
1111
|
+
this.disconnect();
|
|
1112
|
+
this.accessToken = null;
|
|
1113
|
+
}
|
|
1114
|
+
isTerminal() {
|
|
1115
|
+
return !!(typeof process !== "undefined" && process.versions && process.versions.node && !this.isElectron());
|
|
1116
|
+
}
|
|
1117
|
+
isElectron() {
|
|
1118
|
+
return typeof window !== "undefined" && window.electronAPI !== void 0;
|
|
1119
|
+
}
|
|
1120
|
+
generateId() {
|
|
1121
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1122
|
+
}
|
|
1123
|
+
// MCP-specific methods
|
|
1124
|
+
async createMemory(title, content, options) {
|
|
1125
|
+
return this.request("memory/create", {
|
|
1126
|
+
title,
|
|
1127
|
+
content,
|
|
1128
|
+
...options
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
async searchMemories(query, options) {
|
|
1132
|
+
return this.request("memory/search", {
|
|
1133
|
+
query,
|
|
1134
|
+
...options
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
async getMemory(id) {
|
|
1138
|
+
return this.request("memory/get", { id });
|
|
1139
|
+
}
|
|
1140
|
+
async updateMemory(id, updates) {
|
|
1141
|
+
return this.request("memory/update", {
|
|
1142
|
+
id,
|
|
1143
|
+
...updates
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
async deleteMemory(id) {
|
|
1147
|
+
return this.request("memory/delete", { id });
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
// src/storage/api-key-storage-web.ts
|
|
1152
|
+
var ApiKeyStorageWeb = class {
|
|
1153
|
+
constructor() {
|
|
1154
|
+
this.storageKey = "lanonasis_api_key";
|
|
1155
|
+
this.webEncryptionKeyStorage = "lanonasis_web_enc_key";
|
|
1156
|
+
}
|
|
1157
|
+
async store(data) {
|
|
1158
|
+
const payload = JSON.stringify({
|
|
1159
|
+
...data,
|
|
1160
|
+
createdAt: data.createdAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
1161
|
+
});
|
|
1162
|
+
const encrypted = await this.encrypt(payload);
|
|
1163
|
+
localStorage.setItem(this.storageKey, encrypted);
|
|
1164
|
+
}
|
|
1165
|
+
async retrieve() {
|
|
1166
|
+
const encrypted = localStorage.getItem(this.storageKey);
|
|
1167
|
+
if (!encrypted) return null;
|
|
1168
|
+
try {
|
|
1169
|
+
const decrypted = await this.decrypt(encrypted);
|
|
1170
|
+
return JSON.parse(decrypted);
|
|
1171
|
+
} catch {
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
async clear() {
|
|
1176
|
+
localStorage.removeItem(this.storageKey);
|
|
1177
|
+
}
|
|
1178
|
+
async encrypt(text) {
|
|
1179
|
+
if (typeof window === "undefined" || !window.crypto?.subtle) {
|
|
1180
|
+
const encoder2 = new TextEncoder();
|
|
1181
|
+
return this.base64Encode(encoder2.encode(text));
|
|
1182
|
+
}
|
|
1183
|
+
const encoder = new TextEncoder();
|
|
1184
|
+
const data = encoder.encode(text);
|
|
1185
|
+
const passphrase = await this.getWebEncryptionKey();
|
|
1186
|
+
const keyMaterial = await window.crypto.subtle.importKey(
|
|
1187
|
+
"raw",
|
|
1188
|
+
encoder.encode(passphrase),
|
|
1189
|
+
"PBKDF2",
|
|
1190
|
+
false,
|
|
1191
|
+
["deriveBits", "deriveKey"]
|
|
1192
|
+
);
|
|
1193
|
+
const key = await window.crypto.subtle.deriveKey(
|
|
1194
|
+
{
|
|
1195
|
+
name: "PBKDF2",
|
|
1196
|
+
salt: encoder.encode("lanonasis-api-key-salt"),
|
|
1197
|
+
iterations: 1e5,
|
|
1198
|
+
hash: "SHA-256"
|
|
1199
|
+
},
|
|
1200
|
+
keyMaterial,
|
|
1201
|
+
{ name: "AES-GCM", length: 256 },
|
|
1202
|
+
true,
|
|
1203
|
+
["encrypt", "decrypt"]
|
|
1204
|
+
);
|
|
1205
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
1206
|
+
const encrypted = await window.crypto.subtle.encrypt(
|
|
1207
|
+
{ name: "AES-GCM", iv },
|
|
1208
|
+
key,
|
|
1209
|
+
data
|
|
1210
|
+
);
|
|
1211
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
1212
|
+
combined.set(iv, 0);
|
|
1213
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
1214
|
+
return this.base64Encode(combined);
|
|
1215
|
+
}
|
|
1216
|
+
async decrypt(encrypted) {
|
|
1217
|
+
if (typeof window === "undefined" || !window.crypto?.subtle) {
|
|
1218
|
+
const decoder2 = new TextDecoder();
|
|
1219
|
+
return decoder2.decode(this.base64Decode(encrypted));
|
|
1220
|
+
}
|
|
1221
|
+
const bytes = this.base64Decode(encrypted);
|
|
1222
|
+
const iv = bytes.slice(0, 12);
|
|
1223
|
+
const data = bytes.slice(12);
|
|
1224
|
+
const encoder = new TextEncoder();
|
|
1225
|
+
const decoder = new TextDecoder();
|
|
1226
|
+
const passphrase = await this.getWebEncryptionKey();
|
|
1227
|
+
const keyMaterial = await window.crypto.subtle.importKey(
|
|
1228
|
+
"raw",
|
|
1229
|
+
encoder.encode(passphrase),
|
|
1230
|
+
"PBKDF2",
|
|
1231
|
+
false,
|
|
1232
|
+
["deriveBits", "deriveKey"]
|
|
1233
|
+
);
|
|
1234
|
+
const key = await window.crypto.subtle.deriveKey(
|
|
1235
|
+
{
|
|
1236
|
+
name: "PBKDF2",
|
|
1237
|
+
salt: encoder.encode("lanonasis-api-key-salt"),
|
|
1238
|
+
iterations: 1e5,
|
|
1239
|
+
hash: "SHA-256"
|
|
1240
|
+
},
|
|
1241
|
+
keyMaterial,
|
|
1242
|
+
{ name: "AES-GCM", length: 256 },
|
|
1243
|
+
true,
|
|
1244
|
+
["encrypt", "decrypt"]
|
|
1245
|
+
);
|
|
1246
|
+
const decrypted = await window.crypto.subtle.decrypt(
|
|
1247
|
+
{ name: "AES-GCM", iv },
|
|
1248
|
+
key,
|
|
1249
|
+
data
|
|
1250
|
+
);
|
|
1251
|
+
return decoder.decode(decrypted);
|
|
1252
|
+
}
|
|
1253
|
+
async getWebEncryptionKey() {
|
|
1254
|
+
const existing = localStorage.getItem(this.webEncryptionKeyStorage);
|
|
1255
|
+
if (existing) return existing;
|
|
1256
|
+
const buf = new Uint8Array(32);
|
|
1257
|
+
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
|
1258
|
+
window.crypto.getRandomValues(buf);
|
|
1259
|
+
}
|
|
1260
|
+
const raw = Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1261
|
+
localStorage.setItem(this.webEncryptionKeyStorage, raw);
|
|
1262
|
+
return raw;
|
|
1263
|
+
}
|
|
1264
|
+
base64Encode(bytes) {
|
|
1265
|
+
let binary = "";
|
|
1266
|
+
bytes.forEach((b) => {
|
|
1267
|
+
binary += String.fromCharCode(b);
|
|
1268
|
+
});
|
|
1269
|
+
return btoa(binary);
|
|
1270
|
+
}
|
|
1271
|
+
base64Decode(value) {
|
|
1272
|
+
const binary = atob(value);
|
|
1273
|
+
const bytes = new Uint8Array(binary.length);
|
|
1274
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1275
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1276
|
+
}
|
|
1277
|
+
return bytes;
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
export {
|
|
1281
|
+
ApiKeyStorageWeb,
|
|
1282
|
+
BaseOAuthFlow,
|
|
1283
|
+
DesktopOAuthFlow,
|
|
1284
|
+
MCPClient,
|
|
1285
|
+
TokenStorageWeb
|
|
1286
|
+
};
|