@atxp/client 0.3.0 → 0.5.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.
Files changed (40) hide show
  1. package/dist/index.cjs +745 -142
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +745 -142
  4. package/dist/index.js.map +1 -1
  5. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js +369 -99
  6. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js.map +1 -1
  7. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js +18 -18
  8. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js.map +1 -1
  9. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/server/auth/errors.js +162 -0
  10. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/server/auth/errors.js.map +1 -0
  11. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/auth.js +86 -14
  12. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/auth.js.map +1 -1
  13. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js +47 -9
  14. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js.map +1 -1
  15. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js +64 -5
  16. package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js.map +1 -1
  17. package/dist/node_modules/eventsource-parser/dist/index.js +1 -1
  18. package/dist/node_modules/eventsource-parser/dist/index.js.map +1 -1
  19. package/dist/node_modules/zod/{dist/esm/v3 → v3}/ZodError.js +3 -2
  20. package/dist/node_modules/zod/v3/ZodError.js.map +1 -0
  21. package/dist/node_modules/zod/v3/errors.js.map +1 -0
  22. package/dist/node_modules/zod/v3/helpers/errorUtil.js.map +1 -0
  23. package/dist/node_modules/zod/v3/helpers/parseUtil.js.map +1 -0
  24. package/dist/node_modules/zod/v3/helpers/util.js.map +1 -0
  25. package/dist/node_modules/zod/{dist/esm/v3 → v3}/locales/en.js +2 -0
  26. package/dist/node_modules/zod/v3/locales/en.js.map +1 -0
  27. package/dist/node_modules/zod/{dist/esm/v3 → v3}/types.js +5 -2
  28. package/dist/node_modules/zod/v3/types.js.map +1 -0
  29. package/package.json +2 -2
  30. package/dist/node_modules/zod/dist/esm/v3/ZodError.js.map +0 -1
  31. package/dist/node_modules/zod/dist/esm/v3/errors.js.map +0 -1
  32. package/dist/node_modules/zod/dist/esm/v3/helpers/errorUtil.js.map +0 -1
  33. package/dist/node_modules/zod/dist/esm/v3/helpers/parseUtil.js.map +0 -1
  34. package/dist/node_modules/zod/dist/esm/v3/helpers/util.js.map +0 -1
  35. package/dist/node_modules/zod/dist/esm/v3/locales/en.js.map +0 -1
  36. package/dist/node_modules/zod/dist/esm/v3/types.js.map +0 -1
  37. /package/dist/node_modules/zod/{dist/esm/v3 → v3}/errors.js +0 -0
  38. /package/dist/node_modules/zod/{dist/esm/v3 → v3}/helpers/errorUtil.js +0 -0
  39. /package/dist/node_modules/zod/{dist/esm/v3 → v3}/helpers/parseUtil.js +0 -0
  40. /package/dist/node_modules/zod/{dist/esm/v3 → v3}/helpers/util.js +0 -0
@@ -1,24 +1,156 @@
1
1
  import pkceChallenge from '../../../../../pkce-challenge/dist/index.browser.js';
2
2
  import { LATEST_PROTOCOL_VERSION } from '../types.js';
3
- import { OAuthProtectedResourceMetadataSchema, OAuthMetadataSchema, OAuthClientInformationFullSchema, OAuthTokensSchema } from '../shared/auth.js';
3
+ import { OAuthProtectedResourceMetadataSchema, OAuthMetadataSchema, OpenIdProviderDiscoveryMetadataSchema, OAuthClientInformationFullSchema, OAuthTokensSchema, OAuthErrorResponseSchema } from '../shared/auth.js';
4
4
  import { resourceUrlFromServerUrl, checkResourceAllowed } from '../shared/auth-utils.js';
5
+ import { InvalidClientError, UnauthorizedClientError, InvalidGrantError, OAuthError, ServerError, OAUTH_ERRORS } from '../server/auth/errors.js';
5
6
 
6
7
  class UnauthorizedError extends Error {
7
8
  constructor(message) {
8
9
  super(message !== null && message !== void 0 ? message : "Unauthorized");
9
10
  }
10
11
  }
