@arcis/node 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -6
- package/dist/astro/index.js +6141 -0
- package/dist/astro/index.js.map +1 -0
- package/dist/astro/index.mjs +6136 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/bun/index.js +6195 -0
- package/dist/bun/index.js.map +1 -0
- package/dist/bun/index.mjs +6189 -0
- package/dist/bun/index.mjs.map +1 -0
- package/dist/core/constants.d.ts +3 -2
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js +4 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +4 -3
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/types.d.ts +32 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/fastify/index.js +6160 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/fastify/index.mjs +6155 -0
- package/dist/fastify/index.mjs.map +1 -0
- package/dist/guards.d.ts +156 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/hono/index.js +6159 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/hono/index.mjs +6154 -0
- package/dist/hono/index.mjs.map +1 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7126 -178
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7088 -179
- package/dist/index.mjs.map +1 -1
- package/dist/koa/index.js +6158 -0
- package/dist/koa/index.js.map +1 -0
- package/dist/koa/index.mjs +6153 -0
- package/dist/koa/index.mjs.map +1 -0
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/index.mjs.map +1 -1
- package/dist/logging/redactor.d.ts.map +1 -1
- package/dist/middleware/astro.d.ts +64 -0
- package/dist/middleware/astro.d.ts.map +1 -0
- package/dist/middleware/bot-detection.d.ts.map +1 -1
- package/dist/middleware/bun.d.ts +75 -0
- package/dist/middleware/bun.d.ts.map +1 -0
- package/dist/middleware/csrf.d.ts.map +1 -1
- package/dist/middleware/error-handler.d.ts.map +1 -1
- package/dist/middleware/fastify.d.ts +89 -0
- package/dist/middleware/fastify.d.ts.map +1 -0
- package/dist/middleware/graphql.d.ts +35 -0
- package/dist/middleware/graphql.d.ts.map +1 -0
- package/dist/middleware/hono.d.ts +63 -0
- package/dist/middleware/hono.d.ts.map +1 -0
- package/dist/middleware/index.d.ts +12 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +6469 -119
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +6459 -120
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/middleware/koa.d.ts +84 -0
- package/dist/middleware/koa.d.ts.map +1 -0
- package/dist/middleware/main.d.ts +0 -30
- package/dist/middleware/main.d.ts.map +1 -1
- package/dist/middleware/mass-assign.d.ts +81 -0
- package/dist/middleware/mass-assign.d.ts.map +1 -0
- package/dist/middleware/method-allowlist.d.ts +66 -0
- package/dist/middleware/method-allowlist.d.ts.map +1 -0
- package/dist/middleware/nestjs.d.ts +62 -0
- package/dist/middleware/nestjs.d.ts.map +1 -0
- package/dist/middleware/nextjs.d.ts +102 -0
- package/dist/middleware/nextjs.d.ts.map +1 -0
- package/dist/middleware/nuxt.d.ts +61 -0
- package/dist/middleware/nuxt.d.ts.map +1 -0
- package/dist/middleware/overload.d.ts +92 -0
- package/dist/middleware/overload.d.ts.map +1 -0
- package/dist/middleware/protect.d.ts +91 -0
- package/dist/middleware/protect.d.ts.map +1 -0
- package/dist/middleware/rate-limit-sliding.d.ts.map +1 -1
- package/dist/middleware/rate-limit-token.d.ts.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/response-splitting.d.ts +83 -0
- package/dist/middleware/response-splitting.d.ts.map +1 -0
- package/dist/middleware/sveltekit.d.ts +68 -0
- package/dist/middleware/sveltekit.d.ts.map +1 -0
- package/dist/middleware/token-budget.d.ts +75 -0
- package/dist/middleware/token-budget.d.ts.map +1 -0
- package/dist/nestjs/index.js +1724 -0
- package/dist/nestjs/index.js.map +1 -0
- package/dist/nestjs/index.mjs +1717 -0
- package/dist/nestjs/index.mjs.map +1 -0
- package/dist/nextjs/index.js +6184 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/nextjs/index.mjs +6178 -0
- package/dist/nextjs/index.mjs.map +1 -0
- package/dist/nuxt/index.js +6141 -0
- package/dist/nuxt/index.js.map +1 -0
- package/dist/nuxt/index.mjs +6136 -0
- package/dist/nuxt/index.mjs.map +1 -0
- package/dist/sanitizers/encode.d.ts.map +1 -1
- package/dist/sanitizers/graphql.d.ts +72 -0
- package/dist/sanitizers/graphql.d.ts.map +1 -0
- package/dist/sanitizers/headers.d.ts +18 -0
- package/dist/sanitizers/headers.d.ts.map +1 -1
- package/dist/sanitizers/index.d.ts +4 -1
- package/dist/sanitizers/index.d.ts.map +1 -1
- package/dist/sanitizers/index.js +140 -66
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +135 -67
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/prompt-injection.d.ts +62 -0
- package/dist/sanitizers/prompt-injection.d.ts.map +1 -0
- package/dist/sanitizers/sanitize.d.ts +1 -1
- package/dist/sanitizers/sanitize.d.ts.map +1 -1
- package/dist/sanitizers/xpath.d.ts +37 -0
- package/dist/sanitizers/xpath.d.ts.map +1 -0
- package/dist/stores/index.js +4 -4
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/index.mjs +4 -4
- package/dist/stores/index.mjs.map +1 -1
- package/dist/stores/redis.d.ts +7 -1
- package/dist/stores/redis.d.ts.map +1 -1
- package/dist/sveltekit/index.js +6142 -0
- package/dist/sveltekit/index.js.map +1 -0
- package/dist/sveltekit/index.mjs +6137 -0
- package/dist/sveltekit/index.mjs.map +1 -0
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +137 -12
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +116 -13
- package/dist/validation/index.mjs.map +1 -1
- package/dist/validation/redirect.d.ts.map +1 -1
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/url-async.d.ts +137 -0
- package/dist/validation/url-async.d.ts.map +1 -0
- package/package.json +57 -12
- package/scripts/postinstall.cjs +26 -0
- package/dist/cli/arcis.d.ts +0 -23
- package/dist/cli/arcis.d.ts.map +0 -1
- package/dist/cli/arcis.js +0 -312
- package/dist/cli/arcis.js.map +0 -1
- package/dist/cli/arcis.mjs +0 -309
- package/dist/cli/arcis.mjs.map +0 -1
|
@@ -0,0 +1,1717 @@
|
|
|
1
|
+
import 'dns';
|
|
2
|
+
|
|
3
|
+
// src/core/constants.ts
|
|
4
|
+
var INPUT = {
|
|
5
|
+
/** Default maximum input size (1MB) */
|
|
6
|
+
DEFAULT_MAX_SIZE: 1e6,
|
|
7
|
+
/** Maximum recursion depth for nested objects */
|
|
8
|
+
MAX_RECURSION_DEPTH: 10
|
|
9
|
+
};
|
|
10
|
+
var RATE_LIMIT = {
|
|
11
|
+
/** Default window size (1 minute) */
|
|
12
|
+
DEFAULT_WINDOW_MS: 6e4,
|
|
13
|
+
/** Default max requests per window */
|
|
14
|
+
DEFAULT_MAX_REQUESTS: 100,
|
|
15
|
+
/** Default HTTP status code for rate limited responses */
|
|
16
|
+
DEFAULT_STATUS_CODE: 429,
|
|
17
|
+
/** Default error message */
|
|
18
|
+
DEFAULT_MESSAGE: "Too many requests, please try again later."};
|
|
19
|
+
var HEADERS = {
|
|
20
|
+
/** Default Content Security Policy */
|
|
21
|
+
DEFAULT_CSP: [
|
|
22
|
+
"default-src 'self'",
|
|
23
|
+
"script-src 'self'",
|
|
24
|
+
"style-src 'self' 'unsafe-inline'",
|
|
25
|
+
"img-src 'self' data: https:",
|
|
26
|
+
"font-src 'self'",
|
|
27
|
+
"object-src 'none'",
|
|
28
|
+
"frame-ancestors 'none'"
|
|
29
|
+
].join("; "),
|
|
30
|
+
/** Default HSTS max age (1 year in seconds) */
|
|
31
|
+
HSTS_MAX_AGE: 31536e3,
|
|
32
|
+
/** Default X-Frame-Options value */
|
|
33
|
+
FRAME_OPTIONS: "DENY",
|
|
34
|
+
/** Default X-Content-Type-Options value */
|
|
35
|
+
CONTENT_TYPE_OPTIONS: "nosniff",
|
|
36
|
+
/** Default Referrer-Policy value */
|
|
37
|
+
REFERRER_POLICY: "strict-origin-when-cross-origin",
|
|
38
|
+
/** Default Permissions-Policy value */
|
|
39
|
+
PERMISSIONS_POLICY: "geolocation=(), microphone=(), camera=()",
|
|
40
|
+
/** Default Cache-Control value for security */
|
|
41
|
+
CACHE_CONTROL: "no-store, no-cache, must-revalidate, proxy-revalidate"
|
|
42
|
+
};
|
|
43
|
+
var XSS_PATTERNS = [
|
|
44
|
+
/** Script tags (ReDoS-safe version) */
|
|
45
|
+
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
46
|
+
/** javascript: protocol (allow optional spaces before colon) */
|
|
47
|
+
/javascript\s*:/gi,
|
|
48
|
+
/** vbscript: protocol */
|
|
49
|
+
/vbscript\s*:/gi,
|
|
50
|
+
/** Event handlers (onclick, onerror, etc.) — any separator before attribute */
|
|
51
|
+
/(?:[\s/])on\w+\s*=/gi,
|
|
52
|
+
/** iframe tags */
|
|
53
|
+
/<iframe/gi,
|
|
54
|
+
/** object tags */
|
|
55
|
+
/<object/gi,
|
|
56
|
+
/** embed tags */
|
|
57
|
+
/<embed/gi,
|
|
58
|
+
/** data: URIs (only dangerous ones, avoid false positives) */
|
|
59
|
+
/(?:^|[\s"'=])data:/gi,
|
|
60
|
+
/** URL-encoded script tags */
|
|
61
|
+
/%3Cscript/gi,
|
|
62
|
+
/** SVG with onload */
|
|
63
|
+
/<svg[^>]*onload/gi,
|
|
64
|
+
/** form tags — phishing/credential harvesting via action= redirection */
|
|
65
|
+
/<form[\s>]/gi,
|
|
66
|
+
/** meta tags — http-equiv refresh redirects or CSP bypass */
|
|
67
|
+
/<meta[\s>]/gi,
|
|
68
|
+
/** base href hijacking — redirects all relative URLs to attacker domain */
|
|
69
|
+
/<base[\s>]/gi,
|
|
70
|
+
/** link tag injection — stylesheet or preload CSRF attacks */
|
|
71
|
+
/<link[\s>]/gi,
|
|
72
|
+
/** style tag — CSS expression() / behavior: / IE-era attacks. Mirrors
|
|
73
|
+
* Python's xss-style-tag from packages/core/patterns.json. */
|
|
74
|
+
/<style[\s>]/gi
|
|
75
|
+
];
|
|
76
|
+
var XSS_REMOVE_PATTERNS = [
|
|
77
|
+
/** Full script blocks (content + tags) */
|
|
78
|
+
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
79
|
+
/** Standalone/unclosed script tags */
|
|
80
|
+
/<script[^>]*>/gi,
|
|
81
|
+
/** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
|
|
82
|
+
/<style[^>]*>[\s\S]*?<\/style>/gi,
|
|
83
|
+
/<style[^>]*/gi,
|
|
84
|
+
/** iframe — full block and partial/unclosed */
|
|
85
|
+
/<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
|
|
86
|
+
/<iframe[^>]*/gi,
|
|
87
|
+
/** object — full block and partial/unclosed */
|
|
88
|
+
/<object[^>]*>[\s\S]*?<\/object>/gi,
|
|
89
|
+
/<object[^>]*/gi,
|
|
90
|
+
/** embed tags */
|
|
91
|
+
/<embed[^>]*/gi,
|
|
92
|
+
/** SVG with inline event handlers */
|
|
93
|
+
/<svg[^>]*onload[^>]*>/gi,
|
|
94
|
+
/** URL-encoded script tags */
|
|
95
|
+
/%3Cscript/gi,
|
|
96
|
+
/** Event handlers with quoted values: onclick="...", onerror='...' */
|
|
97
|
+
/(?:[\s/])on\w+\s*=\s*["'][^"']*["']/gi,
|
|
98
|
+
/** Event handlers with unquoted values: onload=value */
|
|
99
|
+
/(?:[\s/])on\w+\s*=\s*[^\s>]*/gi,
|
|
100
|
+
/** javascript: and vbscript: protocols (allow optional spaces before colon) */
|
|
101
|
+
/javascript\s*:/gi,
|
|
102
|
+
/vbscript\s*:/gi,
|
|
103
|
+
/** data: URIs with HTML or SVG content (SVG can run JS via inline event handlers) */
|
|
104
|
+
/data\s*:\s*(?:text\/html|image\/svg)[^>\s]*/gi,
|
|
105
|
+
/** form tag injection — phishing via action= redirection */
|
|
106
|
+
/<form[\s>][^>]*/gi,
|
|
107
|
+
/** meta tag injection — http-equiv refresh or CSP bypass */
|
|
108
|
+
/<meta[\s>][^>]*/gi,
|
|
109
|
+
/** base href hijacking */
|
|
110
|
+
/<base[\s>][^>]*/gi,
|
|
111
|
+
/** link tag injection — stylesheet or preload attacks */
|
|
112
|
+
/<link[\s>][^>]*/gi
|
|
113
|
+
];
|
|
114
|
+
var SQL_PATTERNS = [
|
|
115
|
+
/** SQL keywords */
|
|
116
|
+
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)\b)/gi,
|
|
117
|
+
/** SQL comments: ANSI (--), C-style (slash-star ... star-slash), MySQL (#) */
|
|
118
|
+
/(--|\/\*|\*\/|#)/g,
|
|
119
|
+
/** SQL statement separators */
|
|
120
|
+
/(;|\|\||&&)/g,
|
|
121
|
+
/** Boolean injection: OR 1=1 */
|
|
122
|
+
/\bOR\s+\d+\s*=\s*\d+/gi,
|
|
123
|
+
/** Boolean injection: OR 'a'='a' or OR "a"="a" (including mixed quotes) */
|
|
124
|
+
/\bOR\s+(['"])[^'"]*\1\s*=\s*(['"])[^'"]*\2/gi,
|
|
125
|
+
/\bOR\s+('[^']*'|"[^"]*")\s*=\s*('[^']*'|"[^"]*")/gi,
|
|
126
|
+
/** Boolean injection: AND 1=1 */
|
|
127
|
+
/\bAND\s+\d+\s*=\s*\d+/gi,
|
|
128
|
+
/** Boolean injection: AND 'a'='a' or AND "a"="a" (including mixed quotes) */
|
|
129
|
+
/\bAND\s+(['"])[^'"]*\1\s*=\s*(['"])[^'"]*\2/gi,
|
|
130
|
+
/\bAND\s+('[^']*'|"[^"]*")\s*=\s*('[^']*'|"[^"]*")/gi,
|
|
131
|
+
/** Time-based blind: SLEEP() */
|
|
132
|
+
/\bSLEEP\s*\(\s*\d+\s*\)/gi,
|
|
133
|
+
/** Time-based blind: BENCHMARK() */
|
|
134
|
+
/\bBENCHMARK\s*\(/gi,
|
|
135
|
+
/** Time-based blind: PostgreSQL pg_sleep() */
|
|
136
|
+
/\bpg_sleep\s*\(/gi,
|
|
137
|
+
/** Time-based blind: MSSQL WAITFOR DELAY */
|
|
138
|
+
/\bWAITFOR\s+DELAY\b/gi
|
|
139
|
+
];
|
|
140
|
+
var PATH_PATTERNS = [
|
|
141
|
+
/** Unix path traversal */
|
|
142
|
+
/\.\.\//g,
|
|
143
|
+
/** Windows path traversal */
|
|
144
|
+
/\.\.\\/g,
|
|
145
|
+
/** URL-encoded traversal (%2e%2e) */
|
|
146
|
+
/%2e%2e/gi,
|
|
147
|
+
/** Double URL-encoded traversal (%252e) */
|
|
148
|
+
/%252e/gi,
|
|
149
|
+
/** Mixed encoding: ..%2F */
|
|
150
|
+
/\.\.%2F/gi,
|
|
151
|
+
/** Mixed encoding: %2e./ and .%2e/ */
|
|
152
|
+
/%2e\.[\\/]/gi,
|
|
153
|
+
/\.%2e[\\/]/gi,
|
|
154
|
+
/** Fully URL-encoded: %2e%2e%2f */
|
|
155
|
+
/%2e%2e%2f/gi,
|
|
156
|
+
/** Double URL-encoded forward slash: %252f */
|
|
157
|
+
/%252f/gi,
|
|
158
|
+
/** Dotdotslash bypass: ....// or ....\\ */
|
|
159
|
+
/\.{2,}[/\\]{2,}/g,
|
|
160
|
+
/** Null byte injection in paths */
|
|
161
|
+
/\0/g
|
|
162
|
+
];
|
|
163
|
+
var COMMAND_PATTERNS = [
|
|
164
|
+
/**
|
|
165
|
+
* Shell metacharacters that enable command chaining/substitution.
|
|
166
|
+
* Bare ( and ) are excluded — they appear in common legitimate values
|
|
167
|
+
* (function calls in code fields, math expressions, etc.).
|
|
168
|
+
* Command substitution is caught by the $( combined pattern below.
|
|
169
|
+
* NOTE: ';', '&', '|' may appear in legitimate URL query strings
|
|
170
|
+
* and Markdown; consider disabling command checking (command: false)
|
|
171
|
+
* for fields that intentionally allow those characters.
|
|
172
|
+
*/
|
|
173
|
+
/[;&|`]/g,
|
|
174
|
+
/** Command substitution: $( ... ) — matched as a pair to reduce false positives */
|
|
175
|
+
/\$\(/g,
|
|
176
|
+
/** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
|
|
177
|
+
/%0[0-9a-f]/gi
|
|
178
|
+
];
|
|
179
|
+
var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
180
|
+
"__proto__",
|
|
181
|
+
"constructor",
|
|
182
|
+
"prototype",
|
|
183
|
+
"__definegetter__",
|
|
184
|
+
"__definesetter__",
|
|
185
|
+
"__lookupgetter__",
|
|
186
|
+
"__lookupsetter__"
|
|
187
|
+
]);
|
|
188
|
+
var NOSQL_DANGEROUS_KEYS = /* @__PURE__ */ new Set([
|
|
189
|
+
// Comparison
|
|
190
|
+
"$gt",
|
|
191
|
+
"$gte",
|
|
192
|
+
"$lt",
|
|
193
|
+
"$lte",
|
|
194
|
+
"$ne",
|
|
195
|
+
"$eq",
|
|
196
|
+
"$in",
|
|
197
|
+
"$nin",
|
|
198
|
+
// Logical
|
|
199
|
+
"$and",
|
|
200
|
+
"$or",
|
|
201
|
+
"$not",
|
|
202
|
+
"$nor",
|
|
203
|
+
// Element / evaluation
|
|
204
|
+
"$exists",
|
|
205
|
+
"$type",
|
|
206
|
+
"$regex",
|
|
207
|
+
"$where",
|
|
208
|
+
"$expr",
|
|
209
|
+
"$mod",
|
|
210
|
+
"$text",
|
|
211
|
+
"$jsonSchema",
|
|
212
|
+
// Array
|
|
213
|
+
"$elemMatch",
|
|
214
|
+
"$all",
|
|
215
|
+
"$size",
|
|
216
|
+
// JavaScript execution (critical)
|
|
217
|
+
"$function",
|
|
218
|
+
"$accumulator",
|
|
219
|
+
// Aggregation pipeline operators (injectable via $lookup etc.)
|
|
220
|
+
"$lookup",
|
|
221
|
+
"$match",
|
|
222
|
+
"$project",
|
|
223
|
+
"$group",
|
|
224
|
+
"$sort",
|
|
225
|
+
"$limit",
|
|
226
|
+
"$skip",
|
|
227
|
+
"$unwind",
|
|
228
|
+
"$addFields",
|
|
229
|
+
"$replaceRoot"
|
|
230
|
+
]);
|
|
231
|
+
var REDACTION = {
|
|
232
|
+
/** Replacement text for redacted values */
|
|
233
|
+
REPLACEMENT: "[REDACTED]",
|
|
234
|
+
/** Truncation indicator */
|
|
235
|
+
TRUNCATED: "[TRUNCATED]",
|
|
236
|
+
/** Max depth indicator */
|
|
237
|
+
MAX_DEPTH: "[MAX_DEPTH]",
|
|
238
|
+
/** Default max message length */
|
|
239
|
+
DEFAULT_MAX_LENGTH: 1e4,
|
|
240
|
+
/** Default sensitive keys to redact */
|
|
241
|
+
SENSITIVE_KEYS: /* @__PURE__ */ new Set([
|
|
242
|
+
"password",
|
|
243
|
+
"passwd",
|
|
244
|
+
"pwd",
|
|
245
|
+
"secret",
|
|
246
|
+
"token",
|
|
247
|
+
"apikey",
|
|
248
|
+
"api_key",
|
|
249
|
+
"apiKey",
|
|
250
|
+
"auth",
|
|
251
|
+
"authorization",
|
|
252
|
+
"credit_card",
|
|
253
|
+
"creditcard",
|
|
254
|
+
"cc",
|
|
255
|
+
"ssn",
|
|
256
|
+
"social_security",
|
|
257
|
+
"private_key",
|
|
258
|
+
"privateKey",
|
|
259
|
+
"access_token",
|
|
260
|
+
"accessToken",
|
|
261
|
+
"refresh_token",
|
|
262
|
+
"refreshToken",
|
|
263
|
+
"bearer",
|
|
264
|
+
"jwt",
|
|
265
|
+
"session",
|
|
266
|
+
"cookie",
|
|
267
|
+
"credentials",
|
|
268
|
+
"x-api-key",
|
|
269
|
+
"x-auth-token"
|
|
270
|
+
])
|
|
271
|
+
};
|
|
272
|
+
var VALIDATION = {
|
|
273
|
+
/**
|
|
274
|
+
* Email regex pattern.
|
|
275
|
+
* Rejects consecutive dots in local part (e.g. test..foo@example.com),
|
|
276
|
+
* leading/trailing dots, and other common invalid forms.
|
|
277
|
+
*/
|
|
278
|
+
EMAIL: /^[^\s@.][^\s@]*(?:\.[^\s@.][^\s@]*)*@[^\s@]+\.[^\s@]+$/,
|
|
279
|
+
/**
|
|
280
|
+
* URL regex pattern.
|
|
281
|
+
* Only allows http:// and https:// (case-insensitive scheme per
|
|
282
|
+
* RFC 3986); explicitly rejects javascript:, data:, vbscript:, and
|
|
283
|
+
* other dangerous URI schemes.
|
|
284
|
+
*/
|
|
285
|
+
URL: /^https?:\/\/[^\s/$.?#][^\s]*$/i,
|
|
286
|
+
/** UUID regex pattern (v4) */
|
|
287
|
+
UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
288
|
+
};
|
|
289
|
+
var ERRORS = {
|
|
290
|
+
/** Generic error message (production) */
|
|
291
|
+
INTERNAL_SERVER_ERROR: "Internal Server Error",
|
|
292
|
+
/** Validation error messages */
|
|
293
|
+
VALIDATION: {
|
|
294
|
+
REQUIRED: (field) => `${field} is required`,
|
|
295
|
+
INVALID_TYPE: (field, type) => `${field} must be a ${type}`,
|
|
296
|
+
MIN_LENGTH: (field, min) => `${field} must be at least ${min} characters`,
|
|
297
|
+
MAX_LENGTH: (field, max) => `${field} must be at most ${max} characters`,
|
|
298
|
+
MIN_VALUE: (field, min) => `${field} must be at least ${min}`,
|
|
299
|
+
MAX_VALUE: (field, max) => `${field} must be at most ${max}`,
|
|
300
|
+
INVALID_FORMAT: (field) => `${field} format is invalid`,
|
|
301
|
+
INVALID_EMAIL: (field) => `${field} must be a valid email`,
|
|
302
|
+
INVALID_URL: (field) => `${field} must be a valid URL`,
|
|
303
|
+
INVALID_UUID: (field) => `${field} must be a valid UUID`,
|
|
304
|
+
INVALID_ENUM: (field, values) => `${field} must be one of: ${values.join(", ")}`,
|
|
305
|
+
MIN_ITEMS: (field, min) => `${field} must have at least ${min} items`,
|
|
306
|
+
MAX_ITEMS: (field, max) => `${field} must have at most ${max} items`
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/middleware/headers.ts
|
|
311
|
+
function createHeaders(options = {}) {
|
|
312
|
+
const {
|
|
313
|
+
contentSecurityPolicy = true,
|
|
314
|
+
xssFilter = true,
|
|
315
|
+
noSniff = true,
|
|
316
|
+
frameOptions = HEADERS.FRAME_OPTIONS,
|
|
317
|
+
hsts = true,
|
|
318
|
+
referrerPolicy = HEADERS.REFERRER_POLICY,
|
|
319
|
+
permissionsPolicy = HEADERS.PERMISSIONS_POLICY,
|
|
320
|
+
cacheControl = true,
|
|
321
|
+
crossOriginOpenerPolicy = "same-origin",
|
|
322
|
+
crossOriginResourcePolicy = "same-origin",
|
|
323
|
+
crossOriginEmbedderPolicy = "require-corp",
|
|
324
|
+
originAgentCluster = true,
|
|
325
|
+
dnsPrefetchControl = true
|
|
326
|
+
} = options;
|
|
327
|
+
return (req, res, next) => {
|
|
328
|
+
if (contentSecurityPolicy) {
|
|
329
|
+
const csp = typeof contentSecurityPolicy === "string" ? contentSecurityPolicy : HEADERS.DEFAULT_CSP;
|
|
330
|
+
res.setHeader("Content-Security-Policy", csp);
|
|
331
|
+
}
|
|
332
|
+
if (xssFilter) {
|
|
333
|
+
res.setHeader("X-XSS-Protection", "0");
|
|
334
|
+
}
|
|
335
|
+
if (noSniff) {
|
|
336
|
+
res.setHeader("X-Content-Type-Options", HEADERS.CONTENT_TYPE_OPTIONS);
|
|
337
|
+
}
|
|
338
|
+
if (frameOptions) {
|
|
339
|
+
res.setHeader("X-Frame-Options", frameOptions);
|
|
340
|
+
}
|
|
341
|
+
const forwardedProto = req.headers["x-forwarded-proto"]?.split(",")[0].trim().toLowerCase();
|
|
342
|
+
const trustedForwardedProto = forwardedProto === "https" || forwardedProto === "http" ? forwardedProto : void 0;
|
|
343
|
+
const isHttps = req.secure || trustedForwardedProto === "https";
|
|
344
|
+
if (hsts && isHttps) {
|
|
345
|
+
const hstsOpts = typeof hsts === "object" ? hsts : {};
|
|
346
|
+
const maxAge = hstsOpts.maxAge ?? HEADERS.HSTS_MAX_AGE;
|
|
347
|
+
const includeSubDomains = hstsOpts.includeSubDomains !== false;
|
|
348
|
+
const preload = hstsOpts.preload === true;
|
|
349
|
+
let hstsValue = `max-age=${maxAge}`;
|
|
350
|
+
if (includeSubDomains) hstsValue += "; includeSubDomains";
|
|
351
|
+
if (preload) hstsValue += "; preload";
|
|
352
|
+
res.setHeader("Strict-Transport-Security", hstsValue);
|
|
353
|
+
}
|
|
354
|
+
if (referrerPolicy) {
|
|
355
|
+
res.setHeader("Referrer-Policy", referrerPolicy);
|
|
356
|
+
}
|
|
357
|
+
if (permissionsPolicy) {
|
|
358
|
+
res.setHeader("Permissions-Policy", permissionsPolicy);
|
|
359
|
+
}
|
|
360
|
+
if (crossOriginOpenerPolicy) {
|
|
361
|
+
res.setHeader("Cross-Origin-Opener-Policy", crossOriginOpenerPolicy);
|
|
362
|
+
}
|
|
363
|
+
if (crossOriginResourcePolicy) {
|
|
364
|
+
res.setHeader("Cross-Origin-Resource-Policy", crossOriginResourcePolicy);
|
|
365
|
+
}
|
|
366
|
+
if (crossOriginEmbedderPolicy) {
|
|
367
|
+
res.setHeader("Cross-Origin-Embedder-Policy", crossOriginEmbedderPolicy);
|
|
368
|
+
}
|
|
369
|
+
if (originAgentCluster) {
|
|
370
|
+
res.setHeader("Origin-Agent-Cluster", "?1");
|
|
371
|
+
}
|
|
372
|
+
if (dnsPrefetchControl) {
|
|
373
|
+
res.setHeader("X-DNS-Prefetch-Control", "off");
|
|
374
|
+
}
|
|
375
|
+
res.setHeader("X-Permitted-Cross-Domain-Policies", "none");
|
|
376
|
+
if (cacheControl) {
|
|
377
|
+
const cacheControlValue = typeof cacheControl === "string" ? cacheControl : HEADERS.CACHE_CONTROL;
|
|
378
|
+
res.setHeader("Cache-Control", cacheControlValue);
|
|
379
|
+
res.setHeader("Pragma", "no-cache");
|
|
380
|
+
res.setHeader("Expires", "0");
|
|
381
|
+
}
|
|
382
|
+
res.removeHeader("X-Powered-By");
|
|
383
|
+
next();
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/middleware/rate-limit.ts
|
|
388
|
+
function createRateLimiter(options = {}) {
|
|
389
|
+
const {
|
|
390
|
+
max = RATE_LIMIT.DEFAULT_MAX_REQUESTS,
|
|
391
|
+
windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS,
|
|
392
|
+
message = RATE_LIMIT.DEFAULT_MESSAGE,
|
|
393
|
+
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
394
|
+
keyGenerator = (req) => {
|
|
395
|
+
const ip = req.ip ?? req.socket?.remoteAddress;
|
|
396
|
+
if (ip) return ip;
|
|
397
|
+
const ua = req.headers["user-agent"] ?? "";
|
|
398
|
+
const lang = req.headers["accept-language"] ?? "";
|
|
399
|
+
const fp = `${ua}|${lang}`;
|
|
400
|
+
let hash = 0;
|
|
401
|
+
for (let i = 0; i < fp.length; i++) hash = (hash << 5) - hash + fp.charCodeAt(i) | 0;
|
|
402
|
+
return `unknown:${hash.toString(36)}`;
|
|
403
|
+
},
|
|
404
|
+
skip,
|
|
405
|
+
store: externalStore
|
|
406
|
+
} = options;
|
|
407
|
+
const inMemoryStore = /* @__PURE__ */ Object.create(null);
|
|
408
|
+
let cleanupInterval = null;
|
|
409
|
+
if (!externalStore) {
|
|
410
|
+
cleanupInterval = setInterval(() => {
|
|
411
|
+
const now = Date.now();
|
|
412
|
+
for (const key of Object.keys(inMemoryStore)) {
|
|
413
|
+
if (inMemoryStore[key].resetTime < now) {
|
|
414
|
+
delete inMemoryStore[key];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}, windowMs);
|
|
418
|
+
if (typeof cleanupInterval.unref === "function") {
|
|
419
|
+
cleanupInterval.unref();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const handler = async (req, res, next) => {
|
|
423
|
+
try {
|
|
424
|
+
if (skip?.(req)) {
|
|
425
|
+
return next();
|
|
426
|
+
}
|
|
427
|
+
const key = keyGenerator(req);
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
let count;
|
|
430
|
+
let resetTime;
|
|
431
|
+
if (externalStore) {
|
|
432
|
+
const entry = await externalStore.get(key);
|
|
433
|
+
if (!entry || entry.resetTime < now) {
|
|
434
|
+
await externalStore.set(key, { count: 1, resetTime: now + windowMs });
|
|
435
|
+
count = 1;
|
|
436
|
+
resetTime = now + windowMs;
|
|
437
|
+
} else {
|
|
438
|
+
count = await externalStore.increment(key);
|
|
439
|
+
resetTime = entry.resetTime;
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
|
|
443
|
+
inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
|
|
444
|
+
} else {
|
|
445
|
+
inMemoryStore[key].count++;
|
|
446
|
+
}
|
|
447
|
+
count = inMemoryStore[key].count;
|
|
448
|
+
resetTime = inMemoryStore[key].resetTime;
|
|
449
|
+
}
|
|
450
|
+
const remaining = Math.max(0, max - count);
|
|
451
|
+
const resetSeconds = Math.ceil((resetTime - now) / 1e3);
|
|
452
|
+
res.setHeader("X-RateLimit-Limit", max.toString());
|
|
453
|
+
res.setHeader("X-RateLimit-Remaining", remaining.toString());
|
|
454
|
+
res.setHeader("X-RateLimit-Reset", resetSeconds.toString());
|
|
455
|
+
if (count > max) {
|
|
456
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
457
|
+
res.status(statusCode).json({
|
|
458
|
+
error: message,
|
|
459
|
+
retryAfter: resetSeconds
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
next();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.error("[arcis] Rate limiter store error, using in-memory fallback:", error);
|
|
466
|
+
try {
|
|
467
|
+
const key = keyGenerator(req);
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
|
|
470
|
+
inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
|
|
471
|
+
} else {
|
|
472
|
+
inMemoryStore[key].count++;
|
|
473
|
+
}
|
|
474
|
+
const count = inMemoryStore[key].count;
|
|
475
|
+
if (count > max) {
|
|
476
|
+
const resetSeconds = Math.ceil((inMemoryStore[key].resetTime - now) / 1e3);
|
|
477
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
478
|
+
res.status(statusCode).json({ error: message, retryAfter: resetSeconds });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
next();
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
const middleware = handler;
|
|
487
|
+
middleware.close = () => {
|
|
488
|
+
if (cleanupInterval) {
|
|
489
|
+
clearInterval(cleanupInterval);
|
|
490
|
+
cleanupInterval = null;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
return middleware;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/middleware/error-handler.ts
|
|
497
|
+
var SENSITIVE_ERROR_PATTERNS = [
|
|
498
|
+
// SQL database errors
|
|
499
|
+
/\b(SQLITE_ERROR|SQLSTATE|ORA-\d|PG::|mysql_|pg_query|ECONNREFUSED)/i,
|
|
500
|
+
/\b(syntax error at or near|relation ".*" does not exist)/i,
|
|
501
|
+
/\b(column ".*" (does not exist|of relation))/i,
|
|
502
|
+
/\b(duplicate key value violates unique constraint)/i,
|
|
503
|
+
/\b(table .* doesn't exist|unknown column)/i,
|
|
504
|
+
// MongoDB errors
|
|
505
|
+
/\b(MongoError|MongoServerError|MongoNetworkError|E11000 duplicate key)/i,
|
|
506
|
+
// Redis errors
|
|
507
|
+
/\b(WRONGTYPE|CROSSSLOT|CLUSTERDOWN|READONLY|ReplyError)/i,
|
|
508
|
+
// Connection strings and DSNs
|
|
509
|
+
/\b(mongodb(\+srv)?:\/\/|postgres(ql)?:\/\/|mysql:\/\/|redis:\/\/)/i,
|
|
510
|
+
// Stack traces with file paths
|
|
511
|
+
/\bat\s+.*\.(js|ts|py|go|java):\d+/i,
|
|
512
|
+
// Internal IP addresses
|
|
513
|
+
/\b(127\.0\.0\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)\b/
|
|
514
|
+
];
|
|
515
|
+
function containsSensitiveInfo(message) {
|
|
516
|
+
return SENSITIVE_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
517
|
+
}
|
|
518
|
+
function errorHandler(options = false) {
|
|
519
|
+
const isDev = typeof options === "boolean" ? options : options.isDev ?? false;
|
|
520
|
+
const logErrors = typeof options === "object" ? options.logErrors ?? true : true;
|
|
521
|
+
const logger = typeof options === "object" ? options.logger : void 0;
|
|
522
|
+
const customHandler = typeof options === "object" ? options.customHandler : void 0;
|
|
523
|
+
return (err, req, res, _next) => {
|
|
524
|
+
const rawStatus = err.statusCode ?? err.status ?? 500;
|
|
525
|
+
const statusCode = Number.isFinite(rawStatus) && rawStatus >= 400 && rawStatus <= 599 ? Math.floor(rawStatus) : 500;
|
|
526
|
+
if (customHandler) {
|
|
527
|
+
return customHandler(err, req, res);
|
|
528
|
+
}
|
|
529
|
+
if (logErrors) {
|
|
530
|
+
const logData = {
|
|
531
|
+
error: err.message,
|
|
532
|
+
stack: err.stack,
|
|
533
|
+
statusCode,
|
|
534
|
+
path: req.path,
|
|
535
|
+
method: req.method
|
|
536
|
+
};
|
|
537
|
+
if (logger) {
|
|
538
|
+
logger.error("Request error", logData);
|
|
539
|
+
} else {
|
|
540
|
+
console.error("[arcis] Request error:", logData);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const exposeMessage = isDev || err.expose === true;
|
|
544
|
+
let clientMessage;
|
|
545
|
+
if (!exposeMessage) {
|
|
546
|
+
clientMessage = ERRORS.INTERNAL_SERVER_ERROR;
|
|
547
|
+
} else if (containsSensitiveInfo(err.message)) {
|
|
548
|
+
clientMessage = isDev ? err.message : ERRORS.INTERNAL_SERVER_ERROR;
|
|
549
|
+
} else {
|
|
550
|
+
clientMessage = err.message;
|
|
551
|
+
}
|
|
552
|
+
const response = {
|
|
553
|
+
error: clientMessage
|
|
554
|
+
};
|
|
555
|
+
if (isDev) {
|
|
556
|
+
response.stack = err.stack;
|
|
557
|
+
response.details = err.message;
|
|
558
|
+
}
|
|
559
|
+
res.status(statusCode).json(response);
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
var createErrorHandler = errorHandler;
|
|
563
|
+
|
|
564
|
+
// src/core/errors.ts
|
|
565
|
+
var ArcisError = class extends Error {
|
|
566
|
+
constructor(message, statusCode = 500, code = "ARCIS_ERROR") {
|
|
567
|
+
super(message);
|
|
568
|
+
this.name = "ArcisError";
|
|
569
|
+
this.statusCode = statusCode;
|
|
570
|
+
this.code = code;
|
|
571
|
+
this.expose = statusCode < 500;
|
|
572
|
+
if (Error.captureStackTrace) {
|
|
573
|
+
Error.captureStackTrace(this, this.constructor);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
var InputTooLargeError = class extends ArcisError {
|
|
578
|
+
constructor(maxSize, actualSize) {
|
|
579
|
+
super(`Input exceeds maximum size of ${maxSize} bytes`, 413, "INPUT_TOO_LARGE");
|
|
580
|
+
this.name = "InputTooLargeError";
|
|
581
|
+
this.maxSize = maxSize;
|
|
582
|
+
this.actualSize = actualSize;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
var SecurityThreatError = class extends ArcisError {
|
|
586
|
+
constructor(threatType, pattern) {
|
|
587
|
+
super("Request blocked for security reasons", 400, "SECURITY_THREAT");
|
|
588
|
+
this.name = "SecurityThreatError";
|
|
589
|
+
this.threatType = threatType;
|
|
590
|
+
this.pattern = pattern;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// src/middleware/telemetry.ts
|
|
595
|
+
var THREAT_TO_VECTOR = {
|
|
596
|
+
xss: "xss",
|
|
597
|
+
sql_injection: "sql",
|
|
598
|
+
nosql_injection: "nosql",
|
|
599
|
+
path_traversal: "path",
|
|
600
|
+
command_injection: "command",
|
|
601
|
+
prototype_pollution: "prototype",
|
|
602
|
+
header_injection: "header",
|
|
603
|
+
ssti: "ssti",
|
|
604
|
+
xxe: "xxe"
|
|
605
|
+
};
|
|
606
|
+
function createTelemetryEmitter(client) {
|
|
607
|
+
return (req, res, next) => {
|
|
608
|
+
const start = performance.now();
|
|
609
|
+
res.on("finish", () => {
|
|
610
|
+
try {
|
|
611
|
+
const event = buildEvent(req, res.statusCode, performance.now() - start);
|
|
612
|
+
client.record(event);
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
next();
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function tapSanitizerThreats(handler) {
|
|
620
|
+
return (req, res, next) => {
|
|
621
|
+
handler(req, res, (err) => {
|
|
622
|
+
if (err instanceof SecurityThreatError) {
|
|
623
|
+
const vector = THREAT_TO_VECTOR[err.threatType] ?? err.threatType;
|
|
624
|
+
req.__arcis = {
|
|
625
|
+
vector,
|
|
626
|
+
rule: `${vector}/match`,
|
|
627
|
+
severity: "high",
|
|
628
|
+
matchedPattern: err.pattern,
|
|
629
|
+
reason: err.message,
|
|
630
|
+
decision: "deny"
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
next(err);
|
|
634
|
+
});
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function buildEvent(req, status, latencyMs) {
|
|
638
|
+
const marker = req.__arcis;
|
|
639
|
+
const decision = marker?.decision ?? inferDecision(status);
|
|
640
|
+
return {
|
|
641
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
642
|
+
ip: extractIp(req),
|
|
643
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
644
|
+
path: req.path ?? req.url ?? "/",
|
|
645
|
+
decision,
|
|
646
|
+
vector: marker?.vector ?? (status === 429 ? "rate-limit" : void 0),
|
|
647
|
+
rule: marker?.rule ?? (status === 429 ? "rate-limit/exceeded" : void 0),
|
|
648
|
+
severity: marker?.severity ?? (status === 429 ? "medium" : void 0),
|
|
649
|
+
userAgent: typeof req.headers?.["user-agent"] === "string" ? req.headers["user-agent"] : "",
|
|
650
|
+
reason: marker?.reason,
|
|
651
|
+
status,
|
|
652
|
+
matchedPattern: marker?.matchedPattern,
|
|
653
|
+
latencyMs: Math.max(0, latencyMs)
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function inferDecision(status) {
|
|
657
|
+
if (status === 429) return "deny";
|
|
658
|
+
if (status === 400) return "deny";
|
|
659
|
+
if (status === 403) return "deny";
|
|
660
|
+
return "allow";
|
|
661
|
+
}
|
|
662
|
+
function extractIp(req) {
|
|
663
|
+
if (typeof req.ip === "string" && req.ip.length > 0) return req.ip;
|
|
664
|
+
const remote = req.socket?.remoteAddress;
|
|
665
|
+
return typeof remote === "string" ? remote : "0.0.0.0";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/sanitizers/utils.ts
|
|
669
|
+
function encodeHtmlEntities(str) {
|
|
670
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/sanitizers/xss.ts
|
|
674
|
+
function sanitizeXss(input, collectThreats = false, htmlEncode = false) {
|
|
675
|
+
if (typeof input !== "string") {
|
|
676
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
677
|
+
}
|
|
678
|
+
const threats = [];
|
|
679
|
+
let value = input;
|
|
680
|
+
let wasSanitized = false;
|
|
681
|
+
for (const pattern of XSS_REMOVE_PATTERNS) {
|
|
682
|
+
pattern.lastIndex = 0;
|
|
683
|
+
if (pattern.test(value)) {
|
|
684
|
+
pattern.lastIndex = 0;
|
|
685
|
+
if (collectThreats) {
|
|
686
|
+
const matches = value.match(pattern);
|
|
687
|
+
if (matches) {
|
|
688
|
+
for (const match of matches) {
|
|
689
|
+
threats.push({
|
|
690
|
+
type: "xss",
|
|
691
|
+
pattern: pattern.source,
|
|
692
|
+
original: match
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
value = value.replace(pattern, "");
|
|
698
|
+
wasSanitized = true;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (htmlEncode) {
|
|
702
|
+
const encoded = encodeHtmlEntities(value);
|
|
703
|
+
if (encoded !== value) {
|
|
704
|
+
wasSanitized = true;
|
|
705
|
+
}
|
|
706
|
+
value = encoded;
|
|
707
|
+
}
|
|
708
|
+
if (collectThreats) {
|
|
709
|
+
return { value, wasSanitized, threats };
|
|
710
|
+
}
|
|
711
|
+
return value;
|
|
712
|
+
}
|
|
713
|
+
function detectXss(input) {
|
|
714
|
+
if (typeof input !== "string") return false;
|
|
715
|
+
if (/\s+on\w+\s*=/i.test(input)) return true;
|
|
716
|
+
if (/javascript\s*:/i.test(input)) return true;
|
|
717
|
+
if (/vbscript\s*:/i.test(input)) return true;
|
|
718
|
+
if (/data\s*:\s*text\/html/i.test(input)) return true;
|
|
719
|
+
for (const pattern of XSS_PATTERNS) {
|
|
720
|
+
pattern.lastIndex = 0;
|
|
721
|
+
if (pattern.test(input)) {
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/sanitizers/sql.ts
|
|
729
|
+
function sanitizeSql(input, collectThreats = false) {
|
|
730
|
+
if (typeof input !== "string") {
|
|
731
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
732
|
+
}
|
|
733
|
+
const threats = [];
|
|
734
|
+
let value = input;
|
|
735
|
+
let wasSanitized = false;
|
|
736
|
+
for (const pattern of SQL_PATTERNS) {
|
|
737
|
+
pattern.lastIndex = 0;
|
|
738
|
+
if (pattern.test(value)) {
|
|
739
|
+
pattern.lastIndex = 0;
|
|
740
|
+
if (collectThreats) {
|
|
741
|
+
const matches = value.match(pattern);
|
|
742
|
+
if (matches) {
|
|
743
|
+
for (const match of matches) {
|
|
744
|
+
threats.push({
|
|
745
|
+
type: "sql_injection",
|
|
746
|
+
pattern: pattern.source,
|
|
747
|
+
original: match
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
value = value.replace(pattern, " ");
|
|
753
|
+
wasSanitized = true;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (collectThreats) {
|
|
757
|
+
return { value, wasSanitized, threats };
|
|
758
|
+
}
|
|
759
|
+
return value;
|
|
760
|
+
}
|
|
761
|
+
function detectSql(input) {
|
|
762
|
+
if (typeof input !== "string") return false;
|
|
763
|
+
for (const pattern of SQL_PATTERNS) {
|
|
764
|
+
pattern.lastIndex = 0;
|
|
765
|
+
if (pattern.test(input)) {
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/sanitizers/path.ts
|
|
773
|
+
function sanitizePath(input, collectThreats = false) {
|
|
774
|
+
if (typeof input !== "string") {
|
|
775
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
776
|
+
}
|
|
777
|
+
const threats = [];
|
|
778
|
+
let value = input;
|
|
779
|
+
let wasSanitized = false;
|
|
780
|
+
value = value.normalize("NFKC");
|
|
781
|
+
let prev;
|
|
782
|
+
do {
|
|
783
|
+
prev = value;
|
|
784
|
+
for (const pattern of PATH_PATTERNS) {
|
|
785
|
+
pattern.lastIndex = 0;
|
|
786
|
+
if (pattern.test(value)) {
|
|
787
|
+
pattern.lastIndex = 0;
|
|
788
|
+
if (collectThreats) {
|
|
789
|
+
const matches = value.match(pattern);
|
|
790
|
+
if (matches) {
|
|
791
|
+
for (const match of matches) {
|
|
792
|
+
threats.push({
|
|
793
|
+
type: "path_traversal",
|
|
794
|
+
pattern: pattern.source,
|
|
795
|
+
original: match
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
value = value.replace(pattern, "");
|
|
801
|
+
wasSanitized = true;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
} while (value !== prev);
|
|
805
|
+
if (collectThreats) {
|
|
806
|
+
return { value, wasSanitized, threats };
|
|
807
|
+
}
|
|
808
|
+
return value;
|
|
809
|
+
}
|
|
810
|
+
function detectPathTraversal(input) {
|
|
811
|
+
if (typeof input !== "string") return false;
|
|
812
|
+
const normalized = input.normalize("NFKC");
|
|
813
|
+
for (const pattern of PATH_PATTERNS) {
|
|
814
|
+
pattern.lastIndex = 0;
|
|
815
|
+
if (pattern.test(normalized)) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/sanitizers/command.ts
|
|
823
|
+
function sanitizeCommand(input, collectThreats = false) {
|
|
824
|
+
if (typeof input !== "string") {
|
|
825
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
826
|
+
}
|
|
827
|
+
const threats = [];
|
|
828
|
+
let value = input;
|
|
829
|
+
let wasSanitized = false;
|
|
830
|
+
for (const pattern of COMMAND_PATTERNS) {
|
|
831
|
+
pattern.lastIndex = 0;
|
|
832
|
+
if (pattern.test(value)) {
|
|
833
|
+
pattern.lastIndex = 0;
|
|
834
|
+
if (collectThreats) {
|
|
835
|
+
const matches = value.match(pattern);
|
|
836
|
+
if (matches) {
|
|
837
|
+
for (const match of matches) {
|
|
838
|
+
threats.push({
|
|
839
|
+
type: "command_injection",
|
|
840
|
+
pattern: pattern.source,
|
|
841
|
+
original: match
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
value = value.replace(pattern, " ");
|
|
847
|
+
wasSanitized = true;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (collectThreats) {
|
|
851
|
+
return { value, wasSanitized, threats };
|
|
852
|
+
}
|
|
853
|
+
return value;
|
|
854
|
+
}
|
|
855
|
+
function detectCommandInjection(input) {
|
|
856
|
+
if (typeof input !== "string") return false;
|
|
857
|
+
for (const pattern of COMMAND_PATTERNS) {
|
|
858
|
+
pattern.lastIndex = 0;
|
|
859
|
+
if (pattern.test(input)) {
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/sanitizers/ssti.ts
|
|
867
|
+
var SSTI_DETECT_PATTERNS = [
|
|
868
|
+
/** Jinja2 / Twig / Nunjucks: {{ ... }} */
|
|
869
|
+
/\{\{.*?\}\}/g,
|
|
870
|
+
/** Freemarker / Thymeleaf / Spring EL: ${ ... } */
|
|
871
|
+
/\$\{.*?\}/g,
|
|
872
|
+
/** ERB / EJS: <%= ... %> or <% ... %> */
|
|
873
|
+
/<%[=\-]?.*?%>/gs,
|
|
874
|
+
/** Pug / Jade / Slim: #{ ... } */
|
|
875
|
+
/#\{.*?\}/g,
|
|
876
|
+
/** Python dunder sandbox escape */
|
|
877
|
+
/__(?:class|mro|subclasses|globals|builtins|import)__/gi,
|
|
878
|
+
/** Jinja2 config leak: {{config.X}} or {{config['X']}} */
|
|
879
|
+
/\{\{\s*config[.\[]/gi,
|
|
880
|
+
/** Jinja2 built-in objects */
|
|
881
|
+
/\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
|
|
882
|
+
];
|
|
883
|
+
function detectSsti(input) {
|
|
884
|
+
if (typeof input !== "string") return false;
|
|
885
|
+
for (const pattern of SSTI_DETECT_PATTERNS) {
|
|
886
|
+
pattern.lastIndex = 0;
|
|
887
|
+
if (pattern.test(input)) {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/sanitizers/xxe.ts
|
|
895
|
+
var XXE_DETECT_PATTERNS = [
|
|
896
|
+
/** DOCTYPE declaration */
|
|
897
|
+
/<!DOCTYPE\b/gi,
|
|
898
|
+
/** ENTITY declaration */
|
|
899
|
+
/<!ENTITY\b/gi,
|
|
900
|
+
/** SYSTEM keyword with URI */
|
|
901
|
+
/\bSYSTEM\s+["']/gi,
|
|
902
|
+
/** PUBLIC keyword with URI */
|
|
903
|
+
/\bPUBLIC\s+["']/gi,
|
|
904
|
+
/** Parameter entity reference (%entity;) */
|
|
905
|
+
/%\s*\w+\s*;/g,
|
|
906
|
+
/** CDATA section (often used to smuggle payloads) */
|
|
907
|
+
/<!\[CDATA\[/gi
|
|
908
|
+
];
|
|
909
|
+
function detectXxe(input) {
|
|
910
|
+
if (typeof input !== "string") return false;
|
|
911
|
+
for (const pattern of XXE_DETECT_PATTERNS) {
|
|
912
|
+
pattern.lastIndex = 0;
|
|
913
|
+
if (pattern.test(input)) {
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/sanitizers/ldap.ts
|
|
921
|
+
var LDAP_DETECT_PATTERN = /[*()\\\x00]/;
|
|
922
|
+
var LDAP_INJECTION_PATTERN = /\)\s*\(|\*\s*\)\s*\(/;
|
|
923
|
+
function detectLdapInjection(input) {
|
|
924
|
+
if (typeof input !== "string") return false;
|
|
925
|
+
return LDAP_DETECT_PATTERN.test(input) || LDAP_INJECTION_PATTERN.test(input);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/sanitizers/xpath.ts
|
|
929
|
+
var XPATH_INJECTION_CHARS = /['"|,()]/;
|
|
930
|
+
var XPATH_INJECTION_PATTERN = /('\s*(or|and)\s*'|"\s*(or|and)\s*"|\)\s*(or|and)\s*\(|\|\s*\/)/i;
|
|
931
|
+
function detectXpathInjection(input) {
|
|
932
|
+
if (typeof input !== "string" || input.length === 0) return false;
|
|
933
|
+
if (!XPATH_INJECTION_CHARS.test(input)) return false;
|
|
934
|
+
return XPATH_INJECTION_PATTERN.test(input);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/sanitizers/headers.ts
|
|
938
|
+
var HEADER_INJECTION_PATTERN = /\r\n|\r|\n|\0/g;
|
|
939
|
+
function detectHeaderInjection(input) {
|
|
940
|
+
if (typeof input !== "string") return false;
|
|
941
|
+
HEADER_INJECTION_PATTERN.lastIndex = 0;
|
|
942
|
+
return HEADER_INJECTION_PATTERN.test(input);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/sanitizers/sanitize.ts
|
|
946
|
+
function sanitizeString(value, options = {}) {
|
|
947
|
+
if (typeof value !== "string") return value;
|
|
948
|
+
const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
|
|
949
|
+
if (value.length > maxSize) {
|
|
950
|
+
throw new InputTooLargeError(maxSize, value.length);
|
|
951
|
+
}
|
|
952
|
+
const reject = options.mode === "reject";
|
|
953
|
+
let result = value;
|
|
954
|
+
if (options.sql !== false) {
|
|
955
|
+
if (reject) {
|
|
956
|
+
if (detectSql(result)) {
|
|
957
|
+
throw new SecurityThreatError("sql_injection", "SQL pattern detected in input");
|
|
958
|
+
}
|
|
959
|
+
} else {
|
|
960
|
+
result = sanitizeSql(result);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (options.path !== false) {
|
|
964
|
+
result = sanitizePath(result);
|
|
965
|
+
}
|
|
966
|
+
if (options.command !== false) {
|
|
967
|
+
if (reject) {
|
|
968
|
+
if (detectCommandInjection(result)) {
|
|
969
|
+
throw new SecurityThreatError("command_injection", "Shell metacharacter detected in input");
|
|
970
|
+
}
|
|
971
|
+
} else {
|
|
972
|
+
result = sanitizeCommand(result);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (options.xss !== false) {
|
|
976
|
+
result = sanitizeXss(result, false, options.htmlEncode ?? false);
|
|
977
|
+
}
|
|
978
|
+
return result;
|
|
979
|
+
}
|
|
980
|
+
function sanitizeObject(obj, options = {}) {
|
|
981
|
+
if (obj === null || obj === void 0) return obj;
|
|
982
|
+
if (typeof obj === "string") return sanitizeString(obj, options);
|
|
983
|
+
if (typeof obj !== "object") return obj;
|
|
984
|
+
if (Array.isArray(obj)) return obj.map((item) => sanitizeObject(item, options));
|
|
985
|
+
const result = sanitizeObjectDepth(obj, options, 0);
|
|
986
|
+
return options.freeze ? Object.freeze(result) : result;
|
|
987
|
+
}
|
|
988
|
+
function sanitizeObjectDepth(obj, options, depth) {
|
|
989
|
+
if (depth >= INPUT.MAX_RECURSION_DEPTH) return obj;
|
|
990
|
+
const result = {};
|
|
991
|
+
for (const key of Object.keys(obj)) {
|
|
992
|
+
if (options.proto !== false && DANGEROUS_PROTO_KEYS.has(key.toLowerCase())) {
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
if (options.nosql !== false && NOSQL_DANGEROUS_KEYS.has(key)) {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
const sanitizedKey = sanitizeString(key, options);
|
|
999
|
+
const value = obj[key];
|
|
1000
|
+
if (value === null || value === void 0) {
|
|
1001
|
+
result[sanitizedKey] = value;
|
|
1002
|
+
} else if (typeof value === "string") {
|
|
1003
|
+
result[sanitizedKey] = sanitizeString(value, options);
|
|
1004
|
+
} else if (Array.isArray(value)) {
|
|
1005
|
+
result[sanitizedKey] = value.map((item) => sanitizeObject(item, options));
|
|
1006
|
+
} else if (typeof value === "object") {
|
|
1007
|
+
result[sanitizedKey] = sanitizeObjectDepth(value, options, depth + 1);
|
|
1008
|
+
} else {
|
|
1009
|
+
result[sanitizedKey] = value;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
function scanThreats(data, depth = 0) {
|
|
1015
|
+
if (depth > INPUT.MAX_RECURSION_DEPTH) return null;
|
|
1016
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
1017
|
+
for (const key of Object.keys(data)) {
|
|
1018
|
+
const lower = key.toLowerCase();
|
|
1019
|
+
if (DANGEROUS_PROTO_KEYS.has(lower)) {
|
|
1020
|
+
return { vector: "prototype", rule: "prototype/match", matchedPattern: key };
|
|
1021
|
+
}
|
|
1022
|
+
if (NOSQL_DANGEROUS_KEYS.has(key)) {
|
|
1023
|
+
return { vector: "nosql", rule: "nosql/match", matchedPattern: key };
|
|
1024
|
+
}
|
|
1025
|
+
const inner = scanThreats(data[key], depth + 1);
|
|
1026
|
+
if (inner) return inner;
|
|
1027
|
+
}
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
if (Array.isArray(data)) {
|
|
1031
|
+
for (const item of data) {
|
|
1032
|
+
const inner = scanThreats(item, depth + 1);
|
|
1033
|
+
if (inner) return inner;
|
|
1034
|
+
}
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
if (typeof data !== "string") return null;
|
|
1038
|
+
const sample = data.slice(0, 80);
|
|
1039
|
+
if (detectXss(data)) {
|
|
1040
|
+
return { vector: "xss", rule: "xss/match", matchedPattern: sample };
|
|
1041
|
+
}
|
|
1042
|
+
if (detectSsti(data)) {
|
|
1043
|
+
return { vector: "ssti", rule: "ssti/match", matchedPattern: sample };
|
|
1044
|
+
}
|
|
1045
|
+
if (detectXxe(data)) {
|
|
1046
|
+
return { vector: "xxe", rule: "xxe/match", matchedPattern: sample };
|
|
1047
|
+
}
|
|
1048
|
+
if (detectSql(data)) {
|
|
1049
|
+
return { vector: "sql", rule: "sql/match", matchedPattern: sample };
|
|
1050
|
+
}
|
|
1051
|
+
if (detectPathTraversal(data)) {
|
|
1052
|
+
return { vector: "path", rule: "path/match", matchedPattern: sample };
|
|
1053
|
+
}
|
|
1054
|
+
if (detectCommandInjection(data)) {
|
|
1055
|
+
return { vector: "command", rule: "command/match", matchedPattern: sample };
|
|
1056
|
+
}
|
|
1057
|
+
if (detectLdapInjection(data)) {
|
|
1058
|
+
return { vector: "ldap", rule: "ldap/match", matchedPattern: sample };
|
|
1059
|
+
}
|
|
1060
|
+
if (detectXpathInjection(data)) {
|
|
1061
|
+
return { vector: "xpath", rule: "xpath/match", matchedPattern: sample };
|
|
1062
|
+
}
|
|
1063
|
+
if (detectHeaderInjection(data)) {
|
|
1064
|
+
return { vector: "header", rule: "header/match", matchedPattern: sample };
|
|
1065
|
+
}
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
function createSanitizer(options = {}) {
|
|
1069
|
+
return (req, res, next) => {
|
|
1070
|
+
try {
|
|
1071
|
+
if (options.block) {
|
|
1072
|
+
const hit = scanThreats(req.body) || scanThreats(req.query) || scanThreats(req.params) || scanThreats(req.path);
|
|
1073
|
+
if (hit) {
|
|
1074
|
+
req.__arcis = {
|
|
1075
|
+
vector: hit.vector,
|
|
1076
|
+
rule: hit.rule,
|
|
1077
|
+
severity: "high",
|
|
1078
|
+
matchedPattern: hit.matchedPattern,
|
|
1079
|
+
reason: `${hit.vector} pattern detected in request`,
|
|
1080
|
+
decision: "deny"
|
|
1081
|
+
};
|
|
1082
|
+
res.status(403).json({
|
|
1083
|
+
error: "Request blocked for security reasons",
|
|
1084
|
+
code: "SECURITY_THREAT",
|
|
1085
|
+
vector: hit.vector
|
|
1086
|
+
});
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (req.body && typeof req.body === "object") {
|
|
1091
|
+
req.body = sanitizeObject(req.body, options);
|
|
1092
|
+
}
|
|
1093
|
+
if (req.query && typeof req.query === "object") {
|
|
1094
|
+
const sanitizedQuery = sanitizeObject(req.query, options);
|
|
1095
|
+
Object.defineProperty(req, "query", { value: sanitizedQuery, writable: true, configurable: true });
|
|
1096
|
+
}
|
|
1097
|
+
if (req.params && typeof req.params === "object") {
|
|
1098
|
+
const sanitizedParams = sanitizeObject(req.params, options);
|
|
1099
|
+
Object.defineProperty(req, "params", { value: sanitizedParams, writable: true, configurable: true });
|
|
1100
|
+
}
|
|
1101
|
+
next();
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
next(err);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/validation/schema.ts
|
|
1109
|
+
function validate(schema, source = "body") {
|
|
1110
|
+
return (req, res, next) => {
|
|
1111
|
+
const data = req[source] || {};
|
|
1112
|
+
const errors = [];
|
|
1113
|
+
const validated = {};
|
|
1114
|
+
for (const [field, rules] of Object.entries(schema)) {
|
|
1115
|
+
const value = data[field];
|
|
1116
|
+
const result = validateField(field, value, rules);
|
|
1117
|
+
if (result.errors.length > 0) {
|
|
1118
|
+
errors.push(...result.errors);
|
|
1119
|
+
} else if (result.value !== void 0) {
|
|
1120
|
+
validated[field] = result.value;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (errors.length > 0) {
|
|
1124
|
+
res.status(400).json({ errors });
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
Object.defineProperty(req, source, {
|
|
1128
|
+
value: validated,
|
|
1129
|
+
writable: true,
|
|
1130
|
+
configurable: true,
|
|
1131
|
+
enumerable: true
|
|
1132
|
+
});
|
|
1133
|
+
next();
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function validateField(field, value, rules) {
|
|
1137
|
+
const errors = [];
|
|
1138
|
+
if (rules.required && (value === void 0 || value === null || value === "")) {
|
|
1139
|
+
errors.push(ERRORS.VALIDATION.REQUIRED(field));
|
|
1140
|
+
return { errors };
|
|
1141
|
+
}
|
|
1142
|
+
if (value === void 0 || value === null) {
|
|
1143
|
+
return { errors: [] };
|
|
1144
|
+
}
|
|
1145
|
+
let typedValue = value;
|
|
1146
|
+
let isValid = true;
|
|
1147
|
+
switch (rules.type) {
|
|
1148
|
+
case "string":
|
|
1149
|
+
if (typeof value !== "string") {
|
|
1150
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "string"));
|
|
1151
|
+
isValid = false;
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
if (rules.min !== void 0 && value.length < rules.min) {
|
|
1155
|
+
errors.push(ERRORS.VALIDATION.MIN_LENGTH(field, rules.min));
|
|
1156
|
+
isValid = false;
|
|
1157
|
+
}
|
|
1158
|
+
if (rules.max !== void 0 && value.length > rules.max) {
|
|
1159
|
+
errors.push(ERRORS.VALIDATION.MAX_LENGTH(field, rules.max));
|
|
1160
|
+
isValid = false;
|
|
1161
|
+
}
|
|
1162
|
+
if (rules.pattern && !rules.pattern.test(value)) {
|
|
1163
|
+
errors.push(ERRORS.VALIDATION.INVALID_FORMAT(field));
|
|
1164
|
+
isValid = false;
|
|
1165
|
+
}
|
|
1166
|
+
if (isValid && rules.enum && !rules.enum.includes(value)) {
|
|
1167
|
+
errors.push(ERRORS.VALIDATION.INVALID_ENUM(field, rules.enum));
|
|
1168
|
+
isValid = false;
|
|
1169
|
+
}
|
|
1170
|
+
if (isValid && rules.sanitize !== false) {
|
|
1171
|
+
typedValue = sanitizeString(value);
|
|
1172
|
+
}
|
|
1173
|
+
break;
|
|
1174
|
+
case "number":
|
|
1175
|
+
typedValue = Number(value);
|
|
1176
|
+
if (isNaN(typedValue)) {
|
|
1177
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "number"));
|
|
1178
|
+
isValid = false;
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
if (rules.min !== void 0 && typedValue < rules.min) {
|
|
1182
|
+
errors.push(ERRORS.VALIDATION.MIN_VALUE(field, rules.min));
|
|
1183
|
+
isValid = false;
|
|
1184
|
+
}
|
|
1185
|
+
if (rules.max !== void 0 && typedValue > rules.max) {
|
|
1186
|
+
errors.push(ERRORS.VALIDATION.MAX_VALUE(field, rules.max));
|
|
1187
|
+
isValid = false;
|
|
1188
|
+
}
|
|
1189
|
+
break;
|
|
1190
|
+
case "boolean":
|
|
1191
|
+
if (value === "true" || value === true || value === 1 || value === "1") {
|
|
1192
|
+
typedValue = true;
|
|
1193
|
+
} else if (value === "false" || value === false || value === 0 || value === "0") {
|
|
1194
|
+
typedValue = false;
|
|
1195
|
+
} else {
|
|
1196
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "boolean"));
|
|
1197
|
+
isValid = false;
|
|
1198
|
+
}
|
|
1199
|
+
break;
|
|
1200
|
+
case "email":
|
|
1201
|
+
if (!VALIDATION.EMAIL.test(String(value))) {
|
|
1202
|
+
errors.push(ERRORS.VALIDATION.INVALID_EMAIL(field));
|
|
1203
|
+
isValid = false;
|
|
1204
|
+
}
|
|
1205
|
+
if (isValid) {
|
|
1206
|
+
typedValue = sanitizeString(String(value).toLowerCase().trim());
|
|
1207
|
+
}
|
|
1208
|
+
break;
|
|
1209
|
+
case "url":
|
|
1210
|
+
if (!VALIDATION.URL.test(String(value))) {
|
|
1211
|
+
errors.push(ERRORS.VALIDATION.INVALID_URL(field));
|
|
1212
|
+
isValid = false;
|
|
1213
|
+
}
|
|
1214
|
+
if (isValid) {
|
|
1215
|
+
typedValue = sanitizeString(String(value));
|
|
1216
|
+
}
|
|
1217
|
+
break;
|
|
1218
|
+
case "uuid":
|
|
1219
|
+
if (!VALIDATION.UUID.test(String(value))) {
|
|
1220
|
+
errors.push(ERRORS.VALIDATION.INVALID_UUID(field));
|
|
1221
|
+
isValid = false;
|
|
1222
|
+
}
|
|
1223
|
+
break;
|
|
1224
|
+
case "array":
|
|
1225
|
+
if (!Array.isArray(value)) {
|
|
1226
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "array"));
|
|
1227
|
+
isValid = false;
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
if (rules.min !== void 0 && value.length < rules.min) {
|
|
1231
|
+
errors.push(ERRORS.VALIDATION.MIN_ITEMS(field, rules.min));
|
|
1232
|
+
isValid = false;
|
|
1233
|
+
}
|
|
1234
|
+
if (rules.max !== void 0 && value.length > rules.max) {
|
|
1235
|
+
errors.push(ERRORS.VALIDATION.MAX_ITEMS(field, rules.max));
|
|
1236
|
+
isValid = false;
|
|
1237
|
+
}
|
|
1238
|
+
break;
|
|
1239
|
+
case "object":
|
|
1240
|
+
if (typeof value !== "object" || Array.isArray(value) || value === null) {
|
|
1241
|
+
errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "object"));
|
|
1242
|
+
isValid = false;
|
|
1243
|
+
}
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
if (isValid && rules.enum && rules.type !== "string" && !rules.enum.includes(typedValue)) {
|
|
1247
|
+
errors.push(ERRORS.VALIDATION.INVALID_ENUM(field, rules.enum));
|
|
1248
|
+
isValid = false;
|
|
1249
|
+
}
|
|
1250
|
+
if (isValid && rules.custom) {
|
|
1251
|
+
const customResult = rules.custom(typedValue);
|
|
1252
|
+
if (customResult === void 0) {
|
|
1253
|
+
throw new TypeError(
|
|
1254
|
+
`Custom validator for field "${field}" returned undefined. Return true to pass, false to fail, or a string error message.`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
if (customResult !== true) {
|
|
1258
|
+
errors.push(typeof customResult === "string" && customResult.length > 0 ? customResult : `${field} is invalid`);
|
|
1259
|
+
isValid = false;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
value: isValid ? typedValue : void 0,
|
|
1264
|
+
errors
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/validation/file.ts
|
|
1269
|
+
({
|
|
1270
|
+
// Images
|
|
1271
|
+
"image/jpeg": [Buffer.from([255, 216, 255])],
|
|
1272
|
+
"image/png": [Buffer.from([137, 80, 78, 71])],
|
|
1273
|
+
"image/gif": [Buffer.from("GIF87a"), Buffer.from("GIF89a")],
|
|
1274
|
+
"image/webp": [Buffer.from("RIFF")],
|
|
1275
|
+
// RIFF....WEBP
|
|
1276
|
+
"image/bmp": [Buffer.from([66, 77])],
|
|
1277
|
+
// text-based, check separately
|
|
1278
|
+
// Documents
|
|
1279
|
+
"application/pdf": [Buffer.from("%PDF")],
|
|
1280
|
+
"application/zip": [Buffer.from([80, 75, 3, 4])],
|
|
1281
|
+
// Audio/Video
|
|
1282
|
+
"audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])]});
|
|
1283
|
+
|
|
1284
|
+
// src/logging/redactor.ts
|
|
1285
|
+
var LOG_LEVELS = {
|
|
1286
|
+
debug: 0,
|
|
1287
|
+
info: 1,
|
|
1288
|
+
warn: 2,
|
|
1289
|
+
error: 3,
|
|
1290
|
+
silent: 4
|
|
1291
|
+
};
|
|
1292
|
+
function createSafeLogger(options = {}) {
|
|
1293
|
+
const {
|
|
1294
|
+
redactKeys = [],
|
|
1295
|
+
maxLength = REDACTION.DEFAULT_MAX_LENGTH,
|
|
1296
|
+
redactPatterns = [],
|
|
1297
|
+
level: minLevel = "debug"
|
|
1298
|
+
} = options;
|
|
1299
|
+
const minLevelNum = LOG_LEVELS[minLevel] ?? 0;
|
|
1300
|
+
const allRedactKeys = /* @__PURE__ */ new Set([
|
|
1301
|
+
...Array.from(REDACTION.SENSITIVE_KEYS),
|
|
1302
|
+
...redactKeys.map((k) => k.toLowerCase())
|
|
1303
|
+
]);
|
|
1304
|
+
function redact(obj, depth = 0) {
|
|
1305
|
+
if (depth > INPUT.MAX_RECURSION_DEPTH) return REDACTION.MAX_DEPTH;
|
|
1306
|
+
if (obj === null || obj === void 0) return obj;
|
|
1307
|
+
if (typeof obj === "string") {
|
|
1308
|
+
return redactString(obj, maxLength, redactPatterns);
|
|
1309
|
+
}
|
|
1310
|
+
if (typeof obj !== "object") return obj;
|
|
1311
|
+
if (Array.isArray(obj)) {
|
|
1312
|
+
return obj.map((item) => redact(item, depth + 1));
|
|
1313
|
+
}
|
|
1314
|
+
const result = {};
|
|
1315
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1316
|
+
if (allRedactKeys.has(key.toLowerCase())) {
|
|
1317
|
+
result[key] = REDACTION.REPLACEMENT;
|
|
1318
|
+
} else {
|
|
1319
|
+
result[key] = redact(value, depth + 1);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return result;
|
|
1323
|
+
}
|
|
1324
|
+
function log(level, message, data) {
|
|
1325
|
+
const levelNum = LOG_LEVELS[level] ?? 0;
|
|
1326
|
+
if (levelNum < minLevelNum) return;
|
|
1327
|
+
const entry = {
|
|
1328
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1329
|
+
level,
|
|
1330
|
+
message: redactString(message, maxLength, redactPatterns)
|
|
1331
|
+
};
|
|
1332
|
+
if (data !== void 0) {
|
|
1333
|
+
entry.data = redact(data);
|
|
1334
|
+
}
|
|
1335
|
+
console.log(JSON.stringify(entry));
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
log,
|
|
1339
|
+
info: (msg, data) => log("info", msg, data),
|
|
1340
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
1341
|
+
error: (msg, data) => log("error", msg, data),
|
|
1342
|
+
debug: (msg, data) => log("debug", msg, data)
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
function redactString(str, maxLength, patterns) {
|
|
1346
|
+
let safe = str.replace(/[\r\n\t]/g, " ").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g, "");
|
|
1347
|
+
for (const pattern of patterns) {
|
|
1348
|
+
safe = safe.replace(pattern, REDACTION.REPLACEMENT);
|
|
1349
|
+
}
|
|
1350
|
+
if (safe.length > maxLength) {
|
|
1351
|
+
safe = safe.substring(0, maxLength) + `...${REDACTION.TRUNCATED}`;
|
|
1352
|
+
}
|
|
1353
|
+
return safe;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/telemetry/client.ts
|
|
1357
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
1358
|
+
var MAX_BATCH_SIZE = 500;
|
|
1359
|
+
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
1360
|
+
var MIN_FLUSH_INTERVAL_MS = 500;
|
|
1361
|
+
var FLUSH_TIMEOUT_MS = 1e4;
|
|
1362
|
+
var DEFAULT_MAX_QUEUE_SIZE = 1e4;
|
|
1363
|
+
var TelemetryClient = class {
|
|
1364
|
+
constructor(options) {
|
|
1365
|
+
this.queue = [];
|
|
1366
|
+
this.flushing = false;
|
|
1367
|
+
this.closed = false;
|
|
1368
|
+
// Counts events dropped since the last successful flush. Resets to 0
|
|
1369
|
+
// each flush so onQueueOverflow callbacks see "drops in this window"
|
|
1370
|
+
// rather than a monotonic lifetime counter.
|
|
1371
|
+
this.droppedSinceLastFlush = 0;
|
|
1372
|
+
if (!options.endpoint || typeof options.endpoint !== "string") {
|
|
1373
|
+
throw new TypeError("TelemetryClient: `endpoint` is required");
|
|
1374
|
+
}
|
|
1375
|
+
this.endpoint = options.endpoint;
|
|
1376
|
+
this.apiKey = options.apiKey;
|
|
1377
|
+
this.workspaceId = options.workspaceId;
|
|
1378
|
+
this.batchSize = clamp(
|
|
1379
|
+
options.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
1380
|
+
1,
|
|
1381
|
+
MAX_BATCH_SIZE
|
|
1382
|
+
);
|
|
1383
|
+
this.flushIntervalMs = Math.max(
|
|
1384
|
+
options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
1385
|
+
MIN_FLUSH_INTERVAL_MS
|
|
1386
|
+
);
|
|
1387
|
+
this.maxQueueSize = Math.max(
|
|
1388
|
+
options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
|
|
1389
|
+
this.batchSize
|
|
1390
|
+
);
|
|
1391
|
+
this.onError = options.onError ?? (() => {
|
|
1392
|
+
});
|
|
1393
|
+
this.onQueueOverflow = options.onQueueOverflow ?? (() => {
|
|
1394
|
+
});
|
|
1395
|
+
this.startTimer();
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Enqueue an event. Fast, synchronous, cannot throw.
|
|
1399
|
+
* Triggers a flush if the queue has reached `batchSize`.
|
|
1400
|
+
*/
|
|
1401
|
+
record(event) {
|
|
1402
|
+
if (this.closed) return;
|
|
1403
|
+
this.queue.push(event);
|
|
1404
|
+
if (this.queue.length > this.maxQueueSize) {
|
|
1405
|
+
const drop = this.queue.length - this.maxQueueSize;
|
|
1406
|
+
this.queue.splice(0, drop);
|
|
1407
|
+
this.droppedSinceLastFlush += drop;
|
|
1408
|
+
try {
|
|
1409
|
+
this.onQueueOverflow(this.droppedSinceLastFlush);
|
|
1410
|
+
} catch {
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (this.queue.length >= this.batchSize) {
|
|
1414
|
+
void this.flush();
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Manually flush the queue. Pulls up to `batchSize` events into a batch and
|
|
1419
|
+
* POSTs them. Returns a resolved promise on success OR on handled failure.
|
|
1420
|
+
* Never throws.
|
|
1421
|
+
*/
|
|
1422
|
+
async flush() {
|
|
1423
|
+
if (this.flushing) return;
|
|
1424
|
+
if (this.queue.length === 0) return;
|
|
1425
|
+
this.flushing = true;
|
|
1426
|
+
try {
|
|
1427
|
+
const batch = this.queue.splice(0, this.batchSize);
|
|
1428
|
+
await this.send(batch);
|
|
1429
|
+
this.droppedSinceLastFlush = 0;
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
this.safeNotify(err);
|
|
1432
|
+
} finally {
|
|
1433
|
+
this.flushing = false;
|
|
1434
|
+
}
|
|
1435
|
+
if (!this.closed && this.queue.length > 0) {
|
|
1436
|
+
void this.flush();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Shut down: stop the interval timer and attempt one final flush.
|
|
1441
|
+
* Safe to call multiple times.
|
|
1442
|
+
*/
|
|
1443
|
+
async close() {
|
|
1444
|
+
if (this.closed) return;
|
|
1445
|
+
this.closed = true;
|
|
1446
|
+
if (this.timer !== void 0) {
|
|
1447
|
+
clearInterval(this.timer);
|
|
1448
|
+
this.timer = void 0;
|
|
1449
|
+
}
|
|
1450
|
+
if (this.signalHandler !== void 0) {
|
|
1451
|
+
process.off("SIGTERM", this.signalHandler);
|
|
1452
|
+
process.off("SIGINT", this.signalHandler);
|
|
1453
|
+
this.signalHandler = void 0;
|
|
1454
|
+
}
|
|
1455
|
+
try {
|
|
1456
|
+
await this.flush();
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Register `SIGTERM` / `SIGINT` handlers that call `close()` to drain
|
|
1462
|
+
* the queue on graceful shutdown. Opt-in — libraries should not silently
|
|
1463
|
+
* attach global signal handlers. Safe to call multiple times.
|
|
1464
|
+
*/
|
|
1465
|
+
installShutdownHooks() {
|
|
1466
|
+
if (this.signalHandler !== void 0 || this.closed) return;
|
|
1467
|
+
const handler = () => {
|
|
1468
|
+
void this.close();
|
|
1469
|
+
};
|
|
1470
|
+
this.signalHandler = handler;
|
|
1471
|
+
process.once("SIGTERM", handler);
|
|
1472
|
+
process.once("SIGINT", handler);
|
|
1473
|
+
}
|
|
1474
|
+
/** Count of events currently waiting to be sent. Useful for tests. */
|
|
1475
|
+
get pendingCount() {
|
|
1476
|
+
return this.queue.length;
|
|
1477
|
+
}
|
|
1478
|
+
// ── internals ─────────────────────────────────────────────────────────
|
|
1479
|
+
async send(batch) {
|
|
1480
|
+
const headers = {
|
|
1481
|
+
"content-type": "application/json"
|
|
1482
|
+
};
|
|
1483
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
1484
|
+
if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
|
|
1485
|
+
const controller = new AbortController();
|
|
1486
|
+
const abortTimer = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
|
|
1487
|
+
try {
|
|
1488
|
+
const res = await fetch(this.endpoint, {
|
|
1489
|
+
method: "POST",
|
|
1490
|
+
headers,
|
|
1491
|
+
body: JSON.stringify({ events: batch }),
|
|
1492
|
+
signal: controller.signal
|
|
1493
|
+
});
|
|
1494
|
+
if (!res.ok) {
|
|
1495
|
+
const text = await safeReadBody(res);
|
|
1496
|
+
throw new TelemetryHttpError(res.status, text);
|
|
1497
|
+
}
|
|
1498
|
+
} finally {
|
|
1499
|
+
clearTimeout(abortTimer);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
startTimer() {
|
|
1503
|
+
this.timer = setInterval(() => {
|
|
1504
|
+
void this.flush();
|
|
1505
|
+
}, this.flushIntervalMs);
|
|
1506
|
+
this.timer.unref?.();
|
|
1507
|
+
}
|
|
1508
|
+
safeNotify(err) {
|
|
1509
|
+
try {
|
|
1510
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
1511
|
+
} catch {
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
var TelemetryHttpError = class extends Error {
|
|
1516
|
+
constructor(status, responseBody) {
|
|
1517
|
+
super(`Telemetry ingest returned HTTP ${status}`);
|
|
1518
|
+
this.status = status;
|
|
1519
|
+
this.responseBody = responseBody;
|
|
1520
|
+
this.name = "TelemetryHttpError";
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
function clamp(value, min, max) {
|
|
1524
|
+
if (!Number.isFinite(value)) return min;
|
|
1525
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
1526
|
+
}
|
|
1527
|
+
async function safeReadBody(res) {
|
|
1528
|
+
try {
|
|
1529
|
+
const text = await res.text();
|
|
1530
|
+
return text.slice(0, 500);
|
|
1531
|
+
} catch {
|
|
1532
|
+
return "";
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// src/middleware/main.ts
|
|
1537
|
+
function buildTelemetryFromEnv() {
|
|
1538
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
1539
|
+
const endpoint = env?.ARCIS_ENDPOINT;
|
|
1540
|
+
if (!endpoint) return void 0;
|
|
1541
|
+
const opts = { endpoint };
|
|
1542
|
+
if (env?.ARCIS_WORKSPACE_ID) opts.workspaceId = env.ARCIS_WORKSPACE_ID;
|
|
1543
|
+
if (env?.ARCIS_KEY) opts.apiKey = env.ARCIS_KEY;
|
|
1544
|
+
const batch = env?.ARCIS_BATCH_SIZE ? parseInt(env.ARCIS_BATCH_SIZE, 10) : NaN;
|
|
1545
|
+
if (!Number.isNaN(batch)) opts.batchSize = batch;
|
|
1546
|
+
const flush = env?.ARCIS_FLUSH_INTERVAL_MS ? parseInt(env.ARCIS_FLUSH_INTERVAL_MS, 10) : NaN;
|
|
1547
|
+
if (!Number.isNaN(flush)) opts.flushIntervalMs = flush;
|
|
1548
|
+
return opts;
|
|
1549
|
+
}
|
|
1550
|
+
function createSanitizeObserver(onSanitize) {
|
|
1551
|
+
return (req, _res, next) => {
|
|
1552
|
+
const fields = [
|
|
1553
|
+
["body", req.body],
|
|
1554
|
+
["query", req.query],
|
|
1555
|
+
["params", req.params],
|
|
1556
|
+
["path", req.path]
|
|
1557
|
+
];
|
|
1558
|
+
for (const [name, value] of fields) {
|
|
1559
|
+
const hit = scanThreats(value);
|
|
1560
|
+
if (!hit) continue;
|
|
1561
|
+
try {
|
|
1562
|
+
onSanitize({
|
|
1563
|
+
type: hit.vector,
|
|
1564
|
+
field: name,
|
|
1565
|
+
original: hit.matchedPattern,
|
|
1566
|
+
pattern: hit.matchedPattern
|
|
1567
|
+
});
|
|
1568
|
+
} catch {
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
next();
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function suppressRateLimit429(handler) {
|
|
1575
|
+
return (req, res, next) => {
|
|
1576
|
+
const originalStatus = res.status.bind(res);
|
|
1577
|
+
const originalJson = res.json.bind(res);
|
|
1578
|
+
let suppressed = false;
|
|
1579
|
+
let nextCalled = false;
|
|
1580
|
+
const restore = () => {
|
|
1581
|
+
res.status = originalStatus;
|
|
1582
|
+
res.json = originalJson;
|
|
1583
|
+
};
|
|
1584
|
+
res.status = ((code) => {
|
|
1585
|
+
if (code === 429) {
|
|
1586
|
+
suppressed = true;
|
|
1587
|
+
return res;
|
|
1588
|
+
}
|
|
1589
|
+
return originalStatus(code);
|
|
1590
|
+
});
|
|
1591
|
+
res.json = ((body) => {
|
|
1592
|
+
if (suppressed) {
|
|
1593
|
+
restore();
|
|
1594
|
+
if (!nextCalled) {
|
|
1595
|
+
nextCalled = true;
|
|
1596
|
+
next();
|
|
1597
|
+
}
|
|
1598
|
+
return res;
|
|
1599
|
+
}
|
|
1600
|
+
return originalJson(body);
|
|
1601
|
+
});
|
|
1602
|
+
handler(req, res, (err) => {
|
|
1603
|
+
restore();
|
|
1604
|
+
if (!nextCalled) {
|
|
1605
|
+
nextCalled = true;
|
|
1606
|
+
next(err);
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function arcis(options = {}) {
|
|
1612
|
+
const middlewares = [];
|
|
1613
|
+
const cleanupFns = [];
|
|
1614
|
+
const dryRun = options.dryRun === true;
|
|
1615
|
+
let telemetryClient;
|
|
1616
|
+
const telemetryOpts = options.telemetry?.endpoint ? options.telemetry : buildTelemetryFromEnv();
|
|
1617
|
+
if (telemetryOpts) {
|
|
1618
|
+
const client = new TelemetryClient(telemetryOpts);
|
|
1619
|
+
telemetryClient = client;
|
|
1620
|
+
middlewares.push(createTelemetryEmitter(client));
|
|
1621
|
+
cleanupFns.push(() => {
|
|
1622
|
+
void client.close();
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
if (options.headers !== false) {
|
|
1626
|
+
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
1627
|
+
middlewares.push(createHeaders(headerOpts));
|
|
1628
|
+
}
|
|
1629
|
+
if (options.onSanitize) {
|
|
1630
|
+
middlewares.push(createSanitizeObserver(options.onSanitize));
|
|
1631
|
+
}
|
|
1632
|
+
if (options.rateLimit !== false) {
|
|
1633
|
+
const rateLimitOpts = typeof options.rateLimit === "object" ? options.rateLimit : {};
|
|
1634
|
+
const rateLimiter = createRateLimiter(rateLimitOpts);
|
|
1635
|
+
middlewares.push(dryRun ? suppressRateLimit429(rateLimiter) : rateLimiter);
|
|
1636
|
+
cleanupFns.push(() => rateLimiter.close());
|
|
1637
|
+
}
|
|
1638
|
+
if (options.sanitize !== false) {
|
|
1639
|
+
const sanitizeOpts = typeof options.sanitize === "object" ? { ...options.sanitize } : {};
|
|
1640
|
+
if (options.block && sanitizeOpts.block === void 0) {
|
|
1641
|
+
sanitizeOpts.block = true;
|
|
1642
|
+
}
|
|
1643
|
+
if (dryRun) {
|
|
1644
|
+
sanitizeOpts.block = false;
|
|
1645
|
+
}
|
|
1646
|
+
const sanitizer = createSanitizer(sanitizeOpts);
|
|
1647
|
+
middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
|
|
1648
|
+
}
|
|
1649
|
+
const result = middlewares;
|
|
1650
|
+
result.close = () => {
|
|
1651
|
+
for (const fn of cleanupFns) {
|
|
1652
|
+
fn();
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
return result;
|
|
1656
|
+
}
|
|
1657
|
+
var arcisWithMethods = arcis;
|
|
1658
|
+
arcisWithMethods.sanitize = createSanitizer;
|
|
1659
|
+
arcisWithMethods.rateLimit = createRateLimiter;
|
|
1660
|
+
arcisWithMethods.headers = createHeaders;
|
|
1661
|
+
arcisWithMethods.validate = validate;
|
|
1662
|
+
arcisWithMethods.logger = createSafeLogger;
|
|
1663
|
+
arcisWithMethods.errorHandler = createErrorHandler;
|
|
1664
|
+
|
|
1665
|
+
// src/middleware/nestjs.ts
|
|
1666
|
+
var ARCIS_OPTIONS = /* @__PURE__ */ Symbol("ARCIS_OPTIONS");
|
|
1667
|
+
var ArcisMiddleware = class {
|
|
1668
|
+
constructor(options = {}) {
|
|
1669
|
+
this.handlers = arcis(options);
|
|
1670
|
+
}
|
|
1671
|
+
use(req, res, next) {
|
|
1672
|
+
const handlers = this.handlers;
|
|
1673
|
+
let i = 0;
|
|
1674
|
+
const run = (err) => {
|
|
1675
|
+
if (err !== void 0) {
|
|
1676
|
+
next(err);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const handler = handlers[i++];
|
|
1680
|
+
if (!handler) {
|
|
1681
|
+
next();
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
try {
|
|
1685
|
+
handler(req, res, run);
|
|
1686
|
+
} catch (caught) {
|
|
1687
|
+
next(caught);
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
run();
|
|
1691
|
+
}
|
|
1692
|
+
/** Release rate-limiter intervals etc. Call from `OnApplicationShutdown`. */
|
|
1693
|
+
close() {
|
|
1694
|
+
this.handlers.close();
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
var ArcisModule = class _ArcisModule {
|
|
1698
|
+
static forRoot(options = {}) {
|
|
1699
|
+
return {
|
|
1700
|
+
module: _ArcisModule,
|
|
1701
|
+
providers: [
|
|
1702
|
+
{ provide: ARCIS_OPTIONS, useValue: options },
|
|
1703
|
+
{
|
|
1704
|
+
provide: ArcisMiddleware,
|
|
1705
|
+
useFactory: (opts) => new ArcisMiddleware(opts),
|
|
1706
|
+
inject: [ARCIS_OPTIONS]
|
|
1707
|
+
}
|
|
1708
|
+
],
|
|
1709
|
+
exports: [ArcisMiddleware]
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
var nestjs_default = ArcisModule;
|
|
1714
|
+
|
|
1715
|
+
export { ARCIS_OPTIONS, ArcisMiddleware, ArcisModule, nestjs_default as default };
|
|
1716
|
+
//# sourceMappingURL=index.mjs.map
|
|
1717
|
+
//# sourceMappingURL=index.mjs.map
|