@mcp-z/client 1.2.0 → 1.3.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 (95) hide show
  1. package/README.md +22 -0
  2. package/dist/cjs/auth/capability-discovery.js +16 -6
  3. package/dist/cjs/auth/capability-discovery.js.map +1 -1
  4. package/dist/cjs/auth/discovery-fetch.d.cts +22 -2
  5. package/dist/cjs/auth/discovery-fetch.d.ts +22 -2
  6. package/dist/cjs/auth/discovery-fetch.js +440 -124
  7. package/dist/cjs/auth/discovery-fetch.js.map +1 -1
  8. package/dist/cjs/auth/interactive-oauth-flow.d.cts +9 -2
  9. package/dist/cjs/auth/interactive-oauth-flow.d.ts +9 -2
  10. package/dist/cjs/auth/interactive-oauth-flow.js +29 -11
  11. package/dist/cjs/auth/interactive-oauth-flow.js.map +1 -1
  12. package/dist/cjs/auth/oauth-callback-listener.js +4 -0
  13. package/dist/cjs/auth/oauth-callback-listener.js.map +1 -1
  14. package/dist/cjs/auth/types.d.cts +29 -4
  15. package/dist/cjs/auth/types.d.ts +29 -4
  16. package/dist/cjs/auth/types.js.map +1 -1
  17. package/dist/cjs/client-helpers.d.cts +5 -6
  18. package/dist/cjs/client-helpers.d.ts +5 -6
  19. package/dist/cjs/client-helpers.js +8 -8
  20. package/dist/cjs/client-helpers.js.map +1 -1
  21. package/dist/cjs/connection/connect-client.d.cts +12 -11
  22. package/dist/cjs/connection/connect-client.d.ts +12 -11
  23. package/dist/cjs/connection/connect-client.js +31 -47
  24. package/dist/cjs/connection/connect-client.js.map +1 -1
  25. package/dist/cjs/connection/existing-process-transport.d.cts +1 -2
  26. package/dist/cjs/connection/existing-process-transport.d.ts +1 -2
  27. package/dist/cjs/connection/existing-process-transport.js +3 -3
  28. package/dist/cjs/connection/existing-process-transport.js.map +1 -1
  29. package/dist/cjs/dcr/dcr-authenticator.d.cts +36 -5
  30. package/dist/cjs/dcr/dcr-authenticator.d.ts +36 -5
  31. package/dist/cjs/dcr/dcr-authenticator.js +473 -57
  32. package/dist/cjs/dcr/dcr-authenticator.js.map +1 -1
  33. package/dist/cjs/dcr/dynamic-client-registrar.d.cts +2 -2
  34. package/dist/cjs/dcr/dynamic-client-registrar.d.ts +2 -2
  35. package/dist/cjs/dcr/dynamic-client-registrar.js +10 -11
  36. package/dist/cjs/dcr/dynamic-client-registrar.js.map +1 -1
  37. package/dist/cjs/index.d.cts +2 -0
  38. package/dist/cjs/index.d.ts +2 -0
  39. package/dist/cjs/index.js +8 -2
  40. package/dist/cjs/index.js.map +1 -1
  41. package/dist/cjs/lib/url-utils.d.cts +17 -0
  42. package/dist/cjs/lib/url-utils.d.ts +17 -0
  43. package/dist/cjs/lib/url-utils.js +22 -2
  44. package/dist/cjs/lib/url-utils.js.map +1 -1
  45. package/dist/cjs/response-wrappers.d.cts +1 -1
  46. package/dist/cjs/response-wrappers.d.ts +1 -1
  47. package/dist/cjs/response-wrappers.js.map +1 -1
  48. package/dist/cjs/search/search.d.cts +1 -1
  49. package/dist/cjs/search/search.d.ts +1 -1
  50. package/dist/cjs/search/search.js.map +1 -1
  51. package/dist/esm/auth/capability-discovery.js +16 -6
  52. package/dist/esm/auth/capability-discovery.js.map +1 -1
  53. package/dist/esm/auth/discovery-fetch.d.ts +22 -2
  54. package/dist/esm/auth/discovery-fetch.js +234 -54
  55. package/dist/esm/auth/discovery-fetch.js.map +1 -1
  56. package/dist/esm/auth/interactive-oauth-flow.d.ts +9 -2
  57. package/dist/esm/auth/interactive-oauth-flow.js +28 -10
  58. package/dist/esm/auth/interactive-oauth-flow.js.map +1 -1
  59. package/dist/esm/auth/oauth-callback-listener.js +4 -0
  60. package/dist/esm/auth/oauth-callback-listener.js.map +1 -1
  61. package/dist/esm/auth/types.d.ts +29 -4
  62. package/dist/esm/auth/types.js.map +1 -1
  63. package/dist/esm/client-helpers.d.ts +5 -6
  64. package/dist/esm/client-helpers.js +8 -8
  65. package/dist/esm/client-helpers.js.map +1 -1
  66. package/dist/esm/connection/connect-client.d.ts +12 -11
  67. package/dist/esm/connection/connect-client.js +32 -38
  68. package/dist/esm/connection/connect-client.js.map +1 -1
  69. package/dist/esm/connection/existing-process-transport.d.ts +1 -2
  70. package/dist/esm/connection/existing-process-transport.js +1 -1
  71. package/dist/esm/connection/existing-process-transport.js.map +1 -1
  72. package/dist/esm/dcr/dcr-authenticator.d.ts +36 -5
  73. package/dist/esm/dcr/dcr-authenticator.js +129 -52
  74. package/dist/esm/dcr/dcr-authenticator.js.map +1 -1
  75. package/dist/esm/dcr/dynamic-client-registrar.d.ts +2 -2
  76. package/dist/esm/dcr/dynamic-client-registrar.js +7 -6
  77. package/dist/esm/dcr/dynamic-client-registrar.js.map +1 -1
  78. package/dist/esm/index.d.ts +2 -0
  79. package/dist/esm/index.js +4 -1
  80. package/dist/esm/index.js.map +1 -1
  81. package/dist/esm/lib/url-utils.d.ts +17 -0
  82. package/dist/esm/lib/url-utils.js +34 -2
  83. package/dist/esm/lib/url-utils.js.map +1 -1
  84. package/dist/esm/response-wrappers.d.ts +1 -1
  85. package/dist/esm/response-wrappers.js.map +1 -1
  86. package/dist/esm/search/search.d.ts +1 -1
  87. package/dist/esm/search/search.js.map +1 -1
  88. package/package.json +4 -4
  89. package/dist/cjs/monkey-patches.d.cts +0 -6
  90. package/dist/cjs/monkey-patches.d.ts +0 -6
  91. package/dist/cjs/monkey-patches.js +0 -233
  92. package/dist/cjs/monkey-patches.js.map +0 -1
  93. package/dist/esm/monkey-patches.d.ts +0 -6
  94. package/dist/esm/monkey-patches.js +0 -32
  95. package/dist/esm/monkey-patches.js.map +0 -1
