@frontmcp/adapters 0.5.0 → 0.6.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.
@@ -1,8 +1,74 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateBaseUrl = validateBaseUrl;
3
4
  exports.buildRequest = buildRequest;
4
5
  exports.applyAdditionalHeaders = applyAdditionalHeaders;
5
6
  exports.parseResponse = parseResponse;
7
+ /**
8
+ * Coerce a value to string with type validation.
9
+ * Throws if the value is an object/array that can't be safely stringified.
10
+ *
11
+ * @param value - Value to coerce
12
+ * @param paramName - Parameter name for error messages
13
+ * @param location - Parameter location (path/query/header)
14
+ * @returns String representation of the value
15
+ */
16
+ function coerceToString(value, paramName, location) {
17
+ if (value === null || value === undefined) {
18
+ return '';
19
+ }
20
+ if (typeof value === 'object') {
21
+ if (Array.isArray(value)) {
22
+ // Arrays in query params are common - join with comma
23
+ if (location === 'query') {
24
+ return value.map(String).join(',');
25
+ }
26
+ throw new Error(`${location} parameter '${paramName}' cannot be an array. Received: ${JSON.stringify(value)}`);
27
+ }
28
+ throw new Error(`${location} parameter '${paramName}' cannot be an object. Received: ${JSON.stringify(value)}`);
29
+ }
30
+ return String(value);
31
+ }
32
+ /**
33
+ * Validate and normalize a base URL.
34
+ *
35
+ * @param url - URL to validate
36
+ * @returns Validated URL object
37
+ * @throws Error if URL is invalid or uses unsupported protocol
38
+ */
39
+ function validateBaseUrl(url) {
40
+ try {
41
+ const parsed = new URL(url);
42
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
43
+ throw new Error(`Unsupported protocol: ${parsed.protocol}. Only http: and https: are supported.`);
44
+ }
45
+ return parsed;
46
+ }
47
+ catch (err) {
48
+ if (err instanceof Error && err.message.includes('Unsupported protocol')) {
49
+ throw err;
50
+ }
51
+ throw new Error(`Invalid base URL: ${url}`);
52
+ }
53
+ }
54
+ /**
55
+ * Append a cookie to the Cookie header.
56
+ * Validates cookie name according to RFC 6265.
57
+ *
58
+ * @param headers - Headers object to modify
59
+ * @param name - Cookie name
60
+ * @param value - Cookie value (will be URI encoded)
61
+ */
62
+ function appendCookie(headers, name, value) {
63
+ // RFC 6265 cookie-name validation (simplified)
64
+ if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
65
+ throw new Error(`Invalid cookie name: '${name}'. Cookie names must be valid tokens.`);
66
+ }
67
+ const existing = headers.get('Cookie') || '';
68
+ const cookiePair = `${name}=${encodeURIComponent(coerceToString(value, name, 'cookie'))}`;
69
+ const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;
70
+ headers.set('Cookie', combined);
71
+ }
6
72
  /**
7
73
  * Build HTTP request from OpenAPI tool and input parameters
8
74
  *
@@ -13,7 +79,11 @@ exports.parseResponse = parseResponse;
13
79
  * @returns Request configuration ready for fetch
14
80
  */