12
+ /**
13
+ * Determines the best client authentication method to use based on server support and client configuration.
14
+ *
15
+ * Priority order (highest to lowest):
16
+ * 1. client_secret_basic (if client secret is available)
17
+ * 2. client_secret_post (if client secret is available)
18
+ * 3. none (for public clients)
19
+ *
20
+ * @param clientInformation - OAuth client information containing credentials
21
+ * @param supportedMethods - Authentication methods supported by the authorization server
22
+ * @returns The selected authentication method
23
+ */
24
+ function selectClientAuthMethod(clientInformation, supportedMethods) {
25
+ const hasClientSecret = clientInformation.client_secret !== undefined;
26
+ // If server doesn't specify supported methods, use RFC 6749 defaults
27
+ if (supportedMethods.length === 0) {
28
+ return hasClientSecret ? "client_secret_post" : "none";
29
+ }
30
+ // Try methods in priority order (most secure first)
31
+ if (hasClientSecret && supportedMethods.includes("client_secret_basic")) {
32
+ return "client_secret_basic";
33
+ }
34
+ if (hasClientSecret && supportedMethods.includes("client_secret_post")) {
35
+ return "client_secret_post";
36
+ }
37
+ if (supportedMethods.includes("none")) {
38
+ return "none";
39
+ }
40
+ // Fallback: use what we have
41
+ return hasClientSecret ? "client_secret_post" : "none";
42
+ }
43
+ /**
44
+ * Applies client authentication to the request based on the specified method.
45
+ *
46
+ * Implements OAuth 2.1 client authentication methods:
47
+ * - client_secret_basic: HTTP Basic authentication (RFC 6749 Section 2.3.1)
48
+ * - client_secret_post: Credentials in request body (RFC 6749 Section 2.3.1)
49
+ * - none: Public client authentication (RFC 6749 Section 2.1)
50
+ *
51
+ * @param method - The authentication method to use
52
+ * @param clientInformation - OAuth client information containing credentials
53
+ * @param headers - HTTP headers object to modify
54
+ * @param params - URL search parameters to modify
55
+ * @throws {Error} When required credentials are missing
56
+ */
57
+ function applyClientAuthentication(method, clientInformation, headers, params) {
58
+ const { client_id, client_secret } = clientInformation;
59
+ switch (method) {
60
+ case "client_secret_basic":
61
+ applyBasicAuth(client_id, client_secret, headers);
62
+ return;
63
+ case "client_secret_post":
64
+ applyPostAuth(client_id, client_secret, params);
65
+ return;
66
+ case "none":
67
+ applyPublicAuth(client_id, params);
68
+ return;
69
+ default:
70
+ throw new Error(`Unsupported client authentication method: ${method}`);
71
+ }
72
+ }
73
+ /**
74
+ * Applies HTTP Basic authentication (RFC 6749 Section 2.3.1)
75
+ */
76
+ function applyBasicAuth(clientId, clientSecret, headers) {
77
+ if (!clientSecret) {
78
+ throw new Error("client_secret_basic authentication requires a client_secret");
79
+ }
80
+ const credentials = btoa(`${clientId}:${clientSecret}`);
81
+ headers.set("Authorization", `Basic ${credentials}`);
82
+ }
83
+ /**
84
+ * Applies POST body authentication (RFC 6749 Section 2.3.1)
85
+ */
86
+ function applyPostAuth(clientId, clientSecret, params) {
87
+ params.set("client_id", clientId);
88
+ if (clientSecret) {
89
+ params.set("client_secret", clientSecret);
90
+ }
91
+ }
92
+ /**
93
+ * Applies public client authentication (RFC 6749 Section 2.1)
94
+ */
95
+ function applyPublicAuth(clientId, params) {
96
+ params.set("client_id", clientId);
97
+ }
98
+ /**
99
+ * Parses an OAuth error response from a string or Response object.
100
+ *
101
+ * If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
102
+ * and an instance of the appropriate OAuthError subclass will be returned.
103
+ * If parsing fails, it falls back to a generic ServerError that includes
104
+ * the response status (if available) and original content.
105
+ *
106
+ * @param input - A Response object or string containing the error response
107
+ * @returns A Promise that resolves to an OAuthError instance
108
+ */
109
+ async function parseErrorResponse(input) {
110
+ const statusCode = input instanceof Response ? input.status : undefined;
111
+ const body = input instanceof Response ? await input.text() : input;
112
+ try {
113
+ const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
114
+ const { error, error_description, error_uri } = result;
115
+ const errorClass = OAUTH_ERRORS[error] || ServerError;
116
+ return new errorClass(error_description || '', error_uri);
117
+ }
118
+ catch (error) {
119
+ // Not a valid OAuth error response, but try to inform the user of the raw data anyway
120
+ const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ''}Invalid OAuth error response: ${error}. Raw body: ${body}`;
121
+ return new ServerError(errorMessage);
122
+ }
123
+ }
11
124
  /**
12
125
  * Orchestrates the full auth flow with a server.
13
126
  *
14
127
  * This can be used as a single entry point for all authorization functionality,
15
128
  * instead of linking together the other lower-level functions in this module.
16
129
  */
17
- async function auth(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl }) {
130
+ async function auth(provider, options) {
131
+ var _a, _b;
132
+ try {
133
+ return await authInternal(provider, options);
134
+ }
135
+ catch (error) {
136
+ // Handle recoverable error types by invalidating credentials and retrying
137
+ if (error instanceof InvalidClientError || error instanceof UnauthorizedClientError) {
138
+ await ((_a = provider.invalidateCredentials) === null || _a === void 0 ? void 0 : _a.call(provider, 'all'));
139
+ return await authInternal(provider, options);
140
+ }
141
+ else if (error instanceof InvalidGrantError) {
142
+ await ((_b = provider.invalidateCredentials) === null || _b === void 0 ? void 0 : _b.call(provider, 'tokens'));
143
+ return await authInternal(provider, options);
144
+ }
145
+ // Throw otherwise
146
+ throw error;
147
+ }
148
+ }
149
+ async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn, }) {
18
150
  let resourceMetadata;
19
- let authorizationServerUrl = serverUrl;
151
+ let authorizationServerUrl;
20
152
  try {
21
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl });
153
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl }, fetchFn);
22
154
  if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
23
155
  authorizationServerUrl = resourceMetadata.authorization_servers[0];
24
156
  }
@@ -26,8 +158,17 @@ async function auth(provider, { serverUrl, authorizationCode, scope, resourceMet
26
158
  catch (_a) {
27
159
  // Ignore errors and fall back to /.well-known/oauth-authorization-server
28
160
  }
161
+ /**
162
+ * If we don't get a valid authorization server metadata from protected resource metadata,
163
+ * fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
164
+ */
165
+ if (!authorizationServerUrl) {
166
+ authorizationServerUrl = serverUrl;
167
+ }
29
168
  const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
30
- const metadata = await discoverOAuthMetadata(authorizationServerUrl);
169
+ const metadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, {
170
+ fetchFn,
171
+ });
31
172
  // Handle client registration if needed
32
173
  let clientInformation = await Promise.resolve(provider.clientInformation());
33
174
  if (!clientInformation) {
@@ -40,6 +181,7 @@ async function auth(provider, { serverUrl, authorizationCode, scope, resourceMet
40
181
  const fullInformation = await registerClient(authorizationServerUrl, {
41
182
  metadata,
42
183
  clientMetadata: provider.clientMetadata,
184
+ fetchFn,
43
185
  });
44
186
  await provider.saveClientInformation(fullInformation);
45
187
  clientInformation = fullInformation;
@@ -54,6 +196,8 @@ async function auth(provider, { serverUrl, authorizationCode, scope, resourceMet
54
196
  codeVerifier,
55
197
  redirectUri: provider.redirectUrl,
56
198
  resource,
199
+ addClientAuthentication: provider.addClientAuthentication,
200
+ fetchFn: fetchFn,
57
201
  });
58
202
  await provider.saveTokens(tokens);
59
203
  return "AUTHORIZED";
@@ -68,12 +212,19 @@ async function auth(provider, { serverUrl, authorizationCode, scope, resourceMet
68
212
  clientInformation,
69
213
  refreshToken: tokens.refresh_token,
70
214
  resource,
215
+ addClientAuthentication: provider.addClientAuthentication,
216
+ fetchFn,
71
217
  });
72
218
  await provider.saveTokens(newTokens);
73
219
  return "AUTHORIZED";
74
220
  }
75
- catch (_b) {
76
- // Could not refresh OAuth tokens
221
+ catch (error) {
222
+ // If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry.
223
+ if (!(error instanceof OAuthError) || error instanceof ServerError) ;
224
+ else {
225
+ // Refresh failed for another reason, re-throw
226
+ throw error;
227
+ }
77
228
  }
78
229
  }
79
230
  const state = provider.state ? await provider.state() : undefined;
@@ -137,33 +288,12 @@ function extractResourceMetadataUrl(res) {
137
288
  * If the server returns a 404 for the well-known endpoint, this function will
138
289
  * return `undefined`. Any other errors will be thrown as exceptions.
139
290
  */
140
- async function discoverOAuthProtectedResourceMetadata(serverUrl, opts) {
141
- var _a;
142
- let url;
143
- if (opts === null || opts === void 0 ? void 0 : opts.resourceMetadataUrl) {
144
- url = new URL(opts === null || opts === void 0 ? void 0 : opts.resourceMetadataUrl);
145
- }
146
- else {
147
- url = new URL("/.well-known/oauth-protected-resource", serverUrl);
148
- }
149
- let response;
150
- try {
151
- response = await fetch(url, {
152
- headers: {
153
- "MCP-Protocol-Version": (_a = opts === null || opts === void 0 ? void 0 : opts.protocolVersion) !== null && _a !== void 0 ? _a : LATEST_PROTOCOL_VERSION
154
- }
155
- });
156
- }
157
- catch (error) {
158
- // CORS errors come back as TypeError
159
- if (error instanceof TypeError) {
160
- response = await fetch(url);
161
- }
162
- else {
163
- throw error;
164
- }
165
- }
166
- if (response.status === 404) {
291
+ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn = fetch) {
292
+ const response = await discoverMetadataWithFallback(serverUrl, 'oauth-protected-resource', fetchFn, {
293
+ protocolVersion: opts === null || opts === void 0 ? void 0 : opts.protocolVersion,
294
+ metadataUrl: opts === null || opts === void 0 ? void 0 : opts.resourceMetadataUrl,
295
+ });
296
+ if (!response || response.status === 404) {
167
297
  throw new Error(`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`);
168
298
  }
169
299
  if (!response.ok) {
@@ -174,15 +304,15 @@ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts) {
174
304
  /**
175
305
  * Helper function to handle fetch with CORS retry logic
176
306
  */
177
- async function fetchWithCorsRetry(url, headers) {
307
+ async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
178
308
  try {
179
- return await fetch(url, { headers });
309
+ return await fetchFn(url, { headers });
180
310
  }
181
311
  catch (error) {
182
312
  if (error instanceof TypeError) {
183
313
  if (headers) {
184
314
  // CORS errors come back as TypeError, retry without headers
185
- return fetchWithCorsRetry(url);
315
+ return fetchWithCorsRetry(url, undefined, fetchFn);
186
316
  }
187
317
  else {
188
318
  // We're getting CORS errors on retry too, return undefined
@@ -193,57 +323,162 @@ async function fetchWithCorsRetry(url, headers) {
193
323
  }
194
324
  }
195
325
  /**
196
- * Constructs the well-known path for OAuth metadata discovery
326
+ * Constructs the well-known path for auth-related metadata discovery
197
327
  */
198
- function buildWellKnownPath(pathname) {
199
- let wellKnownPath = `/.well-known/oauth-authorization-server${pathname}`;
328
+ function buildWellKnownPath(wellKnownPrefix, pathname = '', options = {}) {
329
+ // Strip trailing slash from pathname to avoid double slashes
200
330
  if (pathname.endsWith('/')) {
201
- // Strip trailing slash from pathname to avoid double slashes
202
- wellKnownPath = wellKnownPath.slice(0, -1);
331
+ pathname = pathname.slice(0, -1);
203
332
  }
204
- return wellKnownPath;
333
+ return options.prependPathname
334
+ ? `${pathname}/.well-known/${wellKnownPrefix}`
335
+ : `/.well-known/${wellKnownPrefix}${pathname}`;
205
336
  }
206
337
  /**
207
338
  * Tries to discover OAuth metadata at a specific URL
208
339
  */
209
- async function tryMetadataDiscovery(url, protocolVersion) {
340
+ async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
210
341
  const headers = {
211
342
  "MCP-Protocol-Version": protocolVersion
212
343
  };
213
- return await fetchWithCorsRetry(url, headers);
344
+ return await fetchWithCorsRetry(url, headers, fetchFn);
214
345
  }
215
346
  /**
216
347
  * Determines if fallback to root discovery should be attempted
217
348
  */
218
349
  function shouldAttemptFallback(response, pathname) {
219
- return !response || response.status === 404 && pathname !== '/';
350
+ return !response || (response.status >= 400 && response.status < 500) && pathname !== '/';
220
351
  }
221
352
  /**
222
- * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
223
- *
224
- * If the server returns a 404 for the well-known endpoint, this function will
225
- * return `undefined`. Any other errors will be thrown as exceptions.
353
+ * Generic function for discovering OAuth metadata with fallback support
226
354
  */
227
- async function discoverOAuthMetadata(authorizationServerUrl, opts) {
228
- var _a;
229
- const issuer = new URL(authorizationServerUrl);
230
- const protocolVersion = (_a = void 0 ) !== null && _a !== void 0 ? _a : LATEST_PROTOCOL_VERSION;
231
- // Try path-aware discovery first (RFC 8414 compliant)
232
- const wellKnownPath = buildWellKnownPath(issuer.pathname);
233
- const pathAwareUrl = new URL(wellKnownPath, issuer);
234
- let response = await tryMetadataDiscovery(pathAwareUrl, protocolVersion);
235
- // If path-aware discovery fails with 404, try fallback to root discovery
236
- if (shouldAttemptFallback(response, issuer.pathname)) {
237
- const rootUrl = new URL("/.well-known/oauth-authorization-server", issuer);
238
- response = await tryMetadataDiscovery(rootUrl, protocolVersion);
355
+ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, opts) {
356
+ var _a, _b;
357
+ const issuer = new URL(serverUrl);
358
+ const protocolVersion = (_a = opts === null || opts === void 0 ? void 0 : opts.protocolVersion) !== null && _a !== void 0 ? _a : LATEST_PROTOCOL_VERSION;
359
+ let url;
360
+ if (opts === null || opts === void 0 ? void 0 : opts.metadataUrl) {
361
+ url = new URL(opts.metadataUrl);
239
362
  }
240
- if (!response || response.status === 404) {
241
- return undefined;
363
+ else {
364
+ // Try path-aware discovery first
365
+ const wellKnownPath = buildWellKnownPath(wellKnownType, issuer.pathname);
366
+ url = new URL(wellKnownPath, (_b = opts === null || opts === void 0 ? void 0 : opts.metadataServerUrl) !== null && _b !== void 0 ? _b : issuer);
367
+ url.search = issuer.search;
368
+ }
369
+ let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
370
+ // If path-aware discovery fails with 404 and we're not already at root, try fallback to root discovery
371
+ if (!(opts === null || opts === void 0 ? void 0 : opts.metadataUrl) && shouldAttemptFallback(response, issuer.pathname)) {
372
+ const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
373
+ response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
374
+ }
375
+ return response;
376
+ }
377
+ /**
378
+ * Builds a list of discovery URLs to try for authorization server metadata.
379
+ * URLs are returned in priority order:
380
+ * 1. OAuth metadata at the given URL
381
+ * 2. OAuth metadata at root (if URL has path)
382
+ * 3. OIDC metadata endpoints
383
+ */
384
+ function buildDiscoveryUrls(authorizationServerUrl) {
385
+ const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
386
+ const hasPath = url.pathname !== '/';
387
+ const urlsToTry = [];
388
+ if (!hasPath) {
389
+ // Root path: https://example.com/.well-known/oauth-authorization-server
390
+ urlsToTry.push({
391
+ url: new URL('/.well-known/oauth-authorization-server', url.origin),
392
+ type: 'oauth'
393
+ });
394
+ // OIDC: https://example.com/.well-known/openid-configuration
395
+ urlsToTry.push({
396
+ url: new URL(`/.well-known/openid-configuration`, url.origin),
397
+ type: 'oidc'
398
+ });
399
+ return urlsToTry;
242
400
  }
243
- if (!response.ok) {
244
- throw new Error(`HTTP ${response.status} trying to load well-known OAuth metadata`);
401
+ // Strip trailing slash from pathname to avoid double slashes
402
+ let pathname = url.pathname;
403
+ if (pathname.endsWith('/')) {
404
+ pathname = pathname.slice(0, -1);
405
+ }
406
+ // 1. OAuth metadata at the given URL
407
+ // Insert well-known before the path: https://example.com/.well-known/oauth-authorization-server/tenant1
408
+ urlsToTry.push({
409
+ url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
410
+ type: 'oauth'
411
+ });
412
+ // Root path: https://example.com/.well-known/oauth-authorization-server
413
+ urlsToTry.push({
414
+ url: new URL('/.well-known/oauth-authorization-server', url.origin),
415
+ type: 'oauth'
416
+ });
417
+ // 3. OIDC metadata endpoints
418
+ // RFC 8414 style: Insert /.well-known/openid-configuration before the path
419
+ urlsToTry.push({
420
+ url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
421
+ type: 'oidc'
422
+ });
423
+ // OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path
424
+ urlsToTry.push({
425
+ url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
426
+ type: 'oidc'
427
+ });
428
+ return urlsToTry;
429
+ }
430
+ /**
431
+ * Discovers authorization server metadata with support for RFC 8414 OAuth 2.0 Authorization Server Metadata
432
+ * and OpenID Connect Discovery 1.0 specifications.
433
+ *
434
+ * This function implements a fallback strategy for authorization server discovery:
435
+ * 1. Attempts RFC 8414 OAuth metadata discovery first
436
+ * 2. If OAuth discovery fails, falls back to OpenID Connect Discovery
437
+ *
438
+ * @param authorizationServerUrl - The authorization server URL obtained from the MCP Server's
439
+ * protected resource metadata, or the MCP server's URL if the
440
+ * metadata was not found.
441
+ * @param options - Configuration options
442
+ * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
443
+ * @param options.protocolVersion - MCP protocol version to use, defaults to LATEST_PROTOCOL_VERSION
444
+ * @returns Promise resolving to authorization server metadata, or undefined if discovery fails
445
+ */
446
+ async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn = fetch, protocolVersion = LATEST_PROTOCOL_VERSION, } = {}) {
447
+ var _a;
448
+ const headers = { 'MCP-Protocol-Version': protocolVersion };
449
+ // Get the list of URLs to try
450
+ const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
451
+ // Try each URL in order
452
+ for (const { url: endpointUrl, type } of urlsToTry) {
453
+ const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
454
+ if (!response) {
455
+ /**
456
+ * CORS error occurred - don't throw as the endpoint may not allow CORS,
457
+ * continue trying other possible endpoints
458
+ */
459
+ continue;
460
+ }
461
+ if (!response.ok) {
462
+ // Continue looking for any 4xx response code.
463
+ if (response.status >= 400 && response.status < 500) {
464
+ continue; // Try next URL
465
+ }
466
+ throw new Error(`HTTP ${response.status} trying to load ${type === 'oauth' ? 'OAuth' : 'OpenID provider'} metadata from ${endpointUrl}`);
467
+ }
468
+ // Parse and validate based on type
469
+ if (type === 'oauth') {
470
+ return OAuthMetadataSchema.parse(await response.json());
471
+ }
472
+ else {
473
+ const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
474
+ // MCP spec requires OIDC providers to support S256 PKCE
475
+ if (!((_a = metadata.code_challenge_methods_supported) === null || _a === void 0 ? void 0 : _a.includes('S256'))) {
476
+ throw new Error(`Incompatible OIDC provider at ${endpointUrl}: does not support S256 code challenge method required by MCP specification`);
477
+ }
478
+ return metadata;
479
+ }
245
480
  }
246
- return OAuthMetadataSchema.parse(await response.json());
481
+ return undefined;
247
482
  }
248
483
  /**
249
484
  * Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
@@ -280,6 +515,12 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
280
515
  if (scope) {
281
516
  authorizationUrl.searchParams.set("scope", scope);
282
517
  }
518
+ if (scope === null || scope === void 0 ? void 0 : scope.includes("offline_access")) {
519
+ // if the request includes the OIDC-only "offline_access" scope,
520
+ // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
521
+ // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
522
+ authorizationUrl.searchParams.append("prompt", "consent");
523
+ }
283
524
  if (resource) {
284
525
  authorizationUrl.searchParams.set("resource", resource.href);
285
526
  }
@@ -287,50 +528,73 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
287
528
  }
288
529
  /**
289
530
  * Exchanges an authorization code for an access token with the given server.
531
+ *
532
+ * Supports multiple client authentication methods as specified in OAuth 2.1:
533
+ * - Automatically selects the best authentication method based on server support
534
+ * - Falls back to appropriate defaults when server metadata is unavailable
535
+ *
536
+ * @param authorizationServerUrl - The authorization server's base URL
537
+ * @param options - Configuration object containing client info, auth code, etc.
538
+ * @returns Promise resolving to OAuth tokens
539
+ * @throws {Error} When token exchange fails or authentication is invalid
290
540
  */
291
- async function exchangeAuthorization(authorizationServerUrl, { metadata, clientInformation, authorizationCode, codeVerifier, redirectUri, resource, }) {
541
+ async function exchangeAuthorization(authorizationServerUrl, { metadata, clientInformation, authorizationCode, codeVerifier, redirectUri, resource, addClientAuthentication, fetchFn, }) {
542
+ var _a;
292
543
  const grantType = "authorization_code";
293
- let tokenUrl;
294
- if (metadata) {
295
- tokenUrl = new URL(metadata.token_endpoint);
296
- if (metadata.grant_types_supported &&
297
- !metadata.grant_types_supported.includes(grantType)) {
298
- throw new Error(`Incompatible auth server: does not support grant type ${grantType}`);
299
- }
300
- }
301
- else {
302
- tokenUrl = new URL("/token", authorizationServerUrl);
544
+ const tokenUrl = (metadata === null || metadata === void 0 ? void 0 : metadata.token_endpoint)
545
+ ? new URL(metadata.token_endpoint)
546
+ : new URL("/token", authorizationServerUrl);
547
+ if ((metadata === null || metadata === void 0 ? void 0 : metadata.grant_types_supported) &&
548
+ !metadata.grant_types_supported.includes(grantType)) {
549
+ throw new Error(`Incompatible auth server: does not support grant type ${grantType}`);
303
550
  }
304
551
  // Exchange code for tokens
552
+ const headers = new Headers({
553
+ "Content-Type": "application/x-www-form-urlencoded",
554
+ "Accept": "application/json",
555
+ });
305
556
  const params = new URLSearchParams({
306
557
  grant_type: grantType,
307
- client_id: clientInformation.client_id,
308
558
  code: authorizationCode,
309
559
  code_verifier: codeVerifier,
310
560
  redirect_uri: String(redirectUri),
311
561
  });
312
- if (clientInformation.client_secret) {
313
- params.set("client_secret", clientInformation.client_secret);
562
+ if (addClientAuthentication) {
563
+ addClientAuthentication(headers, params, authorizationServerUrl, metadata);
564
+ }
565
+ else {
566
+ // Determine and apply client authentication method
567
+ const supportedMethods = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.token_endpoint_auth_methods_supported) !== null && _a !== void 0 ? _a : [];
568
+ const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
569
+ applyClientAuthentication(authMethod, clientInformation, headers, params);
314
570
  }
315
571
  if (resource) {
316
572
  params.set("resource", resource.href);
317
573
  }
318
- const response = await fetch(tokenUrl, {
574
+ const response = await (fetchFn !== null && fetchFn !== void 0 ? fetchFn : fetch)(tokenUrl, {
319
575
  method: "POST",
320
- headers: {
321
- "Content-Type": "application/x-www-form-urlencoded",
322
- },
576
+ headers,
323
577
  body: params,
324
578
  });
325
579
  if (!response.ok) {
326
- throw new Error(`Token exchange failed: HTTP ${response.status}`);
580
+ throw await parseErrorResponse(response);
327
581
  }
328
582
  return OAuthTokensSchema.parse(await response.json());
329
583
  }
330
584
  /**
331
585
  * Exchange a refresh token for an updated access token.
586
+ *
587
+ * Supports multiple client authentication methods as specified in OAuth 2.1:
588
+ * - Automatically selects the best authentication method based on server support
589
+ * - Preserves the original refresh token if a new one is not returned
590
+ *
591
+ * @param authorizationServerUrl - The authorization server's base URL
592
+ * @param options - Configuration object containing client info, refresh token, etc.
593
+ * @returns Promise resolving to OAuth tokens (preserves original refresh_token if not replaced)
594
+ * @throws {Error} When token refresh fails or authentication is invalid
332
595
  */
333
- async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, }) {
596
+ async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn, }) {
597
+ var _a;
334
598
  const grantType = "refresh_token";
335
599
  let tokenUrl;
336
600
  if (metadata) {
@@ -344,33 +608,39 @@ async function refreshAuthorization(authorizationServerUrl, { metadata, clientIn
344
608
  tokenUrl = new URL("/token", authorizationServerUrl);
345
609
  }
346
610
  // Exchange refresh token
611
+ const headers = new Headers({
612
+ "Content-Type": "application/x-www-form-urlencoded",
613
+ });
347
614
  const params = new URLSearchParams({
348
615
  grant_type: grantType,
349
- client_id: clientInformation.client_id,
350
616
  refresh_token: refreshToken,
351
617
  });
352
- if (clientInformation.client_secret) {
353
- params.set("client_secret", clientInformation.client_secret);
618
+ if (addClientAuthentication) {
619
+ addClientAuthentication(headers, params, authorizationServerUrl, metadata);
620
+ }
621
+ else {
622
+ // Determine and apply client authentication method
623
+ const supportedMethods = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.token_endpoint_auth_methods_supported) !== null && _a !== void 0 ? _a : [];
624
+ const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
625
+ applyClientAuthentication(authMethod, clientInformation, headers, params);
354
626
  }
355
627
  if (resource) {
356
628
  params.set("resource", resource.href);
357
629
  }
358
- const response = await fetch(tokenUrl, {
630
+ const response = await (fetchFn !== null && fetchFn !== void 0 ? fetchFn : fetch)(tokenUrl, {
359
631
  method: "POST",
360
- headers: {
361
- "Content-Type": "application/x-www-form-urlencoded",
362
- },
632
+ headers,
363
633
  body: params,
364
634
  });
365
635
  if (!response.ok) {
366
- throw new Error(`Token refresh failed: HTTP ${response.status}`);
636
+ throw await parseErrorResponse(response);
367
637
  }
368
638
  return OAuthTokensSchema.parse({ refresh_token: refreshToken, ...(await response.json()) });
369
639
  }
370
640
  /**
371
641
  * Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
372
642
  */
373
- async function registerClient(authorizationServerUrl, { metadata, clientMetadata, }) {
643
+ async function registerClient(authorizationServerUrl, { metadata, clientMetadata, fetchFn, }) {
374
644
  let registrationUrl;
375
645
  if (metadata) {
376
646
  if (!metadata.registration_endpoint) {
@@ -381,7 +651,7 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
381
651
  else {
382
652
  registrationUrl = new URL("/register", authorizationServerUrl);
383
653
  }
384
- const response = await fetch(registrationUrl, {
654
+ const response = await (fetchFn !== null && fetchFn !== void 0 ? fetchFn : fetch)(registrationUrl, {
385
655
  method: "POST",
386
656
  headers: {
387
657
  "Content-Type": "application/json",
@@ -389,10 +659,10 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
389
659
  body: JSON.stringify(clientMetadata),
390
660
  });
391
661
  if (!response.ok) {
392
- throw new Error(`Dynamic client registration failed: HTTP ${response.status}`);
662
+ throw await parseErrorResponse(response);
393
663
  }
394
664
  return OAuthClientInformationFullSchema.parse(await response.json());
395
665
  }
396
666
 
397
- export { UnauthorizedError, auth, discoverOAuthMetadata, discoverOAuthProtectedResourceMetadata, exchangeAuthorization, extractResourceMetadataUrl, refreshAuthorization, registerClient, selectResourceURL, startAuthorization };
667
+ export { UnauthorizedError, auth, buildDiscoveryUrls, discoverAuthorizationServerMetadata, discoverOAuthProtectedResourceMetadata, exchangeAuthorization, extractResourceMetadataUrl, parseErrorResponse, refreshAuthorization, registerClient, selectResourceURL, startAuthorization };
398
668
  //# sourceMappingURL=auth.js.map