@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.
Files changed (51) hide show
  1. package/dist/core/constants.d.ts +1 -1
  2. package/dist/core/constants.d.ts.map +1 -1
  3. package/dist/core/index.js.map +1 -1
  4. package/dist/core/index.mjs.map +1 -1
  5. package/dist/core/types.d.ts +6 -0
  6. package/dist/core/types.d.ts.map +1 -1
  7. package/dist/index.d.ts +4 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +405 -20
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +402 -21
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/logging/index.js.map +1 -1
  14. package/dist/logging/index.mjs.map +1 -1
  15. package/dist/middleware/bot-detection.d.ts.map +1 -1
  16. package/dist/middleware/cookies.d.ts.map +1 -1
  17. package/dist/middleware/csrf.d.ts +10 -0
  18. package/dist/middleware/csrf.d.ts.map +1 -1
  19. package/dist/middleware/hpp.d.ts.map +1 -1
  20. package/dist/middleware/index.d.ts +2 -0
  21. package/dist/middleware/index.d.ts.map +1 -1
  22. package/dist/middleware/index.js +609 -9
  23. package/dist/middleware/index.js.map +1 -1
  24. package/dist/middleware/index.mjs +608 -10
  25. package/dist/middleware/index.mjs.map +1 -1
  26. package/dist/middleware/main.d.ts.map +1 -1
  27. package/dist/middleware/rate-limit.d.ts.map +1 -1
  28. package/dist/middleware/signup-protection.d.ts +65 -0
  29. package/dist/middleware/signup-protection.d.ts.map +1 -0
  30. package/dist/middleware/telemetry.d.ts +36 -0
  31. package/dist/middleware/telemetry.d.ts.map +1 -0
  32. package/dist/sanitizers/index.js +26 -8
  33. package/dist/sanitizers/index.js.map +1 -1
  34. package/dist/sanitizers/index.mjs +26 -8
  35. package/dist/sanitizers/index.mjs.map +1 -1
  36. package/dist/sanitizers/pii.d.ts.map +1 -1
  37. package/dist/sanitizers/ssti.d.ts.map +1 -1
  38. package/dist/sanitizers/xxe.d.ts.map +1 -1
  39. package/dist/stores/index.js.map +1 -1
  40. package/dist/stores/index.mjs.map +1 -1
  41. package/dist/telemetry/client.d.ts +60 -0
  42. package/dist/telemetry/client.d.ts.map +1 -0
  43. package/dist/telemetry/index.d.ts +3 -0
  44. package/dist/telemetry/index.d.ts.map +1 -0
  45. package/dist/telemetry/types.d.ts +59 -0
  46. package/dist/telemetry/types.d.ts.map +1 -0
  47. package/dist/validation/index.js +3 -0
  48. package/dist/validation/index.js.map +1 -1
  49. package/dist/validation/index.mjs +3 -0
  50. package/dist/validation/index.mjs.map +1 -1
  51. package/package.json +7 -1
