@arcis/node 1.4.2 → 1.4.3
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/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +405 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +402 -21
- package/dist/index.mjs.map +1 -1
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/index.mjs.map +1 -1
- package/dist/middleware/bot-detection.d.ts.map +1 -1
- package/dist/middleware/cookies.d.ts.map +1 -1
- package/dist/middleware/csrf.d.ts +10 -0
- package/dist/middleware/csrf.d.ts.map +1 -1
- package/dist/middleware/hpp.d.ts.map +1 -1
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +609 -9
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +608 -10
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/middleware/main.d.ts.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/signup-protection.d.ts +65 -0
- package/dist/middleware/signup-protection.d.ts.map +1 -0
- package/dist/middleware/telemetry.d.ts +36 -0
- package/dist/middleware/telemetry.d.ts.map +1 -0
- package/dist/sanitizers/index.js +26 -8
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +26 -8
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/pii.d.ts.map +1 -1
- package/dist/sanitizers/ssti.d.ts.map +1 -1
- package/dist/sanitizers/xxe.d.ts.map +1 -1
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/index.mjs.map +1 -1
- package/dist/telemetry/client.d.ts +60 -0
- package/dist/telemetry/client.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +3 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/types.d.ts +59 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/validation/index.js +3 -0
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +3 -0
- package/dist/validation/index.mjs.map +1 -1
- package/package.json +7 -1
package/dist/index.mjs
CHANGED
|
@@ -81,6 +81,9 @@ var XSS_REMOVE_PATTERNS = [
|
|
|
81
81
|
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
82
82
|
/** Standalone/unclosed script tags */
|
|
83
83
|
/<script[^>]*>/gi,
|
|
84
|
+
/** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
|
|
85
|
+
/<style[^>]*>[\s\S]*?<\/style>/gi,
|
|
86
|
+
/<style[^>]*/gi,
|
|
84
87
|
/** iframe — full block and partial/unclosed */
|
|
85
88
|
/<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
|
|
86
89
|
/<iframe[^>]*/gi,
|
|
@@ -396,13 +399,13 @@ function createRateLimiter(options = {}) {
|
|
|
396
399
|
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
397
400
|
keyGenerator = (req) => {
|
|
398
401
|
const ip = req.ip ?? req.socket?.remoteAddress;
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return
|
|
402
|
+
if (ip) return ip;
|
|
403
|
+
const ua = req.headers["user-agent"] ?? "";
|
|
404
|
+
const lang = req.headers["accept-language"] ?? "";
|
|
405
|
+
const fp = `${ua}|${lang}`;
|
|
406
|
+
let hash = 0;
|
|
407
|
+
for (let i = 0; i < fp.length; i++) hash = (hash << 5) - hash + fp.charCodeAt(i) | 0;
|
|
408
|
+
return `unknown:${hash.toString(36)}`;
|
|
406
409
|
},
|
|
407
410
|
skip,
|
|
408
411
|
store: externalStore
|
|
@@ -614,6 +617,80 @@ var SanitizationError = class extends ArcisError {
|
|
|
614
617
|
}
|
|
615
618
|
};
|
|
616
619
|
|
|
620
|
+
// src/middleware/telemetry.ts
|
|
621
|
+
var THREAT_TO_VECTOR = {
|
|
622
|
+
xss: "xss",
|
|
623
|
+
sql_injection: "sql",
|
|
624
|
+
nosql_injection: "nosql",
|
|
625
|
+
path_traversal: "path",
|
|
626
|
+
command_injection: "command",
|
|
627
|
+
prototype_pollution: "prototype",
|
|
628
|
+
header_injection: "header",
|
|
629
|
+
ssti: "ssti",
|
|
630
|
+
xxe: "xxe"
|
|
631
|
+
};
|
|
632
|
+
function createTelemetryEmitter(client) {
|
|
633
|
+
return (req, res, next) => {
|
|
634
|
+
const start = performance.now();
|
|
635
|
+
res.on("finish", () => {
|
|
636
|
+
try {
|
|
637
|
+
const event = buildEvent(req, res.statusCode, performance.now() - start);
|
|
638
|
+
client.record(event);
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
next();
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function tapSanitizerThreats(handler) {
|
|
646
|
+
return (req, res, next) => {
|
|
647
|
+
handler(req, res, (err) => {
|
|
648
|
+
if (err instanceof SecurityThreatError) {
|
|
649
|
+
const vector = THREAT_TO_VECTOR[err.threatType] ?? err.threatType;
|
|
650
|
+
req.__arcis = {
|
|
651
|
+
vector,
|
|
652
|
+
rule: `${vector}/match`,
|
|
653
|
+
severity: "high",
|
|
654
|
+
matchedPattern: err.pattern,
|
|
655
|
+
reason: err.message,
|
|
656
|
+
decision: "deny"
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
next(err);
|
|
660
|
+
});
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function buildEvent(req, status, latencyMs) {
|
|
664
|
+
const marker = req.__arcis;
|
|
665
|
+
const decision = marker?.decision ?? inferDecision(status);
|
|
666
|
+
return {
|
|
667
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
668
|
+
ip: extractIp(req),
|
|
669
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
670
|
+
path: req.path ?? req.url ?? "/",
|
|
671
|
+
decision,
|
|
672
|
+
vector: marker?.vector ?? (status === 429 ? "rate-limit" : void 0),
|
|
673
|
+
rule: marker?.rule ?? (status === 429 ? "rate-limit/exceeded" : void 0),
|
|
674
|
+
severity: marker?.severity ?? (status === 429 ? "medium" : void 0),
|
|
675
|
+
userAgent: typeof req.headers?.["user-agent"] === "string" ? req.headers["user-agent"] : "",
|
|
676
|
+
reason: marker?.reason,
|
|
677
|
+
status,
|
|
678
|
+
matchedPattern: marker?.matchedPattern,
|
|
679
|
+
latencyMs: Math.max(0, latencyMs)
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function inferDecision(status) {
|
|
683
|
+
if (status === 429) return "deny";
|
|
684
|
+
if (status === 400) return "deny";
|
|
685
|
+
if (status === 403) return "deny";
|
|
686
|
+
return "allow";
|
|
687
|
+
}
|
|
688
|
+
function extractIp(req) {
|
|
689
|
+
if (typeof req.ip === "string" && req.ip.length > 0) return req.ip;
|
|
690
|
+
const remote = req.socket?.remoteAddress;
|
|
691
|
+
return typeof remote === "string" ? remote : "0.0.0.0";
|
|
692
|
+
}
|
|
693
|
+
|
|
617
694
|
// src/sanitizers/utils.ts
|
|
618
695
|
function encodeHtmlEntities(str) {
|
|
619
696
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -971,17 +1048,19 @@ var SSTI_REMOVE_PATTERNS = [
|
|
|
971
1048
|
/** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
|
|
972
1049
|
/\{\{.*?\}\}/g,
|
|
973
1050
|
/**
|
|
974
|
-
* Freemarker / Spring EL: ${...} —
|
|
975
|
-
*
|
|
1051
|
+
* Freemarker / Spring EL: ${...} — strip when expression contains operators,
|
|
1052
|
+
* method calls, or Python dunder patterns (sandbox escape).
|
|
976
1053
|
* Bare ${name} and ${user.name} are left intact (JS template literal syntax).
|
|
977
1054
|
*/
|
|
1055
|
+
/\$\{[^}]*__\w+__[^}]*\}/g,
|
|
978
1056
|
/\$\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
979
1057
|
/** ERB / EJS: <%= ... %> */
|
|
980
1058
|
/<%[=\-]?.*?%>/gs,
|
|
981
1059
|
/**
|
|
982
|
-
* Pug / Jade: #{...} — same narrowing as ${ above.
|
|
1060
|
+
* Pug / Jade: #{...} — same narrowing as ${ above, plus dunder detection.
|
|
983
1061
|
* #{name} output expressions are left intact.
|
|
984
1062
|
*/
|
|
1063
|
+
/#\{[^}]*__\w+__[^}]*\}/g,
|
|
985
1064
|
/#\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
986
1065
|
/** Python dunder sandbox escape — always strip */
|
|
987
1066
|
/__(?:class|mro|subclasses|globals|builtins|import)__/gi
|
|
@@ -1030,6 +1109,8 @@ function detectSsti(input) {
|
|
|
1030
1109
|
}
|
|
1031
1110
|
|
|
1032
1111
|
// src/sanitizers/xxe.ts
|
|
1112
|
+
var MAX_XXE_INPUT_BYTES = 1e6;
|
|
1113
|
+
var MAX_ENTITY_REFERENCES = 64;
|
|
1033
1114
|
var XXE_DETECT_PATTERNS = [
|
|
1034
1115
|
/** DOCTYPE declaration */
|
|
1035
1116
|
/<!DOCTYPE\b/gi,
|
|
@@ -1059,6 +1140,19 @@ function sanitizeXxe(input, collectThreats = false) {
|
|
|
1059
1140
|
const threats = [];
|
|
1060
1141
|
let value = input;
|
|
1061
1142
|
let wasSanitized = false;
|
|
1143
|
+
if (value.length > MAX_XXE_INPUT_BYTES) {
|
|
1144
|
+
if (collectThreats) {
|
|
1145
|
+
threats.push({ type: "xxe", pattern: "oversize_input", original: `length=${value.length}` });
|
|
1146
|
+
}
|
|
1147
|
+
return collectThreats ? { value: "", wasSanitized: true, threats } : "";
|
|
1148
|
+
}
|
|
1149
|
+
const entityRefs = value.match(/&\w+;/g);
|
|
1150
|
+
if (entityRefs && entityRefs.length > MAX_ENTITY_REFERENCES) {
|
|
1151
|
+
if (collectThreats) {
|
|
1152
|
+
threats.push({ type: "xxe", pattern: "entity_expansion", original: `count=${entityRefs.length}` });
|
|
1153
|
+
}
|
|
1154
|
+
return collectThreats ? { value: "", wasSanitized: true, threats } : "";
|
|
1155
|
+
}
|
|
1062
1156
|
for (const pattern of XXE_REMOVE_PATTERNS) {
|
|
1063
1157
|
pattern.lastIndex = 0;
|
|
1064
1158
|
if (pattern.test(value)) {
|
|
@@ -1096,12 +1190,10 @@ function detectXxe(input) {
|
|
|
1096
1190
|
}
|
|
1097
1191
|
|
|
1098
1192
|
// src/sanitizers/jsonp.ts
|
|
1099
|
-
var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.
|
|
1193
|
+
var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/;
|
|
1100
1194
|
var DANGEROUS_CALLBACK_PATTERNS = [
|
|
1101
|
-
|
|
1195
|
+
/\.\./
|
|
1102
1196
|
// prototype chain traversal
|
|
1103
|
-
/\[\s*\]/
|
|
1104
|
-
// empty bracket access
|
|
1105
1197
|
];
|
|
1106
1198
|
function sanitizeJsonpCallback(callback, maxLength = 128) {
|
|
1107
1199
|
if (typeof callback !== "string" || callback.length === 0) {
|
|
@@ -1186,7 +1278,7 @@ function detectHeaderInjection(input) {
|
|
|
1186
1278
|
|
|
1187
1279
|
// src/sanitizers/pii.ts
|
|
1188
1280
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+/g;
|
|
1189
|
-
var PHONE_RE = /(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g;
|
|
1281
|
+
var PHONE_RE = /(?<!\d)(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?!\d)/g;
|
|
1190
1282
|
var CREDIT_CARD_RE = /\b(?:\d[ -]*?){13,19}\b/g;
|
|
1191
1283
|
var SSN_RE = /\b\d{3}[-\s]\d{2}[-\s]\d{4}\b/g;
|
|
1192
1284
|
var IPV4_RE = /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g;
|
|
@@ -2310,10 +2402,192 @@ function createRedactor(sensitiveKeys = []) {
|
|
|
2310
2402
|
}
|
|
2311
2403
|
var safeLog = createSafeLogger;
|
|
2312
2404
|
|
|
2405
|
+
// src/telemetry/client.ts
|
|
2406
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
2407
|
+
var MAX_BATCH_SIZE = 500;
|
|
2408
|
+
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
2409
|
+
var MIN_FLUSH_INTERVAL_MS = 500;
|
|
2410
|
+
var FLUSH_TIMEOUT_MS = 1e4;
|
|
2411
|
+
var TelemetryClient = class {
|
|
2412
|
+
constructor(options) {
|
|
2413
|
+
this.queue = [];
|
|
2414
|
+
this.flushing = false;
|
|
2415
|
+
this.closed = false;
|
|
2416
|
+
if (!options.endpoint || typeof options.endpoint !== "string") {
|
|
2417
|
+
throw new TypeError("TelemetryClient: `endpoint` is required");
|
|
2418
|
+
}
|
|
2419
|
+
this.endpoint = options.endpoint;
|
|
2420
|
+
this.apiKey = options.apiKey;
|
|
2421
|
+
this.workspaceId = options.workspaceId;
|
|
2422
|
+
this.batchSize = clamp(
|
|
2423
|
+
options.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
2424
|
+
1,
|
|
2425
|
+
MAX_BATCH_SIZE
|
|
2426
|
+
);
|
|
2427
|
+
this.flushIntervalMs = Math.max(
|
|
2428
|
+
options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
2429
|
+
MIN_FLUSH_INTERVAL_MS
|
|
2430
|
+
);
|
|
2431
|
+
this.onError = options.onError ?? (() => {
|
|
2432
|
+
});
|
|
2433
|
+
this.startTimer();
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Enqueue an event. Fast, synchronous, cannot throw.
|
|
2437
|
+
* Triggers a flush if the queue has reached `batchSize`.
|
|
2438
|
+
*/
|
|
2439
|
+
record(event) {
|
|
2440
|
+
if (this.closed) return;
|
|
2441
|
+
this.queue.push(event);
|
|
2442
|
+
if (this.queue.length >= this.batchSize) {
|
|
2443
|
+
void this.flush();
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Manually flush the queue. Pulls up to `batchSize` events into a batch and
|
|
2448
|
+
* POSTs them. Returns a resolved promise on success OR on handled failure.
|
|
2449
|
+
* Never throws.
|
|
2450
|
+
*/
|
|
2451
|
+
async flush() {
|
|
2452
|
+
if (this.flushing) return;
|
|
2453
|
+
if (this.queue.length === 0) return;
|
|
2454
|
+
this.flushing = true;
|
|
2455
|
+
try {
|
|
2456
|
+
const batch = this.queue.splice(0, this.batchSize);
|
|
2457
|
+
await this.send(batch);
|
|
2458
|
+
} catch (err) {
|
|
2459
|
+
this.safeNotify(err);
|
|
2460
|
+
} finally {
|
|
2461
|
+
this.flushing = false;
|
|
2462
|
+
}
|
|
2463
|
+
if (!this.closed && this.queue.length > 0) {
|
|
2464
|
+
void this.flush();
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Shut down: stop the interval timer and attempt one final flush.
|
|
2469
|
+
* Safe to call multiple times.
|
|
2470
|
+
*/
|
|
2471
|
+
async close() {
|
|
2472
|
+
if (this.closed) return;
|
|
2473
|
+
this.closed = true;
|
|
2474
|
+
if (this.timer !== void 0) {
|
|
2475
|
+
clearInterval(this.timer);
|
|
2476
|
+
this.timer = void 0;
|
|
2477
|
+
}
|
|
2478
|
+
if (this.signalHandler !== void 0) {
|
|
2479
|
+
process.off("SIGTERM", this.signalHandler);
|
|
2480
|
+
process.off("SIGINT", this.signalHandler);
|
|
2481
|
+
this.signalHandler = void 0;
|
|
2482
|
+
}
|
|
2483
|
+
try {
|
|
2484
|
+
await this.flush();
|
|
2485
|
+
} catch {
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Register `SIGTERM` / `SIGINT` handlers that call `close()` to drain
|
|
2490
|
+
* the queue on graceful shutdown. Opt-in — libraries should not silently
|
|
2491
|
+
* attach global signal handlers. Safe to call multiple times.
|
|
2492
|
+
*/
|
|
2493
|
+
installShutdownHooks() {
|
|
2494
|
+
if (this.signalHandler !== void 0 || this.closed) return;
|
|
2495
|
+
const handler = () => {
|
|
2496
|
+
void this.close();
|
|
2497
|
+
};
|
|
2498
|
+
this.signalHandler = handler;
|
|
2499
|
+
process.once("SIGTERM", handler);
|
|
2500
|
+
process.once("SIGINT", handler);
|
|
2501
|
+
}
|
|
2502
|
+
/** Count of events currently waiting to be sent. Useful for tests. */
|
|
2503
|
+
get pendingCount() {
|
|
2504
|
+
return this.queue.length;
|
|
2505
|
+
}
|
|
2506
|
+
// ── internals ─────────────────────────────────────────────────────────
|
|
2507
|
+
async send(batch) {
|
|
2508
|
+
const headers = {
|
|
2509
|
+
"content-type": "application/json"
|
|
2510
|
+
};
|
|
2511
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
2512
|
+
if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
|
|
2513
|
+
const controller = new AbortController();
|
|
2514
|
+
const abortTimer = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
|
|
2515
|
+
try {
|
|
2516
|
+
const res = await fetch(this.endpoint, {
|
|
2517
|
+
method: "POST",
|
|
2518
|
+
headers,
|
|
2519
|
+
body: JSON.stringify({ events: batch }),
|
|
2520
|
+
signal: controller.signal
|
|
2521
|
+
});
|
|
2522
|
+
if (!res.ok) {
|
|
2523
|
+
const text = await safeReadBody(res);
|
|
2524
|
+
throw new TelemetryHttpError(res.status, text);
|
|
2525
|
+
}
|
|
2526
|
+
} finally {
|
|
2527
|
+
clearTimeout(abortTimer);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
startTimer() {
|
|
2531
|
+
this.timer = setInterval(() => {
|
|
2532
|
+
void this.flush();
|
|
2533
|
+
}, this.flushIntervalMs);
|
|
2534
|
+
this.timer.unref?.();
|
|
2535
|
+
}
|
|
2536
|
+
safeNotify(err) {
|
|
2537
|
+
try {
|
|
2538
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
2539
|
+
} catch {
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
var TelemetryHttpError = class extends Error {
|
|
2544
|
+
constructor(status, responseBody) {
|
|
2545
|
+
super(`Telemetry ingest returned HTTP ${status}`);
|
|
2546
|
+
this.status = status;
|
|
2547
|
+
this.responseBody = responseBody;
|
|
2548
|
+
this.name = "TelemetryHttpError";
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
function clamp(value, min, max) {
|
|
2552
|
+
if (!Number.isFinite(value)) return min;
|
|
2553
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
2554
|
+
}
|
|
2555
|
+
async function safeReadBody(res) {
|
|
2556
|
+
try {
|
|
2557
|
+
const text = await res.text();
|
|
2558
|
+
return text.slice(0, 500);
|
|
2559
|
+
} catch {
|
|
2560
|
+
return "";
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2313
2564
|
// src/middleware/main.ts
|
|
2565
|
+
function buildTelemetryFromEnv() {
|
|
2566
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
2567
|
+
const endpoint = env?.ARCIS_ENDPOINT;
|
|
2568
|
+
if (!endpoint) return void 0;
|
|
2569
|
+
const opts = { endpoint };
|
|
2570
|
+
if (env?.ARCIS_WORKSPACE_ID) opts.workspaceId = env.ARCIS_WORKSPACE_ID;
|
|
2571
|
+
if (env?.ARCIS_KEY) opts.apiKey = env.ARCIS_KEY;
|
|
2572
|
+
const batch = env?.ARCIS_BATCH_SIZE ? parseInt(env.ARCIS_BATCH_SIZE, 10) : NaN;
|
|
2573
|
+
if (!Number.isNaN(batch)) opts.batchSize = batch;
|
|
2574
|
+
const flush = env?.ARCIS_FLUSH_INTERVAL_MS ? parseInt(env.ARCIS_FLUSH_INTERVAL_MS, 10) : NaN;
|
|
2575
|
+
if (!Number.isNaN(flush)) opts.flushIntervalMs = flush;
|
|
2576
|
+
return opts;
|
|
2577
|
+
}
|
|
2314
2578
|
function arcis(options = {}) {
|
|
2315
2579
|
const middlewares = [];
|
|
2316
2580
|
const cleanupFns = [];
|
|
2581
|
+
let telemetryClient;
|
|
2582
|
+
const telemetryOpts = options.telemetry?.endpoint ? options.telemetry : buildTelemetryFromEnv();
|
|
2583
|
+
if (telemetryOpts) {
|
|
2584
|
+
const client = new TelemetryClient(telemetryOpts);
|
|
2585
|
+
telemetryClient = client;
|
|
2586
|
+
middlewares.push(createTelemetryEmitter(client));
|
|
2587
|
+
cleanupFns.push(() => {
|
|
2588
|
+
void client.close();
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2317
2591
|
if (options.headers !== false) {
|
|
2318
2592
|
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
2319
2593
|
middlewares.push(createHeaders(headerOpts));
|
|
@@ -2326,7 +2600,8 @@ function arcis(options = {}) {
|
|
|
2326
2600
|
}
|
|
2327
2601
|
if (options.sanitize !== false) {
|
|
2328
2602
|
const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
|
|
2329
|
-
|
|
2603
|
+
const sanitizer = createSanitizer(sanitizeOpts);
|
|
2604
|
+
middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
|
|
2330
2605
|
}
|
|
2331
2606
|
const result = middlewares;
|
|
2332
2607
|
result.close = () => {
|
|
@@ -2653,6 +2928,16 @@ function secureCookieDefaults(options = {}) {
|
|
|
2653
2928
|
sameSite: options.sameSite ?? "Lax",
|
|
2654
2929
|
path: options.path
|
|
2655
2930
|
};
|
|
2931
|
+
if (resolved.sameSite === "None" && resolved.secure === false) {
|
|
2932
|
+
throw new Error(
|
|
2933
|
+
"[arcis] secureCookieDefaults: sameSite=None requires secure=true (modern browsers reject the cookie otherwise)"
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
if (resolved.httpOnly === false && resolved.secure === false && isProduction) {
|
|
2937
|
+
console.warn(
|
|
2938
|
+
"[arcis] secureCookieDefaults: httpOnly and secure are both disabled in production \u2014 cookies will be readable by JS and sent over HTTP"
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2656
2941
|
return (_req, res, next) => {
|
|
2657
2942
|
const originalSetHeader = res.setHeader.bind(res);
|
|
2658
2943
|
res.setHeader = function patchedSetHeader(name, value) {
|
|
@@ -2785,7 +3070,16 @@ function detectBehavioralSignals(req) {
|
|
|
2785
3070
|
}
|
|
2786
3071
|
function detectBot(req) {
|
|
2787
3072
|
const rawUa = req.headers["user-agent"] ?? "";
|
|
2788
|
-
|
|
3073
|
+
if (rawUa.length > 2048) {
|
|
3074
|
+
return {
|
|
3075
|
+
isBot: true,
|
|
3076
|
+
category: "UNKNOWN",
|
|
3077
|
+
name: null,
|
|
3078
|
+
confidence: 0.9,
|
|
3079
|
+
signals: detectBehavioralSignals(req)
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
const ua = rawUa;
|
|
2789
3083
|
const signals = detectBehavioralSignals(req);
|
|
2790
3084
|
if (!ua) {
|
|
2791
3085
|
return {
|
|
@@ -2949,6 +3243,10 @@ function csrfProtection(options = {}) {
|
|
|
2949
3243
|
if (!validateCsrfToken(cookieToken, requestToken)) {
|
|
2950
3244
|
return onError(req, res, next);
|
|
2951
3245
|
}
|
|
3246
|
+
if (options.rotateOnUse) {
|
|
3247
|
+
const freshToken = generateCsrfToken(tokenLength);
|
|
3248
|
+
setCsrfCookie(res, cookieName, freshToken, cookieOpts);
|
|
3249
|
+
}
|
|
2952
3250
|
next();
|
|
2953
3251
|
};
|
|
2954
3252
|
}
|
|
@@ -2985,7 +3283,7 @@ var createCsrf = csrfProtection;
|
|
|
2985
3283
|
|
|
2986
3284
|
// src/middleware/hpp.ts
|
|
2987
3285
|
function hpp(options = {}) {
|
|
2988
|
-
const whitelist = new Set(options.whitelist ?? []);
|
|
3286
|
+
const whitelist = new Set((options.whitelist ?? []).map((k) => k.toLowerCase()));
|
|
2989
3287
|
const checkQuery = options.checkQuery ?? true;
|
|
2990
3288
|
const checkBody = options.checkBody ?? true;
|
|
2991
3289
|
return (req, _res, next) => {
|
|
@@ -2995,7 +3293,7 @@ function hpp(options = {}) {
|
|
|
2995
3293
|
for (const [key, value] of Object.entries(req.query)) {
|
|
2996
3294
|
if (Array.isArray(value)) {
|
|
2997
3295
|
const strings = value.filter((v) => typeof v === "string");
|
|
2998
|
-
if (whitelist.has(key)) {
|
|
3296
|
+
if (whitelist.has(key.toLowerCase())) {
|
|
2999
3297
|
clean[key] = strings;
|
|
3000
3298
|
} else {
|
|
3001
3299
|
polluted[key] = strings;
|
|
@@ -3013,7 +3311,7 @@ function hpp(options = {}) {
|
|
|
3013
3311
|
const clean = {};
|
|
3014
3312
|
for (const [key, value] of Object.entries(req.body)) {
|
|
3015
3313
|
if (Array.isArray(value)) {
|
|
3016
|
-
if (whitelist.has(key)) {
|
|
3314
|
+
if (whitelist.has(key.toLowerCase())) {
|
|
3017
3315
|
clean[key] = value;
|
|
3018
3316
|
} else {
|
|
3019
3317
|
polluted[key] = value;
|
|
@@ -3031,6 +3329,89 @@ function hpp(options = {}) {
|
|
|
3031
3329
|
}
|
|
3032
3330
|
var createHpp = hpp;
|
|
3033
3331
|
|
|
3332
|
+
// src/middleware/signup-protection.ts
|
|
3333
|
+
function checkSignup(req, options = {}) {
|
|
3334
|
+
const {
|
|
3335
|
+
emailField = "email",
|
|
3336
|
+
checkEmail = true,
|
|
3337
|
+
blockDisposable = true,
|
|
3338
|
+
checkBot = true,
|
|
3339
|
+
allowedBotCategories = [],
|
|
3340
|
+
allowedEmailDomains = [],
|
|
3341
|
+
blockedEmailDomains = []
|
|
3342
|
+
} = options;
|
|
3343
|
+
if (checkBot) {
|
|
3344
|
+
const bot = detectBot(req);
|
|
3345
|
+
if (bot.isBot && !allowedBotCategories.includes(bot.category)) {
|
|
3346
|
+
return {
|
|
3347
|
+
allowed: false,
|
|
3348
|
+
reason: "bot",
|
|
3349
|
+
details: { category: bot.category, name: bot.name, confidence: bot.confidence }
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
if (checkEmail) {
|
|
3354
|
+
const email = req.body?.[emailField];
|
|
3355
|
+
if (typeof email !== "string" || email.length === 0) {
|
|
3356
|
+
return { allowed: false, reason: "missing_email" };
|
|
3357
|
+
}
|
|
3358
|
+
const result = validateEmail(email, {
|
|
3359
|
+
checkDisposable: blockDisposable,
|
|
3360
|
+
allowedDomains: allowedEmailDomains,
|
|
3361
|
+
blockedDomains: blockedEmailDomains
|
|
3362
|
+
});
|
|
3363
|
+
if (!result.valid) {
|
|
3364
|
+
const reason = result.reason === "disposable" ? "disposable_email" : "invalid_email";
|
|
3365
|
+
return { allowed: false, reason, details: { emailReason: result.reason } };
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
return { allowed: true, reason: "ok" };
|
|
3369
|
+
}
|
|
3370
|
+
function signupProtection(options = {}) {
|
|
3371
|
+
const rateLimitCfg = options.rateLimit;
|
|
3372
|
+
const limiter = rateLimitCfg === false ? null : createRateLimiter({
|
|
3373
|
+
max: rateLimitCfg?.max ?? 5,
|
|
3374
|
+
windowMs: rateLimitCfg?.windowMs ?? 6e4,
|
|
3375
|
+
message: "Too many signup attempts"
|
|
3376
|
+
});
|
|
3377
|
+
const handler = (req, res, next) => {
|
|
3378
|
+
const result = checkSignup(req, options);
|
|
3379
|
+
if (!result.allowed) {
|
|
3380
|
+
options.onBlocked?.(req, result);
|
|
3381
|
+
const status = result.reason === "bot" ? 403 : 400;
|
|
3382
|
+
res.status(status).json({ error: "signup_blocked", reason: result.reason });
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
if (limiter) {
|
|
3386
|
+
let rateLimited = false;
|
|
3387
|
+
const rateLimitNext = (err) => {
|
|
3388
|
+
if (err) return next(err);
|
|
3389
|
+
if (!rateLimited) next();
|
|
3390
|
+
};
|
|
3391
|
+
const patchedRes = new Proxy(res, {
|
|
3392
|
+
get(target, prop) {
|
|
3393
|
+
if (prop === "status") {
|
|
3394
|
+
return (code) => {
|
|
3395
|
+
if (code === 429) {
|
|
3396
|
+
rateLimited = true;
|
|
3397
|
+
options.onBlocked?.(req, { allowed: false, reason: "rate_limited" });
|
|
3398
|
+
}
|
|
3399
|
+
return target.status.call(target, code);
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
return Reflect.get(target, prop);
|
|
3403
|
+
}
|
|
3404
|
+
});
|
|
3405
|
+
limiter(req, patchedRes, rateLimitNext);
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
next();
|
|
3409
|
+
};
|
|
3410
|
+
const middleware = handler;
|
|
3411
|
+
middleware.close = () => limiter?.close();
|
|
3412
|
+
return middleware;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3034
3415
|
// src/utils/ip.ts
|
|
3035
3416
|
var PLATFORM_HEADERS = {
|
|
3036
3417
|
cloudflare: "cf-connecting-ip",
|
|
@@ -3314,6 +3695,6 @@ function createRedisStore(options) {
|
|
|
3314
3695
|
return new RedisStore(options);
|
|
3315
3696
|
}
|
|
3316
3697
|
|
|
3317
|
-
export { ArcisError, ValidationError as ArcisValidationError, BLOCKED, ERRORS, HEADERS, INPUT, InputTooLargeError, MemoryStore, RATE_LIMIT, REDACTION, RateLimitError, RedisStore, SanitizationError, SecurityThreatError, VALIDATION, arcis, arcisWithMethods as arcisFunction, botProtection, createCors, createCsrf, createErrorHandler, createHeaders, createHpp, createRateLimiter, createRedactor, createRedisStore, createSafeLogger, createSanitizer, createSecureCookies, createSlidingWindowLimiter, createTokenBucketLimiter, createValidator, csrfProtection, main_default as default, detectBot, detectClientIp, detectCommandInjection, detectHeaderInjection, detectJsonpInjection, detectNoSqlInjection, detectPathTraversal, detectPii, detectPrototypePollution, detectSql, detectSsti, detectXss, detectXxe, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, fingerprint, formatDuration, generateCsrfToken, hpp, isDangerousExtension, isDangerousNoSqlKey, isDangerousProtoKey, isPrivateIp, isRedirectSafe, isUrlSafe, isValidEmailSyntax, parseDuration, rateLimit, redactObjectPii, redactPii, safeCors, safeLog, sanitizeCommand, sanitizeFilename, sanitizeHeaderValue, sanitizeHeaders, sanitizeJsonpCallback, sanitizeObject, sanitizePath, sanitizeSql, sanitizeSsti, sanitizeString, sanitizeXss, sanitizeXxe, scanObjectPii, scanPii, secureCookieDefaults, securityHeaders, validate, validateCsrfToken, validateEmail, validateFile, validateRedirect, validateUrl, verifyEmailMx };
|
|
3698
|
+
export { ArcisError, ValidationError as ArcisValidationError, BLOCKED, ERRORS, HEADERS, INPUT, InputTooLargeError, MemoryStore, RATE_LIMIT, REDACTION, RateLimitError, RedisStore, SanitizationError, SecurityThreatError, TelemetryClient, TelemetryHttpError, VALIDATION, arcis, arcisWithMethods as arcisFunction, botProtection, checkSignup, createCors, createCsrf, createErrorHandler, createHeaders, createHpp, createRateLimiter, createRedactor, createRedisStore, createSafeLogger, createSanitizer, createSecureCookies, createSlidingWindowLimiter, createTokenBucketLimiter, createValidator, csrfProtection, main_default as default, detectBot, detectClientIp, detectCommandInjection, detectHeaderInjection, detectJsonpInjection, detectNoSqlInjection, detectPathTraversal, detectPii, detectPrototypePollution, detectSql, detectSsti, detectXss, detectXxe, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, fingerprint, formatDuration, generateCsrfToken, hpp, isDangerousExtension, isDangerousNoSqlKey, isDangerousProtoKey, isPrivateIp, isRedirectSafe, isUrlSafe, isValidEmailSyntax, parseDuration, rateLimit, redactObjectPii, redactPii, safeCors, safeLog, sanitizeCommand, sanitizeFilename, sanitizeHeaderValue, sanitizeHeaders, sanitizeJsonpCallback, sanitizeObject, sanitizePath, sanitizeSql, sanitizeSsti, sanitizeString, sanitizeXss, sanitizeXxe, scanObjectPii, scanPii, secureCookieDefaults, securityHeaders, signupProtection, validate, validateCsrfToken, validateEmail, validateFile, validateRedirect, validateUrl, verifyEmailMx };
|
|
3318
3699
|
//# sourceMappingURL=index.mjs.map
|
|
3319
3700
|
//# sourceMappingURL=index.mjs.map
|