@leanmcp/auth 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,499 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/storage/index.ts
22
+ var storage_exports = {};
23
+ __export(storage_exports, {
24
+ FileStorage: () => FileStorage,
25
+ KeychainStorage: () => KeychainStorage,
26
+ MemoryStorage: () => MemoryStorage,
27
+ isKeychainAvailable: () => isKeychainAvailable,
28
+ isTokenExpired: () => isTokenExpired,
29
+ withExpiresAt: () => withExpiresAt
30
+ });
31
+ module.exports = __toCommonJS(storage_exports);
32
+
33
+ // src/storage/types.ts
34
+ function isTokenExpired(tokens, bufferSeconds = 60) {
35
+ if (!tokens.expires_at && !tokens.expires_in) {
36
+ return false;
37
+ }
38
+ const expiresAt = tokens.expires_at ?? Date.now() / 1e3 + (tokens.expires_in ?? 0);
39
+ const now = Date.now() / 1e3;
40
+ return expiresAt <= now + bufferSeconds;
41
+ }
42
+ __name(isTokenExpired, "isTokenExpired");
43
+ function withExpiresAt(tokens) {
44
+ if (tokens.expires_at || !tokens.expires_in) {
45
+ return tokens;
46
+ }
47
+ return {
48
+ ...tokens,
49
+ expires_at: Math.floor(Date.now() / 1e3) + tokens.expires_in
50
+ };
51
+ }
52
+ __name(withExpiresAt, "withExpiresAt");
53
+
54
+ // src/storage/memory.ts
55
+ var MemoryStorage = class {
56
+ static {
57
+ __name(this, "MemoryStorage");
58
+ }
59
+ tokens = /* @__PURE__ */ new Map();
60
+ clients = /* @__PURE__ */ new Map();
61
+ /**
62
+ * Normalize server URL for consistent key lookup
63
+ */
64
+ normalizeUrl(serverUrl) {
65
+ return serverUrl.replace(/\/+$/, "").toLowerCase();
66
+ }
67
+ /**
68
+ * Check if an entry is expired
69
+ */
70
+ isExpired(entry) {
71
+ if (!entry) return true;
72
+ if (!entry.expiresAt) return false;
73
+ return Date.now() / 1e3 >= entry.expiresAt;
74
+ }
75
+ async getTokens(serverUrl) {
76
+ const key = this.normalizeUrl(serverUrl);
77
+ const entry = this.tokens.get(key);
78
+ if (this.isExpired(entry)) {
79
+ this.tokens.delete(key);
80
+ return null;
81
+ }
82
+ return entry?.value ?? null;
83
+ }
84
+ async setTokens(serverUrl, tokens) {
85
+ const key = this.normalizeUrl(serverUrl);
86
+ const enrichedTokens = withExpiresAt(tokens);
87
+ this.tokens.set(key, {
88
+ value: enrichedTokens,
89
+ expiresAt: enrichedTokens.expires_at
90
+ });
91
+ }
92
+ async clearTokens(serverUrl) {
93
+ const key = this.normalizeUrl(serverUrl);
94
+ this.tokens.delete(key);
95
+ }
96
+ async getClientInfo(serverUrl) {
97
+ const key = this.normalizeUrl(serverUrl);
98
+ const entry = this.clients.get(key);
99
+ if (this.isExpired(entry)) {
100
+ this.clients.delete(key);
101
+ return null;
102
+ }
103
+ return entry?.value ?? null;
104
+ }
105
+ async setClientInfo(serverUrl, info) {
106
+ const key = this.normalizeUrl(serverUrl);
107
+ this.clients.set(key, {
108
+ value: info,
109
+ expiresAt: info.client_secret_expires_at
110
+ });
111
+ }
112
+ async clearClientInfo(serverUrl) {
113
+ const key = this.normalizeUrl(serverUrl);
114
+ this.clients.delete(key);
115
+ }
116
+ async clearAll() {
117
+ this.tokens.clear();
118
+ this.clients.clear();
119
+ }
120
+ async getAllSessions() {
121
+ const sessions = [];
122
+ for (const [url, entry] of this.tokens.entries()) {
123
+ if (!this.isExpired(entry)) {
124
+ sessions.push({
125
+ serverUrl: url,
126
+ tokens: entry.value,
127
+ clientInfo: this.clients.get(url)?.value,
128
+ createdAt: Date.now(),
129
+ updatedAt: Date.now()
130
+ });
131
+ }
132
+ }
133
+ return sessions;
134
+ }
135
+ };
136
+
137
+ // src/storage/file.ts
138
+ var import_fs = require("fs");
139
+ var import_path = require("path");
140
+ var import_crypto = require("crypto");
141
+ var CURRENT_VERSION = 1;
142
+ var ALGORITHM = "aes-256-gcm";
143
+ var FileStorage = class {
144
+ static {
145
+ __name(this, "FileStorage");
146
+ }
147
+ filePath;
148
+ encryptionKey;
149
+ prettyPrint;
150
+ cache = null;
151
+ writePromise = null;
152
+ constructor(options) {
153
+ if (typeof options === "string") {
154
+ this.filePath = this.expandPath(options);
155
+ this.prettyPrint = false;
156
+ } else {
157
+ this.filePath = this.expandPath(options.filePath);
158
+ this.prettyPrint = options.prettyPrint ?? false;
159
+ if (options.encryptionKey) {
160
+ this.encryptionKey = (0, import_crypto.scryptSync)(options.encryptionKey, "leanmcp-salt", 32);
161
+ }
162
+ }
163
+ }
164
+ /**
165
+ * Expand ~ to home directory
166
+ */
167
+ expandPath(filePath) {
168
+ if (filePath.startsWith("~")) {
169
+ const home = process.env.HOME || process.env.USERPROFILE || "";
170
+ return filePath.replace("~", home);
171
+ }
172
+ return filePath;
173
+ }
174
+ /**
175
+ * Normalize server URL for consistent key lookup
176
+ */
177
+ normalizeUrl(serverUrl) {
178
+ return serverUrl.replace(/\/+$/, "").toLowerCase();
179
+ }
180
+ /**
181
+ * Encrypt data
182
+ */
183
+ encrypt(data) {
184
+ if (!this.encryptionKey) return data;
185
+ const iv = (0, import_crypto.randomBytes)(16);
186
+ const cipher = (0, import_crypto.createCipheriv)(ALGORITHM, this.encryptionKey, iv);
187
+ let encrypted = cipher.update(data, "utf8", "hex");
188
+ encrypted += cipher.final("hex");
189
+ const authTag = cipher.getAuthTag();
190
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
191
+ }
192
+ /**
193
+ * Decrypt data
194
+ */
195
+ decrypt(data) {
196
+ if (!this.encryptionKey) return data;
197
+ const [ivHex, authTagHex, encrypted] = data.split(":");
198
+ const iv = Buffer.from(ivHex, "hex");
199
+ const authTag = Buffer.from(authTagHex, "hex");
200
+ const decipher = (0, import_crypto.createDecipheriv)(ALGORITHM, this.encryptionKey, iv);
201
+ decipher.setAuthTag(authTag);
202
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
203
+ decrypted += decipher.final("utf8");
204
+ return decrypted;
205
+ }
206
+ /**
207
+ * Read data from file
208
+ */
209
+ async readFile() {
210
+ if (this.cache) return this.cache;
211
+ try {
212
+ const raw = await import_fs.promises.readFile(this.filePath, "utf8");
213
+ const decrypted = this.decrypt(raw);
214
+ this.cache = JSON.parse(decrypted);
215
+ return this.cache;
216
+ } catch (error) {
217
+ if (error.code === "ENOENT") {
218
+ this.cache = {
219
+ version: CURRENT_VERSION,
220
+ sessions: {}
221
+ };
222
+ return this.cache;
223
+ }
224
+ throw error;
225
+ }
226
+ }
227
+ /**
228
+ * Write data to file (coalesced to avoid race conditions)
229
+ */
230
+ async writeFile(data) {
231
+ this.cache = data;
232
+ if (this.writePromise) {
233
+ return this.writePromise;
234
+ }
235
+ this.writePromise = (async () => {
236
+ try {
237
+ await import_fs.promises.mkdir((0, import_path.dirname)(this.filePath), {
238
+ recursive: true
239
+ });
240
+ const json = this.prettyPrint ? JSON.stringify(data, null, 2) : JSON.stringify(data);
241
+ const encrypted = this.encrypt(json);
242
+ await import_fs.promises.writeFile(this.filePath, encrypted, "utf8");
243
+ } finally {
244
+ this.writePromise = null;
245
+ }
246
+ })();
247
+ return this.writePromise;
248
+ }
249
+ async getTokens(serverUrl) {
250
+ const key = this.normalizeUrl(serverUrl);
251
+ const data = await this.readFile();
252
+ const session = data.sessions[key];
253
+ if (!session) return null;
254
+ if (isTokenExpired(session.tokens)) {
255
+ return session.tokens;
256
+ }
257
+ return session.tokens;
258
+ }
259
+ async setTokens(serverUrl, tokens) {
260
+ const key = this.normalizeUrl(serverUrl);
261
+ const data = await this.readFile();
262
+ const now = Date.now();
263
+ const existing = data.sessions[key];
264
+ data.sessions[key] = {
265
+ tokens: withExpiresAt(tokens),
266
+ clientInfo: existing?.clientInfo,
267
+ createdAt: existing?.createdAt ?? now,
268
+ updatedAt: now
269
+ };
270
+ await this.writeFile(data);
271
+ }
272
+ async clearTokens(serverUrl) {
273
+ const key = this.normalizeUrl(serverUrl);
274
+ const data = await this.readFile();
275
+ const session = data.sessions[key];
276
+ if (session) {
277
+ if (session.clientInfo) {
278
+ data.sessions[key] = {
279
+ tokens: {
280
+ access_token: "",
281
+ token_type: "bearer"
282
+ },
283
+ clientInfo: session.clientInfo,
284
+ createdAt: session.createdAt,
285
+ updatedAt: Date.now()
286
+ };
287
+ } else {
288
+ const { [key]: _, ...remaining } = data.sessions;
289
+ data.sessions = remaining;
290
+ }
291
+ }
292
+ await this.writeFile(data);
293
+ }
294
+ async getClientInfo(serverUrl) {
295
+ const key = this.normalizeUrl(serverUrl);
296
+ const data = await this.readFile();
297
+ return data.sessions[key]?.clientInfo ?? null;
298
+ }
299
+ async setClientInfo(serverUrl, info) {
300
+ const key = this.normalizeUrl(serverUrl);
301
+ const data = await this.readFile();
302
+ const now = Date.now();
303
+ const existing = data.sessions[key];
304
+ data.sessions[key] = {
305
+ tokens: existing?.tokens ?? {
306
+ access_token: "",
307
+ token_type: "bearer"
308
+ },
309
+ clientInfo: info,
310
+ createdAt: existing?.createdAt ?? now,
311
+ updatedAt: now
312
+ };
313
+ await this.writeFile(data);
314
+ }
315
+ async clearClientInfo(serverUrl) {
316
+ const key = this.normalizeUrl(serverUrl);
317
+ const data = await this.readFile();
318
+ const session = data.sessions[key];
319
+ if (session) {
320
+ if (session.tokens?.access_token) {
321
+ data.sessions[key] = {
322
+ tokens: session.tokens,
323
+ createdAt: session.createdAt,
324
+ updatedAt: Date.now()
325
+ };
326
+ } else {
327
+ const { [key]: _, ...remaining } = data.sessions;
328
+ data.sessions = remaining;
329
+ }
330
+ }
331
+ await this.writeFile(data);
332
+ }
333
+ async clearAll() {
334
+ await this.writeFile({
335
+ version: CURRENT_VERSION,
336
+ sessions: {}
337
+ });
338
+ }
339
+ async getAllSessions() {
340
+ const data = await this.readFile();
341
+ return Object.entries(data.sessions).map(([url, session]) => ({
342
+ serverUrl: url,
343
+ tokens: session.tokens,
344
+ clientInfo: session.clientInfo,
345
+ createdAt: session.createdAt,
346
+ updatedAt: session.updatedAt
347
+ }));
348
+ }
349
+ };
350
+
351
+ // src/storage/keychain.ts
352
+ var SERVICE_NAME = "leanmcp-auth";
353
+ var KeychainStorage = class {
354
+ static {
355
+ __name(this, "KeychainStorage");
356
+ }
357
+ serviceName;
358
+ keytar = null;
359
+ initPromise = null;
360
+ constructor(options = {}) {
361
+ this.serviceName = options.serviceName ?? SERVICE_NAME;
362
+ }
363
+ /**
364
+ * Initialize keytar (lazy load)
365
+ */
366
+ async init() {
367
+ if (this.keytar) return;
368
+ if (this.initPromise) {
369
+ await this.initPromise;
370
+ return;
371
+ }
372
+ this.initPromise = (async () => {
373
+ try {
374
+ this.keytar = require("keytar");
375
+ } catch (error) {
376
+ throw new Error('KeychainStorage requires the "keytar" package. Install it with: npm install keytar');
377
+ }
378
+ })();
379
+ await this.initPromise;
380
+ }
381
+ /**
382
+ * Normalize server URL for consistent key lookup
383
+ */
384
+ normalizeUrl(serverUrl) {
385
+ return serverUrl.replace(/\/+$/, "").toLowerCase();
386
+ }
387
+ /**
388
+ * Get account key for tokens
389
+ */
390
+ getTokensAccount(serverUrl) {
391
+ return `tokens:${this.normalizeUrl(serverUrl)}`;
392
+ }
393
+ /**
394
+ * Get account key for client info
395
+ */
396
+ getClientAccount(serverUrl) {
397
+ return `client:${this.normalizeUrl(serverUrl)}`;
398
+ }
399
+ async getTokens(serverUrl) {
400
+ await this.init();
401
+ const account = this.getTokensAccount(serverUrl);
402
+ const stored = await this.keytar.getPassword(this.serviceName, account);
403
+ if (!stored) return null;
404
+ try {
405
+ return JSON.parse(stored);
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+ async setTokens(serverUrl, tokens) {
411
+ await this.init();
412
+ const account = this.getTokensAccount(serverUrl);
413
+ const enrichedTokens = withExpiresAt(tokens);
414
+ await this.keytar.setPassword(this.serviceName, account, JSON.stringify(enrichedTokens));
415
+ }
416
+ async clearTokens(serverUrl) {
417
+ await this.init();
418
+ const account = this.getTokensAccount(serverUrl);
419
+ await this.keytar.deletePassword(this.serviceName, account);
420
+ }
421
+ async getClientInfo(serverUrl) {
422
+ await this.init();
423
+ const account = this.getClientAccount(serverUrl);
424
+ const stored = await this.keytar.getPassword(this.serviceName, account);
425
+ if (!stored) return null;
426
+ try {
427
+ return JSON.parse(stored);
428
+ } catch {
429
+ return null;
430
+ }
431
+ }
432
+ async setClientInfo(serverUrl, info) {
433
+ await this.init();
434
+ const account = this.getClientAccount(serverUrl);
435
+ await this.keytar.setPassword(this.serviceName, account, JSON.stringify(info));
436
+ }
437
+ async clearClientInfo(serverUrl) {
438
+ await this.init();
439
+ const account = this.getClientAccount(serverUrl);
440
+ await this.keytar.deletePassword(this.serviceName, account);
441
+ }
442
+ async clearAll() {
443
+ await this.init();
444
+ const credentials = await this.keytar.findCredentials(this.serviceName);
445
+ for (const cred of credentials) {
446
+ await this.keytar.deletePassword(this.serviceName, cred.account);
447
+ }
448
+ }
449
+ async getAllSessions() {
450
+ await this.init();
451
+ const credentials = await this.keytar.findCredentials(this.serviceName);
452
+ const sessions = [];
453
+ const tokensMap = /* @__PURE__ */ new Map();
454
+ const clientMap = /* @__PURE__ */ new Map();
455
+ for (const cred of credentials) {
456
+ if (cred.account.startsWith("tokens:")) {
457
+ const url = cred.account.replace("tokens:", "");
458
+ try {
459
+ tokensMap.set(url, JSON.parse(cred.password));
460
+ } catch {
461
+ }
462
+ } else if (cred.account.startsWith("client:")) {
463
+ const url = cred.account.replace("client:", "");
464
+ try {
465
+ clientMap.set(url, JSON.parse(cred.password));
466
+ } catch {
467
+ }
468
+ }
469
+ }
470
+ for (const [url, tokens] of tokensMap) {
471
+ sessions.push({
472
+ serverUrl: url,
473
+ tokens,
474
+ clientInfo: clientMap.get(url),
475
+ createdAt: Date.now(),
476
+ updatedAt: Date.now()
477
+ });
478
+ }
479
+ return sessions;
480
+ }
481
+ };
482
+ async function isKeychainAvailable() {
483
+ try {
484
+ require("keytar");
485
+ return true;
486
+ } catch {
487
+ return false;
488
+ }
489
+ }
490
+ __name(isKeychainAvailable, "isKeychainAvailable");
491
+ // Annotate the CommonJS export names for ESM import in node:
492
+ 0 && (module.exports = {
493
+ FileStorage,
494
+ KeychainStorage,
495
+ MemoryStorage,
496
+ isKeychainAvailable,
497
+ isTokenExpired,
498
+ withExpiresAt
499
+ });