@@ -45,6 +45,9 @@ var XSS_REMOVE_PATTERNS = [
45
45
  /<script[^>]*>[\s\S]*?<\/script>/gi,
46
46
  /** Standalone/unclosed script tags */
47
47
  /<script[^>]*>/gi,
48
+ /** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
49
+ /<style[^>]*>[\s\S]*?<\/style>/gi,
50
+ /<style[^>]*/gi,
48
51
  /** iframe — full block and partial/unclosed */
49
52
  /<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
50
53
  /<iframe[^>]*/gi,
@@ -357,13 +360,13 @@ function createRateLimiter(options = {}) {
357
360
  statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
358
361
  keyGenerator = (req) => {
359
362
  const ip = req.ip ?? req.socket?.remoteAddress;
360
- if (!ip) {
361
- console.warn(
362
- "[arcis] Rate limiter: cannot resolve client IP. All unresolvable clients share one counter. Set Express trust proxy if behind a reverse proxy."
363
- );
364
- return "unknown";
365
- }
366
- return ip;
363
+ if (ip) return ip;
364
+ const ua = req.headers["user-agent"] ?? "";
365
+ const lang = req.headers["accept-language"] ?? "";
366
+ const fp = `${ua}|${lang}`;
367
+ let hash = 0;
368
+ for (let i = 0; i < fp.length; i++) hash = (hash << 5) - hash + fp.charCodeAt(i) | 0;
369
+ return `unknown:${hash.toString(36)}`;
367
370
  },
368
371
  skip,
369
372
  store: externalStore
@@ -555,6 +558,80 @@ var SecurityThreatError = class extends ArcisError {
555
558
  }
556
559
  };
557
560
 
561
+ // src/middleware/telemetry.ts
562
+ var THREAT_TO_VECTOR = {
563
+ xss: "xss",
564
+ sql_injection: "sql",
565
+ nosql_injection: "nosql",
566
+ path_traversal: "path",
567
+ command_injection: "command",
568
+ prototype_pollution: "prototype",
569
+ header_injection: "header",
570
+ ssti: "ssti",
571
+ xxe: "xxe"
572
+ };
573
+ function createTelemetryEmitter(client) {
574
+ return (req, res, next) => {
575
+ const start = performance.now();
576
+ res.on("finish", () => {
577
+ try {
578
+ const event = buildEvent(req, res.statusCode, performance.now() - start);
579
+ client.record(event);
580
+ } catch {
581
+ }
582
+ });
583
+ next();
584
+ };
585
+ }
586
+ function tapSanitizerThreats(handler) {
587
+ return (req, res, next) => {
588
+ handler(req, res, (err) => {
589
+ if (err instanceof SecurityThreatError) {
590
+ const vector = THREAT_TO_VECTOR[err.threatType] ?? err.threatType;
591
+ req.__arcis = {
592
+ vector,
593
+ rule: `${vector}/match`,
594
+ severity: "high",
595
+ matchedPattern: err.pattern,
596
+ reason: err.message,
597
+ decision: "deny"
598
+ };
599
+ }
600
+ next(err);
601
+ });
602
+ };
603
+ }
604
+ function buildEvent(req, status, latencyMs) {
605
+ const marker = req.__arcis;
606
+ const decision = marker?.decision ?? inferDecision(status);
607
+ return {
608
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
609
+ ip: extractIp(req),
610
+ method: (req.method ?? "GET").toUpperCase(),
611
+ path: req.path ?? req.url ?? "/",
612
+ decision,
613
+ vector: marker?.vector ?? (status === 429 ? "rate-limit" : void 0),
614
+ rule: marker?.rule ?? (status === 429 ? "rate-limit/exceeded" : void 0),
615
+ severity: marker?.severity ?? (status === 429 ? "medium" : void 0),
616
+ userAgent: typeof req.headers?.["user-agent"] === "string" ? req.headers["user-agent"] : "",
617
+ reason: marker?.reason,
618
+ status,
619
+ matchedPattern: marker?.matchedPattern,
620
+ latencyMs: Math.max(0, latencyMs)
621
+ };
622
+ }
623
+ function inferDecision(status) {
624
+ if (status === 429) return "deny";
625
+ if (status === 400) return "deny";
626
+ if (status === 403) return "deny";
627
+ return "allow";
628
+ }
629
+ function extractIp(req) {
630
+ if (typeof req.ip === "string" && req.ip.length > 0) return req.ip;
631
+ const remote = req.socket?.remoteAddress;
632
+ return typeof remote === "string" ? remote : "0.0.0.0";
633
+ }
634
+
558
635
  // src/sanitizers/utils.ts
559
636
  function encodeHtmlEntities(str) {
560
637
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
@@ -989,6 +1066,238 @@ function validateField(field, value, rules) {
989
1066
  // Audio/Video
990
1067
  "audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])]});
991
1068
 
1069
+ // src/validation/email.ts
1070
+ var MAX_EMAIL_LENGTH = 254;
1071
+ var MAX_LOCAL_LENGTH = 64;
1072
+ var MAX_DOMAIN_LENGTH = 255;
1073
+ var EMAIL_SYNTAX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
1074
+ var FREE_PROVIDERS = /* @__PURE__ */ new Set([
1075
+ "gmail.com",
1076
+ "yahoo.com",
1077
+ "hotmail.com",
1078
+ "outlook.com",
1079
+ "aol.com",
1080
+ "protonmail.com",
1081
+ "proton.me",
1082
+ "icloud.com",
1083
+ "mail.com",
1084
+ "zoho.com",
1085
+ "yandex.com",
1086
+ "gmx.com",
1087
+ "gmx.net",
1088
+ "live.com",
1089
+ "msn.com",
1090
+ "me.com",
1091
+ "mac.com",
1092
+ "fastmail.com",
1093
+ "tutanota.com",
1094
+ "hey.com"
1095
+ ]);
1096
+ var DISPOSABLE_DOMAINS = /* @__PURE__ */ new Set([
1097
+ // Popular disposable services
1098
+ "guerrillamail.com",
1099
+ "guerrillamail.net",
1100
+ "guerrillamail.org",
1101
+ "tempmail.com",
1102
+ "temp-mail.org",
1103
+ "temp-mail.io",
1104
+ "throwaway.email",
1105
+ "throwaway.com",
1106
+ "mailinator.com",
1107
+ "mailinator.net",
1108
+ "yopmail.com",
1109
+ "yopmail.fr",
1110
+ "yopmail.net",
1111
+ "sharklasers.com",
1112
+ "grr.la",
1113
+ "guerrillamail.info",
1114
+ "guerrillamail.biz",
1115
+ "guerrillamail.de",
1116
+ "trashmail.com",
1117
+ "trashmail.me",
1118
+ "trashmail.net",
1119
+ "dispostable.com",
1120
+ "maildrop.cc",
1121
+ "mailnesia.com",
1122
+ "tempail.com",
1123
+ "mohmal.com",
1124
+ "getnada.com",
1125
+ "emailondeck.com",
1126
+ "discard.email",
1127
+ "fakeinbox.com",
1128
+ "mailcatch.com",
1129
+ "mintemail.com",
1130
+ "tempr.email",
1131
+ "tempinbox.com",
1132
+ "burnermail.io",
1133
+ "mailsac.com",
1134
+ "harakirimail.com",
1135
+ "tempmailo.com",
1136
+ "emailfake.com",
1137
+ "crazymailing.com",
1138
+ "armyspy.com",
1139
+ "dayrep.com",
1140
+ "einrot.com",
1141
+ "fleckens.hu",
1142
+ "gustr.com",
1143
+ "jourrapide.com",
1144
+ "rhyta.com",
1145
+ "superrito.com",
1146
+ "teleworm.us",
1147
+ "10minutemail.com",
1148
+ "10minutemail.net",
1149
+ "minutemail.com",
1150
+ "tempsky.com",
1151
+ "spamgourmet.com",
1152
+ "mytrashmail.com",
1153
+ "mailexpire.com",
1154
+ "safetymail.info",
1155
+ "filzmail.com",
1156
+ "trashymail.com",
1157
+ "sharkmail.com",
1158
+ "jetable.org",
1159
+ "nospam.ze.tc",
1160
+ "trash-me.com",
1161
+ "dodgit.com",
1162
+ "mailmoat.com",
1163
+ "spamfree24.org",
1164
+ "incognitomail.org",
1165
+ "tempomail.fr",
1166
+ "ephemail.net",
1167
+ "hidemail.de",
1168
+ "spaml.de",
1169
+ "uggsrock.com",
1170
+ "binkmail.com",
1171
+ "suremail.info",
1172
+ "bugmenot.com"
1173
+ ]);
1174
+ var DOMAIN_TYPOS = {
1175
+ "gmial.com": "gmail.com",
1176
+ "gmaill.com": "gmail.com",
1177
+ "gmai.com": "gmail.com",
1178
+ "gamil.com": "gmail.com",
1179
+ "gnail.com": "gmail.com",
1180
+ "gmal.com": "gmail.com",
1181
+ "gmil.com": "gmail.com",
1182
+ "gmail.co": "gmail.com",
1183
+ "gmail.cm": "gmail.com",
1184
+ "gmail.om": "gmail.com",
1185
+ "gmail.con": "gmail.com",
1186
+ "gmail.cim": "gmail.com",
1187
+ "gmail.comm": "gmail.com",
1188
+ "yahooo.com": "yahoo.com",
1189
+ "yaho.com": "yahoo.com",
1190
+ "yahoo.co": "yahoo.com",
1191
+ "yahoo.cm": "yahoo.com",
1192
+ "yahoo.con": "yahoo.com",
1193
+ "yahho.com": "yahoo.com",
1194
+ "hotmial.com": "hotmail.com",
1195
+ "hotmal.com": "hotmail.com",
1196
+ "hotmai.com": "hotmail.com",
1197
+ "hotmil.com": "hotmail.com",
1198
+ "hotmail.co": "hotmail.com",
1199
+ "hotmail.cm": "hotmail.com",
1200
+ "hotmail.con": "hotmail.com",
1201
+ "outlok.com": "outlook.com",
1202
+ "outloo.com": "outlook.com",
1203
+ "outlook.co": "outlook.com",
1204
+ "outlook.cm": "outlook.com",
1205
+ "protonmal.com": "protonmail.com",
1206
+ "protonmail.co": "protonmail.com",
1207
+ "icloud.co": "icloud.com",
1208
+ "icloud.cm": "icloud.com",
1209
+ "icoud.com": "icloud.com"
1210
+ };
1211
+ function invalidResult(reason, email) {
1212
+ return {
1213
+ valid: false,
1214
+ reason,
1215
+ suggestion: null,
1216
+ isFree: false,
1217
+ isDisposable: false,
1218
+ normalized: email
1219
+ };
1220
+ }
1221
+ function validateEmail(email, options = {}) {
1222
+ const {
1223
+ checkDisposable = true,
1224
+ suggestTypoFix = true,
1225
+ blockedDomains = [],
1226
+ allowedDomains = []
1227
+ } = options;
1228
+ const normalized = email.trim().toLowerCase();
1229
+ if (!normalized || normalized.length > MAX_EMAIL_LENGTH) {
1230
+ return invalidResult("invalid_syntax", normalized);
1231
+ }
1232
+ const atIndex = normalized.lastIndexOf("@");
1233
+ if (atIndex === -1) {
1234
+ return invalidResult("invalid_syntax", normalized);
1235
+ }
1236
+ const localPart = normalized.slice(0, atIndex);
1237
+ const domain = normalized.slice(atIndex + 1);
1238
+ if (localPart.length === 0 || localPart.length > MAX_LOCAL_LENGTH) {
1239
+ return invalidResult("invalid_syntax", normalized);
1240
+ }
1241
+ if (domain.length === 0 || domain.length > MAX_DOMAIN_LENGTH) {
1242
+ return invalidResult("invalid_syntax", normalized);
1243
+ }
1244
+ if (localPart.includes("..")) {
1245
+ return invalidResult("invalid_syntax", normalized);
1246
+ }
1247
+ if (localPart.startsWith(".") || localPart.endsWith(".")) {
1248
+ return invalidResult("invalid_syntax", normalized);
1249
+ }
1250
+ if (!EMAIL_SYNTAX.test(normalized)) {
1251
+ return invalidResult("invalid_syntax", normalized);
1252
+ }
1253
+ const allowedSet = new Set(allowedDomains.map((d) => d.toLowerCase()));
1254
+ if (allowedSet.has(domain)) {
1255
+ return {
1256
+ valid: true,
1257
+ reason: "valid",
1258
+ suggestion: null,
1259
+ isFree: FREE_PROVIDERS.has(domain),
1260
+ isDisposable: false,
1261
+ normalized
1262
+ };
1263
+ }
1264
+ const blockedSet = new Set(blockedDomains.map((d) => d.toLowerCase()));
1265
+ if (blockedSet.has(domain)) {
1266
+ return invalidResult("blocked", normalized);
1267
+ }
1268
+ const isDisposable = DISPOSABLE_DOMAINS.has(domain);
1269
+ if (checkDisposable && isDisposable) {
1270
+ return {
1271
+ valid: false,
1272
+ reason: "disposable",
1273
+ suggestion: null,
1274
+ isFree: false,
1275
+ isDisposable: true,
1276
+ normalized
1277
+ };
1278
+ }
1279
+ const isFree = FREE_PROVIDERS.has(domain);
1280
+ if (suggestTypoFix && DOMAIN_TYPOS[domain]) {
1281
+ const corrected = `${localPart}@${DOMAIN_TYPOS[domain]}`;
1282
+ return {
1283
+ valid: true,
1284
+ reason: "typo",
1285
+ suggestion: corrected,
1286
+ isFree: FREE_PROVIDERS.has(DOMAIN_TYPOS[domain]),
1287
+ isDisposable: false,
1288
+ normalized
1289
+ };
1290
+ }
1291
+ return {
1292
+ valid: true,
1293
+ reason: "valid",
1294
+ suggestion: null,
1295
+ isFree,
1296
+ isDisposable,
1297
+ normalized
1298
+ };
1299
+ }
1300
+
992
1301
  // src/logging/redactor.ts
993
1302
  var LOG_LEVELS = {
994
1303
  debug: 0,
@@ -1061,10 +1370,192 @@ function redactString(str, maxLength, patterns) {
1061
1370
  return safe;
1062
1371
  }
1063
1372
 
1373
+ // src/telemetry/client.ts
1374
+ var DEFAULT_BATCH_SIZE = 50;
1375
+ var MAX_BATCH_SIZE = 500;
1376
+ var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
1377
+ var MIN_FLUSH_INTERVAL_MS = 500;
1378
+ var FLUSH_TIMEOUT_MS = 1e4;
1379
+ var TelemetryClient = class {
1380
+ constructor(options) {
1381
+ this.queue = [];
1382
+ this.flushing = false;
1383
+ this.closed = false;
1384
+ if (!options.endpoint || typeof options.endpoint !== "string") {
1385
+ throw new TypeError("TelemetryClient: `endpoint` is required");
1386
+ }
1387
+ this.endpoint = options.endpoint;
1388
+ this.apiKey = options.apiKey;
1389
+ this.workspaceId = options.workspaceId;
1390
+ this.batchSize = clamp(
1391
+ options.batchSize ?? DEFAULT_BATCH_SIZE,
1392
+ 1,
1393
+ MAX_BATCH_SIZE
1394
+ );
1395
+ this.flushIntervalMs = Math.max(
1396
+ options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
1397
+ MIN_FLUSH_INTERVAL_MS
1398
+ );
1399
+ this.onError = options.onError ?? (() => {
1400
+ });
1401
+ this.startTimer();
1402
+ }
1403
+ /**
1404
+ * Enqueue an event. Fast, synchronous, cannot throw.
1405
+ * Triggers a flush if the queue has reached `batchSize`.
1406
+ */
1407
+ record(event) {
1408
+ if (this.closed) return;
1409
+ this.queue.push(event);
1410
+ if (this.queue.length >= this.batchSize) {
1411
+ void this.flush();
1412
+ }
1413
+ }
1414
+ /**
1415
+ * Manually flush the queue. Pulls up to `batchSize` events into a batch and
1416
+ * POSTs them. Returns a resolved promise on success OR on handled failure.
1417
+ * Never throws.
1418
+ */
1419
+ async flush() {
1420
+ if (this.flushing) return;
1421
+ if (this.queue.length === 0) return;
1422
+ this.flushing = true;
1423
+ try {
1424
+ const batch = this.queue.splice(0, this.batchSize);
1425
+ await this.send(batch);
1426
+ } catch (err) {
1427
+ this.safeNotify(err);
1428
+ } finally {
1429
+ this.flushing = false;
1430
+ }
1431
+ if (!this.closed && this.queue.length > 0) {
1432
+ void this.flush();
1433
+ }
1434
+ }
1435
+ /**
1436
+ * Shut down: stop the interval timer and attempt one final flush.
1437
+ * Safe to call multiple times.
1438
+ */
1439
+ async close() {
1440
+ if (this.closed) return;
1441
+ this.closed = true;
1442
+ if (this.timer !== void 0) {
1443
+ clearInterval(this.timer);
1444
+ this.timer = void 0;
1445
+ }
1446
+ if (this.signalHandler !== void 0) {
1447
+ process.off("SIGTERM", this.signalHandler);
1448
+ process.off("SIGINT", this.signalHandler);
1449
+ this.signalHandler = void 0;
1450
+ }
1451
+ try {
1452
+ await this.flush();
1453
+ } catch {
1454
+ }
1455
+ }
1456
+ /**
1457
+ * Register `SIGTERM` / `SIGINT` handlers that call `close()` to drain
1458
+ * the queue on graceful shutdown. Opt-in — libraries should not silently
1459
+ * attach global signal handlers. Safe to call multiple times.
1460
+ */
1461
+ installShutdownHooks() {
1462
+ if (this.signalHandler !== void 0 || this.closed) return;
1463
+ const handler = () => {
1464
+ void this.close();
1465
+ };
1466
+ this.signalHandler = handler;
1467
+ process.once("SIGTERM", handler);
1468
+ process.once("SIGINT", handler);
1469
+ }
1470
+ /** Count of events currently waiting to be sent. Useful for tests. */
1471
+ get pendingCount() {
1472
+ return this.queue.length;
1473
+ }
1474
+ // ── internals ─────────────────────────────────────────────────────────
1475
+ async send(batch) {
1476
+ const headers = {
1477
+ "content-type": "application/json"
1478
+ };
1479
+ if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
1480
+ if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
1481
+ const controller = new AbortController();
1482
+ const abortTimer = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
1483
+ try {
1484
+ const res = await fetch(this.endpoint, {
1485
+ method: "POST",
1486
+ headers,
1487
+ body: JSON.stringify({ events: batch }),
1488
+ signal: controller.signal
1489
+ });
1490
+ if (!res.ok) {
1491
+ const text = await safeReadBody(res);
1492
+ throw new TelemetryHttpError(res.status, text);
1493
+ }
1494
+ } finally {
1495
+ clearTimeout(abortTimer);
1496
+ }
1497
+ }
1498
+ startTimer() {
1499
+ this.timer = setInterval(() => {
1500
+ void this.flush();
1501
+ }, this.flushIntervalMs);
1502
+ this.timer.unref?.();
1503
+ }
1504
+ safeNotify(err) {
1505
+ try {
1506
+ this.onError(err instanceof Error ? err : new Error(String(err)));
1507
+ } catch {
1508
+ }
1509
+ }
1510
+ };
1511
+ var TelemetryHttpError = class extends Error {
1512
+ constructor(status, responseBody) {
1513
+ super(`Telemetry ingest returned HTTP ${status}`);
1514
+ this.status = status;
1515
+ this.responseBody = responseBody;
1516
+ this.name = "TelemetryHttpError";
1517
+ }
1518
+ };
1519
+ function clamp(value, min, max) {
1520
+ if (!Number.isFinite(value)) return min;
1521
+ return Math.max(min, Math.min(max, Math.trunc(value)));
1522
+ }
1523
+ async function safeReadBody(res) {
1524
+ try {
1525
+ const text = await res.text();
1526
+ return text.slice(0, 500);
1527
+ } catch {
1528
+ return "";
1529
+ }
1530
+ }
1531
+
1064
1532
  // src/middleware/main.ts
1533
+ function buildTelemetryFromEnv() {
1534
+ const env = typeof process !== "undefined" ? process.env : void 0;
1535
+ const endpoint = env?.ARCIS_ENDPOINT;
1536
+ if (!endpoint) return void 0;
1537
+ const opts = { endpoint };
1538
+ if (env?.ARCIS_WORKSPACE_ID) opts.workspaceId = env.ARCIS_WORKSPACE_ID;
1539
+ if (env?.ARCIS_KEY) opts.apiKey = env.ARCIS_KEY;
1540
+ const batch = env?.ARCIS_BATCH_SIZE ? parseInt(env.ARCIS_BATCH_SIZE, 10) : NaN;
1541
+ if (!Number.isNaN(batch)) opts.batchSize = batch;
1542
+ const flush = env?.ARCIS_FLUSH_INTERVAL_MS ? parseInt(env.ARCIS_FLUSH_INTERVAL_MS, 10) : NaN;
1543
+ if (!Number.isNaN(flush)) opts.flushIntervalMs = flush;
1544
+ return opts;
1545
+ }
1065
1546
  function arcis(options = {}) {
1066
1547
  const middlewares = [];
1067
1548
  const cleanupFns = [];
1549
+ let telemetryClient;
1550
+ const telemetryOpts = options.telemetry?.endpoint ? options.telemetry : buildTelemetryFromEnv();
1551
+ if (telemetryOpts) {
1552
+ const client = new TelemetryClient(telemetryOpts);
1553
+ telemetryClient = client;
1554
+ middlewares.push(createTelemetryEmitter(client));
1555
+ cleanupFns.push(() => {
1556
+ void client.close();
1557
+ });
1558
+ }
1068
1559
  if (options.headers !== false) {
1069
1560
  const headerOpts = typeof options.headers === "object" ? options.headers : {};
1070
1561
  middlewares.push(createHeaders(headerOpts));
@@ -1077,7 +1568,8 @@ function arcis(options = {}) {
1077
1568
  }
1078
1569
  if (options.sanitize !== false) {
1079
1570
  const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
1080
- middlewares.push(createSanitizer(sanitizeOpts));
1571
+ const sanitizer = createSanitizer(sanitizeOpts);
1572
+ middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
1081
1573
  }
1082
1574
  const result = middlewares;
1083
1575
  result.close = () => {
@@ -1390,6 +1882,16 @@ function secureCookieDefaults(options = {}) {
1390
1882
  sameSite: options.sameSite ?? "Lax",
1391
1883
  path: options.path
1392
1884
  };
1885
+ if (resolved.sameSite === "None" && resolved.secure === false) {
1886
+ throw new Error(
1887
+ "[arcis] secureCookieDefaults: sameSite=None requires secure=true (modern browsers reject the cookie otherwise)"
1888
+ );
1889
+ }
1890
+ if (resolved.httpOnly === false && resolved.secure === false && isProduction) {
1891
+ console.warn(
1892
+ "[arcis] secureCookieDefaults: httpOnly and secure are both disabled in production \u2014 cookies will be readable by JS and sent over HTTP"
1893
+ );
1894
+ }
1393
1895
  return (_req, res, next) => {
1394
1896
  const originalSetHeader = res.setHeader.bind(res);
1395
1897
  res.setHeader = function patchedSetHeader(name, value) {
@@ -1522,7 +2024,16 @@ function detectBehavioralSignals(req) {
1522
2024
  }
1523
2025
  function detectBot(req) {
1524
2026
  const rawUa = req.headers["user-agent"] ?? "";
1525
- const ua = rawUa.length > 2048 ? rawUa.slice(0, 2048) : rawUa;
2027
+ if (rawUa.length > 2048) {
2028
+ return {
2029
+ isBot: true,
2030
+ category: "UNKNOWN",
2031
+ name: null,
2032
+ confidence: 0.9,
2033
+ signals: detectBehavioralSignals(req)
2034
+ };
2035
+ }
2036
+ const ua = rawUa;
1526
2037
  const signals = detectBehavioralSignals(req);
1527
2038
  if (!ua) {
1528
2039
  return {
@@ -1686,6 +2197,10 @@ function csrfProtection(options = {}) {
1686
2197
  if (!validateCsrfToken(cookieToken, requestToken)) {
1687
2198
  return onError(req, res, next);
1688
2199
  }
2200
+ if (options.rotateOnUse) {
2201
+ const freshToken = generateCsrfToken(tokenLength);
2202
+ setCsrfCookie(res, cookieName, freshToken, cookieOpts);
2203
+ }
1689
2204
  next();
1690
2205
  };
1691
2206
  }
@@ -1720,6 +2235,89 @@ function escapeRegex(str) {
1720
2235
  }
1721
2236
  var createCsrf = csrfProtection;
1722
2237
 
1723
- export { arcis, arcisWithMethods as arcisFunction, botProtection, createCors, createCsrf, createErrorHandler, createHeaders, createRateLimiter, createSecureCookies, createSlidingWindowLimiter, createTokenBucketLimiter, csrfProtection, main_default as default, detectBot, enforceSecureCookie, errorHandler, generateCsrfToken, rateLimit, safeCors, secureCookieDefaults, securityHeaders, validateCsrfToken };
2238
+ // src/middleware/signup-protection.ts
2239
+ function checkSignup(req, options = {}) {
2240
+ const {
2241
+ emailField = "email",
2242
+ checkEmail = true,
2243
+ blockDisposable = true,
2244
+ checkBot = true,
2245
+ allowedBotCategories = [],
2246
+ allowedEmailDomains = [],
2247
+ blockedEmailDomains = []
2248
+ } = options;
2249
+ if (checkBot) {
2250
+ const bot = detectBot(req);
2251
+ if (bot.isBot && !allowedBotCategories.includes(bot.category)) {
2252
+ return {
2253
+ allowed: false,
2254
+ reason: "bot",
2255
+ details: { category: bot.category, name: bot.name, confidence: bot.confidence }
2256
+ };
2257
+ }
2258
+ }
2259
+ if (checkEmail) {
2260
+ const email = req.body?.[emailField];
2261
+ if (typeof email !== "string" || email.length === 0) {
2262
+ return { allowed: false, reason: "missing_email" };
2263
+ }
2264
+ const result = validateEmail(email, {
2265
+ checkDisposable: blockDisposable,
2266
+ allowedDomains: allowedEmailDomains,
2267
+ blockedDomains: blockedEmailDomains
2268
+ });
2269
+ if (!result.valid) {
2270
+ const reason = result.reason === "disposable" ? "disposable_email" : "invalid_email";
2271
+ return { allowed: false, reason, details: { emailReason: result.reason } };
2272
+ }
2273
+ }
2274
+ return { allowed: true, reason: "ok" };
2275
+ }
2276
+ function signupProtection(options = {}) {
2277
+ const rateLimitCfg = options.rateLimit;
2278
+ const limiter = rateLimitCfg === false ? null : createRateLimiter({
2279
+ max: rateLimitCfg?.max ?? 5,
2280
+ windowMs: rateLimitCfg?.windowMs ?? 6e4,
2281
+ message: "Too many signup attempts"
2282
+ });
2283
+ const handler = (req, res, next) => {
2284
+ const result = checkSignup(req, options);
2285
+ if (!result.allowed) {
2286
+ options.onBlocked?.(req, result);
2287
+ const status = result.reason === "bot" ? 403 : 400;
2288
+ res.status(status).json({ error: "signup_blocked", reason: result.reason });
2289
+ return;
2290
+ }
2291
+ if (limiter) {
2292
+ let rateLimited = false;
2293
+ const rateLimitNext = (err) => {
2294
+ if (err) return next(err);
2295
+ if (!rateLimited) next();
2296
+ };
2297
+ const patchedRes = new Proxy(res, {
2298
+ get(target, prop) {
2299
+ if (prop === "status") {
2300
+ return (code) => {
2301
+ if (code === 429) {
2302
+ rateLimited = true;
2303
+ options.onBlocked?.(req, { allowed: false, reason: "rate_limited" });
2304
+ }
2305
+ return target.status.call(target, code);
2306
+ };
2307
+ }
2308
+ return Reflect.get(target, prop);
2309
+ }
2310
+ });
2311
+ limiter(req, patchedRes, rateLimitNext);
2312
+ return;
2313
+ }
2314
+ next();
2315
+ };
2316
+ const middleware = handler;
2317
+ middleware.close = () => limiter?.close();
2318
+ return middleware;
2319
+ }
2320
+
2321
+ export { arcis, arcisWithMethods as arcisFunction, botProtection, checkSignup, createCors, createCsrf, createErrorHandler, createHeaders, createRateLimiter, createSecureCookies, createSlidingWindowLimiter, createTokenBucketLimiter, csrfProtection, main_default as default, detectBot, enforceSecureCookie, errorHandler, generateCsrfToken, rateLimit, safeCors, secureCookieDefaults, securityHeaders, signupProtection, validateCsrfToken };
1724
2322
  //# sourceMappingURL=index.mjs.map
1725
2323
  //# sourceMappingURL=index.mjs.map