@leanmcp/auth 0.3.2 → 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,869 @@
1
+ import {
2
+ generateCodeChallenge,
3
+ generateCodeVerifier,
4
+ generatePKCE,
5
+ isValidCodeVerifier,
6
+ verifyPKCE
7
+ } from "../chunk-ZOPKMOPV.mjs";
8
+ import {
9
+ MemoryStorage,
10
+ isTokenExpired,
11
+ withExpiresAt
12
+ } from "../chunk-RGCCBQWG.mjs";
13
+ import {
14
+ __name,
15
+ __require
16
+ } from "../chunk-LPEX4YW6.mjs";
17
+
18
+ // src/client/callback-server.ts
19
+ import { createServer } from "http";
20
+ import { URL as URL2 } from "url";
21
+ var DEFAULT_SUCCESS_HTML = `
22
+ <!DOCTYPE html>
23
+ <html>
24
+ <head>
25
+ <title>Authentication Successful</title>
26
+ <style>
27
+ body {
28
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
29
+ display: flex;
30
+ justify-content: center;
31
+ align-items: center;
32
+ height: 100vh;
33
+ margin: 0;
34
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
35
+ color: white;
36
+ }
37
+ .container {
38
+ text-align: center;
39
+ padding: 40px;
40
+ background: rgba(255,255,255,0.1);
41
+ border-radius: 16px;
42
+ backdrop-filter: blur(10px);
43
+ }
44
+ .success-icon { font-size: 64px; margin-bottom: 20px; }
45
+ h1 { margin: 0 0 10px; font-size: 24px; }
46
+ p { margin: 0; opacity: 0.9; }
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <div class="container">
51
+ <div class="success-icon">\u2713</div>
52
+ <h1>Authentication Successful</h1>
53
+ <p>You can close this window and return to your application.</p>
54
+ </div>
55
+ </body>
56
+ </html>
57
+ `;
58
+ var DEFAULT_ERROR_HTML = `
59
+ <!DOCTYPE html>
60
+ <html>
61
+ <head>
62
+ <title>Authentication Failed</title>
63
+ <style>
64
+ body {
65
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
66
+ display: flex;
67
+ justify-content: center;
68
+ align-items: center;
69
+ height: 100vh;
70
+ margin: 0;
71
+ background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
72
+ color: white;
73
+ }
74
+ .container {
75
+ text-align: center;
76
+ padding: 40px;
77
+ background: rgba(0,0,0,0.2);
78
+ border-radius: 16px;
79
+ }
80
+ .error-icon { font-size: 64px; margin-bottom: 20px; }
81
+ h1 { margin: 0 0 10px; font-size: 24px; }
82
+ p { margin: 0; opacity: 0.9; }
83
+ .details { margin-top: 20px; font-size: 14px; opacity: 0.7; }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <div class="container">
88
+ <div class="error-icon">\u2717</div>
89
+ <h1>Authentication Failed</h1>
90
+ <p>{{ERROR_MESSAGE}}</p>
91
+ <p class="details">{{ERROR_DESCRIPTION}}</p>
92
+ </div>
93
+ </body>
94
+ </html>
95
+ `;
96
+ async function startCallbackServer(options = {}) {
97
+ const { port = 0, host = "127.0.0.1", path = "/callback", timeout = 5 * 60 * 1e3, successHtml = DEFAULT_SUCCESS_HTML, errorHtml = DEFAULT_ERROR_HTML } = options;
98
+ let resolveCallback;
99
+ let rejectCallback;
100
+ const callbackPromise = new Promise((resolve, reject) => {
101
+ resolveCallback = resolve;
102
+ rejectCallback = reject;
103
+ });
104
+ const server = createServer((req, res) => {
105
+ const url = new URL2(req.url || "/", `http://${host}:${port}`);
106
+ if (url.pathname !== path) {
107
+ res.writeHead(404, {
108
+ "Content-Type": "text/plain"
109
+ });
110
+ res.end("Not Found");
111
+ return;
112
+ }
113
+ const code = url.searchParams.get("code");
114
+ const state = url.searchParams.get("state");
115
+ const error = url.searchParams.get("error");
116
+ const errorDescription = url.searchParams.get("error_description");
117
+ if (error) {
118
+ const html = errorHtml.replace("{{ERROR_MESSAGE}}", error).replace("{{ERROR_DESCRIPTION}}", errorDescription || "");
119
+ res.writeHead(400, {
120
+ "Content-Type": "text/html"
121
+ });
122
+ res.end(html);
123
+ rejectCallback(new Error(`OAuth error: ${error} - ${errorDescription || "No description"}`));
124
+ return;
125
+ }
126
+ if (!code) {
127
+ const html = errorHtml.replace("{{ERROR_MESSAGE}}", "Missing authorization code").replace("{{ERROR_DESCRIPTION}}", "The OAuth server did not return an authorization code.");
128
+ res.writeHead(400, {
129
+ "Content-Type": "text/html"
130
+ });
131
+ res.end(html);
132
+ rejectCallback(new Error("No authorization code received"));
133
+ return;
134
+ }
135
+ if (!state) {
136
+ const html = errorHtml.replace("{{ERROR_MESSAGE}}", "Missing state parameter").replace("{{ERROR_DESCRIPTION}}", "The OAuth server did not return the state parameter.");
137
+ res.writeHead(400, {
138
+ "Content-Type": "text/html"
139
+ });
140
+ res.end(html);
141
+ rejectCallback(new Error("No state parameter received"));
142
+ return;
143
+ }
144
+ res.writeHead(200, {
145
+ "Content-Type": "text/html"
146
+ });
147
+ res.end(successHtml);
148
+ resolveCallback({
149
+ code,
150
+ state
151
+ });
152
+ });
153
+ await new Promise((resolve, reject) => {
154
+ server.once("error", reject);
155
+ server.listen(port, host, () => {
156
+ server.removeListener("error", reject);
157
+ resolve();
158
+ });
159
+ });
160
+ const actualPort = server.address().port;
161
+ const redirectUri = `http://${host}:${actualPort}${path}`;
162
+ const timeoutId = setTimeout(() => {
163
+ rejectCallback(new Error(`OAuth callback timed out after ${timeout / 1e3} seconds`));
164
+ }, timeout);
165
+ const shutdown = /* @__PURE__ */ __name(async () => {
166
+ clearTimeout(timeoutId);
167
+ return new Promise((resolve, reject) => {
168
+ server.close((err) => {
169
+ if (err) reject(err);
170
+ else resolve();
171
+ });
172
+ });
173
+ }, "shutdown");
174
+ return {
175
+ redirectUri,
176
+ port: actualPort,
177
+ waitForCallback: /* @__PURE__ */ __name(async () => {
178
+ try {
179
+ return await callbackPromise;
180
+ } finally {
181
+ await shutdown().catch(() => {
182
+ });
183
+ }
184
+ }, "waitForCallback"),
185
+ shutdown
186
+ };
187
+ }
188
+ __name(startCallbackServer, "startCallbackServer");
189
+ async function findAvailablePort(preferredPort) {
190
+ return new Promise((resolve, reject) => {
191
+ const server = createServer();
192
+ server.once("error", reject);
193
+ server.listen(preferredPort || 0, "127.0.0.1", () => {
194
+ const port = server.address().port;
195
+ server.close(() => resolve(port));
196
+ });
197
+ });
198
+ }
199
+ __name(findAvailablePort, "findAvailablePort");
200
+
201
+ // src/client/oauth-client.ts
202
+ var OAuthClient = class {
203
+ static {
204
+ __name(this, "OAuthClient");
205
+ }
206
+ serverUrl;
207
+ scopes;
208
+ clientName;
209
+ storage;
210
+ pkceEnabled;
211
+ autoRefresh;
212
+ refreshBuffer;
213
+ callbackPort;
214
+ timeout;
215
+ // OAuth endpoints
216
+ authorizationEndpoint;
217
+ tokenEndpoint;
218
+ registrationEndpoint;
219
+ // Pre-configured credentials
220
+ preConfiguredClientId;
221
+ preConfiguredClientSecret;
222
+ // Runtime state
223
+ pendingRefresh;
224
+ metadata;
225
+ constructor(options) {
226
+ this.serverUrl = options.serverUrl.replace(/\/+$/, "");
227
+ this.scopes = options.scopes ?? [];
228
+ this.clientName = options.clientName ?? "LeanMCP Client";
229
+ this.storage = options.storage ?? new MemoryStorage();
230
+ this.pkceEnabled = options.pkceEnabled ?? true;
231
+ this.autoRefresh = options.autoRefresh ?? true;
232
+ this.refreshBuffer = options.refreshBuffer ?? 60;
233
+ this.callbackPort = options.callbackPort;
234
+ this.timeout = options.timeout ?? 5 * 60 * 1e3;
235
+ this.authorizationEndpoint = options.authorizationEndpoint;
236
+ this.tokenEndpoint = options.tokenEndpoint;
237
+ this.registrationEndpoint = options.registrationEndpoint;
238
+ this.preConfiguredClientId = options.clientId;
239
+ this.preConfiguredClientSecret = options.clientSecret;
240
+ }
241
+ /**
242
+ * Discover OAuth metadata from .well-known endpoint
243
+ */
244
+ async discoverMetadata() {
245
+ if (this.metadata) return this.metadata;
246
+ const wellKnownUrl = `${this.serverUrl}/.well-known/oauth-authorization-server`;
247
+ try {
248
+ const response = await fetch(wellKnownUrl);
249
+ if (response.ok) {
250
+ this.metadata = await response.json();
251
+ return this.metadata;
252
+ }
253
+ } catch {
254
+ }
255
+ try {
256
+ const oidcUrl = `${this.serverUrl}/.well-known/openid-configuration`;
257
+ const response = await fetch(oidcUrl);
258
+ if (response.ok) {
259
+ this.metadata = await response.json();
260
+ return this.metadata;
261
+ }
262
+ } catch {
263
+ }
264
+ this.metadata = {
265
+ issuer: this.serverUrl,
266
+ authorization_endpoint: this.authorizationEndpoint || `${this.serverUrl}/authorize`,
267
+ token_endpoint: this.tokenEndpoint || `${this.serverUrl}/token`,
268
+ registration_endpoint: this.registrationEndpoint
269
+ };
270
+ return this.metadata;
271
+ }
272
+ /**
273
+ * Get or register OAuth client credentials
274
+ */
275
+ async getClientCredentials(redirectUri) {
276
+ if (this.preConfiguredClientId) {
277
+ return {
278
+ client_id: this.preConfiguredClientId,
279
+ client_secret: this.preConfiguredClientSecret
280
+ };
281
+ }
282
+ const stored = await this.storage.getClientInfo(this.serverUrl);
283
+ if (stored && (!stored.client_secret_expires_at || stored.client_secret_expires_at > Date.now() / 1e3)) {
284
+ return stored;
285
+ }
286
+ const metadata = await this.discoverMetadata();
287
+ if (!metadata.registration_endpoint) {
288
+ throw new Error("No client credentials configured and server does not support dynamic registration. Please provide clientId in OAuthClientOptions.");
289
+ }
290
+ const registrationPayload = {
291
+ client_name: this.clientName,
292
+ redirect_uris: [
293
+ redirectUri
294
+ ],
295
+ grant_types: [
296
+ "authorization_code",
297
+ "refresh_token"
298
+ ],
299
+ response_types: [
300
+ "code"
301
+ ],
302
+ scope: this.scopes.join(" ")
303
+ };
304
+ const response = await fetch(metadata.registration_endpoint, {
305
+ method: "POST",
306
+ headers: {
307
+ "Content-Type": "application/json"
308
+ },
309
+ body: JSON.stringify(registrationPayload)
310
+ });
311
+ if (!response.ok) {
312
+ const error = await response.text();
313
+ throw new Error(`Client registration failed: ${error}`);
314
+ }
315
+ const registration = await response.json();
316
+ await this.storage.setClientInfo(this.serverUrl, registration);
317
+ return registration;
318
+ }
319
+ /**
320
+ * Start the browser-based OAuth flow
321
+ *
322
+ * Opens the user's browser to the authorization URL and waits for the callback.
323
+ *
324
+ * @returns OAuth tokens
325
+ */
326
+ async authenticate() {
327
+ const existing = await this.storage.getTokens(this.serverUrl);
328
+ if (existing && !isTokenExpired(existing, this.refreshBuffer)) {
329
+ return existing;
330
+ }
331
+ if (existing?.refresh_token) {
332
+ try {
333
+ return await this.refreshTokens();
334
+ } catch {
335
+ }
336
+ }
337
+ const callbackServer = await startCallbackServer({
338
+ port: this.callbackPort,
339
+ timeout: this.timeout
340
+ });
341
+ try {
342
+ const metadata = await this.discoverMetadata();
343
+ const clientCredentials = await this.getClientCredentials(callbackServer.redirectUri);
344
+ let pkce;
345
+ if (this.pkceEnabled) {
346
+ pkce = generatePKCE();
347
+ }
348
+ const state = crypto.randomUUID();
349
+ const authUrl = new URL(metadata.authorization_endpoint);
350
+ authUrl.searchParams.set("response_type", "code");
351
+ authUrl.searchParams.set("client_id", clientCredentials.client_id);
352
+ authUrl.searchParams.set("redirect_uri", callbackServer.redirectUri);
353
+ authUrl.searchParams.set("state", state);
354
+ if (this.scopes.length > 0) {
355
+ authUrl.searchParams.set("scope", this.scopes.join(" "));
356
+ }
357
+ if (pkce) {
358
+ authUrl.searchParams.set("code_challenge", pkce.challenge);
359
+ authUrl.searchParams.set("code_challenge_method", pkce.method);
360
+ }
361
+ const openBrowser = await this.openBrowser(authUrl.toString());
362
+ if (!openBrowser) {
363
+ console.log(`
364
+ Please open this URL in your browser:
365
+ ${authUrl.toString()}
366
+ `);
367
+ }
368
+ const result = await callbackServer.waitForCallback();
369
+ if (result.state !== state) {
370
+ throw new Error("State mismatch - possible CSRF attack");
371
+ }
372
+ const tokens = await this.exchangeCodeForTokens(result.code, callbackServer.redirectUri, clientCredentials, pkce?.verifier);
373
+ const enrichedTokens = withExpiresAt(tokens);
374
+ await this.storage.setTokens(this.serverUrl, enrichedTokens);
375
+ return enrichedTokens;
376
+ } finally {
377
+ await callbackServer.shutdown().catch(() => {
378
+ });
379
+ }
380
+ }
381
+ /**
382
+ * Open URL in browser
383
+ */
384
+ async openBrowser(url) {
385
+ try {
386
+ const open = __require("open");
387
+ await open(url);
388
+ return true;
389
+ } catch {
390
+ try {
391
+ const { exec } = __require("child_process");
392
+ const platform = process.platform;
393
+ if (platform === "darwin") {
394
+ exec(`open "${url}"`);
395
+ } else if (platform === "win32") {
396
+ exec(`start "" "${url}"`);
397
+ } else {
398
+ exec(`xdg-open "${url}"`);
399
+ }
400
+ return true;
401
+ } catch {
402
+ return false;
403
+ }
404
+ }
405
+ }
406
+ /**
407
+ * Exchange authorization code for tokens
408
+ */
409
+ async exchangeCodeForTokens(code, redirectUri, credentials, codeVerifier) {
410
+ const metadata = await this.discoverMetadata();
411
+ const tokenPayload = {
412
+ grant_type: "authorization_code",
413
+ code,
414
+ redirect_uri: redirectUri,
415
+ client_id: credentials.client_id
416
+ };
417
+ if (credentials.client_secret) {
418
+ tokenPayload.client_secret = credentials.client_secret;
419
+ }
420
+ if (codeVerifier) {
421
+ tokenPayload.code_verifier = codeVerifier;
422
+ }
423
+ const response = await fetch(metadata.token_endpoint, {
424
+ method: "POST",
425
+ headers: {
426
+ "Content-Type": "application/x-www-form-urlencoded"
427
+ },
428
+ body: new URLSearchParams(tokenPayload)
429
+ });
430
+ if (!response.ok) {
431
+ const error = await response.text();
432
+ throw new Error(`Token exchange failed: ${error}`);
433
+ }
434
+ return response.json();
435
+ }
436
+ /**
437
+ * Refresh the access token using the refresh token
438
+ */
439
+ async refreshTokens() {
440
+ if (this.pendingRefresh) {
441
+ return this.pendingRefresh;
442
+ }
443
+ this.pendingRefresh = this.doRefreshTokens();
444
+ try {
445
+ return await this.pendingRefresh;
446
+ } finally {
447
+ this.pendingRefresh = void 0;
448
+ }
449
+ }
450
+ async doRefreshTokens() {
451
+ const existing = await this.storage.getTokens(this.serverUrl);
452
+ if (!existing?.refresh_token) {
453
+ throw new Error("No refresh token available");
454
+ }
455
+ const metadata = await this.discoverMetadata();
456
+ const credentials = await this.getClientCredentials("");
457
+ const tokenPayload = {
458
+ grant_type: "refresh_token",
459
+ refresh_token: existing.refresh_token,
460
+ client_id: credentials.client_id
461
+ };
462
+ if (credentials.client_secret) {
463
+ tokenPayload.client_secret = credentials.client_secret;
464
+ }
465
+ const response = await fetch(metadata.token_endpoint, {
466
+ method: "POST",
467
+ headers: {
468
+ "Content-Type": "application/x-www-form-urlencoded"
469
+ },
470
+ body: new URLSearchParams(tokenPayload)
471
+ });
472
+ if (!response.ok) {
473
+ await this.storage.clearTokens(this.serverUrl);
474
+ throw new Error("Token refresh failed");
475
+ }
476
+ const tokens = await response.json();
477
+ if (!tokens.refresh_token && existing.refresh_token) {
478
+ tokens.refresh_token = existing.refresh_token;
479
+ }
480
+ const enrichedTokens = withExpiresAt(tokens);
481
+ await this.storage.setTokens(this.serverUrl, enrichedTokens);
482
+ return enrichedTokens;
483
+ }
484
+ /**
485
+ * Get a valid access token, refreshing if necessary
486
+ */
487
+ async getValidToken() {
488
+ let tokens = await this.storage.getTokens(this.serverUrl);
489
+ if (!tokens) {
490
+ throw new Error("Not authenticated. Call authenticate() first.");
491
+ }
492
+ if (this.autoRefresh && isTokenExpired(tokens, this.refreshBuffer)) {
493
+ if (tokens.refresh_token) {
494
+ tokens = await this.refreshTokens();
495
+ } else {
496
+ throw new Error("Token expired and no refresh token available. Re-authenticate required.");
497
+ }
498
+ }
499
+ return tokens.access_token;
500
+ }
501
+ /**
502
+ * Get current tokens (may be expired)
503
+ */
504
+ async getTokens() {
505
+ return this.storage.getTokens(this.serverUrl);
506
+ }
507
+ /**
508
+ * Check if we have valid (non-expired) tokens
509
+ */
510
+ async isAuthenticated() {
511
+ const tokens = await this.storage.getTokens(this.serverUrl);
512
+ if (!tokens) return false;
513
+ return !isTokenExpired(tokens, this.refreshBuffer) || !!tokens.refresh_token;
514
+ }
515
+ /**
516
+ * Clear stored tokens and log out
517
+ */
518
+ async logout() {
519
+ await this.storage.clearTokens(this.serverUrl);
520
+ await this.storage.clearClientInfo(this.serverUrl);
521
+ }
522
+ /**
523
+ * Create an auth handler for HTTP requests
524
+ *
525
+ * @example
526
+ * ```typescript
527
+ * const authHandler = client.asAuthHandler();
528
+ * const authedRequest = await authHandler(request);
529
+ * ```
530
+ */
531
+ asAuthHandler() {
532
+ return async (request) => {
533
+ const token = await this.getValidToken();
534
+ const headers = new Headers(request.headers);
535
+ headers.set("Authorization", `Bearer ${token}`);
536
+ return new Request(request.url, {
537
+ method: request.method,
538
+ headers,
539
+ body: request.body
540
+ });
541
+ };
542
+ }
543
+ };
544
+
545
+ // src/client/discovery.ts
546
+ var metadataCache = /* @__PURE__ */ new Map();
547
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
548
+ async function discoverOAuthMetadata(serverUrl, options = {}) {
549
+ const { timeout = 1e4, cache = true, fetch: customFetch = fetch } = options;
550
+ const normalizedUrl = serverUrl.replace(/\/+$/, "");
551
+ if (cache) {
552
+ const cached = metadataCache.get(normalizedUrl);
553
+ if (cached && cached.expiresAt > Date.now()) {
554
+ return cached.metadata;
555
+ }
556
+ }
557
+ const controller = new AbortController();
558
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
559
+ try {
560
+ const oauthUrl = `${normalizedUrl}/.well-known/oauth-authorization-server`;
561
+ try {
562
+ const response = await customFetch(oauthUrl, {
563
+ signal: controller.signal,
564
+ headers: {
565
+ "Accept": "application/json"
566
+ }
567
+ });
568
+ if (response.ok) {
569
+ const metadata = await response.json();
570
+ cacheMetadata(normalizedUrl, metadata, cache);
571
+ return metadata;
572
+ }
573
+ } catch {
574
+ }
575
+ const oidcUrl = `${normalizedUrl}/.well-known/openid-configuration`;
576
+ try {
577
+ const response = await customFetch(oidcUrl, {
578
+ signal: controller.signal,
579
+ headers: {
580
+ "Accept": "application/json"
581
+ }
582
+ });
583
+ if (response.ok) {
584
+ const metadata = await response.json();
585
+ cacheMetadata(normalizedUrl, metadata, cache);
586
+ return metadata;
587
+ }
588
+ } catch {
589
+ }
590
+ const fallbackMetadata = {
591
+ issuer: normalizedUrl,
592
+ authorization_endpoint: `${normalizedUrl}/authorize`,
593
+ token_endpoint: `${normalizedUrl}/token`
594
+ };
595
+ if (cache) {
596
+ metadataCache.set(normalizedUrl, {
597
+ metadata: fallbackMetadata,
598
+ expiresAt: Date.now() + 5 * 60 * 1e3
599
+ });
600
+ }
601
+ return fallbackMetadata;
602
+ } finally {
603
+ clearTimeout(timeoutId);
604
+ }
605
+ }
606
+ __name(discoverOAuthMetadata, "discoverOAuthMetadata");
607
+ function cacheMetadata(url, metadata, shouldCache) {
608
+ if (shouldCache) {
609
+ metadataCache.set(url, {
610
+ metadata,
611
+ expiresAt: Date.now() + CACHE_TTL_MS
612
+ });
613
+ }
614
+ }
615
+ __name(cacheMetadata, "cacheMetadata");
616
+ function clearMetadataCache(serverUrl) {
617
+ if (serverUrl) {
618
+ const normalizedUrl = serverUrl.replace(/\/+$/, "");
619
+ metadataCache.delete(normalizedUrl);
620
+ } else {
621
+ metadataCache.clear();
622
+ }
623
+ }
624
+ __name(clearMetadataCache, "clearMetadataCache");
625
+ function serverSupports(metadata, feature) {
626
+ switch (feature) {
627
+ case "pkce":
628
+ return metadata.code_challenge_methods_supported?.includes("S256") ?? false;
629
+ case "refresh_token":
630
+ return metadata.grant_types_supported?.includes("refresh_token") ?? true;
631
+ // Assume supported if not specified
632
+ case "dynamic_registration":
633
+ return !!metadata.registration_endpoint;
634
+ case "revocation":
635
+ return !!metadata.revocation_endpoint;
636
+ default:
637
+ return false;
638
+ }
639
+ }
640
+ __name(serverSupports, "serverSupports");
641
+ function validateMetadata(metadata) {
642
+ const errors = [];
643
+ if (!metadata.issuer) {
644
+ errors.push("Missing required field: issuer");
645
+ }
646
+ if (!metadata.authorization_endpoint) {
647
+ errors.push("Missing required field: authorization_endpoint");
648
+ }
649
+ if (!metadata.token_endpoint) {
650
+ errors.push("Missing required field: token_endpoint");
651
+ }
652
+ return {
653
+ valid: errors.length === 0,
654
+ errors
655
+ };
656
+ }
657
+ __name(validateMetadata, "validateMetadata");
658
+
659
+ // src/client/refresh-manager.ts
660
+ var RefreshManager = class {
661
+ static {
662
+ __name(this, "RefreshManager");
663
+ }
664
+ storage;
665
+ refreshFn;
666
+ serverUrl;
667
+ refreshBuffer;
668
+ checkInterval;
669
+ maxRetries;
670
+ retryDelay;
671
+ intervalId;
672
+ pendingRefresh;
673
+ retryCount = 0;
674
+ listeners = [];
675
+ isRunning = false;
676
+ constructor(options) {
677
+ this.storage = options.storage;
678
+ this.refreshFn = options.refreshFn;
679
+ this.serverUrl = options.serverUrl;
680
+ this.refreshBuffer = options.refreshBuffer ?? 300;
681
+ this.checkInterval = options.checkInterval ?? 6e4;
682
+ this.maxRetries = options.maxRetries ?? 3;
683
+ this.retryDelay = options.retryDelay ?? 5e3;
684
+ }
685
+ /**
686
+ * Start background refresh monitoring
687
+ */
688
+ start() {
689
+ if (this.isRunning) return;
690
+ this.isRunning = true;
691
+ this.checkAndRefresh();
692
+ this.intervalId = setInterval(() => {
693
+ this.checkAndRefresh();
694
+ }, this.checkInterval);
695
+ }
696
+ /**
697
+ * Stop background refresh monitoring
698
+ */
699
+ stop() {
700
+ this.isRunning = false;
701
+ if (this.intervalId) {
702
+ clearInterval(this.intervalId);
703
+ this.intervalId = void 0;
704
+ }
705
+ }
706
+ /**
707
+ * Register event listener
708
+ */
709
+ on(listener) {
710
+ this.listeners.push(listener);
711
+ return () => {
712
+ this.listeners = this.listeners.filter((l) => l !== listener);
713
+ };
714
+ }
715
+ /**
716
+ * Emit event to all listeners
717
+ */
718
+ emit(event) {
719
+ for (const listener of this.listeners) {
720
+ try {
721
+ listener(event);
722
+ } catch {
723
+ }
724
+ }
725
+ }
726
+ /**
727
+ * Check token and refresh if needed
728
+ */
729
+ async checkAndRefresh() {
730
+ if (this.pendingRefresh) return;
731
+ try {
732
+ const tokens = await this.storage.getTokens(this.serverUrl);
733
+ if (!tokens) return;
734
+ if (!isTokenExpired(tokens, this.refreshBuffer)) {
735
+ this.retryCount = 0;
736
+ return;
737
+ }
738
+ if (!tokens.refresh_token) {
739
+ this.emit({
740
+ type: "token_expired",
741
+ serverUrl: this.serverUrl
742
+ });
743
+ return;
744
+ }
745
+ await this.performRefresh(tokens.refresh_token);
746
+ } catch (error) {
747
+ console.error("[RefreshManager] Error checking tokens:", error);
748
+ }
749
+ }
750
+ /**
751
+ * Perform token refresh with retry logic
752
+ */
753
+ async performRefresh(refreshToken) {
754
+ this.emit({
755
+ type: "refresh_started",
756
+ serverUrl: this.serverUrl
757
+ });
758
+ this.pendingRefresh = this.doRefresh(refreshToken);
759
+ try {
760
+ const tokens = await this.pendingRefresh;
761
+ this.retryCount = 0;
762
+ this.emit({
763
+ type: "refresh_success",
764
+ serverUrl: this.serverUrl,
765
+ tokens
766
+ });
767
+ } catch (error) {
768
+ this.emit({
769
+ type: "refresh_failed",
770
+ serverUrl: this.serverUrl,
771
+ error: error instanceof Error ? error : new Error(String(error))
772
+ });
773
+ if (this.retryCount < this.maxRetries) {
774
+ this.retryCount++;
775
+ setTimeout(() => {
776
+ this.checkAndRefresh();
777
+ }, this.retryDelay * this.retryCount);
778
+ }
779
+ } finally {
780
+ this.pendingRefresh = void 0;
781
+ }
782
+ }
783
+ /**
784
+ * Actually perform the refresh
785
+ */
786
+ async doRefresh(refreshToken) {
787
+ const newTokens = await this.refreshFn(refreshToken);
788
+ const enrichedTokens = withExpiresAt(newTokens);
789
+ if (!enrichedTokens.refresh_token) {
790
+ enrichedTokens.refresh_token = refreshToken;
791
+ }
792
+ await this.storage.setTokens(this.serverUrl, enrichedTokens);
793
+ return enrichedTokens;
794
+ }
795
+ /**
796
+ * Force an immediate refresh
797
+ */
798
+ async forceRefresh() {
799
+ const tokens = await this.storage.getTokens(this.serverUrl);
800
+ if (!tokens?.refresh_token) {
801
+ throw new Error("No refresh token available");
802
+ }
803
+ if (this.pendingRefresh) {
804
+ return this.pendingRefresh;
805
+ }
806
+ this.pendingRefresh = this.doRefresh(tokens.refresh_token);
807
+ try {
808
+ const newTokens = await this.pendingRefresh;
809
+ this.emit({
810
+ type: "refresh_success",
811
+ serverUrl: this.serverUrl,
812
+ tokens: newTokens
813
+ });
814
+ return newTokens;
815
+ } finally {
816
+ this.pendingRefresh = void 0;
817
+ }
818
+ }
819
+ /**
820
+ * Get current running state
821
+ */
822
+ get running() {
823
+ return this.isRunning;
824
+ }
825
+ };
826
+ function createRefreshManager(storage, serverUrl, tokenEndpoint, clientId, clientSecret) {
827
+ return new RefreshManager({
828
+ storage,
829
+ serverUrl,
830
+ refreshFn: /* @__PURE__ */ __name(async (refreshToken) => {
831
+ const payload = {
832
+ grant_type: "refresh_token",
833
+ refresh_token: refreshToken,
834
+ client_id: clientId
835
+ };
836
+ if (clientSecret) {
837
+ payload.client_secret = clientSecret;
838
+ }
839
+ const response = await fetch(tokenEndpoint, {
840
+ method: "POST",
841
+ headers: {
842
+ "Content-Type": "application/x-www-form-urlencoded"
843
+ },
844
+ body: new URLSearchParams(payload)
845
+ });
846
+ if (!response.ok) {
847
+ throw new Error(`Token refresh failed: ${response.status}`);
848
+ }
849
+ return response.json();
850
+ }, "refreshFn")
851
+ });
852
+ }
853
+ __name(createRefreshManager, "createRefreshManager");
854
+ export {
855
+ OAuthClient,
856
+ RefreshManager,
857
+ clearMetadataCache,
858
+ createRefreshManager,
859
+ discoverOAuthMetadata,
860
+ findAvailablePort,
861
+ generateCodeChallenge,
862
+ generateCodeVerifier,
863
+ generatePKCE,
864
+ isValidCodeVerifier,
865
+ serverSupports,
866
+ startCallbackServer,
867
+ validateMetadata,
868
+ verifyPKCE
869
+ };