@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.
Files changed (52) hide show
  1. package/README.md +222 -0
  2. package/dist/core/index.d.mts +170 -0
  3. package/dist/core/index.d.ts +170 -0
  4. package/dist/core/index.js +327 -0
  5. package/dist/core/index.js.map +1 -0
  6. package/dist/core/index.mjs +307 -0
  7. package/dist/core/index.mjs.map +1 -0
  8. package/dist/headers-BJq2OA0i.d.ts +284 -0
  9. package/dist/headers-DBQedhrb.d.mts +284 -0
  10. package/dist/index-BgHPM7LC.d.ts +129 -0
  11. package/dist/index-BpT7flAQ.d.ts +255 -0
  12. package/dist/index-JaFOUKyK.d.mts +255 -0
  13. package/dist/index-nAgXexwD.d.mts +129 -0
  14. package/dist/index.d.mts +139 -0
  15. package/dist/index.d.ts +139 -0
  16. package/dist/index.js +1860 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +1797 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/dist/logging/index.d.mts +38 -0
  21. package/dist/logging/index.d.ts +38 -0
  22. package/dist/logging/index.js +140 -0
  23. package/dist/logging/index.js.map +1 -0
  24. package/dist/logging/index.mjs +136 -0
  25. package/dist/logging/index.mjs.map +1 -0
  26. package/dist/middleware/index.d.mts +3 -0
  27. package/dist/middleware/index.d.ts +3 -0
  28. package/dist/middleware/index.js +1173 -0
  29. package/dist/middleware/index.js.map +1 -0
  30. package/dist/middleware/index.mjs +1156 -0
  31. package/dist/middleware/index.mjs.map +1 -0
  32. package/dist/sanitizers/index.d.mts +24 -0
  33. package/dist/sanitizers/index.d.ts +24 -0
  34. package/dist/sanitizers/index.js +610 -0
  35. package/dist/sanitizers/index.js.map +1 -0
  36. package/dist/sanitizers/index.mjs +587 -0
  37. package/dist/sanitizers/index.mjs.map +1 -0
  38. package/dist/stores/index.d.mts +106 -0
  39. package/dist/stores/index.d.ts +106 -0
  40. package/dist/stores/index.js +149 -0
  41. package/dist/stores/index.js.map +1 -0
  42. package/dist/stores/index.mjs +145 -0
  43. package/dist/stores/index.mjs.map +1 -0
  44. package/dist/types-BOdL3ZWo.d.mts +264 -0
  45. package/dist/types-BOdL3ZWo.d.ts +264 -0
  46. package/dist/validation/index.d.mts +3 -0
  47. package/dist/validation/index.d.ts +3 -0
  48. package/dist/validation/index.js +705 -0
  49. package/dist/validation/index.js.map +1 -0
  50. package/dist/validation/index.mjs +699 -0
  51. package/dist/validation/index.mjs.map +1 -0
  52. 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
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