middleware.js 9.7 KB

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