@frontmcp/adapters 0.6.0 → 0.6.2
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.
- package/esm/index.mjs +1168 -0
- package/esm/openapi/index.mjs +1168 -0
- package/esm/package.json +63 -0
- package/index.js +1193 -0
- package/openapi/index.js +1192 -0
- package/package.json +30 -12
- package/src/index.js +0 -8
- package/src/index.js.map +0 -1
- package/src/openapi/README.md +0 -1215
- package/src/openapi/index.js +0 -8
- package/src/openapi/index.js.map +0 -1
- package/src/openapi/openapi.adapter.js +0 -434
- package/src/openapi/openapi.adapter.js.map +0 -1
- package/src/openapi/openapi.frontmcp-schema.js +0 -358
- package/src/openapi/openapi.frontmcp-schema.js.map +0 -1
- package/src/openapi/openapi.security.js +0 -242
- package/src/openapi/openapi.security.js.map +0 -1
- package/src/openapi/openapi.tool.js +0 -267
- package/src/openapi/openapi.tool.js.map +0 -1
- package/src/openapi/openapi.types.js +0 -29
- package/src/openapi/openapi.types.js.map +0 -1
- package/src/openapi/openapi.utils.js +0 -229
- package/src/openapi/openapi.utils.js.map +0 -1
- /package/{src/index.d.ts → index.d.ts} +0 -0
- /package/{src/openapi → openapi}/index.d.ts +0 -0
- /package/{src/openapi → openapi}/openapi.adapter.d.ts +0 -0
- /package/{src/openapi → openapi}/openapi.frontmcp-schema.d.ts +0 -0
- /package/{src/openapi → openapi}/openapi.security.d.ts +0 -0
- /package/{src/openapi → openapi}/openapi.tool.d.ts +0 -0
- /package/{src/openapi → openapi}/openapi.types.d.ts +0 -0
- /package/{src/openapi → openapi}/openapi.utils.d.ts +0 -0
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateBaseUrl = validateBaseUrl;
|
|
4
|
-
exports.buildRequest = buildRequest;
|
|
5
|
-
exports.applyAdditionalHeaders = applyAdditionalHeaders;
|
|
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
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Build HTTP request from OpenAPI tool and input parameters
|
|
74
|
-
*
|
|
75
|
-
* @param tool - OpenAPI tool definition with mapper
|
|
76
|
-
* @param input - User input parameters
|
|
77
|
-
* @param security - Resolved security (headers, query params, etc.)
|
|
78
|
-
* @param baseUrl - API base URL
|
|
79
|
-
* @returns Request configuration ready for fetch
|
|
80
|
-
*/
|
|
81
|
-
function buildRequest(tool, input, security, 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(/\/+$/, '');
|
|
87
|
-
let path = tool.metadata.path;
|
|
88
|
-
const queryParams = new URLSearchParams();
|
|
89
|
-
const headers = new Headers({
|
|
90
|
-
accept: 'application/json',
|
|
91
|
-
...security.headers,
|
|
92
|
-
});
|
|
93
|
-
let body;
|
|
94
|
-
// Process each mapper entry
|
|
95
|
-
for (const mapper of tool.mapper) {
|
|
96
|
-
// Skip security parameters (already handled by SecurityResolver)
|
|
97
|
-
if (mapper.security)
|
|
98
|
-
continue;
|
|
99
|
-
const value = input[mapper.inputKey];
|
|
100
|
-
// Check required parameters
|
|
101
|
-
if (value === undefined || value === null) {
|
|
102
|
-
if (mapper.required) {
|
|
103
|
-
throw new Error(`Required ${mapper.type} parameter '${mapper.key}' (input: '${mapper.inputKey}') is missing for operation '${tool.name}'`);
|
|
104
|
-
}
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
// Apply parameter to correct location
|
|
108
|
-
switch (mapper.type) {
|
|
109
|
-
case 'path':
|
|
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')));
|
|
112
|
-
break;
|
|
113
|
-
case 'query':
|
|
114
|
-
queryParams.set(mapper.key, coerceToString(value, mapper.key, 'query'));
|
|
115
|
-
break;
|
|
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);
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
case 'cookie':
|
|
128
|
-
appendCookie(headers, mapper.key, value);
|
|
129
|
-
break;
|
|
130
|
-
case 'body':
|
|
131
|
-
if (!body)
|
|
132
|
-
body = {};
|
|
133
|
-
body[mapper.key] = value;
|
|
134
|
-
break;
|
|
135
|
-
default:
|
|
136
|
-
throw new Error(`Unknown mapper type '${mapper.type}' for parameter '${mapper.key}' in operation '${tool.name}'`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
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)
|
|
141
|
-
Object.entries(security.query).forEach(([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'));
|
|
148
|
-
});
|
|
149
|
-
// Add cookies from a security context
|
|
150
|
-
if (security.cookies && Object.keys(security.cookies).length > 0) {
|
|
151
|
-
Object.entries(security.cookies).forEach(([key, value]) => {
|
|
152
|
-
appendCookie(headers, key, value);
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
// Ensure all path parameters are resolved
|
|
156
|
-
if (path.includes('{')) {
|
|
157
|
-
throw new Error(`Failed to resolve all path parameters in ${path} for operation ${tool.name}`);
|
|
158
|
-
}
|
|
159
|
-
// Build final URL
|
|
160
|
-
const queryString = queryParams.toString();
|
|
161
|
-
const url = `${apiBaseUrl}${path}${queryString ? `?${queryString}` : ''}`;
|
|
162
|
-
return { url, headers, body };
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Apply custom headers to request
|
|
166
|
-
*
|
|
167
|
-
* @param headers - Current headers
|
|
168
|
-
* @param additionalHeaders - Additional static headers to add
|
|
169
|
-
*/
|
|
170
|
-
function applyAdditionalHeaders(headers, additionalHeaders) {
|
|
171
|
-
if (!additionalHeaders)
|
|
172
|
-
return;
|
|
173
|
-
Object.entries(additionalHeaders).forEach(([key, value]) => {
|
|
174
|
-
headers.set(key, value);
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
/** Default max response size: 10MB */
|
|
178
|
-
const DEFAULT_MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
|
|
179
|
-
/**
|
|
180
|
-
* Parse API response based on content type
|
|
181
|
-
*
|
|
182
|
-
* @param response - Fetch response
|
|
183
|
-
* @param options - Optional parsing options
|
|
184
|
-
* @returns Parsed response data
|
|
185
|
-
*/
|
|
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)
|
|
190
|
-
if (!response.ok) {
|
|
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)`);
|
|
214
|
-
}
|
|
215
|
-
// Parse JSON responses - use case-insensitive check
|
|
216
|
-
const contentType = response.headers.get('content-type');
|
|
217
|
-
if (contentType?.toLowerCase().includes('application/json')) {
|
|
218
|
-
try {
|
|
219
|
-
return { data: JSON.parse(text) };
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
// Invalid JSON, return as text (don't log to console in production)
|
|
223
|
-
return { data: text };
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
// Return text for non-JSON responses
|
|
227
|
-
return { data: text };
|
|
228
|
-
}
|
|
229
|
-
//# sourceMappingURL=openapi.utils.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|