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