@doclo/core 0.1.5
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/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/index.d.ts +931 -0
- package/dist/index.js +2293 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/validation-utils.d.ts +1 -0
- package/dist/internal/validation-utils.js +650 -0
- package/dist/internal/validation-utils.js.map +1 -0
- package/dist/observability/index.d.ts +933 -0
- package/dist/observability/index.js +630 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/pdf-utils.d.ts +123 -0
- package/dist/pdf-utils.js +106 -0
- package/dist/pdf-utils.js.map +1 -0
- package/dist/runtime/base64.d.ts +100 -0
- package/dist/runtime/base64.js +52 -0
- package/dist/runtime/base64.js.map +1 -0
- package/dist/runtime/crypto.d.ts +56 -0
- package/dist/runtime/crypto.js +35 -0
- package/dist/runtime/crypto.js.map +1 -0
- package/dist/runtime/env.d.ts +130 -0
- package/dist/runtime/env.js +76 -0
- package/dist/runtime/env.js.map +1 -0
- package/dist/security/index.d.ts +236 -0
- package/dist/security/index.js +260 -0
- package/dist/security/index.js.map +1 -0
- package/dist/validation-CzOz6fwq.d.ts +1126 -0
- package/dist/validation.d.ts +1 -0
- package/dist/validation.js +445 -0
- package/dist/validation.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// src/security/url-validator.ts
|
|
2
|
+
var BLOCKED_IP_RANGES = [
|
|
3
|
+
// Loopback
|
|
4
|
+
{ start: "127.0.0.0", end: "127.255.255.255" },
|
|
5
|
+
// Private Class A
|
|
6
|
+
{ start: "10.0.0.0", end: "10.255.255.255" },
|
|
7
|
+
// Private Class B
|
|
8
|
+
{ start: "172.16.0.0", end: "172.31.255.255" },
|
|
9
|
+
// Private Class C
|
|
10
|
+
{ start: "192.168.0.0", end: "192.168.255.255" },
|
|
11
|
+
// Link Local
|
|
12
|
+
{ start: "169.254.0.0", end: "169.254.255.255" }
|
|
13
|
+
];
|
|
14
|
+
var BLOCKED_METADATA_HOSTS = [
|
|
15
|
+
"169.254.169.254",
|
|
16
|
+
// AWS metadata service
|
|
17
|
+
"169.254.169.253",
|
|
18
|
+
// AWS metadata service (Windows)
|
|
19
|
+
"metadata.google.internal",
|
|
20
|
+
// GCP metadata service
|
|
21
|
+
"metadata",
|
|
22
|
+
// GCP alias
|
|
23
|
+
"100.100.100.200",
|
|
24
|
+
// Aliyun metadata service
|
|
25
|
+
"instance-data"
|
|
26
|
+
// OpenStack alias
|
|
27
|
+
];
|
|
28
|
+
var BLOCKED_IPV6_PATTERNS = [
|
|
29
|
+
/^::1$/,
|
|
30
|
+
// Loopback (::1)
|
|
31
|
+
/^::$/,
|
|
32
|
+
// Any address (::)
|
|
33
|
+
/^::ffff:/i,
|
|
34
|
+
// IPv4-mapped IPv6 (::ffff:0:0/96) - matches ::ffff:127.0.0.1
|
|
35
|
+
/^::ffff:0:/i,
|
|
36
|
+
// IPv4-mapped IPv6 alternative
|
|
37
|
+
/^fe80:/i,
|
|
38
|
+
// Link-local (fe80::/10)
|
|
39
|
+
/^fec0:/i,
|
|
40
|
+
// Site-local deprecated (fec0::/10)
|
|
41
|
+
/^fc00:/i,
|
|
42
|
+
// Unique local address (fc00::/7)
|
|
43
|
+
/^fd00:/i,
|
|
44
|
+
// Unique local address (fd00::/8)
|
|
45
|
+
/^ff00:/i,
|
|
46
|
+
// Multicast (ff00::/8)
|
|
47
|
+
/^0:0:0:0:0:0:0:1$/i
|
|
48
|
+
// Loopback expanded form
|
|
49
|
+
];
|
|
50
|
+
function ipToNumber(ip) {
|
|
51
|
+
const parts = ip.split(".").map(Number);
|
|
52
|
+
if (parts.length !== 4 || parts.some((p) => p < 0 || p > 255)) {
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
|
|
56
|
+
}
|
|
57
|
+
function isIpInBlockedRange(ip) {
|
|
58
|
+
const ipNum = ipToNumber(ip);
|
|
59
|
+
if (ipNum === -1) return false;
|
|
60
|
+
return BLOCKED_IP_RANGES.some((range) => {
|
|
61
|
+
const startNum = ipToNumber(range.start);
|
|
62
|
+
const endNum = ipToNumber(range.end);
|
|
63
|
+
return ipNum >= startNum && ipNum <= endNum;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function isIPv6Blocked(hostname) {
|
|
67
|
+
const addr = hostname.replace(/^\[|\]$/g, "");
|
|
68
|
+
return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(addr));
|
|
69
|
+
}
|
|
70
|
+
function validateUrl(urlString, options = {}) {
|
|
71
|
+
const {
|
|
72
|
+
blockInternal = true,
|
|
73
|
+
allowedProtocols = ["http:", "https:"]
|
|
74
|
+
} = options;
|
|
75
|
+
let url;
|
|
76
|
+
try {
|
|
77
|
+
url = new URL(urlString);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(`Invalid URL: ${urlString}`);
|
|
80
|
+
}
|
|
81
|
+
if (!allowedProtocols.includes(url.protocol)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Blocked protocol: ${url.protocol}. Allowed: ${allowedProtocols.join(", ")}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (blockInternal) {
|
|
87
|
+
const hostname = url.hostname;
|
|
88
|
+
if (BLOCKED_METADATA_HOSTS.includes(hostname)) {
|
|
89
|
+
throw new Error(`Blocked metadata service: ${hostname}`);
|
|
90
|
+
}
|
|
91
|
+
if (hostname.includes(":") || hostname.startsWith("[")) {
|
|
92
|
+
if (isIPv6Blocked(hostname)) {
|
|
93
|
+
throw new Error(`Blocked IPv6 address: ${hostname}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (isIpInBlockedRange(hostname)) {
|
|
97
|
+
throw new Error(`Blocked internal IP address: ${hostname}`);
|
|
98
|
+
}
|
|
99
|
+
if (hostname === "localhost") {
|
|
100
|
+
throw new Error("Blocked localhost access");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return url;
|
|
104
|
+
}
|
|
105
|
+
async function secureFetch(url, timeoutMs = 3e4) {
|
|
106
|
+
const validatedUrl = validateUrl(url);
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(validatedUrl.toString(), {
|
|
111
|
+
signal: controller.signal
|
|
112
|
+
});
|
|
113
|
+
return response;
|
|
114
|
+
} finally {
|
|
115
|
+
clearTimeout(timeoutId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function getHostnameFromUrl(urlString) {
|
|
119
|
+
try {
|
|
120
|
+
const url = new URL(urlString);
|
|
121
|
+
return url.hostname;
|
|
122
|
+
} catch {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/security/path-validator.ts
|
|
128
|
+
import { resolve, normalize, isAbsolute, relative } from "path";
|
|
129
|
+
function validatePath(filePath, basePath) {
|
|
130
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
131
|
+
const normalizedPath = normalize(resolve(basePath, filePath));
|
|
132
|
+
const relativePath = relative(normalizedBase, normalizedPath);
|
|
133
|
+
if (relativePath.startsWith("..")) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Path traversal attempt detected: ${filePath} tries to escape ${basePath}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (isAbsolute(filePath) && !normalizedPath.startsWith(normalizedBase)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Absolute path outside base directory: ${filePath} is not within ${basePath}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return normalizedPath;
|
|
144
|
+
}
|
|
145
|
+
function validatePathSimple(filePath) {
|
|
146
|
+
const normalized = normalize(filePath);
|
|
147
|
+
if (normalized.includes("..") || normalized.startsWith("/etc") || normalized.startsWith("C:\\Windows")) {
|
|
148
|
+
throw new Error(`Suspicious path detected: ${filePath}`);
|
|
149
|
+
}
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
function isPathSafe(filePath, basePath) {
|
|
153
|
+
try {
|
|
154
|
+
if (basePath) {
|
|
155
|
+
validatePath(filePath, basePath);
|
|
156
|
+
} else {
|
|
157
|
+
validatePathSimple(filePath);
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function getSafePath(filePath, basePath) {
|
|
165
|
+
if (basePath) {
|
|
166
|
+
return validatePath(filePath, basePath);
|
|
167
|
+
}
|
|
168
|
+
return resolve(filePath);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/security/resource-limits.ts
|
|
172
|
+
var DEFAULT_LIMITS = {
|
|
173
|
+
// Maximum file size: 100MB
|
|
174
|
+
MAX_FILE_SIZE: 100 * 1024 * 1024,
|
|
175
|
+
// Request timeout: 30 seconds
|
|
176
|
+
REQUEST_TIMEOUT: 3e4,
|
|
177
|
+
// Maximum JSON parse depth
|
|
178
|
+
MAX_JSON_DEPTH: 100
|
|
179
|
+
};
|
|
180
|
+
function validateFileSize(size, maxSize = DEFAULT_LIMITS.MAX_FILE_SIZE) {
|
|
181
|
+
if (size > maxSize) {
|
|
182
|
+
const maxMB = Math.round(maxSize / 1024 / 1024);
|
|
183
|
+
const sizeMB = Math.round(size / 1024 / 1024);
|
|
184
|
+
throw new Error(
|
|
185
|
+
`File size ${sizeMB}MB exceeds maximum allowed size of ${maxMB}MB`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function createFetchController(timeoutMs = DEFAULT_LIMITS.REQUEST_TIMEOUT) {
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
192
|
+
controller.__timeoutId = timeoutId;
|
|
193
|
+
return controller;
|
|
194
|
+
}
|
|
195
|
+
function cleanupFetchController(controller) {
|
|
196
|
+
const timeoutId = controller.__timeoutId;
|
|
197
|
+
if (timeoutId) {
|
|
198
|
+
clearTimeout(timeoutId);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_LIMITS.REQUEST_TIMEOUT) {
|
|
202
|
+
const controller = createFetchController(timeoutMs);
|
|
203
|
+
try {
|
|
204
|
+
const response = await fetch(url, {
|
|
205
|
+
...options,
|
|
206
|
+
signal: controller.signal,
|
|
207
|
+
cache: "no-store"
|
|
208
|
+
// Prevent Next.js cache revalidation which can cause AbortError (see: github.com/vercel/next.js/issues/54045)
|
|
209
|
+
});
|
|
210
|
+
return response;
|
|
211
|
+
} finally {
|
|
212
|
+
cleanupFetchController(controller);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function validateBufferSize(size, maxSize = DEFAULT_LIMITS.MAX_FILE_SIZE) {
|
|
216
|
+
validateFileSize(size, maxSize);
|
|
217
|
+
}
|
|
218
|
+
function safeJsonParse(text, maxDepth = DEFAULT_LIMITS.MAX_JSON_DEPTH) {
|
|
219
|
+
try {
|
|
220
|
+
let checkDepth2 = function(obj2, depth = 0) {
|
|
221
|
+
if (depth > maxDepth) {
|
|
222
|
+
throw new Error(`JSON nesting depth exceeds maximum of ${maxDepth}`);
|
|
223
|
+
}
|
|
224
|
+
if (typeof obj2 === "object" && obj2 !== null) {
|
|
225
|
+
for (const value of Object.values(obj2)) {
|
|
226
|
+
checkDepth2(value, depth + 1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var checkDepth = checkDepth2;
|
|
231
|
+
if (text.length > DEFAULT_LIMITS.MAX_FILE_SIZE) {
|
|
232
|
+
throw new Error("JSON string exceeds maximum size");
|
|
233
|
+
}
|
|
234
|
+
const obj = JSON.parse(text);
|
|
235
|
+
checkDepth2(obj);
|
|
236
|
+
return obj;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error instanceof Error) {
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
throw new Error(`Invalid JSON: ${String(error)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
DEFAULT_LIMITS,
|
|
246
|
+
cleanupFetchController,
|
|
247
|
+
createFetchController,
|
|
248
|
+
fetchWithTimeout,
|
|
249
|
+
getHostnameFromUrl,
|
|
250
|
+
getSafePath,
|
|
251
|
+
isPathSafe,
|
|
252
|
+
safeJsonParse,
|
|
253
|
+
secureFetch,
|
|
254
|
+
validateBufferSize,
|
|
255
|
+
validateFileSize,
|
|
256
|
+
validatePath,
|
|
257
|
+
validatePathSimple,
|
|
258
|
+
validateUrl
|
|
259
|
+
};
|
|
260
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/url-validator.ts","../../src/security/path-validator.ts","../../src/security/resource-limits.ts"],"sourcesContent":["/**\n * URL Validation and SSRF Protection\n *\n * ⚠️ SECURITY CRITICAL: SSRF (Server-Side Request Forgery) Prevention\n *\n * This module blocks URLs that could be used in Server-Side Request Forgery attacks:\n * - Internal IP addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)\n * - Loopback addresses (127.0.0.0/8, ::1)\n * - Cloud metadata services (AWS, GCP, Aliyun, etc.)\n * - Link-local addresses (169.254.0.0/16)\n *\n * Attack example: An attacker tricks your server to make requests to:\n * - http://169.254.169.254 (AWS credentials endpoint)\n * - http://10.0.0.1:8080/admin (internal service)\n * - http://localhost/api/admin (localhost admin endpoint)\n *\n * Prevents Server-Side Request Forgery attacks by validating URLs against blocklist\n */\n\n/**\n * IP ranges that should be blocked (internal networks)\n * RFC 1918 private ranges + loopback + cloud metadata\n */\nconst BLOCKED_IP_RANGES = [\n // Loopback\n { start: '127.0.0.0', end: '127.255.255.255' },\n // Private Class A\n { start: '10.0.0.0', end: '10.255.255.255' },\n // Private Class B\n { start: '172.16.0.0', end: '172.31.255.255' },\n // Private Class C\n { start: '192.168.0.0', end: '192.168.255.255' },\n // Link Local\n { start: '169.254.0.0', end: '169.254.255.255' },\n];\n\nconst BLOCKED_METADATA_HOSTS = [\n '169.254.169.254', // AWS metadata service\n '169.254.169.253', // AWS metadata service (Windows)\n 'metadata.google.internal', // GCP metadata service\n 'metadata', // GCP alias\n '100.100.100.200', // Aliyun metadata service\n 'instance-data', // OpenStack alias\n];\n\n/**\n * IPv6 address patterns that should be blocked\n * Prevents SSRF attacks using IPv6 addresses\n */\nconst BLOCKED_IPV6_PATTERNS = [\n /^::1$/, // Loopback (::1)\n /^::$/, // Any address (::)\n /^::ffff:/i, // IPv4-mapped IPv6 (::ffff:0:0/96) - matches ::ffff:127.0.0.1\n /^::ffff:0:/i, // IPv4-mapped IPv6 alternative\n /^fe80:/i, // Link-local (fe80::/10)\n /^fec0:/i, // Site-local deprecated (fec0::/10)\n /^fc00:/i, // Unique local address (fc00::/7)\n /^fd00:/i, // Unique local address (fd00::/8)\n /^ff00:/i, // Multicast (ff00::/8)\n /^0:0:0:0:0:0:0:1$/i, // Loopback expanded form\n];\n\n/**\n * Convert IPv4 string to number for range comparison\n */\nfunction ipToNumber(ip: string): number {\n const parts = ip.split('.').map(Number);\n if (parts.length !== 4 || parts.some((p) => p < 0 || p > 255)) {\n return -1;\n }\n return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];\n}\n\n/**\n * Check if IP is in blocked range\n */\nfunction isIpInBlockedRange(ip: string): boolean {\n const ipNum = ipToNumber(ip);\n if (ipNum === -1) return false;\n\n return BLOCKED_IP_RANGES.some((range) => {\n const startNum = ipToNumber(range.start);\n const endNum = ipToNumber(range.end);\n return ipNum >= startNum && ipNum <= endNum;\n });\n}\n\n/**\n * Check if IPv6 address is blocked\n * Handles both bracket notation ([::1]) and standard notation (::1)\n */\nfunction isIPv6Blocked(hostname: string): boolean {\n // Remove brackets if present ([::1] -> ::1)\n const addr = hostname.replace(/^\\[|\\]$/g, '');\n\n return BLOCKED_IPV6_PATTERNS.some(pattern => pattern.test(addr));\n}\n\n/**\n * Validate a URL to prevent SSRF (Server-Side Request Forgery) attacks\n *\n * ⚠️ SECURITY CRITICAL: SSRF Prevention\n *\n * This function blocks URLs that could be used to exploit the server:\n * - Internal IP addresses (breaks firewall perimeter security)\n * - Cloud metadata services (leaks credentials, API keys)\n * - Localhost/loopback (access admin services, debug ports)\n *\n * By default, blocks internal network access automatically.\n *\n * @param urlString - The URL to validate\n * @param options - Validation options\n * - blockInternal (default: true) - Block internal IP ranges. ⚠️ Set to false only if you understand SSRF risks\n * - allowedProtocols (default: ['http:', 'https:']) - Restrict to specific protocols\n * @throws Error if URL is invalid, uses blocked protocol, or points to blocked IP/host\n * @returns The validated URL object\n * @security Always validate user-provided URLs. Do not set blockInternal to false without SSRF security review\n *\n * @example\n * ```typescript\n * // Validate user-provided URL\n * try {\n * const url = validateUrl(userInput);\n * const response = await fetch(url.toString());\n * } catch (error) {\n * // URL was malicious or pointed to internal resource\n * }\n * ```\n */\nexport function validateUrl(\n urlString: string,\n options: {\n blockInternal?: boolean;\n allowedProtocols?: string[];\n } = {}\n): URL {\n const {\n blockInternal = true,\n allowedProtocols = ['http:', 'https:'],\n } = options;\n\n let url: URL;\n\n try {\n url = new URL(urlString);\n } catch (error) {\n throw new Error(`Invalid URL: ${urlString}`);\n }\n\n // Check protocol\n if (!allowedProtocols.includes(url.protocol)) {\n throw new Error(\n `Blocked protocol: ${url.protocol}. Allowed: ${allowedProtocols.join(', ')}`\n );\n }\n\n // Check for internal access if enabled\n if (blockInternal) {\n const hostname = url.hostname;\n\n // Check blocked metadata hosts\n if (BLOCKED_METADATA_HOSTS.includes(hostname)) {\n throw new Error(`Blocked metadata service: ${hostname}`);\n }\n\n // Check IPv6 addresses (includes bracket notation)\n if (hostname.includes(':') || hostname.startsWith('[')) {\n if (isIPv6Blocked(hostname)) {\n throw new Error(`Blocked IPv6 address: ${hostname}`);\n }\n }\n\n // Check if IPv4 is in blocked range\n if (isIpInBlockedRange(hostname)) {\n throw new Error(`Blocked internal IP address: ${hostname}`);\n }\n\n // Block localhost keyword\n if (hostname === 'localhost') {\n throw new Error('Blocked localhost access');\n }\n }\n\n return url;\n}\n\n/**\n * Fetch a URL with SSRF protection and timeout\n * @param url - The URL to fetch\n * @param timeoutMs - Request timeout in milliseconds (default 30s)\n * @throws Error if URL is invalid, blocked, or request times out\n */\nexport async function secureFetch(\n url: string,\n timeoutMs: number = 30000\n): Promise<Response> {\n // Validate URL first\n const validatedUrl = validateUrl(url);\n\n // Create abort controller for timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(validatedUrl.toString(), {\n signal: controller.signal,\n });\n return response;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\n/**\n * Extract hostname from URL string for validation\n * Useful for pre-validation checks before full URL parsing\n */\nexport function getHostnameFromUrl(urlString: string): string {\n try {\n const url = new URL(urlString);\n return url.hostname;\n } catch {\n return '';\n }\n}\n","/**\n * Path Validation and Path Traversal Prevention\n * Prevents attackers from accessing arbitrary files through path traversal\n */\n\nimport { resolve, normalize, isAbsolute, relative } from 'path';\n\n/**\n * Validate and normalize a file path to prevent traversal attacks\n * @param filePath - The file path to validate\n * @param basePath - The base directory that paths must be within\n * @throws Error if path attempts to traverse outside basePath\n * @returns The normalized safe path\n */\nexport function validatePath(filePath: string, basePath: string): string {\n // Normalize both paths to handle . and .. references\n const normalizedBase = normalize(resolve(basePath));\n const normalizedPath = normalize(resolve(basePath, filePath));\n\n // Check if the resolved path is within the base path\n const relativePath = relative(normalizedBase, normalizedPath);\n\n // If relative path starts with .., it's trying to escape the base\n if (relativePath.startsWith('..')) {\n throw new Error(\n `Path traversal attempt detected: ${filePath} tries to escape ${basePath}`\n );\n }\n\n // Also check if it's an absolute path that's not based on basePath\n if (isAbsolute(filePath) && !normalizedPath.startsWith(normalizedBase)) {\n throw new Error(\n `Absolute path outside base directory: ${filePath} is not within ${basePath}`\n );\n }\n\n return normalizedPath;\n}\n\n/**\n * Validate a path without a required base path (more permissive)\n * Still prevents obvious traversal attempts\n * @param filePath - The file path to validate\n * @throws Error if path contains suspicious traversal patterns\n * @returns The normalized path\n */\nexport function validatePathSimple(filePath: string): string {\n const normalized = normalize(filePath);\n\n // Check for obvious traversal patterns\n if (normalized.includes('..') || normalized.startsWith('/etc') ||\n normalized.startsWith('C:\\\\Windows')) {\n throw new Error(`Suspicious path detected: ${filePath}`);\n }\n\n return normalized;\n}\n\n/**\n * Check if a path would be safe to access\n * @param filePath - The file path to check\n * @param basePath - Optional base path restriction\n * @returns true if path is safe, false otherwise\n */\nexport function isPathSafe(filePath: string, basePath?: string): boolean {\n try {\n if (basePath) {\n validatePath(filePath, basePath);\n } else {\n validatePathSimple(filePath);\n }\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get the absolute path with validation\n * @param filePath - The file path\n * @param basePath - Optional base directory\n * @returns Absolute path if valid\n * @throws Error if path is invalid\n */\nexport function getSafePath(filePath: string, basePath?: string): string {\n if (basePath) {\n return validatePath(filePath, basePath);\n }\n return resolve(filePath);\n}\n","/**\n * Resource Limits and DoS Protection\n *\n * ⚠️ SECURITY WARNING: Resource limits are critical for protecting against:\n * - Resource exhaustion attacks (large file downloads)\n * - Denial of Service (slow loris, timeout attacks)\n * - Memory exhaustion (deeply nested JSON, large arrays)\n *\n * Prevents resource exhaustion attacks through file size limits and timeouts\n */\n\n/**\n * Default resource limits\n *\n * ⚠️ SECURITY CRITICAL: These are conservative defaults designed to prevent DoS attacks.\n *\n * - MAX_FILE_SIZE (100MB): Prevents downloading very large files that could exhaust memory or disk\n * - REQUEST_TIMEOUT (30s): Prevents slow-loris attacks and hung connections\n * - MAX_JSON_DEPTH (100): Prevents billion laughs attack (deeply nested JSON)\n *\n * Do not increase these limits without understanding the security implications:\n * - Higher file size limit → Greater risk of resource exhaustion\n * - Lower timeout → May reject legitimate slow requests\n * - Lower JSON depth → May reject valid documents\n *\n * @security These limits can be overridden by SDK users, but doing so reduces security.\n */\nexport const DEFAULT_LIMITS = {\n // Maximum file size: 100MB\n MAX_FILE_SIZE: 100 * 1024 * 1024,\n // Request timeout: 30 seconds\n REQUEST_TIMEOUT: 30000,\n // Maximum JSON parse depth\n MAX_JSON_DEPTH: 100,\n};\n\n/**\n * Validate file size before processing\n *\n * ⚠️ SECURITY WARNING: File size validation prevents resource exhaustion.\n * - Without limits: attackers can force downloads of multi-gigabyte files\n * - Memory impact: files are loaded into memory for base64 encoding\n * - Disk impact: temporary storage of downloaded files\n *\n * @param size - The file size in bytes\n * @param maxSize - Maximum allowed size in bytes (default 100MB, can be customized)\n * @throws Error if file exceeds size limit\n * @security Do not disable this check without understanding resource implications\n *\n * @example\n * ```typescript\n * // Standard check with default limit (100MB)\n * validateFileSize(fileSize);\n *\n * // Custom limit for use case that requires larger files\n * validateFileSize(fileSize, 500 * 1024 * 1024); // 500MB\n * ```\n */\nexport function validateFileSize(\n size: number,\n maxSize: number = DEFAULT_LIMITS.MAX_FILE_SIZE\n): void {\n if (size > maxSize) {\n const maxMB = Math.round(maxSize / 1024 / 1024);\n const sizeMB = Math.round(size / 1024 / 1024);\n throw new Error(\n `File size ${sizeMB}MB exceeds maximum allowed size of ${maxMB}MB`\n );\n }\n}\n\n/**\n * Create a fetch controller with timeout\n * @param timeoutMs - Timeout in milliseconds (default 30s)\n * @returns AbortController with timeout configured\n */\nexport function createFetchController(\n timeoutMs: number = DEFAULT_LIMITS.REQUEST_TIMEOUT\n): AbortController {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n // Store timeout ID so caller can clean it up\n (controller as any).__timeoutId = timeoutId;\n\n return controller;\n}\n\n/**\n * Clean up fetch controller timeout\n * @param controller - The AbortController to clean up\n */\nexport function cleanupFetchController(controller: AbortController): void {\n const timeoutId = (controller as any).__timeoutId;\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n}\n\n/**\n * Execute a fetch with automatic timeout and cleanup\n *\n * ⚠️ SECURITY WARNING: Timeout protection prevents slow-loris and other timing attacks.\n * - Without timeout: requests can hang indefinitely, consuming server resources\n * - Slow-loris attack: attacker sends requests slowly to exhaust connection pool\n * - Zombie connections: closed clients but open server connections\n *\n * Default timeout (30s) balances security with legitimate use:\n * - Too short: may reject slow networks or legitimate large downloads\n * - Too long: keeps server resources tied up, enables DoS attacks\n *\n * @param url - The URL to fetch\n * @param options - Fetch options (method, headers, body, etc.)\n * @param timeoutMs - Timeout in milliseconds (default 30s, can be customized)\n * @returns The fetch response\n * @throws Error if request times out\n * @security Do not use very long timeouts without understanding DoS implications\n *\n * @example\n * ```typescript\n * // Default timeout (30 seconds)\n * const response = await fetchWithTimeout('https://example.com/file.pdf');\n *\n * // Custom timeout for slower connections\n * const response = await fetchWithTimeout(url, options, 60000); // 60 seconds\n * ```\n */\nexport async function fetchWithTimeout(\n url: string,\n options: RequestInit = {},\n timeoutMs: number = DEFAULT_LIMITS.REQUEST_TIMEOUT\n): Promise<Response> {\n const controller = createFetchController(timeoutMs);\n\n try {\n const response = await fetch(url, {\n ...options,\n signal: controller.signal,\n cache: 'no-store', // Prevent Next.js cache revalidation which can cause AbortError (see: github.com/vercel/next.js/issues/54045)\n });\n return response;\n } finally {\n cleanupFetchController(controller);\n }\n}\n\n/**\n * Check buffer size before allocation\n * @param size - The buffer size in bytes\n * @param maxSize - Maximum allowed size (default 100MB)\n * @throws Error if buffer would exceed memory limit\n */\nexport function validateBufferSize(\n size: number,\n maxSize: number = DEFAULT_LIMITS.MAX_FILE_SIZE\n): void {\n validateFileSize(size, maxSize);\n}\n\n/**\n * Memory-safe JSON parse with depth limit\n *\n * ⚠️ SECURITY WARNING: Depth limits prevent billion laughs / XML bomb attacks in JSON format.\n * - Billion laughs attack: deeply nested JSON causes exponential memory expansion\n * - Stack overflow: deeply nested structures can exhaust call stack\n * - Resource exhaustion: parsing deeply nested JSON consumes CPU and memory\n *\n * Attack example (evil.json):\n * ```json\n * {\"a\":{\"b\":{\"c\":{\"d\":{\"e\":...}}}}} // 1000+ levels deep\n * ```\n *\n * Default limit (100 levels) prevents attacks while allowing legitimate documents.\n *\n * @param text - JSON string to parse\n * @param maxDepth - Maximum nesting depth in levels (default 100, can be customized)\n * @returns Parsed object\n * @throws Error if JSON exceeds depth limit, size limit, or is invalid\n * @security Do not disable depth checking without understanding XML bomb implications\n *\n * @example\n * ```typescript\n * // Standard parsing with default depth limit (100 levels)\n * const data = safeJsonParse(jsonString);\n *\n * // Custom depth limit for data that needs deeper nesting\n * const data = safeJsonParse(jsonString, 200); // 200 levels\n * ```\n */\nexport function safeJsonParse(\n text: string,\n maxDepth: number = DEFAULT_LIMITS.MAX_JSON_DEPTH\n): unknown {\n try {\n // Quick size check first\n if (text.length > DEFAULT_LIMITS.MAX_FILE_SIZE) {\n throw new Error('JSON string exceeds maximum size');\n }\n\n const obj = JSON.parse(text);\n\n // Check nesting depth\n function checkDepth(obj: unknown, depth: number = 0): void {\n if (depth > maxDepth) {\n throw new Error(`JSON nesting depth exceeds maximum of ${maxDepth}`);\n }\n\n if (typeof obj === 'object' && obj !== null) {\n for (const value of Object.values(obj)) {\n checkDepth(value, depth + 1);\n }\n }\n }\n\n checkDepth(obj);\n return obj;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n }\n throw new Error(`Invalid JSON: ${String(error)}`);\n }\n}\n"],"mappings":"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","names":["checkDepth","obj"]}
|