@arcis/node 1.0.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.
- package/README.md +222 -0
- package/dist/core/index.d.mts +170 -0
- package/dist/core/index.d.ts +170 -0
- package/dist/core/index.js +327 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +307 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/headers-BJq2OA0i.d.ts +284 -0
- package/dist/headers-DBQedhrb.d.mts +284 -0
- package/dist/index-BgHPM7LC.d.ts +129 -0
- package/dist/index-BpT7flAQ.d.ts +255 -0
- package/dist/index-JaFOUKyK.d.mts +255 -0
- package/dist/index-nAgXexwD.d.mts +129 -0
- package/dist/index.d.mts +139 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +1860 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1797 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logging/index.d.mts +38 -0
- package/dist/logging/index.d.ts +38 -0
- package/dist/logging/index.js +140 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/index.mjs +136 -0
- package/dist/logging/index.mjs.map +1 -0
- package/dist/middleware/index.d.mts +3 -0
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +1173 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/index.mjs +1156 -0
- package/dist/middleware/index.mjs.map +1 -0
- package/dist/sanitizers/index.d.mts +24 -0
- package/dist/sanitizers/index.d.ts +24 -0
- package/dist/sanitizers/index.js +610 -0
- package/dist/sanitizers/index.js.map +1 -0
- package/dist/sanitizers/index.mjs +587 -0
- package/dist/sanitizers/index.mjs.map +1 -0
- package/dist/stores/index.d.mts +106 -0
- package/dist/stores/index.d.ts +106 -0
- package/dist/stores/index.js +149 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/stores/index.mjs +145 -0
- package/dist/stores/index.mjs.map +1 -0
- package/dist/types-BOdL3ZWo.d.mts +264 -0
- package/dist/types-BOdL3ZWo.d.ts +264 -0
- package/dist/validation/index.d.mts +3 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.js +705 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/index.mjs +699 -0
- package/dist/validation/index.mjs.map +1 -0
- package/package.json +109 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
// src/core/constants.ts
|
|
6
|
+
var INPUT = {
|
|
7
|
+
/** Default maximum input size (1MB) */
|
|
8
|
+
DEFAULT_MAX_SIZE: 1e6,
|
|
9
|
+
/** Maximum recursion depth for nested objects */
|
|
10
|
+
MAX_RECURSION_DEPTH: 10
|
|
11
|
+
};
|
|
12
|
+
var RATE_LIMIT = {
|
|
13
|
+
/** Default window size (1 minute) */
|
|
14
|
+
DEFAULT_WINDOW_MS: 6e4,
|
|
15
|
+
/** Default max requests per window */
|
|
16
|
+
DEFAULT_MAX_REQUESTS: 100,
|
|
17
|
+
/** Default HTTP status code for rate limited responses */
|
|
18
|
+
DEFAULT_STATUS_CODE: 429,
|
|
19
|
+
/** Default error message */
|
|
20
|
+
DEFAULT_MESSAGE: "Too many requests, please try again later.",
|
|
21
|
+
/** Minimum window size (1 second) */
|
|
22
|
+
MIN_WINDOW_MS: 1e3,
|
|
23
|
+
/** Maximum window size (24 hours) */
|
|
24
|
+
MAX_WINDOW_MS: 864e5
|
|
25
|
+
};
|
|
26
|
+
var HEADERS = {
|
|
27
|
+
/** Default Content Security Policy */
|
|
28
|
+
DEFAULT_CSP: [
|
|
29
|
+
"default-src 'self'",
|
|
30
|
+
"script-src 'self'",
|
|
31
|
+
"style-src 'self' 'unsafe-inline'",
|
|
32
|
+
"img-src 'self' data: https:",
|
|
33
|
+
"font-src 'self'",
|
|
34
|
+
"object-src 'none'",
|
|
35
|
+
"frame-ancestors 'none'"
|
|
36
|
+
].join("; "),
|
|
37
|
+
/** Default HSTS max age (1 year in seconds) */
|
|
38
|
+
HSTS_MAX_AGE: 31536e3,
|
|
39
|
+
/** Default X-Frame-Options value */
|
|
40
|
+
FRAME_OPTIONS: "DENY",
|
|
41
|
+
/** Default X-Content-Type-Options value */
|
|
42
|
+
CONTENT_TYPE_OPTIONS: "nosniff",
|
|
43
|
+
/** Default Referrer-Policy value */
|
|
44
|
+
REFERRER_POLICY: "strict-origin-when-cross-origin",
|
|
45
|
+
/** Default Permissions-Policy value */
|
|
46
|
+
PERMISSIONS_POLICY: "geolocation=(), microphone=(), camera=()",
|
|
47
|
+
/** Default Cache-Control value for security */
|
|
48
|
+
CACHE_CONTROL: "no-store, no-cache, must-revalidate, proxy-revalidate"
|
|
49
|
+
};
|
|
50
|
+
var XSS_PATTERNS = [
|
|
51
|
+
/** Script tags (ReDoS-safe version) */
|
|
52
|
+
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
53
|
+
/** javascript: protocol (allow optional spaces before colon) */
|
|
54
|
+
/javascript\s*:/gi,
|
|
55
|
+
/** vbscript: protocol */
|
|
56
|
+
/vbscript\s*:/gi,
|
|
57
|
+
/** Event handlers (onclick, onerror, etc.) — any separator before attribute */
|
|
58
|
+
/(?:[\s/])on\w+\s*=/gi,
|
|
59
|
+
/** iframe tags */
|
|
60
|
+
/<iframe/gi,
|
|
61
|
+
/** object tags */
|
|
62
|
+
/<object/gi,
|
|
63
|
+
/** embed tags */
|
|
64
|
+
/<embed/gi,
|
|
65
|
+
/** data: URIs (only dangerous ones, avoid false positives) */
|
|
66
|
+
/(?:^|[\s"'=])data:/gi,
|
|
67
|
+
/** URL-encoded script tags */
|
|
68
|
+
/%3Cscript/gi,
|
|
69
|
+
/** SVG with onload */
|
|
70
|
+
/<svg[^>]*onload/gi
|
|
71
|
+
];
|
|
72
|
+
var XSS_REMOVE_PATTERNS = [
|
|
73
|
+
/** Full script blocks (content + tags) */
|
|
74
|
+
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
75
|
+
/** Standalone/unclosed script tags */
|
|
76
|
+
/<script[^>]*>/gi,
|
|
77
|
+
/** iframe — full block and partial/unclosed */
|
|
78
|
+
/<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
|
|
79
|
+
/<iframe[^>]*/gi,
|
|
80
|
+
/** object — full block and partial/unclosed */
|
|
81
|
+
/<object[^>]*>[\s\S]*?<\/object>/gi,
|
|
82
|
+
/<object[^>]*/gi,
|
|
83
|
+
/** embed tags */
|
|
84
|
+
/<embed[^>]*/gi,
|
|
85
|
+
/** SVG with inline event handlers */
|
|
86
|
+
/<svg[^>]*onload[^>]*>/gi,
|
|
87
|
+
/** URL-encoded script tags */
|
|
88
|
+
/%3Cscript/gi,
|
|
89
|
+
/** Event handlers with quoted values: onclick="...", onerror='...' */
|
|
90
|
+
/(?:[\s/])on\w+\s*=\s*["'][^"']*["']/gi,
|
|
91
|
+
/** Event handlers with unquoted values: onload=value */
|
|
92
|
+
/(?:[\s/])on\w+\s*=\s*[^\s>]*/gi,
|
|
93
|
+
/** javascript: and vbscript: protocols (allow optional spaces before colon) */
|
|
94
|
+
/javascript\s*:/gi,
|
|
95
|
+
/vbscript\s*:/gi,
|
|
96
|
+
/** data: URIs with HTML/script content */
|
|
97
|
+
/data\s*:\s*text\/html[^>\s]*/gi
|
|
98
|
+
];
|
|
99
|
+
var SQL_PATTERNS = [
|
|
100
|
+
/** SQL keywords */
|
|
101
|
+
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)\b)/gi,
|
|
102
|
+
/** SQL comments: ANSI (--), C-style (slash-star ... star-slash), MySQL (#) */
|
|
103
|
+
/(--|\/\*|\*\/|#)/g,
|
|
104
|
+
/** SQL statement separators */
|
|
105
|
+
/(;|\|\||&&)/g,
|
|
106
|
+
/** Boolean injection: OR 1=1 */
|
|
107
|
+
/\bOR\s+\d+\s*=\s*\d+/gi,
|
|
108
|
+
/** Boolean injection: OR 'a'='a' or OR "a"="a" (including mixed quotes) */
|
|
109
|
+
/\bOR\s+(['"])[^'"]*\1\s*=\s*(['"])[^'"]*\2/gi,
|
|
110
|
+
/\bOR\s+('[^']*'|"[^"]*")\s*=\s*('[^']*'|"[^"]*")/gi,
|
|
111
|
+
/** Boolean injection: AND 1=1 */
|
|
112
|
+
/\bAND\s+\d+\s*=\s*\d+/gi,
|
|
113
|
+
/** Boolean injection: AND 'a'='a' or AND "a"="a" (including mixed quotes) */
|
|
114
|
+
/\bAND\s+(['"])[^'"]*\1\s*=\s*(['"])[^'"]*\2/gi,
|
|
115
|
+
/\bAND\s+('[^']*'|"[^"]*")\s*=\s*('[^']*'|"[^"]*")/gi,
|
|
116
|
+
/** Time-based blind: SLEEP() */
|
|
117
|
+
/\bSLEEP\s*\(\s*\d+\s*\)/gi,
|
|
118
|
+
/** Time-based blind: BENCHMARK() */
|
|
119
|
+
/\bBENCHMARK\s*\(/gi
|
|
120
|
+
];
|
|
121
|
+
var PATH_PATTERNS = [
|
|
122
|
+
/** Unix path traversal */
|
|
123
|
+
/\.\.\//g,
|
|
124
|
+
/** Windows path traversal */
|
|
125
|
+
/\.\.\\/g,
|
|
126
|
+
/** URL-encoded traversal (%2e%2e) */
|
|
127
|
+
/%2e%2e/gi,
|
|
128
|
+
/** Double URL-encoded traversal (%252e) */
|
|
129
|
+
/%252e/gi,
|
|
130
|
+
/** Mixed encoding: ..%2F */
|
|
131
|
+
/\.\.%2F/gi,
|
|
132
|
+
/** Mixed encoding: %2e./ and .%2e/ */
|
|
133
|
+
/%2e\.[\\/]/gi,
|
|
134
|
+
/\.%2e[\\/]/gi,
|
|
135
|
+
/** Fully URL-encoded: %2e%2e%2f */
|
|
136
|
+
/%2e%2e%2f/gi,
|
|
137
|
+
/** Null byte injection in paths */
|
|
138
|
+
/\0/g
|
|
139
|
+
];
|
|
140
|
+
var COMMAND_PATTERNS = [
|
|
141
|
+
/**
|
|
142
|
+
* Shell metacharacters that enable command chaining/substitution.
|
|
143
|
+
* Bare ( and ) are excluded — they appear in common legitimate values
|
|
144
|
+
* (function calls in code fields, math expressions, etc.).
|
|
145
|
+
* Command substitution is caught by the $( combined pattern below.
|
|
146
|
+
* NOTE: ';', '&', '|' may appear in legitimate URL query strings
|
|
147
|
+
* and Markdown; consider disabling command checking (command: false)
|
|
148
|
+
* for fields that intentionally allow those characters.
|
|
149
|
+
*/
|
|
150
|
+
/[;&|`]/g,
|
|
151
|
+
/** Command substitution: $( ... ) — matched as a pair to reduce false positives */
|
|
152
|
+
/\$\(/g
|
|
153
|
+
];
|
|
154
|
+
var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
155
|
+
"__proto__",
|
|
156
|
+
"constructor",
|
|
157
|
+
"prototype",
|
|
158
|
+
"__definegetter__",
|
|
159
|
+
"__definesetter__",
|
|
160
|
+
"__lookupgetter__",
|
|
161
|
+
"__lookupsetter__"
|
|
162
|
+
]);
|
|
163
|
+
var NOSQL_DANGEROUS_KEYS = /* @__PURE__ */ new Set([
|
|
164
|
+
// Comparison
|
|
165
|
+
"$gt",
|
|
166
|
+
"$gte",
|
|
167
|
+
"$lt",
|
|
168
|
+
"$lte",
|
|
169
|
+
"$ne",
|
|
170
|
+
"$eq",
|
|
171
|
+
"$in",
|
|
172
|
+
"$nin",
|
|
173
|
+
// Logical
|
|
174
|
+
"$and",
|
|
175
|
+
"$or",
|
|
176
|
+
"$not",
|
|
177
|
+
"$nor",
|
|
178
|
+
// Element / evaluation
|
|
179
|
+
"$exists",
|
|
180
|
+
"$type",
|
|
181
|
+
"$regex",
|
|
182
|
+
"$where",
|
|
183
|
+
"$expr",
|
|
184
|
+
"$mod",
|
|
185
|
+
"$text",
|
|
186
|
+
// Array
|
|
187
|
+
"$elemMatch",
|
|
188
|
+
"$all",
|
|
189
|
+
"$size",
|
|
190
|
+
// JavaScript execution (critical)
|
|
191
|
+
"$function",
|
|
192
|
+
"$accumulator",
|
|
193
|
+
// Aggregation pipeline operators (injectable via $lookup etc.)
|
|
194
|
+
"$lookup",
|
|
195
|
+
"$match",
|
|
196
|
+
"$project",
|
|
197
|
+
"$group",
|
|
198
|
+
"$sort",
|
|
199
|
+
"$limit",
|
|
200
|
+
"$skip",
|
|
201
|
+
"$unwind",
|
|
202
|
+
"$addFields",
|
|
203
|
+
"$replaceRoot"
|
|
204
|
+
]);
|
|
205
|
+
var REDACTION = {
|
|
206
|
+
/** Replacement text for redacted values */
|
|
207
|
+
REPLACEMENT: "[REDACTED]",
|
|
208
|
+
/** Truncation indicator */
|
|
209
|
+
TRUNCATED: "[TRUNCATED]",
|
|
210
|
+
/** Max depth indicator */
|
|
211
|
+
MAX_DEPTH: "[MAX_DEPTH]",
|
|
212
|
+
/** Default max message length */
|
|
213
|
+
DEFAULT_MAX_LENGTH: 1e4,
|
|
214
|
+
/** Default sensitive keys to redact */
|
|
215
|
+
SENSITIVE_KEYS: /* @__PURE__ */ new Set([
|
|
216
|
+
"password",
|
|
217
|
+
"passwd",
|
|
218
|
+
"pwd",
|
|
219
|
+
"secret",
|
|
220
|
+
"token",
|
|
221
|
+
"apikey",
|
|
222
|
+
"api_key",
|
|
223
|
+
"apiKey",
|
|
224
|
+
"auth",
|
|
225
|
+
"authorization",
|
|
226
|
+
"credit_card",
|
|
227
|
+
"creditcard",
|
|
228
|
+
"cc",
|
|
229
|
+
"ssn",
|
|
230
|
+
"social_security",
|
|
231
|
+
"private_key",
|
|
232
|
+
"privateKey",
|
|
233
|
+
"access_token",
|
|
234
|
+
"accessToken",
|
|
235
|
+
"refresh_token",
|
|
236
|
+
"refreshToken",
|
|
237
|
+
"bearer",
|
|
238
|
+
"jwt",
|
|
239
|
+
"session",
|
|
240
|
+
"cookie",
|
|
241
|
+
"credentials",
|
|
242
|
+
"x-api-key",
|
|
243
|
+
"x-auth-token"
|
|
244
|
+
])
|
|
245
|
+
};
|
|
246
|
+
var VALIDATION = {
|
|
247
|
+
/**
|
|
248
|
+
* Email regex pattern.
|
|
249
|
+
* Rejects consecutive dots in local part (e.g. test..foo@example.com),
|
|
250
|
+
* leading/trailing dots, and other common invalid forms.
|
|
251
|
+
*/
|
|
252
|
+
EMAIL: /^[^\s@.][^\s@]*(?:\.[^\s@.][^\s@]*)*@[^\s@]+\.[^\s@]+$/,
|
|
253
|
+
/**
|
|
254
|
+
* URL regex pattern.
|
|
255
|
+
* Only allows http:// and https:// — explicitly rejects javascript:,
|
|
256
|
+
* data:, vbscript:, and other dangerous URI schemes.
|
|
257
|
+
*/
|
|
258
|
+
URL: /^https?:\/\/[^\s/$.?#][^\s]*$/,
|
|
259
|
+
/** UUID regex pattern (v4) */
|
|
260
|
+
UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
261
|
+
};
|
|
262
|
+
var ERRORS = {
|
|
263
|
+
/** Generic error message (production) */
|
|
264
|
+
INTERNAL_SERVER_ERROR: "Internal Server Error",
|
|
265
|
+
/** Input too large error */
|
|
266
|
+
INPUT_TOO_LARGE: (maxSize) => `Input exceeds maximum size of ${maxSize} bytes`,
|
|
267
|
+
/** Validation error messages */
|
|
268
|
+
VALIDATION: {
|
|
269
|
+
REQUIRED: (field) => `${field} is required`,
|
|
270
|
+
INVALID_TYPE: (field, type) => `${field} must be a ${type}`,
|
|
271
|
+
MIN_LENGTH: (field, min) => `${field} must be at least ${min} characters`,
|
|
272
|
+
MAX_LENGTH: (field, max) => `${field} must be at most ${max} characters`,
|
|
273
|
+
MIN_VALUE: (field, min) => `${field} must be at least ${min}`,
|
|
274
|
+
MAX_VALUE: (field, max) => `${field} must be at most ${max}`,
|
|
275
|
+
INVALID_FORMAT: (field) => `${field} format is invalid`,
|
|
276
|
+
INVALID_EMAIL: (field) => `${field} must be a valid email`,
|
|
277
|
+
INVALID_URL: (field) => `${field} must be a valid URL`,
|
|
278
|
+
INVALID_UUID: (field) => `${field} must be a valid UUID`,
|
|
279
|
+
INVALID_ENUM: (field, values) => `${field} must be one of: ${values.join(", ")}`,
|
|
280
|
+
MIN_ITEMS: (field, min) => `${field} must have at least ${min} items`,
|
|
281
|
+
MAX_ITEMS: (field, max) => `${field} must have at most ${max} items`
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
var BLOCKED = "[BLOCKED]";
|
|
285
|
+
|
|
286
|
+
// src/middleware/headers.ts
|
|
287
|
+
function createHeaders(options = {}) {
|
|
288
|
+
const {
|
|
289
|
+
contentSecurityPolicy = true,
|
|
290
|
+
xssFilter = true,
|
|
291
|
+
noSniff = true,
|
|
292
|
+
frameOptions = HEADERS.FRAME_OPTIONS,
|
|
293
|
+
hsts = true,
|
|
294
|
+
referrerPolicy = HEADERS.REFERRER_POLICY,
|
|
295
|
+
permissionsPolicy = HEADERS.PERMISSIONS_POLICY,
|
|
296
|
+
cacheControl = true
|
|
297
|
+
} = options;
|
|
298
|
+
return (req, res, next) => {
|
|
299
|
+
if (contentSecurityPolicy) {
|
|
300
|
+
const csp = typeof contentSecurityPolicy === "string" ? contentSecurityPolicy : HEADERS.DEFAULT_CSP;
|
|
301
|
+
res.setHeader("Content-Security-Policy", csp);
|
|
302
|
+
}
|
|
303
|
+
if (xssFilter) {
|
|
304
|
+
res.setHeader("X-XSS-Protection", "1; mode=block");
|
|
305
|
+
}
|
|
306
|
+
if (noSniff) {
|
|
307
|
+
res.setHeader("X-Content-Type-Options", HEADERS.CONTENT_TYPE_OPTIONS);
|
|
308
|
+
}
|
|
309
|
+
if (frameOptions) {
|
|
310
|
+
res.setHeader("X-Frame-Options", frameOptions);
|
|
311
|
+
}
|
|
312
|
+
const forwardedProto = req.headers["x-forwarded-proto"]?.split(",")[0].trim().toLowerCase();
|
|
313
|
+
const trustedForwardedProto = forwardedProto === "https" || forwardedProto === "http" ? forwardedProto : void 0;
|
|
314
|
+
const isHttps = req.secure || trustedForwardedProto === "https";
|
|
315
|
+
if (hsts && isHttps) {
|
|
316
|
+
const hstsOpts = typeof hsts === "object" ? hsts : {};
|
|
317
|
+
const maxAge = hstsOpts.maxAge ?? HEADERS.HSTS_MAX_AGE;
|
|
318
|
+
const includeSubDomains = hstsOpts.includeSubDomains !== false;
|
|
319
|
+
const preload = hstsOpts.preload === true;
|
|
320
|
+
let hstsValue = `max-age=${maxAge}`;
|
|
321
|
+
if (includeSubDomains) hstsValue += "; includeSubDomains";
|
|
322
|
+
if (preload) hstsValue += "; preload";
|
|
323
|
+
res.setHeader("Strict-Transport-Security", hstsValue);
|
|
324
|
+
}
|
|
325
|
+
if (referrerPolicy) {
|
|
326
|
+
res.setHeader("Referrer-Policy", referrerPolicy);
|
|
327
|
+
}
|
|
328
|
+
if (permissionsPolicy) {
|
|
329
|
+
res.setHeader("Permissions-Policy", permissionsPolicy);
|
|
330
|
+
}
|
|
331
|
+
res.setHeader("X-Permitted-Cross-Domain-Policies", "none");
|
|
332
|
+
if (cacheControl) {
|
|
333
|
+
const cacheControlValue = typeof cacheControl === "string" ? cacheControl : HEADERS.CACHE_CONTROL;
|
|
334
|
+
res.setHeader("Cache-Control", cacheControlValue);
|
|
335
|
+
res.setHeader("Pragma", "no-cache");
|
|
336
|
+
res.setHeader("Expires", "0");
|
|
337
|
+
}
|
|
338
|
+
res.removeHeader("X-Powered-By");
|
|
339
|
+
next();
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
var securityHeaders = createHeaders;
|
|
343
|
+
|
|
344
|
+
// src/middleware/rate-limit.ts
|
|
345
|
+
function createRateLimiter(options = {}) {
|
|
346
|
+
const {
|
|
347
|
+
max = RATE_LIMIT.DEFAULT_MAX_REQUESTS,
|
|
348
|
+
windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS,
|
|
349
|
+
message = RATE_LIMIT.DEFAULT_MESSAGE,
|
|
350
|
+
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
351
|
+
keyGenerator = (req) => {
|
|
352
|
+
const ip = req.ip ?? req.socket?.remoteAddress;
|
|
353
|
+
if (!ip) {
|
|
354
|
+
console.warn(
|
|
355
|
+
"[arcis] Rate limiter: cannot resolve client IP. All unresolvable clients share one counter. Set Express trust proxy if behind a reverse proxy."
|
|
356
|
+
);
|
|
357
|
+
return "unknown";
|
|
358
|
+
}
|
|
359
|
+
return ip;
|
|
360
|
+
},
|
|
361
|
+
skip,
|
|
362
|
+
store: externalStore
|
|
363
|
+
} = options;
|
|
364
|
+
const inMemoryStore = /* @__PURE__ */ Object.create(null);
|
|
365
|
+
let cleanupInterval = null;
|
|
366
|
+
if (!externalStore) {
|
|
367
|
+
cleanupInterval = setInterval(() => {
|
|
368
|
+
const now = Date.now();
|
|
369
|
+
for (const key of Object.keys(inMemoryStore)) {
|
|
370
|
+
if (inMemoryStore[key].resetTime < now) {
|
|
371
|
+
delete inMemoryStore[key];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}, windowMs);
|
|
375
|
+
if (typeof cleanupInterval.unref === "function") {
|
|
376
|
+
cleanupInterval.unref();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const handler = async (req, res, next) => {
|
|
380
|
+
try {
|
|
381
|
+
if (skip?.(req)) {
|
|
382
|
+
return next();
|
|
383
|
+
}
|
|
384
|
+
const key = keyGenerator(req);
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
let count;
|
|
387
|
+
let resetTime;
|
|
388
|
+
if (externalStore) {
|
|
389
|
+
const entry = await externalStore.get(key);
|
|
390
|
+
if (!entry || entry.resetTime < now) {
|
|
391
|
+
await externalStore.set(key, { count: 1, resetTime: now + windowMs });
|
|
392
|
+
count = 1;
|
|
393
|
+
resetTime = now + windowMs;
|
|
394
|
+
} else {
|
|
395
|
+
count = await externalStore.increment(key);
|
|
396
|
+
resetTime = entry.resetTime;
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
|
|
400
|
+
inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
|
|
401
|
+
} else {
|
|
402
|
+
inMemoryStore[key].count++;
|
|
403
|
+
}
|
|
404
|
+
count = inMemoryStore[key].count;
|
|
405
|
+
resetTime = inMemoryStore[key].resetTime;
|
|
406
|
+
}
|
|
407
|
+
const remaining = Math.max(0, max - count);
|
|
408
|
+
const resetSeconds = Math.ceil((resetTime - now) / 1e3);
|
|
409
|
+
res.setHeader("X-RateLimit-Limit", max.toString());
|
|
410
|
+
res.setHeader("X-RateLimit-Remaining", remaining.toString());
|
|
411
|
+
res.setHeader("X-RateLimit-Reset", resetSeconds.toString());
|
|
412
|
+
if (count > max) {
|
|
413
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
414
|
+
res.status(statusCode).json({
|
|
415
|
+
error: message,
|
|
416
|
+
retryAfter: resetSeconds
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
next();
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error("[arcis] Rate limiter error:", error);
|
|
423
|
+
next();
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const middleware = handler;
|
|
427
|
+
middleware.close = () => {
|
|
428
|
+
if (cleanupInterval) {
|
|
429
|
+
clearInterval(cleanupInterval);
|
|
430
|
+
cleanupInterval = null;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
return middleware;
|
|
434
|
+
}
|
|
435
|
+
var rateLimit = createRateLimiter;
|
|
436
|
+
|
|
437
|
+
// src/middleware/error-handler.ts
|
|
438
|
+
var SENSITIVE_ERROR_PATTERNS = [
|
|
439
|
+
// SQL database errors
|
|
440
|
+
/\b(SQLITE_ERROR|SQLSTATE|ORA-\d|PG::|mysql_|pg_query|ECONNREFUSED)/i,
|
|
441
|
+
/\b(syntax error at or near|relation ".*" does not exist)/i,
|
|
442
|
+
/\b(column ".*" (does not exist|of relation))/i,
|
|
443
|
+
/\b(duplicate key value violates unique constraint)/i,
|
|
444
|
+
/\b(table .* doesn't exist|unknown column)/i,
|
|
445
|
+
// MongoDB errors
|
|
446
|
+
/\b(MongoError|MongoServerError|MongoNetworkError|E11000 duplicate key)/i,
|
|
447
|
+
// Redis errors
|
|
448
|
+
/\b(WRONGTYPE|CROSSSLOT|CLUSTERDOWN|READONLY|ReplyError)/i,
|
|
449
|
+
// Connection strings and DSNs
|
|
450
|
+
/\b(mongodb(\+srv)?:\/\/|postgres(ql)?:\/\/|mysql:\/\/|redis:\/\/)/i,
|
|
451
|
+
// Stack traces with file paths
|
|
452
|
+
/\bat\s+.*\.(js|ts|py|go|java):\d+/i,
|
|
453
|
+
// Internal IP addresses
|
|
454
|
+
/\b(127\.0\.0\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)\b/
|
|
455
|
+
];
|
|
456
|
+
function containsSensitiveInfo(message) {
|
|
457
|
+
return SENSITIVE_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
458
|
+
}
|
|
459
|
+
function errorHandler(options = false) {
|
|
460
|
+
const isDev = typeof options === "boolean" ? options : options.isDev ?? false;
|
|
461
|
+
const logErrors = typeof options === "object" ? options.logErrors ?? true : true;
|
|
462
|
+
const logger = typeof options === "object" ? options.logger : void 0;
|
|
463
|
+
const customHandler = typeof options === "object" ? options.customHandler : void 0;
|
|
464
|
+
return (err, req, res, _next) => {
|
|
465
|
+
const statusCode = err.statusCode || err.status || 500;
|
|
466
|
+
if (customHandler) {
|
|
467
|
+
return customHandler(err, req, res);
|
|
468
|
+
}
|
|
469
|
+
if (logErrors) {
|
|
470
|
+
const logData = {
|
|
471
|
+
error: err.message,
|
|
472
|
+
stack: err.stack,
|
|
473
|
+
statusCode,
|
|
474
|
+
path: req.path,
|
|
475
|
+
method: req.method
|
|
476
|
+
};
|
|
477
|
+
if (logger) {
|
|
478
|
+
logger.error("Request error", logData);
|
|
479
|
+
} else {
|
|
480
|
+
console.error("[arcis] Request error:", logData);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const exposeMessage = isDev || err.expose === true;
|
|
484
|
+
let clientMessage;
|
|
485
|
+
if (!exposeMessage) {
|
|
486
|
+
clientMessage = ERRORS.INTERNAL_SERVER_ERROR;
|
|
487
|
+
} else if (containsSensitiveInfo(err.message)) {
|
|
488
|
+
clientMessage = isDev ? err.message : ERRORS.INTERNAL_SERVER_ERROR;
|
|
489
|
+
} else {
|
|
490
|
+
clientMessage = err.message;
|
|
491
|
+
}
|
|
492
|
+
const response = {
|
|
493
|
+
error: clientMessage
|
|
494
|
+
};
|
|
495
|
+
if (isDev) {
|
|
496
|
+
response.stack = err.stack;
|
|
497
|
+
response.details = err.message;
|
|
498
|
+
}
|
|
499
|
+
res.status(statusCode).json(response);
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
var createErrorHandler = errorHandler;
|
|
503
|
+
|
|
504
|
+
// src/core/errors.ts
|
|
505
|
+
var ArcisError = class extends Error {
|
|
506
|
+
constructor(message, statusCode = 500, code = "ARCIS_ERROR") {
|
|
507
|
+
super(message);
|
|
508
|
+
this.name = "ArcisError";
|
|
509
|
+
this.statusCode = statusCode;
|
|
510
|
+
this.code = code;
|
|
511
|
+
this.expose = statusCode < 500;
|
|
512
|
+
if (Error.captureStackTrace) {
|
|
513
|
+
Error.captureStackTrace(this, this.constructor);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
var ValidationError = class extends ArcisError {
|
|
518
|
+
constructor(errors) {
|
|
519
|
+
super("Validation failed", 400, "VALIDATION_ERROR");
|
|
520
|
+
this.name = "ValidationError";
|
|
521
|
+
this.errors = errors;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
var RateLimitError = class extends ArcisError {
|
|
525
|
+
constructor(message, retryAfter) {
|
|
526
|
+
super(message, 429, "RATE_LIMIT_EXCEEDED");
|
|
527
|
+
this.name = "RateLimitError";
|
|
528
|
+
this.retryAfter = retryAfter;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
var InputTooLargeError = class extends ArcisError {
|
|
532
|
+
constructor(maxSize, actualSize) {
|
|
533
|
+
super(`Input exceeds maximum size of ${maxSize} bytes`, 413, "INPUT_TOO_LARGE");
|
|
534
|
+
this.name = "InputTooLargeError";
|
|
535
|
+
this.maxSize = maxSize;
|
|
536
|
+
this.actualSize = actualSize;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
var SecurityThreatError = class extends ArcisError {
|
|
540
|
+
constructor(threatType, pattern) {
|
|
541
|
+
super("Request blocked for security reasons", 400, "SECURITY_THREAT");
|
|
542
|
+
this.name = "SecurityThreatError";
|
|
543
|
+
this.threatType = threatType;
|
|
544
|
+
this.pattern = pattern;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
var SanitizationError = class extends ArcisError {
|
|
548
|
+
constructor(message) {
|
|
549
|
+
super(message, 400, "SANITIZATION_ERROR");
|
|
550
|
+
this.name = "SanitizationError";
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// src/sanitizers/utils.ts
|
|
555
|
+
function encodeHtmlEntities(str) {
|
|
556
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/sanitizers/xss.ts
|
|
560
|
+
function sanitizeXss(input, collectThreats = false, htmlEncode = false) {
|
|
561
|
+
if (typeof input !== "string") {
|
|
562
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
563
|
+
}
|
|
564
|
+
const threats = [];
|
|
565
|
+
let value = input;
|
|
566
|
+
let wasSanitized = false;
|
|
567
|
+
for (const pattern of XSS_REMOVE_PATTERNS) {
|
|
568
|
+
pattern.lastIndex = 0;
|
|
569
|
+
if (pattern.test(value)) {
|
|
570
|
+
pattern.lastIndex = 0;
|
|
571
|
+
if (collectThreats) {
|
|
572
|
+
const matches = value.match(pattern);
|
|
573
|
+
if (matches) {
|
|
574
|
+
for (const match of matches) {
|
|
575
|
+
threats.push({
|
|
576
|
+
type: "xss",
|
|
577
|
+
pattern: pattern.source,
|
|
578
|
+
original: match
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
value = value.replace(pattern, "");
|
|
584
|
+
wasSanitized = true;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (htmlEncode) {
|
|
588
|
+
const encoded = encodeHtmlEntities(value);
|
|
589
|
+
if (encoded !== value) {
|
|
590
|
+
wasSanitized = true;
|
|
591
|
+
}
|
|
592
|
+
value = encoded;
|
|
593
|
+
}
|
|
594
|
+
if (collectThreats) {
|
|
595
|
+
return { value, wasSanitized, threats };
|
|
596
|
+
}
|
|
597
|
+
return value;
|
|
598
|
+
}
|
|
599
|
+
function detectXss(input) {
|
|
600
|
+
if (typeof input !== "string") return false;
|
|
601
|
+
if (/\s+on\w+\s*=/i.test(input)) return true;
|
|
602
|
+
if (/javascript\s*:/i.test(input)) return true;
|
|
603
|
+
if (/vbscript\s*:/i.test(input)) return true;
|
|
604
|
+
if (/data\s*:\s*text\/html/i.test(input)) return true;
|
|
605
|
+
for (const pattern of XSS_PATTERNS) {
|
|
606
|
+
pattern.lastIndex = 0;
|
|
607
|
+
if (pattern.test(input)) {
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/sanitizers/sql.ts
|
|
615
|
+
function sanitizeSql(input, collectThreats = false) {
|
|
616
|
+
if (typeof input !== "string") {
|
|
617
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
618
|
+
}
|
|
619
|
+
const threats = [];
|
|
620
|
+
let value = input;
|
|
621
|
+
let wasSanitized = false;
|
|
622
|
+
for (const pattern of SQL_PATTERNS) {
|
|
623
|
+
pattern.lastIndex = 0;
|
|
624
|
+
if (pattern.test(value)) {
|
|
625
|
+
pattern.lastIndex = 0;
|
|
626
|
+
if (collectThreats) {
|
|
627
|
+
const matches = value.match(pattern);
|
|
628
|
+
if (matches) {
|
|
629
|
+
for (const match of matches) {
|
|
630
|
+
threats.push({
|
|
631
|
+
type: "sql_injection",
|
|
632
|
+
pattern: pattern.source,
|
|
633
|
+
original: match
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
value = value.replace(pattern, " ");
|
|
639
|
+
wasSanitized = true;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (collectThreats) {
|
|
643
|
+
return { value, wasSanitized, threats };
|
|
644
|
+
}
|
|
645
|
+
return value;
|
|
646
|
+
}
|
|
647
|
+
function detectSql(input) {
|
|
648
|
+
if (typeof input !== "string") return false;
|
|
649
|
+
for (const pattern of SQL_PATTERNS) {
|
|
650
|
+
pattern.lastIndex = 0;
|
|
651
|
+
if (pattern.test(input)) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/sanitizers/path.ts
|
|
659
|
+
function sanitizePath(input, collectThreats = false) {
|
|
660
|
+
if (typeof input !== "string") {
|
|
661
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
662
|
+
}
|
|
663
|
+
const threats = [];
|
|
664
|
+
let value = input;
|
|
665
|
+
let wasSanitized = false;
|
|
666
|
+
for (const pattern of PATH_PATTERNS) {
|
|
667
|
+
pattern.lastIndex = 0;
|
|
668
|
+
if (pattern.test(value)) {
|
|
669
|
+
pattern.lastIndex = 0;
|
|
670
|
+
if (collectThreats) {
|
|
671
|
+
const matches = value.match(pattern);
|
|
672
|
+
if (matches) {
|
|
673
|
+
for (const match of matches) {
|
|
674
|
+
threats.push({
|
|
675
|
+
type: "path_traversal",
|
|
676
|
+
pattern: pattern.source,
|
|
677
|
+
original: match
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
value = value.replace(pattern, "");
|
|
683
|
+
wasSanitized = true;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (collectThreats) {
|
|
687
|
+
return { value, wasSanitized, threats };
|
|
688
|
+
}
|
|
689
|
+
return value;
|
|
690
|
+
}
|
|
691
|
+
function detectPathTraversal(input) {
|
|
692
|
+
if (typeof input !== "string") return false;
|
|
693
|
+
for (const pattern of PATH_PATTERNS) {
|
|
694
|
+
pattern.lastIndex = 0;
|
|
695
|
+
if (pattern.test(input)) {
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/sanitizers/command.ts
|
|
703
|
+
function sanitizeCommand(input, collectThreats = false) {
|
|
704
|
+
if (typeof input !== "string") {
|
|
705
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
706
|
+
}
|
|
707
|
+
const threats = [];
|
|
708
|
+
let value = input;
|
|
709
|
+
let wasSanitized = false;
|
|
710
|
+
for (const pattern of COMMAND_PATTERNS) {
|
|
711
|
+
pattern.lastIndex = 0;
|
|
712
|
+
if (pattern.test(value)) {
|
|
713
|
+
pattern.lastIndex = 0;
|
|
714
|
+
if (collectThreats) {
|
|
715
|
+
const matches = value.match(pattern);
|
|
716
|
+
if (matches) {
|
|
717
|
+
for (const match of matches) {
|
|
718
|
+
threats.push({
|
|
719
|
+
type: "command_injection",
|
|
720
|
+
pattern: pattern.source,
|
|
721
|
+
original: match
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
value = value.replace(pattern, " ");
|
|
727
|
+
wasSanitized = true;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (collectThreats) {
|
|
731
|
+
return { value, wasSanitized, threats };
|
|
732
|
+
}
|
|
733
|
+
return value;
|
|
734
|
+
}
|
|
735
|
+
function detectCommandInjection(input) {
|
|
736
|
+
if (typeof input !== "string") return false;
|
|
737
|
+
for (const pattern of COMMAND_PATTERNS) {
|
|
738
|
+
pattern.lastIndex = 0;
|
|
739
|
+
if (pattern.test(input)) {
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/sanitizers/sanitize.ts
|
|
747
|
+
function sanitizeString(value, options = {}) {
|
|
748
|
+
if (typeof value !== "string") return value;
|
|
749
|
+
const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
|
|
750
|
+
if (value.length > maxSize) {
|
|
751
|
+
throw new InputTooLargeError(maxSize, value.length);
|
|
752
|
+
}
|
|
753
|
+
const reject = options.mode !== "sanitize";
|
|
754
|
+
let result = value;
|
|
755
|
+
if (options.sql !== false) {
|
|
756
|
+
if (reject) {
|
|
757
|
+
if (detectSql(result)) {
|
|
758
|
+
throw new SecurityThreatError("sql_injection", "SQL pattern detected in input");
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
result = sanitizeSql(result);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (options.path !== false) {
|
|
765
|
+
result = sanitizePath(result);
|
|
766
|
+
}
|
|
767
|
+
if (options.command !== false) {
|
|
768
|
+
if (reject) {
|
|
769
|
+
if (detectCommandInjection(result)) {
|
|
770
|
+
throw new SecurityThreatError("command_injection", "Shell metacharacter detected in input");
|
|
771
|
+
}
|
|
772
|
+
} else {
|
|
773
|
+
result = sanitizeCommand(result);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (options.xss !== false) {
|
|
777
|
+
result = sanitizeXss(result, false, options.htmlEncode ?? false);
|
|
778
|
+
}
|
|
779
|
+
return result;
|
|
780
|
+
}
|
|
781
|
+
function sanitizeObject(obj, options = {}) {
|
|
782
|
+
if (obj === null || obj === void 0) return obj;
|
|
783
|
+
if (typeof obj === "string") return sanitizeString(obj, options);
|
|
784
|
+
if (typeof obj !== "object") return obj;
|
|
785
|
+
if (Array.isArray(obj)) return obj.map((item) => sanitizeObject(item, options));
|
|
786
|
+
return sanitizeObjectDepth(obj, options, 0);
|
|
787
|
+
}
|
|
788
|
+
function sanitizeObjectDepth(obj, options, depth) {
|
|
789
|
+
if (depth >= INPUT.MAX_RECURSION_DEPTH) return obj;
|
|
790
|
+
const result = {};
|
|
791
|
+
for (const key of Object.keys(obj)) {
|
|
792
|
+
if (options.proto !== false && DANGEROUS_PROTO_KEYS.has(key.toLowerCase())) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (options.nosql !== false && NOSQL_DANGEROUS_KEYS.has(key)) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const sanitizedKey = sanitizeString(key, options);
|
|
799
|
+
const value = obj[key];
|
|
800
|
+
if (value === null || value === void 0) {
|
|
801
|
+
result[sanitizedKey] = value;
|
|
802
|
+
} else if (typeof value === "string") {
|
|
803
|
+
result[sanitizedKey] = sanitizeString(value, options);
|
|
804
|
+
} else if (Array.isArray(value)) {
|
|
805
|
+
result[sanitizedKey] = value.map((item) => sanitizeObject(item, options));
|
|
806
|
+
} else if (typeof value === "object") {
|
|
807
|
+
result[sanitizedKey] = sanitizeObjectDepth(value, options, depth + 1);
|
|
808
|
+
} else {
|
|
809
|
+
result[sanitizedKey] = value;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return result;
|
|
813
|
+
}
|
|
814
|
+
function createSanitizer(options = {}) {
|
|
815
|
+
return (req, _res, next) => {
|
|
816
|
+
try {
|
|
817
|
+
if (req.body && typeof req.body === "object") {
|
|
818
|
+
req.body = sanitizeObject(req.body, options);
|
|
819
|
+
}
|
|
820
|
+
if (req.query && typeof req.query === "object") {
|
|
821
|
+
const sanitizedQuery = sanitizeObject(req.query, options);
|
|
822
|
+
Object.defineProperty(req, "query", { value: sanitizedQuery, writable: true, configurable: true });
|
|
823
|
+
}
|
|
824
|
+
if (req.params && typeof req.params === "object") {
|
|
825
|
+
const sanitizedParams = sanitizeObject(req.params, options);
|
|
826
|
+
Object.defineProperty(req, "params", { value: sanitizedParams, writable: true, configurable: true });
|
|
827
|
+
}
|
|
828
|
+
next();
|
|
829
|
+
} catch (err) {
|
|
830
|
+
next(err);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/sanitizers/nosql.ts
|
|
836
|
+
function isDangerousNoSqlKey(key) {
|
|
837
|
+
return NOSQL_DANGEROUS_KEYS.has(key);
|
|
838
|
+
}
|
|
839
|
+
function detectNoSqlInjection(obj, maxDepth = 10) {
|
|
840
|
+
if (maxDepth <= 0) return false;
|
|
841
|
+
if (obj === null || typeof obj !== "object") return false;
|
|
842
|
+
if (Array.isArray(obj)) {
|
|
843
|
+
return obj.some((item) => detectNoSqlInjection(item, maxDepth - 1));
|
|
844
|
+
}
|
|
845
|
+
for (const key of Object.keys(obj)) {
|
|
846
|
+
if (isDangerousNoSqlKey(key)) {
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
const value = obj[key];
|
|
850
|
+
if (typeof value === "object" && value !== null) {
|
|
851
|
+
if (detectNoSqlInjection(value, maxDepth - 1)) {
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/sanitizers/prototype.ts
|
|
860
|
+
function isDangerousProtoKey(key) {
|
|
861
|
+
return DANGEROUS_PROTO_KEYS.has(key.toLowerCase());
|
|
862
|
+
}
|
|
863
|
+
function detectPrototypePollution(obj, maxDepth = 10) {
|
|
864
|
+
if (maxDepth <= 0) return false;
|
|
865
|
+
if (obj === null || typeof obj !== "object") return false;
|
|
866
|
+
if (Array.isArray(obj)) {
|
|
867
|
+
return obj.some((item) => detectPrototypePollution(item, maxDepth - 1));
|
|
868
|
+
}
|
|
869
|
+
for (const key of Object.keys(obj)) {
|
|
870
|
+
if (DANGEROUS_PROTO_KEYS.has(key.toLowerCase())) {
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
const value = obj[key];
|
|
874
|
+
if (typeof value === "object" && value !== null) {
|
|
875
|
+
if (detectPrototypePollution(value, maxDepth - 1)) {
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/sanitizers/headers.ts
|
|
884
|
+
var HEADER_INJECTION_PATTERN = /\r\n|\r|\n|\0/g;
|
|
885
|
+
function sanitizeHeaderValue(input, collectThreats = false) {
|
|
886
|
+
if (typeof input !== "string") {
|
|
887
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
888
|
+
}
|
|
889
|
+
const threats = [];
|
|
890
|
+
let wasSanitized = false;
|
|
891
|
+
if (HEADER_INJECTION_PATTERN.test(input)) {
|
|
892
|
+
HEADER_INJECTION_PATTERN.lastIndex = 0;
|
|
893
|
+
wasSanitized = true;
|
|
894
|
+
if (collectThreats) {
|
|
895
|
+
const matches = input.match(HEADER_INJECTION_PATTERN);
|
|
896
|
+
if (matches) {
|
|
897
|
+
for (const match of matches) {
|
|
898
|
+
threats.push({
|
|
899
|
+
type: "header_injection",
|
|
900
|
+
pattern: HEADER_INJECTION_PATTERN.source,
|
|
901
|
+
original: match
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
HEADER_INJECTION_PATTERN.lastIndex = 0;
|
|
908
|
+
const value = input.replace(HEADER_INJECTION_PATTERN, "");
|
|
909
|
+
if (collectThreats) {
|
|
910
|
+
return { value, wasSanitized, threats };
|
|
911
|
+
}
|
|
912
|
+
return value;
|
|
913
|
+
}
|
|
914
|
+
function sanitizeHeaders(headers) {
|
|
915
|
+
if (!headers || typeof headers !== "object") {
|
|
916
|
+
return {};
|
|
917
|
+
}
|
|
918
|
+
const result = {};
|
|
919
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
920
|
+
const sanitizedKey = sanitizeHeaderValue(String(key));
|
|
921
|
+
const sanitizedValue = sanitizeHeaderValue(String(value));
|
|
922
|
+
result[sanitizedKey] = sanitizedValue;
|
|
923
|
+
}
|
|
924
|
+
return result;
|
|
925
|
+
}
|
|
926
|
+
function detectHeaderInjection(input) {
|
|
927
|
+
if (typeof input !== "string") return false;
|
|
928
|
+
HEADER_INJECTION_PATTERN.lastIndex = 0;
|
|
929
|
+
return HEADER_INJECTION_PATTERN.test(input);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/validation/schema.ts
|
|
933
|
+
function validate(schema, source = "body") {
|
|
934
|
+
return (req, res, next) => {
|
|
935
|
+
const data = req[source] || {};
|
|
936
|
+
const errors = [];
|
|
937
|
+
const validated = {};
|
|
938
|
+
for (const [field, rules] of Object.entries(schema)) {
|
|
939
|
+
const value = data[field];
|
|
940
|
+
const result = validateField(field, value, rules);
|
|
941
|
+
if (result.errors.length > 0) {
|
|
942
|
+
errors.push(...result.errors);
|
|
943
|
+
} else if (result.value !== void 0) {
|
|
944
|
+
validated[field] = result.value;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (errors.length > 0) {
|
|
948
|
+
res.status(400).json({ errors });
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
req[source] = validated;
|
|
952
|
+
next();
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function validateField(field, value, rules) {
|
|
956
|
+
const errors = [];
|
|
957
|
+
if (rules.required && (value === void 0 || value === null || value === "")) {
|
|
958
|
+
errors.push(ERRORS.VALIDATION.REQUIRED(field));
|
|
959
|
+
return { errors };
|
|
960
|
+
}
|
|
961
|
+
if (value === void 0 || value === null) {
|
|
962
|
+
return { errors: [] };
|
|
963
|
+
}
|
|
964
|
+
let typedValue = value;
|
|
965
|
+
let isValid = true;
|
|
966
|
+
switch (rules.type) {
|
|
967
|
+
case "string":
|
|
968
|
+
if (typeof value !== "string") {
|
|
969
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "string"));
|
|
970
|
+
isValid = false;
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
if (rules.min !== void 0 && value.length < rules.min) {
|
|
974
|
+
errors.push(ERRORS.VALIDATION.MIN_LENGTH(field, rules.min));
|
|
975
|
+
isValid = false;
|
|
976
|
+
}
|
|
977
|
+
if (rules.max !== void 0 && value.length > rules.max) {
|
|
978
|
+
errors.push(ERRORS.VALIDATION.MAX_LENGTH(field, rules.max));
|
|
979
|
+
isValid = false;
|
|
980
|
+
}
|
|
981
|
+
if (rules.pattern && !rules.pattern.test(value)) {
|
|
982
|
+
errors.push(ERRORS.VALIDATION.INVALID_FORMAT(field));
|
|
983
|
+
isValid = false;
|
|
984
|
+
}
|
|
985
|
+
if (isValid && rules.enum && !rules.enum.includes(value)) {
|
|
986
|
+
errors.push(ERRORS.VALIDATION.INVALID_ENUM(field, rules.enum));
|
|
987
|
+
isValid = false;
|
|
988
|
+
}
|
|
989
|
+
if (isValid && rules.sanitize !== false) {
|
|
990
|
+
typedValue = sanitizeString(value);
|
|
991
|
+
}
|
|
992
|
+
break;
|
|
993
|
+
case "number":
|
|
994
|
+
typedValue = Number(value);
|
|
995
|
+
if (isNaN(typedValue)) {
|
|
996
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "number"));
|
|
997
|
+
isValid = false;
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
if (rules.min !== void 0 && typedValue < rules.min) {
|
|
1001
|
+
errors.push(ERRORS.VALIDATION.MIN_VALUE(field, rules.min));
|
|
1002
|
+
isValid = false;
|
|
1003
|
+
}
|
|
1004
|
+
if (rules.max !== void 0 && typedValue > rules.max) {
|
|
1005
|
+
errors.push(ERRORS.VALIDATION.MAX_VALUE(field, rules.max));
|
|
1006
|
+
isValid = false;
|
|
1007
|
+
}
|
|
1008
|
+
break;
|
|
1009
|
+
case "boolean":
|
|
1010
|
+
if (value === "true" || value === true || value === 1 || value === "1") {
|
|
1011
|
+
typedValue = true;
|
|
1012
|
+
} else if (value === "false" || value === false || value === 0 || value === "0") {
|
|
1013
|
+
typedValue = false;
|
|
1014
|
+
} else {
|
|
1015
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "boolean"));
|
|
1016
|
+
isValid = false;
|
|
1017
|
+
}
|
|
1018
|
+
break;
|
|
1019
|
+
case "email":
|
|
1020
|
+
if (!VALIDATION.EMAIL.test(String(value))) {
|
|
1021
|
+
errors.push(ERRORS.VALIDATION.INVALID_EMAIL(field));
|
|
1022
|
+
isValid = false;
|
|
1023
|
+
}
|
|
1024
|
+
if (isValid) {
|
|
1025
|
+
typedValue = sanitizeString(String(value).toLowerCase().trim());
|
|
1026
|
+
}
|
|
1027
|
+
break;
|
|
1028
|
+
case "url":
|
|
1029
|
+
if (!VALIDATION.URL.test(String(value))) {
|
|
1030
|
+
errors.push(ERRORS.VALIDATION.INVALID_URL(field));
|
|
1031
|
+
isValid = false;
|
|
1032
|
+
}
|
|
1033
|
+
if (isValid) {
|
|
1034
|
+
typedValue = sanitizeString(String(value));
|
|
1035
|
+
}
|
|
1036
|
+
break;
|
|
1037
|
+
case "uuid":
|
|
1038
|
+
if (!VALIDATION.UUID.test(String(value))) {
|
|
1039
|
+
errors.push(ERRORS.VALIDATION.INVALID_UUID(field));
|
|
1040
|
+
isValid = false;
|
|
1041
|
+
}
|
|
1042
|
+
break;
|
|
1043
|
+
case "array":
|
|
1044
|
+
if (!Array.isArray(value)) {
|
|
1045
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "array"));
|
|
1046
|
+
isValid = false;
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
if (rules.min !== void 0 && value.length < rules.min) {
|
|
1050
|
+
errors.push(ERRORS.VALIDATION.MIN_ITEMS(field, rules.min));
|
|
1051
|
+
isValid = false;
|
|
1052
|
+
}
|
|
1053
|
+
if (rules.max !== void 0 && value.length > rules.max) {
|
|
1054
|
+
errors.push(ERRORS.VALIDATION.MAX_ITEMS(field, rules.max));
|
|
1055
|
+
isValid = false;
|
|
1056
|
+
}
|
|
1057
|
+
break;
|
|
1058
|
+
case "object":
|
|
1059
|
+
if (typeof value !== "object" || Array.isArray(value) || value === null) {
|
|
1060
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "object"));
|
|
1061
|
+
isValid = false;
|
|
1062
|
+
}
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
if (isValid && rules.enum && rules.type !== "string" && !rules.enum.includes(typedValue)) {
|
|
1066
|
+
errors.push(ERRORS.VALIDATION.INVALID_ENUM(field, rules.enum));
|
|
1067
|
+
isValid = false;
|
|
1068
|
+
}
|
|
1069
|
+
if (isValid && rules.custom) {
|
|
1070
|
+
const customResult = rules.custom(typedValue);
|
|
1071
|
+
if (customResult === void 0) {
|
|
1072
|
+
throw new TypeError(
|
|
1073
|
+
`Custom validator for field "${field}" returned undefined. Return true to pass, false to fail, or a string error message.`
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
if (customResult !== true) {
|
|
1077
|
+
errors.push(typeof customResult === "string" && customResult.length > 0 ? customResult : `${field} is invalid`);
|
|
1078
|
+
isValid = false;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
value: isValid ? typedValue : void 0,
|
|
1083
|
+
errors
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
var createValidator = validate;
|
|
1087
|
+
|
|
1088
|
+
// src/validation/file.ts
|
|
1089
|
+
var MAGIC_BYTES = {
|
|
1090
|
+
// Images
|
|
1091
|
+
"image/jpeg": [Buffer.from([255, 216, 255])],
|
|
1092
|
+
"image/png": [Buffer.from([137, 80, 78, 71])],
|
|
1093
|
+
"image/gif": [Buffer.from("GIF87a"), Buffer.from("GIF89a")],
|
|
1094
|
+
"image/webp": [Buffer.from("RIFF")],
|
|
1095
|
+
// RIFF....WEBP
|
|
1096
|
+
"image/bmp": [Buffer.from([66, 77])],
|
|
1097
|
+
"image/svg+xml": [],
|
|
1098
|
+
// text-based, check separately
|
|
1099
|
+
// Documents
|
|
1100
|
+
"application/pdf": [Buffer.from("%PDF")],
|
|
1101
|
+
"application/zip": [Buffer.from([80, 75, 3, 4])],
|
|
1102
|
+
// Audio/Video
|
|
1103
|
+
"audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])],
|
|
1104
|
+
"video/mp4": []
|
|
1105
|
+
// ftyp at offset 4
|
|
1106
|
+
};
|
|
1107
|
+
var DANGEROUS_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1108
|
+
// Scripts
|
|
1109
|
+
".exe",
|
|
1110
|
+
".bat",
|
|
1111
|
+
".cmd",
|
|
1112
|
+
".com",
|
|
1113
|
+
".msi",
|
|
1114
|
+
".scr",
|
|
1115
|
+
".pif",
|
|
1116
|
+
".vbs",
|
|
1117
|
+
".vbe",
|
|
1118
|
+
".js",
|
|
1119
|
+
".jse",
|
|
1120
|
+
".ws",
|
|
1121
|
+
".wsf",
|
|
1122
|
+
".wsc",
|
|
1123
|
+
".wsh",
|
|
1124
|
+
".ps1",
|
|
1125
|
+
".ps1xml",
|
|
1126
|
+
".ps2",
|
|
1127
|
+
".ps2xml",
|
|
1128
|
+
".psc1",
|
|
1129
|
+
".psc2",
|
|
1130
|
+
".sh",
|
|
1131
|
+
".bash",
|
|
1132
|
+
".csh",
|
|
1133
|
+
".ksh",
|
|
1134
|
+
// Server-side
|
|
1135
|
+
".php",
|
|
1136
|
+
".php3",
|
|
1137
|
+
".php4",
|
|
1138
|
+
".php5",
|
|
1139
|
+
".phtml",
|
|
1140
|
+
".pht",
|
|
1141
|
+
".asp",
|
|
1142
|
+
".aspx",
|
|
1143
|
+
".ashx",
|
|
1144
|
+
".asmx",
|
|
1145
|
+
".cer",
|
|
1146
|
+
".jsp",
|
|
1147
|
+
".jspx",
|
|
1148
|
+
".jsw",
|
|
1149
|
+
".jsv",
|
|
1150
|
+
".cgi",
|
|
1151
|
+
".pl",
|
|
1152
|
+
".py",
|
|
1153
|
+
".rb",
|
|
1154
|
+
// Java
|
|
1155
|
+
".jar",
|
|
1156
|
+
".war",
|
|
1157
|
+
".ear",
|
|
1158
|
+
".class",
|
|
1159
|
+
// Config that can execute
|
|
1160
|
+
".htaccess",
|
|
1161
|
+
".htpasswd",
|
|
1162
|
+
// Template engines
|
|
1163
|
+
".ejs",
|
|
1164
|
+
".pug",
|
|
1165
|
+
".hbs",
|
|
1166
|
+
".handlebars",
|
|
1167
|
+
".njk",
|
|
1168
|
+
".twig",
|
|
1169
|
+
// Shortcuts/links
|
|
1170
|
+
".lnk",
|
|
1171
|
+
".inf",
|
|
1172
|
+
".reg",
|
|
1173
|
+
".url",
|
|
1174
|
+
// Office macros
|
|
1175
|
+
".docm",
|
|
1176
|
+
".xlsm",
|
|
1177
|
+
".pptm",
|
|
1178
|
+
".dotm"
|
|
1179
|
+
]);
|
|
1180
|
+
var DEFAULT_MAX_SIZE = 5 * 1024 * 1024;
|
|
1181
|
+
function sanitizeFilename(filename) {
|
|
1182
|
+
let name = filename;
|
|
1183
|
+
name = name.replace(/\0/g, "");
|
|
1184
|
+
name = name.replace(/^.*[/\\]/, "");
|
|
1185
|
+
name = name.replace(/[\x00-\x1F\x7F]/g, "");
|
|
1186
|
+
name = name.replace(/[<>:"/\\|?*]/g, "");
|
|
1187
|
+
name = name.replace(/[\s()]+/g, "_");
|
|
1188
|
+
name = name.replace(/^\.+/, "");
|
|
1189
|
+
name = name.replace(/_{2,}/g, "_");
|
|
1190
|
+
name = name.replace(/\.{2,}/g, ".");
|
|
1191
|
+
name = name.replace(/_+\./g, ".");
|
|
1192
|
+
name = name.replace(/^_+|_+$/g, "");
|
|
1193
|
+
if (!name || name === ".") {
|
|
1194
|
+
name = "unnamed";
|
|
1195
|
+
}
|
|
1196
|
+
return name;
|
|
1197
|
+
}
|
|
1198
|
+
function matchesMagicBytes(buffer, mimetype) {
|
|
1199
|
+
const signatures = MAGIC_BYTES[mimetype];
|
|
1200
|
+
if (!signatures || signatures.length === 0) return true;
|
|
1201
|
+
return signatures.some((sig) => {
|
|
1202
|
+
if (buffer.length < sig.length) return false;
|
|
1203
|
+
return buffer.subarray(0, sig.length).equals(sig);
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
function getExtension(filename) {
|
|
1207
|
+
const lastDot = filename.lastIndexOf(".");
|
|
1208
|
+
if (lastDot < 1) return "";
|
|
1209
|
+
return filename.slice(lastDot).toLowerCase();
|
|
1210
|
+
}
|
|
1211
|
+
function hasDoubleExtension(filename) {
|
|
1212
|
+
const parts = filename.split(".");
|
|
1213
|
+
if (parts.length < 3) return false;
|
|
1214
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
1215
|
+
const ext = "." + parts[i].toLowerCase();
|
|
1216
|
+
if (DANGEROUS_EXTENSIONS.has(ext)) return true;
|
|
1217
|
+
}
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
function validateFile(file, options = {}) {
|
|
1221
|
+
const {
|
|
1222
|
+
maxSize = DEFAULT_MAX_SIZE,
|
|
1223
|
+
allowedTypes,
|
|
1224
|
+
allowedExtensions,
|
|
1225
|
+
blockExecutables = true,
|
|
1226
|
+
validateMagicBytes = true,
|
|
1227
|
+
blockNoExtension = true,
|
|
1228
|
+
blockDoubleExtensions = true
|
|
1229
|
+
} = options;
|
|
1230
|
+
const errors = [];
|
|
1231
|
+
const sanitizedFilename = sanitizeFilename(file.filename);
|
|
1232
|
+
const extension = getExtension(sanitizedFilename);
|
|
1233
|
+
if (file.size > maxSize) {
|
|
1234
|
+
errors.push(`File size ${file.size} exceeds maximum ${maxSize} bytes`);
|
|
1235
|
+
}
|
|
1236
|
+
if (file.size === 0) {
|
|
1237
|
+
errors.push("File is empty");
|
|
1238
|
+
}
|
|
1239
|
+
if (blockNoExtension && !extension) {
|
|
1240
|
+
errors.push("File has no extension");
|
|
1241
|
+
}
|
|
1242
|
+
if (blockExecutables && extension && DANGEROUS_EXTENSIONS.has(extension)) {
|
|
1243
|
+
errors.push(`Executable extension "${extension}" is not allowed`);
|
|
1244
|
+
}
|
|
1245
|
+
if (blockDoubleExtensions && hasDoubleExtension(sanitizedFilename)) {
|
|
1246
|
+
errors.push("Double extensions with executable types are not allowed");
|
|
1247
|
+
}
|
|
1248
|
+
if (allowedExtensions && extension) {
|
|
1249
|
+
const normalizedAllowed = allowedExtensions.map((e) => e.toLowerCase());
|
|
1250
|
+
if (!normalizedAllowed.includes(extension)) {
|
|
1251
|
+
errors.push(`Extension "${extension}" is not allowed. Allowed: ${normalizedAllowed.join(", ")}`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (allowedTypes && !allowedTypes.includes(file.mimetype)) {
|
|
1255
|
+
errors.push(`MIME type "${file.mimetype}" is not allowed. Allowed: ${allowedTypes.join(", ")}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (validateMagicBytes && file.buffer && file.buffer.length > 0) {
|
|
1258
|
+
if (!matchesMagicBytes(file.buffer, file.mimetype)) {
|
|
1259
|
+
errors.push(`File content does not match claimed MIME type "${file.mimetype}"`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
valid: errors.length === 0,
|
|
1264
|
+
errors,
|
|
1265
|
+
sanitizedFilename
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function isDangerousExtension(filename) {
|
|
1269
|
+
const ext = getExtension(filename);
|
|
1270
|
+
return ext !== "" && DANGEROUS_EXTENSIONS.has(ext);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/logging/redactor.ts
|
|
1274
|
+
function createSafeLogger(options = {}) {
|
|
1275
|
+
const {
|
|
1276
|
+
redactKeys = [],
|
|
1277
|
+
maxLength = REDACTION.DEFAULT_MAX_LENGTH,
|
|
1278
|
+
redactPatterns = []
|
|
1279
|
+
} = options;
|
|
1280
|
+
const allRedactKeys = /* @__PURE__ */ new Set([
|
|
1281
|
+
...Array.from(REDACTION.SENSITIVE_KEYS),
|
|
1282
|
+
...redactKeys.map((k) => k.toLowerCase())
|
|
1283
|
+
]);
|
|
1284
|
+
function redact(obj, depth = 0) {
|
|
1285
|
+
if (depth > INPUT.MAX_RECURSION_DEPTH) return REDACTION.MAX_DEPTH;
|
|
1286
|
+
if (obj === null || obj === void 0) return obj;
|
|
1287
|
+
if (typeof obj === "string") {
|
|
1288
|
+
return redactString(obj, maxLength, redactPatterns);
|
|
1289
|
+
}
|
|
1290
|
+
if (typeof obj !== "object") return obj;
|
|
1291
|
+
if (Array.isArray(obj)) {
|
|
1292
|
+
return obj.map((item) => redact(item, depth + 1));
|
|
1293
|
+
}
|
|
1294
|
+
const result = {};
|
|
1295
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1296
|
+
if (allRedactKeys.has(key.toLowerCase())) {
|
|
1297
|
+
result[key] = REDACTION.REPLACEMENT;
|
|
1298
|
+
} else {
|
|
1299
|
+
result[key] = redact(value, depth + 1);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return result;
|
|
1303
|
+
}
|
|
1304
|
+
function log(level, message, data) {
|
|
1305
|
+
const entry = {
|
|
1306
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1307
|
+
level,
|
|
1308
|
+
message: redactString(message, maxLength, redactPatterns)
|
|
1309
|
+
};
|
|
1310
|
+
if (data !== void 0) {
|
|
1311
|
+
entry.data = redact(data);
|
|
1312
|
+
}
|
|
1313
|
+
console.log(JSON.stringify(entry));
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
log,
|
|
1317
|
+
info: (msg, data) => log("info", msg, data),
|
|
1318
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
1319
|
+
error: (msg, data) => log("error", msg, data),
|
|
1320
|
+
debug: (msg, data) => log("debug", msg, data)
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
function redactString(str, maxLength, patterns) {
|
|
1324
|
+
let safe = str.replace(/[\r\n\t]/g, " ").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g, "");
|
|
1325
|
+
for (const pattern of patterns) {
|
|
1326
|
+
safe = safe.replace(pattern, REDACTION.REPLACEMENT);
|
|
1327
|
+
}
|
|
1328
|
+
if (safe.length > maxLength) {
|
|
1329
|
+
safe = safe.substring(0, maxLength) + `...${REDACTION.TRUNCATED}`;
|
|
1330
|
+
}
|
|
1331
|
+
return safe;
|
|
1332
|
+
}
|
|
1333
|
+
function createRedactor(sensitiveKeys = []) {
|
|
1334
|
+
const allKeys = /* @__PURE__ */ new Set([
|
|
1335
|
+
...Array.from(REDACTION.SENSITIVE_KEYS),
|
|
1336
|
+
...sensitiveKeys.map((k) => k.toLowerCase())
|
|
1337
|
+
]);
|
|
1338
|
+
function redact(obj, depth = 0) {
|
|
1339
|
+
if (depth > INPUT.MAX_RECURSION_DEPTH) return REDACTION.MAX_DEPTH;
|
|
1340
|
+
if (obj === null || obj === void 0) return obj;
|
|
1341
|
+
if (typeof obj !== "object") return obj;
|
|
1342
|
+
if (Array.isArray(obj)) {
|
|
1343
|
+
return obj.map((item) => redact(item, depth + 1));
|
|
1344
|
+
}
|
|
1345
|
+
const result = {};
|
|
1346
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1347
|
+
if (allKeys.has(key.toLowerCase())) {
|
|
1348
|
+
result[key] = REDACTION.REPLACEMENT;
|
|
1349
|
+
} else {
|
|
1350
|
+
result[key] = redact(value, depth + 1);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return result;
|
|
1354
|
+
}
|
|
1355
|
+
return redact;
|
|
1356
|
+
}
|
|
1357
|
+
var safeLog = createSafeLogger;
|
|
1358
|
+
|
|
1359
|
+
// src/middleware/main.ts
|
|
1360
|
+
function arcis(options = {}) {
|
|
1361
|
+
const middlewares = [];
|
|
1362
|
+
const cleanupFns = [];
|
|
1363
|
+
if (options.headers !== false) {
|
|
1364
|
+
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
1365
|
+
middlewares.push(createHeaders(headerOpts));
|
|
1366
|
+
}
|
|
1367
|
+
if (options.rateLimit !== false) {
|
|
1368
|
+
const rateLimitOpts = typeof options.rateLimit === "object" ? options.rateLimit : {};
|
|
1369
|
+
const rateLimiter = createRateLimiter(rateLimitOpts);
|
|
1370
|
+
middlewares.push(rateLimiter);
|
|
1371
|
+
cleanupFns.push(() => rateLimiter.close());
|
|
1372
|
+
}
|
|
1373
|
+
if (options.sanitize !== false) {
|
|
1374
|
+
const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
|
|
1375
|
+
middlewares.push(createSanitizer(sanitizeOpts));
|
|
1376
|
+
}
|
|
1377
|
+
const result = middlewares;
|
|
1378
|
+
result.close = () => {
|
|
1379
|
+
for (const fn of cleanupFns) {
|
|
1380
|
+
fn();
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
return result;
|
|
1384
|
+
}
|
|
1385
|
+
var arcisWithMethods = arcis;
|
|
1386
|
+
arcisWithMethods.sanitize = createSanitizer;
|
|
1387
|
+
arcisWithMethods.rateLimit = createRateLimiter;
|
|
1388
|
+
arcisWithMethods.headers = createHeaders;
|
|
1389
|
+
arcisWithMethods.validate = validate;
|
|
1390
|
+
arcisWithMethods.logger = createSafeLogger;
|
|
1391
|
+
arcisWithMethods.errorHandler = createErrorHandler;
|
|
1392
|
+
var main_default = arcisWithMethods;
|
|
1393
|
+
|
|
1394
|
+
// src/middleware/cors.ts
|
|
1395
|
+
var DEFAULT_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"];
|
|
1396
|
+
var DEFAULT_HEADERS = ["Content-Type", "Authorization"];
|
|
1397
|
+
var DEFAULT_MAX_AGE = 600;
|
|
1398
|
+
function isOriginAllowed(requestOrigin, allowed) {
|
|
1399
|
+
if (requestOrigin === "null") return false;
|
|
1400
|
+
if (allowed === true) return true;
|
|
1401
|
+
if (typeof allowed === "string") {
|
|
1402
|
+
return requestOrigin === allowed;
|
|
1403
|
+
}
|
|
1404
|
+
if (Array.isArray(allowed)) {
|
|
1405
|
+
return allowed.includes(requestOrigin);
|
|
1406
|
+
}
|
|
1407
|
+
if (allowed instanceof RegExp) {
|
|
1408
|
+
return allowed.test(requestOrigin);
|
|
1409
|
+
}
|
|
1410
|
+
if (typeof allowed === "function") {
|
|
1411
|
+
return allowed(requestOrigin);
|
|
1412
|
+
}
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
function safeCors(options) {
|
|
1416
|
+
const {
|
|
1417
|
+
origin,
|
|
1418
|
+
methods = DEFAULT_METHODS,
|
|
1419
|
+
allowedHeaders = DEFAULT_HEADERS,
|
|
1420
|
+
exposedHeaders = [],
|
|
1421
|
+
credentials = false,
|
|
1422
|
+
maxAge = DEFAULT_MAX_AGE,
|
|
1423
|
+
preflightContinue = true
|
|
1424
|
+
} = options;
|
|
1425
|
+
return (req, res, next) => {
|
|
1426
|
+
const requestOrigin = req.headers.origin;
|
|
1427
|
+
res.setHeader("Vary", "Origin");
|
|
1428
|
+
if (!requestOrigin) {
|
|
1429
|
+
return next();
|
|
1430
|
+
}
|
|
1431
|
+
const allowed = isOriginAllowed(requestOrigin, origin);
|
|
1432
|
+
if (!allowed) {
|
|
1433
|
+
return next();
|
|
1434
|
+
}
|
|
1435
|
+
res.setHeader("Access-Control-Allow-Origin", requestOrigin);
|
|
1436
|
+
if (credentials) {
|
|
1437
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
1438
|
+
}
|
|
1439
|
+
if (exposedHeaders.length > 0) {
|
|
1440
|
+
res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(", "));
|
|
1441
|
+
}
|
|
1442
|
+
if (req.method === "OPTIONS") {
|
|
1443
|
+
res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
|
|
1444
|
+
res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
|
|
1445
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
1446
|
+
if (preflightContinue) {
|
|
1447
|
+
res.status(204).end();
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
next();
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
var createCors = safeCors;
|
|
1455
|
+
|
|
1456
|
+
// src/middleware/cookies.ts
|
|
1457
|
+
var COOKIE_ATTRS = {
|
|
1458
|
+
HTTP_ONLY: "; HttpOnly",
|
|
1459
|
+
SECURE: "; Secure",
|
|
1460
|
+
SAME_SITE_STRICT: "; SameSite=Strict",
|
|
1461
|
+
SAME_SITE_LAX: "; SameSite=Lax",
|
|
1462
|
+
SAME_SITE_NONE: "; SameSite=None"
|
|
1463
|
+
};
|
|
1464
|
+
function enforceSecureCookie(cookieStr, options) {
|
|
1465
|
+
const lower = cookieStr.toLowerCase();
|
|
1466
|
+
let result = cookieStr;
|
|
1467
|
+
if (options.httpOnly && !lower.includes("httponly")) {
|
|
1468
|
+
result += COOKIE_ATTRS.HTTP_ONLY;
|
|
1469
|
+
}
|
|
1470
|
+
if (options.secure && !lower.includes("; secure")) {
|
|
1471
|
+
result += COOKIE_ATTRS.SECURE;
|
|
1472
|
+
}
|
|
1473
|
+
if (options.sameSite !== false && !lower.includes("samesite")) {
|
|
1474
|
+
switch (options.sameSite) {
|
|
1475
|
+
case "Strict":
|
|
1476
|
+
result += COOKIE_ATTRS.SAME_SITE_STRICT;
|
|
1477
|
+
break;
|
|
1478
|
+
case "None":
|
|
1479
|
+
result += COOKIE_ATTRS.SAME_SITE_NONE;
|
|
1480
|
+
if (!result.toLowerCase().includes("; secure")) {
|
|
1481
|
+
result += COOKIE_ATTRS.SECURE;
|
|
1482
|
+
}
|
|
1483
|
+
break;
|
|
1484
|
+
case "Lax":
|
|
1485
|
+
default:
|
|
1486
|
+
result += COOKIE_ATTRS.SAME_SITE_LAX;
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (options.path) {
|
|
1491
|
+
if (lower.includes("path=")) {
|
|
1492
|
+
result = result.replace(/;\s*path=[^;]*/i, `; Path=${options.path}`);
|
|
1493
|
+
} else {
|
|
1494
|
+
result += `; Path=${options.path}`;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return result;
|
|
1498
|
+
}
|
|
1499
|
+
function secureCookieDefaults(options = {}) {
|
|
1500
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
1501
|
+
const resolved = {
|
|
1502
|
+
httpOnly: options.httpOnly ?? true,
|
|
1503
|
+
secure: options.secure ?? isProduction,
|
|
1504
|
+
sameSite: options.sameSite ?? "Lax",
|
|
1505
|
+
path: options.path
|
|
1506
|
+
};
|
|
1507
|
+
return (_req, res, next) => {
|
|
1508
|
+
const originalSetHeader = res.setHeader.bind(res);
|
|
1509
|
+
res.setHeader = function patchedSetHeader(name, value) {
|
|
1510
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
1511
|
+
if (Array.isArray(value)) {
|
|
1512
|
+
value = value.map((v) => enforceSecureCookie(String(v), resolved));
|
|
1513
|
+
} else {
|
|
1514
|
+
value = enforceSecureCookie(String(value), resolved);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return originalSetHeader(name, value);
|
|
1518
|
+
};
|
|
1519
|
+
next();
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
var createSecureCookies = secureCookieDefaults;
|
|
1523
|
+
|
|
1524
|
+
// src/validation/url.ts
|
|
1525
|
+
function validateUrl(url, options = {}) {
|
|
1526
|
+
const {
|
|
1527
|
+
allowedProtocols = ["http:", "https:"],
|
|
1528
|
+
blockedHosts = [],
|
|
1529
|
+
allowedHosts = [],
|
|
1530
|
+
allowLocalhost = false,
|
|
1531
|
+
allowPrivate = false
|
|
1532
|
+
} = options;
|
|
1533
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
1534
|
+
return { safe: false, reason: "invalid URL: empty or not a string" };
|
|
1535
|
+
}
|
|
1536
|
+
let parsed;
|
|
1537
|
+
try {
|
|
1538
|
+
parsed = new URL(url);
|
|
1539
|
+
} catch {
|
|
1540
|
+
return { safe: false, reason: "invalid URL: failed to parse" };
|
|
1541
|
+
}
|
|
1542
|
+
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
1543
|
+
return { safe: false, reason: `disallowed protocol: ${parsed.protocol}` };
|
|
1544
|
+
}
|
|
1545
|
+
if (parsed.username || parsed.password) {
|
|
1546
|
+
return { safe: false, reason: "URL contains credentials" };
|
|
1547
|
+
}
|
|
1548
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1549
|
+
if (allowedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1550
|
+
return { safe: true };
|
|
1551
|
+
}
|
|
1552
|
+
if (blockedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1553
|
+
return { safe: false, reason: `blocked host: ${hostname}` };
|
|
1554
|
+
}
|
|
1555
|
+
if (!allowLocalhost) {
|
|
1556
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1" || hostname === "0.0.0.0" || hostname.endsWith(".localhost")) {
|
|
1557
|
+
return { safe: false, reason: "loopback address" };
|
|
1558
|
+
}
|
|
1559
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1560
|
+
return { safe: false, reason: "loopback address" };
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (!allowPrivate) {
|
|
1564
|
+
const privateCheck = checkPrivateIp(hostname);
|
|
1565
|
+
if (privateCheck) {
|
|
1566
|
+
return { safe: false, reason: privateCheck };
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return { safe: true };
|
|
1570
|
+
}
|
|
1571
|
+
function isUrlSafe(url, options = {}) {
|
|
1572
|
+
return validateUrl(url, options).safe;
|
|
1573
|
+
}
|
|
1574
|
+
function checkPrivateIp(hostname) {
|
|
1575
|
+
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1576
|
+
return "private address (10.0.0.0/8)";
|
|
1577
|
+
}
|
|
1578
|
+
const match172 = hostname.match(/^172\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/);
|
|
1579
|
+
if (match172) {
|
|
1580
|
+
const second = parseInt(match172[1], 10);
|
|
1581
|
+
if (second >= 16 && second <= 31) {
|
|
1582
|
+
return "private address (172.16.0.0/12)";
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1586
|
+
return "private address (192.168.0.0/16)";
|
|
1587
|
+
}
|
|
1588
|
+
if (/^169\.254\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1589
|
+
return "link-local address (169.254.0.0/16)";
|
|
1590
|
+
}
|
|
1591
|
+
if (/^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1592
|
+
return "current network address (0.0.0.0/8)";
|
|
1593
|
+
}
|
|
1594
|
+
if (hostname === "metadata.google.internal" || hostname === "metadata.internal") {
|
|
1595
|
+
return "cloud metadata endpoint";
|
|
1596
|
+
}
|
|
1597
|
+
const ipv6 = hostname.replace(/^\[|\]$/g, "");
|
|
1598
|
+
if (ipv6 === "::1" || ipv6 === "::" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80")) {
|
|
1599
|
+
return "private IPv6 address";
|
|
1600
|
+
}
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// src/validation/redirect.ts
|
|
1605
|
+
var DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript|blob):/i;
|
|
1606
|
+
var CONTROL_CHARS = /[\t\n\r]/g;
|
|
1607
|
+
function validateRedirect(url, options = {}) {
|
|
1608
|
+
const {
|
|
1609
|
+
allowedHosts = [],
|
|
1610
|
+
allowProtocolRelative = false,
|
|
1611
|
+
allowedProtocols = ["http:", "https:"]
|
|
1612
|
+
} = options;
|
|
1613
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
1614
|
+
return { safe: false, reason: "invalid redirect: empty or not a string" };
|
|
1615
|
+
}
|
|
1616
|
+
const cleaned = url.replace(CONTROL_CHARS, "");
|
|
1617
|
+
if (DANGEROUS_PROTOCOLS.test(cleaned)) {
|
|
1618
|
+
const proto = cleaned.match(DANGEROUS_PROTOCOLS);
|
|
1619
|
+
return { safe: false, reason: `dangerous protocol: ${proto[0]}` };
|
|
1620
|
+
}
|
|
1621
|
+
if (cleaned.startsWith("\\")) {
|
|
1622
|
+
return { safe: false, reason: "backslash-prefixed URL (browser treats as protocol-relative)" };
|
|
1623
|
+
}
|
|
1624
|
+
if (cleaned.startsWith("//")) {
|
|
1625
|
+
if (!allowProtocolRelative) {
|
|
1626
|
+
const host2 = extractHost(cleaned);
|
|
1627
|
+
if (host2 && allowedHosts.some((h) => host2 === h.toLowerCase())) {
|
|
1628
|
+
return { safe: true };
|
|
1629
|
+
}
|
|
1630
|
+
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1631
|
+
}
|
|
1632
|
+
const host = extractHost(cleaned);
|
|
1633
|
+
if (host && allowedHosts.length > 0 && !allowedHosts.some((h) => host === h.toLowerCase())) {
|
|
1634
|
+
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1635
|
+
}
|
|
1636
|
+
return { safe: true };
|
|
1637
|
+
}
|
|
1638
|
+
let parsed;
|
|
1639
|
+
try {
|
|
1640
|
+
parsed = new URL(cleaned);
|
|
1641
|
+
} catch {
|
|
1642
|
+
return { safe: true };
|
|
1643
|
+
}
|
|
1644
|
+
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
1645
|
+
return { safe: false, reason: `disallowed protocol: ${parsed.protocol}` };
|
|
1646
|
+
}
|
|
1647
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1648
|
+
if (allowedHosts.length === 0) {
|
|
1649
|
+
return { safe: false, reason: "absolute URL not in allowed hosts" };
|
|
1650
|
+
}
|
|
1651
|
+
if (!allowedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1652
|
+
return { safe: false, reason: `host not allowed: ${hostname}` };
|
|
1653
|
+
}
|
|
1654
|
+
return { safe: true };
|
|
1655
|
+
}
|
|
1656
|
+
function isRedirectSafe(url, options = {}) {
|
|
1657
|
+
return validateRedirect(url, options).safe;
|
|
1658
|
+
}
|
|
1659
|
+
function extractHost(url) {
|
|
1660
|
+
const match = url.match(/^\/\/([^/:?#]+)/);
|
|
1661
|
+
return match ? match[1].toLowerCase() : null;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/stores/memory.ts
|
|
1665
|
+
var MemoryStore = class {
|
|
1666
|
+
constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS) {
|
|
1667
|
+
this.store = /* @__PURE__ */ new Map();
|
|
1668
|
+
this.cleanupInterval = null;
|
|
1669
|
+
if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {
|
|
1670
|
+
throw new RangeError(
|
|
1671
|
+
`MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
this.windowMs = windowMs;
|
|
1675
|
+
this.startCleanup();
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Start the cleanup interval to remove expired entries.
|
|
1679
|
+
*/
|
|
1680
|
+
startCleanup() {
|
|
1681
|
+
const CLEANUP_MIN_MS = 3e4;
|
|
1682
|
+
const CLEANUP_MAX_MS = 3e5;
|
|
1683
|
+
const cleanupMs = Math.min(Math.max(this.windowMs, CLEANUP_MIN_MS), CLEANUP_MAX_MS);
|
|
1684
|
+
this.cleanupInterval = setInterval(() => {
|
|
1685
|
+
const now = Date.now();
|
|
1686
|
+
for (const [key, entry] of this.store.entries()) {
|
|
1687
|
+
if (entry.resetTime < now) {
|
|
1688
|
+
this.store.delete(key);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}, cleanupMs);
|
|
1692
|
+
if (typeof this.cleanupInterval.unref === "function") {
|
|
1693
|
+
this.cleanupInterval.unref();
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
async get(key) {
|
|
1697
|
+
const entry = this.store.get(key);
|
|
1698
|
+
if (!entry) return null;
|
|
1699
|
+
if (entry.resetTime < Date.now()) {
|
|
1700
|
+
this.store.delete(key);
|
|
1701
|
+
return null;
|
|
1702
|
+
}
|
|
1703
|
+
return entry;
|
|
1704
|
+
}
|
|
1705
|
+
async set(key, entry) {
|
|
1706
|
+
this.store.set(key, entry);
|
|
1707
|
+
}
|
|
1708
|
+
async increment(key) {
|
|
1709
|
+
const now = Date.now();
|
|
1710
|
+
const entry = this.store.get(key);
|
|
1711
|
+
if (!entry || entry.resetTime < now) {
|
|
1712
|
+
this.store.set(key, { count: 1, resetTime: now + this.windowMs });
|
|
1713
|
+
return 1;
|
|
1714
|
+
}
|
|
1715
|
+
entry.count++;
|
|
1716
|
+
return entry.count;
|
|
1717
|
+
}
|
|
1718
|
+
async decrement(key) {
|
|
1719
|
+
const entry = this.store.get(key);
|
|
1720
|
+
if (entry && entry.count > 0) {
|
|
1721
|
+
entry.count--;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
async reset(key) {
|
|
1725
|
+
this.store.delete(key);
|
|
1726
|
+
}
|
|
1727
|
+
async close() {
|
|
1728
|
+
if (this.cleanupInterval) {
|
|
1729
|
+
clearInterval(this.cleanupInterval);
|
|
1730
|
+
this.cleanupInterval = null;
|
|
1731
|
+
}
|
|
1732
|
+
this.store.clear();
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Get current store size (for monitoring).
|
|
1736
|
+
*/
|
|
1737
|
+
get size() {
|
|
1738
|
+
return this.store.size;
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
// src/stores/redis.ts
|
|
1743
|
+
var RedisStore = class {
|
|
1744
|
+
constructor(options) {
|
|
1745
|
+
this.client = options.client;
|
|
1746
|
+
this.prefix = options.prefix ?? "arcis:rl:";
|
|
1747
|
+
this.windowMs = options.windowMs ?? RATE_LIMIT.DEFAULT_WINDOW_MS;
|
|
1748
|
+
this.windowSec = Math.ceil(this.windowMs / 1e3);
|
|
1749
|
+
}
|
|
1750
|
+
getKey(key) {
|
|
1751
|
+
return `${this.prefix}${key}`;
|
|
1752
|
+
}
|
|
1753
|
+
async get(key) {
|
|
1754
|
+
const redisKey = this.getKey(key);
|
|
1755
|
+
const [countStr, ttl] = await Promise.all([
|
|
1756
|
+
this.client.get(redisKey),
|
|
1757
|
+
this.client.ttl(redisKey)
|
|
1758
|
+
]);
|
|
1759
|
+
if (!countStr || ttl < 0) {
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
const count = parseInt(countStr, 10);
|
|
1763
|
+
if (isNaN(count)) {
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
return {
|
|
1767
|
+
count,
|
|
1768
|
+
resetTime: Date.now() + ttl * 1e3
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
async set(key, entry) {
|
|
1772
|
+
const redisKey = this.getKey(key);
|
|
1773
|
+
const ttlSec = Math.max(1, Math.ceil((entry.resetTime - Date.now()) / 1e3));
|
|
1774
|
+
await this.client.setex(redisKey, ttlSec, entry.count.toString());
|
|
1775
|
+
}
|
|
1776
|
+
async increment(key) {
|
|
1777
|
+
const redisKey = this.getKey(key);
|
|
1778
|
+
const count = await this.client.incr(redisKey);
|
|
1779
|
+
if (count === 1) {
|
|
1780
|
+
await this.client.expire(redisKey, this.windowSec);
|
|
1781
|
+
}
|
|
1782
|
+
return count;
|
|
1783
|
+
}
|
|
1784
|
+
async decrement(key) {
|
|
1785
|
+
const redisKey = this.getKey(key);
|
|
1786
|
+
await this.client.decr(redisKey);
|
|
1787
|
+
}
|
|
1788
|
+
async reset(key) {
|
|
1789
|
+
const redisKey = this.getKey(key);
|
|
1790
|
+
await this.client.del(redisKey);
|
|
1791
|
+
}
|
|
1792
|
+
async close() {
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
function createRedisStore(options) {
|
|
1796
|
+
return new RedisStore(options);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
exports.ArcisError = ArcisError;
|
|
1800
|
+
exports.ArcisValidationError = ValidationError;
|
|
1801
|
+
exports.BLOCKED = BLOCKED;
|
|
1802
|
+
exports.ERRORS = ERRORS;
|
|
1803
|
+
exports.HEADERS = HEADERS;
|
|
1804
|
+
exports.INPUT = INPUT;
|
|
1805
|
+
exports.InputTooLargeError = InputTooLargeError;
|
|
1806
|
+
exports.MemoryStore = MemoryStore;
|
|
1807
|
+
exports.RATE_LIMIT = RATE_LIMIT;
|
|
1808
|
+
exports.REDACTION = REDACTION;
|
|
1809
|
+
exports.RateLimitError = RateLimitError;
|
|
1810
|
+
exports.RedisStore = RedisStore;
|
|
1811
|
+
exports.SanitizationError = SanitizationError;
|
|
1812
|
+
exports.SecurityThreatError = SecurityThreatError;
|
|
1813
|
+
exports.VALIDATION = VALIDATION;
|
|
1814
|
+
exports.arcis = arcis;
|
|
1815
|
+
exports.arcisFunction = arcisWithMethods;
|
|
1816
|
+
exports.createCors = createCors;
|
|
1817
|
+
exports.createErrorHandler = createErrorHandler;
|
|
1818
|
+
exports.createHeaders = createHeaders;
|
|
1819
|
+
exports.createRateLimiter = createRateLimiter;
|
|
1820
|
+
exports.createRedactor = createRedactor;
|
|
1821
|
+
exports.createRedisStore = createRedisStore;
|
|
1822
|
+
exports.createSafeLogger = createSafeLogger;
|
|
1823
|
+
exports.createSanitizer = createSanitizer;
|
|
1824
|
+
exports.createSecureCookies = createSecureCookies;
|
|
1825
|
+
exports.createValidator = createValidator;
|
|
1826
|
+
exports.default = main_default;
|
|
1827
|
+
exports.detectCommandInjection = detectCommandInjection;
|
|
1828
|
+
exports.detectHeaderInjection = detectHeaderInjection;
|
|
1829
|
+
exports.detectNoSqlInjection = detectNoSqlInjection;
|
|
1830
|
+
exports.detectPathTraversal = detectPathTraversal;
|
|
1831
|
+
exports.detectPrototypePollution = detectPrototypePollution;
|
|
1832
|
+
exports.detectSql = detectSql;
|
|
1833
|
+
exports.detectXss = detectXss;
|
|
1834
|
+
exports.enforceSecureCookie = enforceSecureCookie;
|
|
1835
|
+
exports.errorHandler = errorHandler;
|
|
1836
|
+
exports.isDangerousExtension = isDangerousExtension;
|
|
1837
|
+
exports.isDangerousNoSqlKey = isDangerousNoSqlKey;
|
|
1838
|
+
exports.isDangerousProtoKey = isDangerousProtoKey;
|
|
1839
|
+
exports.isRedirectSafe = isRedirectSafe;
|
|
1840
|
+
exports.isUrlSafe = isUrlSafe;
|
|
1841
|
+
exports.rateLimit = rateLimit;
|
|
1842
|
+
exports.safeCors = safeCors;
|
|
1843
|
+
exports.safeLog = safeLog;
|
|
1844
|
+
exports.sanitizeCommand = sanitizeCommand;
|
|
1845
|
+
exports.sanitizeFilename = sanitizeFilename;
|
|
1846
|
+
exports.sanitizeHeaderValue = sanitizeHeaderValue;
|
|
1847
|
+
exports.sanitizeHeaders = sanitizeHeaders;
|
|
1848
|
+
exports.sanitizeObject = sanitizeObject;
|
|
1849
|
+
exports.sanitizePath = sanitizePath;
|
|
1850
|
+
exports.sanitizeSql = sanitizeSql;
|
|
1851
|
+
exports.sanitizeString = sanitizeString;
|
|
1852
|
+
exports.sanitizeXss = sanitizeXss;
|
|
1853
|
+
exports.secureCookieDefaults = secureCookieDefaults;
|
|
1854
|
+
exports.securityHeaders = securityHeaders;
|
|
1855
|
+
exports.validate = validate;
|
|
1856
|
+
exports.validateFile = validateFile;
|
|
1857
|
+
exports.validateRedirect = validateRedirect;
|
|
1858
|
+
exports.validateUrl = validateUrl;
|
|
1859
|
+
//# sourceMappingURL=index.js.map
|
|
1860
|
+
//# sourceMappingURL=index.js.map
|