15
81
  function buildRequest(tool, input, security, baseUrl) {
16
- const apiBaseUrl = tool.metadata.servers?.[0]?.url || baseUrl;
82
+ // Normalize base URL by removing trailing slash to prevent double slashes
83
+ // Validate server URL from OpenAPI spec to prevent SSRF attacks
84
+ const rawBaseUrl = tool.metadata.servers?.[0]?.url || baseUrl;
85
+ validateBaseUrl(rawBaseUrl); // Throws if invalid protocol (e.g., file://, javascript:)
86
+ const apiBaseUrl = rawBaseUrl.replace(/\/+$/, '');
17
87
  let path = tool.metadata.path;
18
88
  const queryParams = new URLSearchParams();
19
89
  const headers = new Headers({
@@ -30,49 +100,57 @@ function buildRequest(tool, input, security, baseUrl) {
30
100
  // Check required parameters
31
101
  if (value === undefined || value === null) {
32
102
  if (mapper.required) {
33
- throw new Error(`Required parameter '${mapper.inputKey}' is missing for operation ${tool.name}`);
103
+ throw new Error(`Required ${mapper.type} parameter '${mapper.key}' (input: '${mapper.inputKey}') is missing for operation '${tool.name}'`);
34
104
  }
35
105
  continue;
36
106
  }
37
107
  // Apply parameter to correct location
38
108
  switch (mapper.type) {
39
109
  case 'path':
40
- path = path.replace(`{${mapper.key}}`, encodeURIComponent(String(value)));
110
+ // Use replaceAll to handle duplicate path parameters (e.g., /users/{id}/posts/{id})
111
+ path = path.replaceAll(`{${mapper.key}}`, encodeURIComponent(coerceToString(value, mapper.key, 'path')));
41
112
  break;
42
113
  case 'query':
43
- queryParams.set(mapper.key, String(value));
114
+ queryParams.set(mapper.key, coerceToString(value, mapper.key, 'query'));
44
115
  break;
45
- case 'header':
46
- headers.set(mapper.key, String(value));
116
+ case 'header': {
117
+ const headerValue = coerceToString(value, mapper.key, 'header');
118
+ // Validate header values for injection attacks
119
+ // Check for: CR, LF, null byte, form feed, vertical tab
120
+ // eslint-disable-next-line no-control-regex
121
+ if (/[\r\n\x00\f\v]/.test(headerValue)) {
122
+ throw new Error(`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`);
123
+ }
124
+ headers.set(mapper.key, headerValue);
47
125
  break;
126
+ }
48
127
  case 'cookie':
49
- // Simple cookie header merge; you may want a more robust cookie encoder.
50
- {
51
- const existing = headers.get('cookie') ?? headers.get('Cookie');
52
- const cookiePair = `${mapper.key}=${encodeURIComponent(String(value))}`;
53
- const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;
54
- headers.set('Cookie', combined);
55
- }
128
+ appendCookie(headers, mapper.key, value);
56
129
  break;
57
130
  case 'body':
58
131
  if (!body)
59
132
  body = {};
60
133
  body[mapper.key] = value;
61
134
  break;
135
+ default:
136
+ throw new Error(`Unknown mapper type '${mapper.type}' for parameter '${mapper.key}' in operation '${tool.name}'`);
62
137
  }
63
138
  }
64
139
  // Add query parameters from security (e.g., API keys in query string)
140
+ // Detect collisions with user-provided query params (security params take precedence)
65
141
  Object.entries(security.query).forEach(([key, value]) => {
66
- queryParams.set(key, value);
142
+ if (queryParams.has(key)) {
143
+ // Security params override user params, but warn about potential misconfiguration
144
+ throw new Error(`Query parameter collision: '${key}' is provided both as user input and security parameter. ` +
145
+ `This could indicate a security misconfiguration in operation '${tool.name}'.`);
146
+ }
147
+ queryParams.set(key, coerceToString(value, key, 'security query'));
67
148
  });
68
149
  // Add cookies from a security context
69
150
  if (security.cookies && Object.keys(security.cookies).length > 0) {
70
- const existing = headers.get('cookie') ?? headers.get('Cookie');
71
- const securityCookieString = Object.entries(security.cookies)
72
- .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
73
- .join('; ');
74
- const combined = existing ? `${existing}; ${securityCookieString}` : securityCookieString;
75
- headers.set('Cookie', combined);
151
+ Object.entries(security.cookies).forEach(([key, value]) => {
152
+ appendCookie(headers, key, value);
153
+ });
76
154
  }
77
155
  // Ensure all path parameters are resolved
78
156
  if (path.includes('{')) {
@@ -96,27 +174,52 @@ function applyAdditionalHeaders(headers, additionalHeaders) {
96
174
  headers.set(key, value);
97
175
  });
98
176
  }
177
+ /** Default max response size: 10MB */
178
+ const DEFAULT_MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
99
179
  /**
100
180
  * Parse API response based on content type
101
181
  *
102
182
  * @param response - Fetch response
183
+ * @param options - Optional parsing options
103
184
  * @returns Parsed response data
104
185
  */
105
- async function parseResponse(response) {
106
- const contentType = response.headers.get('content-type');
107
- const text = await response.text();
108
- // Check for error responses
186
+ async function parseResponse(response, options) {
187
+ const maxSize = options?.maxResponseSize ?? DEFAULT_MAX_RESPONSE_SIZE;
188
+ // Check for error responses FIRST - don't expose response body in error
189
+ // Only include status code, not statusText (which could contain sensitive info)
109
190
  if (!response.ok) {
110
- throw new Error(`API request failed: ${response.status} ${response.statusText}\n${text}`);
191
+ throw new Error(`API request failed: ${response.status}`);
192
+ }
193
+ // Check Content-Length header first to avoid loading huge responses
194
+ const contentLength = response.headers.get('content-length');
195
+ if (contentLength) {
196
+ const length = parseInt(contentLength, 10);
197
+ // Check for NaN, Infinity (from very large numbers), and actual size limit
198
+ if (!isNaN(length) && isFinite(length) && length > maxSize) {
199
+ throw new Error(`Response size (${length} bytes) exceeds maximum allowed (${maxSize} bytes)`);
200
+ }
201
+ // If length is Infinity or NaN, we'll catch it in the actual byte size check below
202
+ }
203
+ // Read response body
204
+ // NOTE: This size check occurs AFTER loading the full response into memory.
205
+ // For responses without Content-Length headers, this provides defense-in-depth
206
+ // (detecting oversized responses) but does not protect against memory exhaustion.
207
+ // A streaming approach would be required for true memory protection, but adds
208
+ // complexity. The Content-Length check above handles the common case.
209
+ const text = await response.text();
210
+ // Check actual byte size (Content-Length may be missing or incorrect)
211
+ const byteSize = new TextEncoder().encode(text).length;
212
+ if (byteSize > maxSize) {
213
+ throw new Error(`Response size (${byteSize} bytes) exceeds maximum allowed (${maxSize} bytes)`);
111
214
  }
112
- // Parse JSON responses
113
- if (contentType?.includes('application/json')) {
215
+ // Parse JSON responses - use case-insensitive check
216
+ const contentType = response.headers.get('content-type');
217
+ if (contentType?.toLowerCase().includes('application/json')) {
114
218
  try {
115
219
  return { data: JSON.parse(text) };
116
220
  }
117
- catch (error) {
118
- // Invalid JSON, return as text
119
- console.warn('Failed to parse JSON response:', error);
221
+ catch {
222
+ // Invalid JSON, return as text (don't log to console in production)
120
223
  return { data: text };
121
224
  }
122
225
  }
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.utils.js","sourceRoot":"","sources":["../../../src/openapi/openapi.utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpOpenAPITool, SecurityResolver } from 'mcp-from-openapi';\n\n/**\n * Request configuration for building HTTP requests\n */\nexport interface RequestConfig {\n url: string;\n headers: Headers;\n body?: Record<string, unknown>;\n}\n\n/**\n * Build HTTP request from OpenAPI tool and input parameters\n *\n * @param tool - OpenAPI tool definition with mapper\n * @param input - User input parameters\n * @param security - Resolved security (headers, query params, etc.)\n * @param baseUrl - API base URL\n * @returns Request configuration ready for fetch\n */\nexport function buildRequest(\n tool: McpOpenAPITool,\n input: Record<string, unknown>,\n security: Awaited<ReturnType<SecurityResolver['resolve']>>,\n baseUrl: string,\n): RequestConfig {\n const apiBaseUrl = tool.metadata.servers?.[0]?.url || baseUrl;\n let path = tool.metadata.path;\n const queryParams = new URLSearchParams();\n const headers = new Headers({\n accept: 'application/json',\n ...security.headers,\n });\n let body: Record<string, unknown> | undefined;\n\n // Process each mapper entry\n for (const mapper of tool.mapper) {\n // Skip security parameters (already handled by SecurityResolver)\n if (mapper.security) continue;\n\n const value = input[mapper.inputKey];\n\n // Check required parameters\n if (value === undefined || value === null) {\n if (mapper.required) {\n throw new Error(`Required parameter '${mapper.inputKey}' is missing for operation ${tool.name}`);\n }\n continue;\n }\n\n // Apply parameter to correct location\n switch (mapper.type) {\n case 'path':\n path = path.replace(`{${mapper.key}}`, encodeURIComponent(String(value)));\n break;\n\n case 'query':\n queryParams.set(mapper.key, String(value));\n break;\n\n case 'header':\n headers.set(mapper.key, String(value));\n break;\n case 'cookie':\n // Simple cookie header merge; you may want a more robust cookie encoder.\n {\n const existing = headers.get('cookie') ?? headers.get('Cookie');\n const cookiePair = `${mapper.key}=${encodeURIComponent(String(value))}`;\n const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;\n headers.set('Cookie', combined);\n }\n break;\n\n case 'body':\n if (!body) body = {};\n body[mapper.key] = value;\n break;\n }\n }\n\n // Add query parameters from security (e.g., API keys in query string)\n Object.entries(security.query).forEach(([key, value]) => {\n queryParams.set(key, value);\n });\n\n // Add cookies from a security context\n if (security.cookies && Object.keys(security.cookies).length > 0) {\n const existing = headers.get('cookie') ?? headers.get('Cookie');\n const securityCookieString = Object.entries(security.cookies)\n .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)\n .join('; ');\n const combined = existing ? `${existing}; ${securityCookieString}` : securityCookieString;\n headers.set('Cookie', combined);\n }\n\n // Ensure all path parameters are resolved\n if (path.includes('{')) {\n throw new Error(`Failed to resolve all path parameters in ${path} for operation ${tool.name}`);\n }\n\n // Build final URL\n const queryString = queryParams.toString();\n const url = `${apiBaseUrl}${path}${queryString ? `?${queryString}` : ''}`;\n\n return { url, headers, body };\n}\n\n/**\n * Apply custom headers to request\n *\n * @param headers - Current headers\n * @param additionalHeaders - Additional static headers to add\n */\nexport function applyAdditionalHeaders(headers: Headers, additionalHeaders?: Record<string, string>): void {\n if (!additionalHeaders) return;\n\n Object.entries(additionalHeaders).forEach(([key, value]) => {\n headers.set(key, value);\n });\n}\n\n/**\n * Parse API response based on content type\n *\n * @param response - Fetch response\n * @returns Parsed response data\n */\nexport async function parseResponse(response: Response): Promise<{ data: unknown }> {\n const contentType = response.headers.get('content-type');\n const text = await response.text();\n\n // Check for error responses\n if (!response.ok) {\n throw new Error(`API request failed: ${response.status} ${response.statusText}\\n${text}`);\n }\n\n // Parse JSON responses\n if (contentType?.includes('application/json')) {\n try {\n return { data: JSON.parse(text) };\n } catch (error) {\n // Invalid JSON, return as text\n console.warn('Failed to parse JSON response:', error);\n return { data: text };\n }\n }\n\n // Return text for non-JSON responses\n return { data: text };\n}\n"]}
1
+ {"version":3,"file":"openapi.utils.js","sourceRoot":"","sources":["../../../src/openapi/openapi.utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpOpenAPITool, SecurityResolver } from 'mcp-from-openapi';\n\n/**\n * Request configuration for building HTTP requests\n */\nexport interface RequestConfig {\n url: string;\n headers: Headers;\n body?: Record<string, unknown>;\n}\n\n/**\n * Coerce a value to string with type validation.\n * Throws if the value is an object/array that can't be safely stringified.\n *\n * @param value - Value to coerce\n * @param paramName - Parameter name for error messages\n * @param location - Parameter location (path/query/header)\n * @returns String representation of the value\n */\nfunction coerceToString(value: unknown, paramName: string, location: string): string {\n if (value === null || value === undefined) {\n return '';\n }\n if (typeof value === 'object') {\n if (Array.isArray(value)) {\n // Arrays in query params are common - join with comma\n if (location === 'query') {\n return value.map(String).join(',');\n }\n throw new Error(`${location} parameter '${paramName}' cannot be an array. Received: ${JSON.stringify(value)}`);\n }\n throw new Error(`${location} parameter '${paramName}' cannot be an object. Received: ${JSON.stringify(value)}`);\n }\n return String(value);\n}\n\n/**\n * Validate and normalize a base URL.\n *\n * @param url - URL to validate\n * @returns Validated URL object\n * @throws Error if URL is invalid or uses unsupported protocol\n */\nexport function validateBaseUrl(url: string): URL {\n try {\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error(`Unsupported protocol: ${parsed.protocol}. Only http: and https: are supported.`);\n }\n return parsed;\n } catch (err) {\n if (err instanceof Error && err.message.includes('Unsupported protocol')) {\n throw err;\n }\n throw new Error(`Invalid base URL: ${url}`);\n }\n}\n\n/**\n * Append a cookie to the Cookie header.\n * Validates cookie name according to RFC 6265.\n *\n * @param headers - Headers object to modify\n * @param name - Cookie name\n * @param value - Cookie value (will be URI encoded)\n */\nfunction appendCookie(headers: Headers, name: string, value: unknown): void {\n // RFC 6265 cookie-name validation (simplified)\n if (!/^[\\w!#$%&'*+\\-.^`|~]+$/.test(name)) {\n throw new Error(`Invalid cookie name: '${name}'. Cookie names must be valid tokens.`);\n }\n\n const existing = headers.get('Cookie') || '';\n const cookiePair = `${name}=${encodeURIComponent(coerceToString(value, name, 'cookie'))}`;\n const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;\n headers.set('Cookie', combined);\n}\n\n/**\n * Build HTTP request from OpenAPI tool and input parameters\n *\n * @param tool - OpenAPI tool definition with mapper\n * @param input - User input parameters\n * @param security - Resolved security (headers, query params, etc.)\n * @param baseUrl - API base URL\n * @returns Request configuration ready for fetch\n */\nexport function buildRequest(\n tool: McpOpenAPITool,\n input: Record<string, unknown>,\n security: Awaited<ReturnType<SecurityResolver['resolve']>>,\n baseUrl: string,\n): RequestConfig {\n // Normalize base URL by removing trailing slash to prevent double slashes\n // Validate server URL from OpenAPI spec to prevent SSRF attacks\n const rawBaseUrl = tool.metadata.servers?.[0]?.url || baseUrl;\n validateBaseUrl(rawBaseUrl); // Throws if invalid protocol (e.g., file://, javascript:)\n const apiBaseUrl = rawBaseUrl.replace(/\\/+$/, '');\n let path = tool.metadata.path;\n const queryParams = new URLSearchParams();\n const headers = new Headers({\n accept: 'application/json',\n ...security.headers,\n });\n let body: Record<string, unknown> | undefined;\n\n // Process each mapper entry\n for (const mapper of tool.mapper) {\n // Skip security parameters (already handled by SecurityResolver)\n if (mapper.security) continue;\n\n const value = input[mapper.inputKey];\n\n // Check required parameters\n if (value === undefined || value === null) {\n if (mapper.required) {\n throw new Error(\n `Required ${mapper.type} parameter '${mapper.key}' (input: '${mapper.inputKey}') is missing for operation '${tool.name}'`,\n );\n }\n continue;\n }\n\n // Apply parameter to correct location\n switch (mapper.type) {\n case 'path':\n // Use replaceAll to handle duplicate path parameters (e.g., /users/{id}/posts/{id})\n path = path.replaceAll(`{${mapper.key}}`, encodeURIComponent(coerceToString(value, mapper.key, 'path')));\n break;\n\n case 'query':\n queryParams.set(mapper.key, coerceToString(value, mapper.key, 'query'));\n break;\n\n case 'header': {\n const headerValue = coerceToString(value, mapper.key, 'header');\n // Validate header values for injection attacks\n // Check for: CR, LF, null byte, form feed, vertical tab\n // eslint-disable-next-line no-control-regex\n if (/[\\r\\n\\x00\\f\\v]/.test(headerValue)) {\n throw new Error(\n `Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,\n );\n }\n headers.set(mapper.key, headerValue);\n break;\n }\n\n case 'cookie':\n appendCookie(headers, mapper.key, value);\n break;\n\n case 'body':\n if (!body) body = {};\n body[mapper.key] = value;\n break;\n\n default:\n throw new Error(\n `Unknown mapper type '${(mapper as { type: string }).type}' for parameter '${mapper.key}' in operation '${\n tool.name\n }'`,\n );\n }\n }\n\n // Add query parameters from security (e.g., API keys in query string)\n // Detect collisions with user-provided query params (security params take precedence)\n Object.entries(security.query).forEach(([key, value]) => {\n if (queryParams.has(key)) {\n // Security params override user params, but warn about potential misconfiguration\n throw new Error(\n `Query parameter collision: '${key}' is provided both as user input and security parameter. ` +\n `This could indicate a security misconfiguration in operation '${tool.name}'.`,\n );\n }\n queryParams.set(key, coerceToString(value, key, 'security query'));\n });\n\n // Add cookies from a security context\n if (security.cookies && Object.keys(security.cookies).length > 0) {\n Object.entries(security.cookies).forEach(([key, value]) => {\n appendCookie(headers, key, value);\n });\n }\n\n // Ensure all path parameters are resolved\n if (path.includes('{')) {\n throw new Error(`Failed to resolve all path parameters in ${path} for operation ${tool.name}`);\n }\n\n // Build final URL\n const queryString = queryParams.toString();\n const url = `${apiBaseUrl}${path}${queryString ? `?${queryString}` : ''}`;\n\n return { url, headers, body };\n}\n\n/**\n * Apply custom headers to request\n *\n * @param headers - Current headers\n * @param additionalHeaders - Additional static headers to add\n */\nexport function applyAdditionalHeaders(headers: Headers, additionalHeaders?: Record<string, string>): void {\n if (!additionalHeaders) return;\n\n Object.entries(additionalHeaders).forEach(([key, value]) => {\n headers.set(key, value);\n });\n}\n\n/**\n * Options for response parsing\n */\nexport interface ParseResponseOptions {\n /** Maximum response size in bytes (default: 10MB) */\n maxResponseSize?: number;\n}\n\n/** Default max response size: 10MB */\nconst DEFAULT_MAX_RESPONSE_SIZE = 10 * 1024 * 1024;\n\n/**\n * Parse API response based on content type\n *\n * @param response - Fetch response\n * @param options - Optional parsing options\n * @returns Parsed response data\n */\nexport async function parseResponse(response: Response, options?: ParseResponseOptions): Promise<{ data: unknown }> {\n const maxSize = options?.maxResponseSize ?? DEFAULT_MAX_RESPONSE_SIZE;\n\n // Check for error responses FIRST - don't expose response body in error\n // Only include status code, not statusText (which could contain sensitive info)\n if (!response.ok) {\n throw new Error(`API request failed: ${response.status}`);\n }\n\n // Check Content-Length header first to avoid loading huge responses\n const contentLength = response.headers.get('content-length');\n if (contentLength) {\n const length = parseInt(contentLength, 10);\n // Check for NaN, Infinity (from very large numbers), and actual size limit\n if (!isNaN(length) && isFinite(length) && length > maxSize) {\n throw new Error(`Response size (${length} bytes) exceeds maximum allowed (${maxSize} bytes)`);\n }\n // If length is Infinity or NaN, we'll catch it in the actual byte size check below\n }\n\n // Read response body\n // NOTE: This size check occurs AFTER loading the full response into memory.\n // For responses without Content-Length headers, this provides defense-in-depth\n // (detecting oversized responses) but does not protect against memory exhaustion.\n // A streaming approach would be required for true memory protection, but adds\n // complexity. The Content-Length check above handles the common case.\n const text = await response.text();\n\n // Check actual byte size (Content-Length may be missing or incorrect)\n const byteSize = new TextEncoder().encode(text).length;\n if (byteSize > maxSize) {\n throw new Error(`Response size (${byteSize} bytes) exceeds maximum allowed (${maxSize} bytes)`);\n }\n\n // Parse JSON responses - use case-insensitive check\n const contentType = response.headers.get('content-type');\n if (contentType?.toLowerCase().includes('application/json')) {\n try {\n return { data: JSON.parse(text) };\n } catch {\n // Invalid JSON, return as text (don't log to console in production)\n return { data: text };\n }\n }\n\n // Return text for non-JSON responses\n return { data: text };\n}\n"]}