middleware.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.createMiddleware = exports.applyMiddlewares = exports.withLogging = exports.withOAuth = void 0;
  4. const auth_js_1 = require("./auth.js");
  5. /**
  6. * Creates a fetch wrapper that handles OAuth authentication automatically.
  7. *
  8. * This wrapper will:
  9. * - Add Authorization headers with access tokens
  10. * - Handle 401 responses by attempting re-authentication
  11. * - Retry the original request after successful auth
  12. * - Handle OAuth errors appropriately (InvalidClientError, etc.)
  13. *
  14. * The baseUrl parameter is optional and defaults to using the domain from the request URL.
  15. * However, you should explicitly provide baseUrl when:
  16. * - Making requests to multiple subdomains (e.g., api.example.com, cdn.example.com)
  17. * - Using API paths that differ from OAuth discovery paths (e.g., requesting /api/v1/data but OAuth is at /)
  18. * - The OAuth server is on a different domain than your API requests
  19. * - You want to ensure consistent OAuth behavior regardless of request URLs
  20. *
  21. * For MCP transports, set baseUrl to the same URL you pass to the transport constructor.
  22. *
  23. * Note: This wrapper is designed for general-purpose fetch operations.
  24. * MCP transports (SSE and StreamableHTTP) already have built-in OAuth handling
  25. * and should not need this wrapper.
  26. *
  27. * @param provider - OAuth client provider for authentication
  28. * @param baseUrl - Base URL for OAuth server discovery (defaults to request URL domain)
  29. * @returns A fetch middleware function
  30. */
  31. const withOAuth = (provider, baseUrl) => next => {
  32. return async (input, init) => {
  33. const makeRequest = async () => {
  34. const headers = new Headers(init?.headers);
  35. // Add authorization header if tokens are available
  36. const tokens = await provider.tokens();
  37. if (tokens) {
  38. headers.set('Authorization', `Bearer ${tokens.access_token}`);
  39. }
  40. return await next(input, { ...init, headers });
  41. };
  42. let response = await makeRequest();
  43. // Handle 401 responses by attempting re-authentication
  44. if (response.status === 401) {
  45. try {
  46. const { resourceMetadataUrl, scope } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
  47. // Use provided baseUrl or extract from request URL
  48. const serverUrl = baseUrl || (typeof input === 'string' ? new URL(input).origin : input.origin);
  49. const result = await (0, auth_js_1.auth)(provider, {
  50. serverUrl,
  51. resourceMetadataUrl,
  52. scope,
  53. fetchFn: next
  54. });
  55. if (result === 'REDIRECT') {
  56. throw new auth_js_1.UnauthorizedError('Authentication requires user authorization - redirect initiated');
  57. }
  58. if (result !== 'AUTHORIZED') {
  59. throw new auth_js_1.UnauthorizedError(`Authentication failed with result: ${result}`);
  60. }
  61. // Retry the request with fresh tokens
  62. response = await makeRequest();
  63. }
  64. catch (error) {
  65. if (error instanceof auth_js_1.UnauthorizedError) {
  66. throw error;
  67. }
  68. throw new auth_js_1.UnauthorizedError(`Failed to re-authenticate: ${error instanceof Error ? error.message : String(error)}`);
  69. }
  70. }
  71. // If we still have a 401 after re-auth attempt, throw an error
  72. if (response.status === 401) {
  73. const url = typeof input === 'string' ? input : input.toString();
  74. throw new auth_js_1.UnauthorizedError(`Authentication failed for ${url}`);
  75. }
  76. return response;
  77. };
  78. };
  79. exports.withOAuth = withOAuth;
  80. /**
  81. * Creates a fetch middleware that logs HTTP requests and responses.
  82. *
  83. * When called without arguments `withLogging()`, it uses the default logger that:
  84. * - Logs successful requests (2xx) to `console.log`
  85. * - Logs error responses (4xx/5xx) and network errors to `console.error`
  86. * - Logs all requests regardless of status (statusLevel: 0)
  87. * - Does not include request or response headers in logs
  88. * - Measures and displays request duration in milliseconds
  89. *
  90. * Important: the default logger uses both `console.log` and `console.error` so it should not be used with
  91. * `stdio` transports and applications.
  92. *
  93. * @param options - Logging configuration options
  94. * @returns A fetch middleware function
  95. */
  96. const withLogging = (options = {}) => {
  97. const { logger, includeRequestHeaders = false, includeResponseHeaders = false, statusLevel = 0 } = options;
  98. const defaultLogger = input => {
  99. const { method, url, status, statusText, duration, requestHeaders, responseHeaders, error } = input;
  100. let message = error
  101. ? `HTTP ${method} ${url} failed: ${error.message} (${duration}ms)`
  102. : `HTTP ${method} ${url} ${status} ${statusText} (${duration}ms)`;
  103. // Add headers to message if requested
  104. if (includeRequestHeaders && requestHeaders) {
  105. const reqHeaders = Array.from(requestHeaders.entries())
  106. .map(([key, value]) => `${key}: ${value}`)
  107. .join(', ');
  108. message += `\n Request Headers: {${reqHeaders}}`;
  109. }
  110. if (includeResponseHeaders && responseHeaders) {
  111. const resHeaders = Array.from(responseHeaders.entries())
  112. .map(([key, value]) => `${key}: ${value}`)
  113. .join(', ');
  114. message += `\n Response Headers: {${resHeaders}}`;
  115. }
  116. if (error || status >= 400) {
  117. // eslint-disable-next-line no-console
  118. console.error(message);
  119. }
  120. else {
  121. // eslint-disable-next-line no-console
  122. console.log(message);
  123. }
  124. };
  125. const logFn = logger || defaultLogger;
  126. return next => async (input, init) => {
  127. const startTime = performance.now();
  128. const method = init?.method || 'GET';
  129. const url = typeof input === 'string' ? input : input.toString();
  130. const requestHeaders = includeRequestHeaders ? new Headers(init?.headers) : undefined;
  131. try {
  132. const response = await next(input, init);
  133. const duration = performance.now() - startTime;
  134. // Only log if status meets the log level threshold
  135. if (response.status >= statusLevel) {
  136. logFn({
  137. method,
  138. url,
  139. status: response.status,
  140. statusText: response.statusText,
  141. duration,
  142. requestHeaders,
  143. responseHeaders: includeResponseHeaders ? response.headers : undefined
  144. });
  145. }
  146. return response;
  147. }
  148. catch (error) {
  149. const duration = performance.now() - startTime;
  150. // Always log errors regardless of log level
  151. logFn({
  152. method,
  153. url,
  154. status: 0,
  155. statusText: 'Network Error',
  156. duration,
  157. requestHeaders,
  158. error: error
  159. });
  160. throw error;
  161. }
  162. };
  163. };
  164. exports.withLogging = withLogging;
  165. /**
  166. * Composes multiple fetch middleware functions into a single middleware pipeline.
  167. * Middleware are applied in the order they appear, creating a chain of handlers.
  168. *
  169. * @example
  170. * ```typescript
  171. * // Create a middleware pipeline that handles both OAuth and logging
  172. * const enhancedFetch = applyMiddlewares(
  173. * withOAuth(oauthProvider, 'https://api.example.com'),
  174. * withLogging({ statusLevel: 400 })
  175. * )(fetch);
  176. *
  177. * // Use the enhanced fetch - it will handle auth and log errors
  178. * const response = await enhancedFetch('https://api.example.com/data');
  179. * ```
  180. *
  181. * @param middleware - Array of fetch middleware to compose into a pipeline
  182. * @returns A single composed middleware function
  183. */
  184. const applyMiddlewares = (...middleware) => {
  185. return next => {
  186. return middleware.reduce((handler, mw) => mw(handler), next);
  187. };
  188. };
  189. exports.applyMiddlewares = applyMiddlewares;
  190. /**
  191. * Helper function to create custom fetch middleware with cleaner syntax.
  192. * Provides the next handler and request details as separate parameters for easier access.
  193. *
  194. * @example
  195. * ```typescript
  196. * // Create custom authentication middleware
  197. * const customAuthMiddleware = createMiddleware(async (next, input, init) => {
  198. * const headers = new Headers(init?.headers);
  199. * headers.set('X-Custom-Auth', 'my-token');
  200. *
  201. * const response = await next(input, { ...init, headers });
  202. *
  203. * if (response.status === 401) {
  204. * console.log('Authentication failed');
  205. * }
  206. *
  207. * return response;
  208. * });
  209. *
  210. * // Create conditional middleware
  211. * const conditionalMiddleware = createMiddleware(async (next, input, init) => {
  212. * const url = typeof input === 'string' ? input : input.toString();
  213. *
  214. * // Only add headers for API routes
  215. * if (url.includes('/api/')) {
  216. * const headers = new Headers(init?.headers);
  217. * headers.set('X-API-Version', 'v2');
  218. * return next(input, { ...init, headers });
  219. * }
  220. *
  221. * // Pass through for non-API routes
  222. * return next(input, init);
  223. * });
  224. *
  225. * // Create caching middleware
  226. * const cacheMiddleware = createMiddleware(async (next, input, init) => {
  227. * const cacheKey = typeof input === 'string' ? input : input.toString();
  228. *
  229. * // Check cache first
  230. * const cached = await getFromCache(cacheKey);
  231. * if (cached) {
  232. * return new Response(cached, { status: 200 });
  233. * }
  234. *
  235. * // Make request and cache result
  236. * const response = await next(input, init);
  237. * if (response.ok) {
  238. * await saveToCache(cacheKey, await response.clone().text());
  239. * }
  240. *
  241. * return response;
  242. * });
  243. * ```
  244. *
  245. * @param handler - Function that receives the next handler and request parameters
  246. * @returns A fetch middleware function
  247. */
  248. const createMiddleware = (handler) => {
  249. return next => (input, init) => handler(next, input, init);
  250. };
  251. exports.createMiddleware = createMiddleware;
  252. //# sourceMappingURL=middleware.js.map