package/README.md CHANGED
@@ -156,6 +156,28 @@ const client = await registry.connect('todoist', {
156
156
  });
157
157
  ```
158
158
 
159
+ ## Protocol version negotiation
160
+
161
+ By default a connect performs the plain 2025 MCP connect sequence. Pass `versionNegotiation` to negotiate the protocol revision instead:
162
+
163
+ ```ts
164
+ // Probe the server first; connect at the newest revision it offers,
165
+ // falling back to the 2025 sequence when it cannot serve the modern era
166
+ const client = await registry.connect('modern-server', {
167
+ versionNegotiation: { mode: 'auto' }
168
+ });
169
+
170
+ // Require the 2026-07-28 revision; a server that cannot serve it fails
171
+ // the connect with SdkErrorCode.EraNegotiationFailed
172
+ const pinned = await registry.connect('strict-server', {
173
+ versionNegotiation: { mode: { pin: '2026-07-28' } }
174
+ });
175
+ ```
176
+
177
+ After connecting, `client.getProtocolEra()` returns `'modern'` or `'legacy'` and `client.getNegotiatedProtocolVersion()` the revision the server settled on.
178
+
179
+ Note: with `mode: 'auto'` against a stdio server, a legacy server that never answers the `server/discover` probe costs the full request timeout (60s) before the client falls back to the 2025 sequence. The probe ends fast when the server answers it at all — with any reply, even a "method not found" error.
180
+
159
181
  ## Requirements
160
182
 
161
183
  - Node.js >= 22
@@ -173,11 +173,21 @@ function _ts_generator(thisArg, body) {
173
173
  * if (caps.supportsDcr) {
174
174
  * console.log('Registration endpoint:', caps.registrationEndpoint);
175
175
  * }
176
- */ function buildCapabilities(metadata, scopes) {
176
+ */ function buildCapabilities(metadata, scopes, resource) {
177
177
  var supportsDcr = !!metadata.registration_endpoint;
178
178
  var capabilities = {
179
- supportsDcr: supportsDcr
179
+ supportsDcr: supportsDcr,
180
+ authorizationResponseIssSupported: metadata.authorization_response_iss_parameter_supported === true
180
181
  };
182
+ if (metadata.issuer) {
183
+ capabilities.issuer = metadata.issuer;
184
+ }
185
+ // Carried from the RFC 9728 document, not from the URL we were given: the
186
+ // resource server names itself, and that name is what RFC 8707 audience-binds
187
+ // a token to.
188
+ if (resource) {
189
+ capabilities.resource = resource;
190
+ }
181
191
  if (metadata.registration_endpoint) {
182
192
  capabilities.registrationEndpoint = metadata.registration_endpoint;
183
193
  }
@@ -195,7 +205,7 @@ function _ts_generator(thisArg, body) {
195
205
  }
196
206
  return capabilities;
197
207
  }
198
- function resolveCapabilitiesFromAuthorizationServer(authServerUrl, scopes, allowLoopback) {
208
+ function resolveCapabilitiesFromAuthorizationServer(authServerUrl, scopes, allowLoopback, resource) {
199
209
  return _async_to_generator(function() {
200
210
  var metadata;
201
211
  return _ts_generator(this, function(_state) {
@@ -215,7 +225,7 @@ function resolveCapabilitiesFromAuthorizationServer(authServerUrl, scopes, allow
215
225
  ];
216
226
  return [
217
227
  2,
218
- buildCapabilities(metadata, scopes)
228
+ buildCapabilities(metadata, scopes, resource)
219
229
  ];
220
230
  }
221
231
  });
@@ -261,7 +271,7 @@ function probeAuthCapabilities(baseUrl) {
261
271
  }
262
272
  return [
263
273
  4,
264
- resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported, allowLoopback)
274
+ resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported, allowLoopback, resourceMetadata.resource)
265
275
  ];
266
276
  case 2:
267
277
  capabilities = _state.sent();
@@ -283,7 +293,7 @@ function probeAuthCapabilities(baseUrl) {
283
293
  ];
284
294
  return [
285
295
  4,
286
- resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported, allowLoopback)
296
+ resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported, allowLoopback, resourceMetadata.resource)
287
297
  ];
288
298
  case 4:
289
299
  issuerCapabilities = _state.sent();
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { normalizeUrl } from '../lib/url-utils.ts';\nimport { isLoopbackUrl } from './discovery-fetch.ts';\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 → RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[]): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr };\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes: string[] | undefined, allowLoopback: boolean): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl, { allowLoopback });\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n const normalizedBaseUrl = normalizeUrl(baseUrl);\n // Trust signal for every authorization-server fetch below: whether the\n // configured MCP server is itself loopback, never a remote-supplied URL.\n const allowLoopback = isLoopbackUrl(normalizedBaseUrl);\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp → todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(normalizedBaseUrl);\n\n if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {\n // Found protected resource metadata with authorization servers\n // Discover the authorization server's metadata (RFC 8414)\n const authServerUrl = resourceMetadata.authorization_servers[0];\n if (!authServerUrl) {\n // Array has length > 0 but first element is undefined/null - skip this path\n return { supportsDcr: false };\n }\n const capabilities = await resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported, allowLoopback);\n if (capabilities) {\n return capabilities;\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported, allowLoopback);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, undefined, allowLoopback);\n if (issuerCapabilities) return issuerCapabilities;\n }\n\n // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin\n // This handles same-domain OAuth (traditional setup)\n const origin = getOrigin(normalizedBaseUrl);\n const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin, undefined, allowLoopback);\n if (originCapabilities) return originCapabilities;\n\n // No OAuth metadata found\n return { supportsDcr: false };\n } catch (_error) {\n // Network error, invalid JSON, or other fetch failure\n // Gracefully degrade - assume no DCR support\n return { supportsDcr: false };\n }\n}\n"],"names":["probeAuthCapabilities","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","supportsDcr","registration_endpoint","capabilities","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","allowLoopback","discoverAuthorizationServerMetadata","baseUrl","normalizedBaseUrl","resourceMetadata","issuer","issuerCapabilities","originCapabilities","_error","normalizeUrl","isLoopbackUrl","discoverProtectedResourceMetadata","authorization_servers","discoverAuthorizationServerIssuer","undefined"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { normalizeUrl } from '../lib/url-utils.ts';\nimport { isLoopbackUrl } from './discovery-fetch.ts';\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 → RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[], resource?: string): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr, authorizationResponseIssSupported: metadata.authorization_response_iss_parameter_supported === true };\n\n if (metadata.issuer) {\n capabilities.issuer = metadata.issuer;\n }\n\n // Carried from the RFC 9728 document, not from the URL we were given: the\n // resource server names itself, and that name is what RFC 8707 audience-binds\n // a token to.\n if (resource) {\n capabilities.resource = resource;\n }\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes: string[] | undefined, allowLoopback: boolean, resource?: string): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl, { allowLoopback });\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes, resource);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n const normalizedBaseUrl = normalizeUrl(baseUrl);\n // Trust signal for every authorization-server fetch below: whether the\n // configured MCP server is itself loopback, never a remote-supplied URL.\n const allowLoopback = isLoopbackUrl(normalizedBaseUrl);\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp → todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(normalizedBaseUrl);\n\n if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {\n // Found protected resource metadata with authorization servers\n // Discover the authorization server's metadata (RFC 8414)\n const authServerUrl = resourceMetadata.authorization_servers[0];\n if (!authServerUrl) {\n // Array has length > 0 but first element is undefined/null - skip this path\n return { supportsDcr: false };\n }\n const capabilities = await resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported, allowLoopback, resourceMetadata.resource);\n if (capabilities) {\n return capabilities;\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported, allowLoopback, resourceMetadata.resource);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, undefined, allowLoopback);\n if (issuerCapabilities) return issuerCapabilities;\n }\n\n // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin\n // This handles same-domain OAuth (traditional setup)\n const origin = getOrigin(normalizedBaseUrl);\n const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin, undefined, allowLoopback);\n if (originCapabilities) return originCapabilities;\n\n // No OAuth metadata found\n return { supportsDcr: false };\n } catch (_error) {\n // Network error, invalid JSON, or other fetch failure\n // Gracefully degrade - assume no DCR support\n return { supportsDcr: false };\n }\n}\n"],"names":["probeAuthCapabilities","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","resource","supportsDcr","registration_endpoint","capabilities","authorizationResponseIssSupported","authorization_response_iss_parameter_supported","issuer","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","allowLoopback","discoverAuthorizationServerMetadata","baseUrl","normalizedBaseUrl","resourceMetadata","issuerCapabilities","originCapabilities","_error","normalizeUrl","isLoopbackUrl","discoverProtectedResourceMetadata","authorization_servers","discoverAuthorizationServerIssuer","undefined"],"mappings":"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"}
@@ -1,6 +1,15 @@
1
1
  export declare class DiscoveryFetchError extends Error {
2
2
  constructor(message: string);
3
3
  }
4
+ type LookupRecord = {
5
+ address: string;
6
+ family: number;
7
+ };
8
+ /** DNS implementation, injectable for deterministic tests. Same contract as `dns.lookup` with `{ all: true, verbatim: true }`. */
9
+ type Lookup = (hostname: string, options: {
10
+ all: true;
11
+ verbatim: true;
12
+ }, callback: (error: NodeJS.ErrnoException | null, addresses: LookupRecord[]) => void) => void;
4
13
  /**
5
14
  * True if `rawUrl`'s host is loopback, used to decide `allowLoopback` grants
6
15
  * from the URL the caller configured, not remote-supplied data. Fails closed on an unparseable URL.
@@ -13,15 +22,25 @@ export interface DiscoveryFetchOptions {
13
22
  */
14
23
  allowLoopback?: boolean;
15
24
  /**
16
- * Per-request connect/read timeout in ms. Defaults to `DEFAULT_TIMEOUT_MS`;
17
- * overridable so tests can bound slow-failure cases.
25
+ * Per-hop timeout in ms, applied to both the DNS resolution and the request.
26
+ * Defaults to `DEFAULT_TIMEOUT_MS`; overridable so tests can bound slow-failure cases.
18
27
  */
19
28
  timeoutMs?: number;
29
+ /**
30
+ * DNS implementation used for the pre-request resolution, injectable for
31
+ * deterministic tests. Defaults to `dns.lookup`.
32
+ */
33
+ lookup?: Lookup;
20
34
  }
21
35
  /**
22
36
  * Fetches an OAuth-discovery URL with SSRF mitigations applied to the
23
37
  * initial URL and every redirect hop; redirects are validated, not auto-followed.
24
38
  *
39
+ * The hostname is resolved once, every returned address is validated, and the
40
+ * validated set is pinned into the request through a custom `lookup`, so the
41
+ * request cannot resolve the hostname a second time and cannot be steered to
42
+ * a different address by a DNS-rebinding answer.
43
+ *
25
44
  * @param rawUrl - URL to fetch; may be remote-server-supplied (the threat this guards against).
26
45
  * @param init - Standard fetch options; `redirect` is always forced to `'manual'`.
27
46
  * @param context - Short label used only in error messages, never echoing the URL.
@@ -33,3 +52,4 @@ export declare function discoveryFetch(rawUrl: string, init?: RequestInit, conte
33
52
  * while reading (not after buffering the whole thing).
34
53
  */
35
54
  export declare function readDiscoveryJson<T>(response: Response, context: string): Promise<T>;
55
+ export {};
@@ -1,6 +1,15 @@
1
1
  export declare class DiscoveryFetchError extends Error {
2
2
  constructor(message: string);
3
3
  }
4
+ type LookupRecord = {
5
+ address: string;
6
+ family: number;
7
+ };
8
+ /** DNS implementation, injectable for deterministic tests. Same contract as `dns.lookup` with `{ all: true, verbatim: true }`. */
9
+ type Lookup = (hostname: string, options: {
10
+ all: true;
11
+ verbatim: true;
12
+ }, callback: (error: NodeJS.ErrnoException | null, addresses: LookupRecord[]) => void) => void;
4
13
  /**
5
14
  * True if `rawUrl`'s host is loopback, used to decide `allowLoopback` grants
6
15
  * from the URL the caller configured, not remote-supplied data. Fails closed on an unparseable URL.
@@ -13,15 +22,25 @@ export interface DiscoveryFetchOptions {
13
22
  */
14
23
  allowLoopback?: boolean;
15
24
  /**
16
- * Per-request connect/read timeout in ms. Defaults to `DEFAULT_TIMEOUT_MS`;
17
- * overridable so tests can bound slow-failure cases.
25
+ * Per-hop timeout in ms, applied to both the DNS resolution and the request.
26
+ * Defaults to `DEFAULT_TIMEOUT_MS`; overridable so tests can bound slow-failure cases.
18
27
  */
19
28
  timeoutMs?: number;
29
+ /**
30
+ * DNS implementation used for the pre-request resolution, injectable for
31
+ * deterministic tests. Defaults to `dns.lookup`.
32
+ */
33
+ lookup?: Lookup;
20
34
  }
21
35
  /**
22
36
  * Fetches an OAuth-discovery URL with SSRF mitigations applied to the
23
37
  * initial URL and every redirect hop; redirects are validated, not auto-followed.
24
38
  *
39
+ * The hostname is resolved once, every returned address is validated, and the
40
+ * validated set is pinned into the request through a custom `lookup`, so the
41
+ * request cannot resolve the hostname a second time and cannot be steered to
42
+ * a different address by a DNS-rebinding answer.
43
+ *
25
44
  * @param rawUrl - URL to fetch; may be remote-server-supplied (the threat this guards against).
26
45
  * @param init - Standard fetch options; `redirect` is always forced to `'manual'`.
27
46
  * @param context - Short label used only in error messages, never echoing the URL.
@@ -33,3 +52,4 @@ export declare function discoveryFetch(rawUrl: string, init?: RequestInit, conte
33
52
  * while reading (not after buffering the whole thing).
34
53
  */
35
54
  export declare function readDiscoveryJson<T>(response: Response, context: string): Promise<T>;
55
+ export {};