@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
@@ -49,6 +49,9 @@ var XSS_REMOVE_PATTERNS = [
49
49
  /<script[^>]*>[\s\S]*?<\/script>/gi,
50
50
  /** Standalone/unclosed script tags */
51
51
  /<script[^>]*>/gi,
52
+ /** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
53
+ /<style[^>]*>[\s\S]*?<\/style>/gi,
54
+ /<style[^>]*/gi,
52
55
  /** iframe — full block and partial/unclosed */
53
56
  /<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
54
57
  /<iframe[^>]*/gi,
@@ -361,13 +364,13 @@ function createRateLimiter(options = {}) {
361
364
  statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
362
365
  keyGenerator = (req) => {
363
366
  const ip = req.ip ?? req.socket?.remoteAddress;
364
- if (!ip) {
365
- console.warn(
366
- "[arcis] Rate limiter: cannot resolve client IP. All unresolvable clients share one counter. Set Express trust proxy if behind a reverse proxy."
367
- );
368
- return "unknown";
369
- }
370
- return ip;
367
+ if (ip) return ip;
368
+ const ua = req.headers["user-agent"] ?? "";
369
+ const lang = req.headers["accept-language"] ?? "";
370
+ const fp = `${ua}|${lang}`;
371
+ let hash = 0;
372
+ for (let i = 0; i < fp.length; i++) hash = (hash << 5) - hash + fp.charCodeAt(i) | 0;
373
+ return `unknown:${hash.toString(36)}`;
371
374
  },
372
375
  skip,
373
376
  store: externalStore
@@ -559,6 +562,80 @@ var SecurityThreatError = class extends ArcisError {
559
562
  }
560
563
  };
561
564
 
565
+ // src/middleware/telemetry.ts
566
+ var THREAT_TO_VECTOR = {
567
+ xss: "xss",
568
+ sql_injection: "sql",
569
+ nosql_injection: "nosql",
570
+ path_traversal: "path",
571
+ command_injection: "command",
572
+ prototype_pollution: "prototype",
573
+ header_injection: "header",
574
+ ssti: "ssti",
575
+ xxe: "xxe"
576
+ };
577
+ function createTelemetryEmitter(client) {
578
+ return (req, res, next) => {
579
+ const start = performance.now();
580
+ res.on("finish", () => {
581
+ try {
582
+ const event = buildEvent(req, res.statusCode, performance.now() - start);
583
+ client.record(event);
584
+ } catch {
585
+ }
586
+ });
587
+ next();
588
+ };
589
+ }
590
+ function tapSanitizerThreats(handler) {
591
+ return (req, res, next) => {
592
+ handler(req, res, (err) => {
593
+ if (err instanceof SecurityThreatError) {
594
+ const vector = THREAT_TO_VECTOR[err.threatType] ?? err.threatType;
595
+ req.__arcis = {
596
+ vector,
597
+ rule: `${vector}/match`,
598
+ severity: "high",
599
+ matchedPattern: err.pattern,
600
+ reason: err.message,
601
+ decision: "deny"
602
+ };
603
+ }
604
+ next(err);
605
+ });
606
+ };
607
+ }
608
+ function buildEvent(req, status, latencyMs) {
609
+ const marker = req.__arcis;
610
+ const decision = marker?.decision ?? inferDecision(status);
611
+ return {
612
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
613
+ ip: extractIp(req),
614
+ method: (req.method ?? "GET").toUpperCase(),
615
+ path: req.path ?? req.url ?? "/",
616
+ decision,
617
+ vector: marker?.vector ?? (status === 429 ? "rate-limit" : void 0),
618
+ rule: marker?.rule ?? (status === 429 ? "rate-limit/exceeded" : void 0),
619
+ severity: marker?.severity ?? (status === 429 ? "medium" : void 0),
620
+ userAgent: typeof req.headers?.["user-agent"] === "string" ? req.headers["user-agent"] : "",
621
+ reason: marker?.reason,
622
+ status,
623
+ matchedPattern: marker?.matchedPattern,
624
+ latencyMs: Math.max(0, latencyMs)
625
+ };
626
+ }
627
+ function inferDecision(status) {
628
+ if (status === 429) return "deny";
629
+ if (status === 400) return "deny";
630
+ if (status === 403) return "deny";
631
+ return "allow";
632
+ }
633
+ function extractIp(req) {
634
+ if (typeof req.ip === "string" && req.ip.length > 0) return req.ip;
635
+ const remote = req.socket?.remoteAddress;
636
+ return typeof remote === "string" ? remote : "0.0.0.0";
637
+ }
638
+
562
639
  // src/sanitizers/utils.ts
563
640
  function encodeHtmlEntities(str) {
564
641
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
@@ -993,6 +1070,238 @@ function validateField(field, value, rules) {
993
1070
  // Audio/Video
994
1071
  "audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])]});
995
1072
 
1073
+ // src/validation/email.ts
1074
+ var MAX_EMAIL_LENGTH = 254;
1075
+ var MAX_LOCAL_LENGTH = 64;
1076
+ var MAX_DOMAIN_LENGTH = 255;
1077
+ 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,}$/;
1078
+ var FREE_PROVIDERS = /* @__PURE__ */ new Set([
1079
+ "gmail.com",
1080
+ "yahoo.com",
1081
+ "hotmail.com",
1082
+ "outlook.com",
1083
+ "aol.com",
1084
+ "protonmail.com",
1085
+ "proton.me",
1086
+ "icloud.com",
1087
+ "mail.com",
1088
+ "zoho.com",
1089
+ "yandex.com",
1090
+ "gmx.com",
1091
+ "gmx.net",
1092
+ "live.com",
1093
+ "msn.com",
1094
+ "me.com",
1095
+ "mac.com",
1096
+ "fastmail.com",
1097
+ "tutanota.com",
1098
+ "hey.com"
1099
+ ]);
1100
+ var DISPOSABLE_DOMAINS = /* @__PURE__ */ new Set([
1101
+ // Popular disposable services
1102
+ "guerrillamail.com",
1103
+ "guerrillamail.net",
1104
+ "guerrillamail.org",
1105
+ "tempmail.com",
1106
+ "temp-mail.org",
1107
+ "temp-mail.io",
1108
+ "throwaway.email",
1109
+ "throwaway.com",
1110
+ "mailinator.com",
1111
+ "mailinator.net",
1112
+ "yopmail.com",
1113
+ "yopmail.fr",
1114
+ "yopmail.net",
1115
+ "sharklasers.com",
1116
+ "grr.la",
1117
+ "guerrillamail.info",
1118
+ "guerrillamail.biz",
1119
+ "guerrillamail.de",
1120
+ "trashmail.com",
1121
+ "trashmail.me",
1122
+ "trashmail.net",
1123
+ "dispostable.com",
1124
+ "maildrop.cc",
1125
+ "mailnesia.com",
1126
+ "tempail.com",
1127
+ "mohmal.com",
1128
+ "getnada.com",
1129
+ "emailondeck.com",
1130
+ "discard.email",
1131
+ "fakeinbox.com",
1132
+ "mailcatch.com",
1133
+ "mintemail.com",
1134
+ "tempr.email",
1135
+ "tempinbox.com",
1136
+ "burnermail.io",
1137
+ "mailsac.com",
1138
+ "harakirimail.com",
1139
+ "tempmailo.com",
1140
+ "emailfake.com",
1141
+ "crazymailing.com",
1142
+ "armyspy.com",
1143
+ "dayrep.com",
1144
+ "einrot.com",
1145
+ "fleckens.hu",
1146
+ "gustr.com",
1147
+ "jourrapide.com",
1148
+ "rhyta.com",
1149
+ "superrito.com",
1150
+ "teleworm.us",
1151
+ "10minutemail.com",
1152
+ "10minutemail.net",
1153
+ "minutemail.com",
1154
+ "tempsky.com",
1155
+ "spamgourmet.com",
1156
+ "mytrashmail.com",
1157
+ "mailexpire.com",
1158
+ "safetymail.info",
1159
+ "filzmail.com",
1160
+ "trashymail.com",
1161
+ "sharkmail.com",
1162
+ "jetable.org",
1163
+ "nospam.ze.tc",
1164
+ "trash-me.com",
1165
+ "dodgit.com",
1166
+ "mailmoat.com",
1167
+ "spamfree24.org",
1168
+ "incognitomail.org",
1169
+ "tempomail.fr",
1170
+ "ephemail.net",
1171
+ "hidemail.de",
1172
+ "spaml.de",
1173
+ "uggsrock.com",
1174
+ "binkmail.com",
1175
+ "suremail.info",
1176
+ "bugmenot.com"
1177
+ ]);
1178
+ var DOMAIN_TYPOS = {
1179
+ "gmial.com": "gmail.com",
1180
+ "gmaill.com": "gmail.com",
1181
+ "gmai.com": "gmail.com",
1182
+ "gamil.com": "gmail.com",
1183
+ "gnail.com": "gmail.com",
1184
+ "gmal.com": "gmail.com",
1185
+ "gmil.com": "gmail.com",
1186
+ "gmail.co": "gmail.com",
1187
+ "gmail.cm": "gmail.com",
1188
+ "gmail.om": "gmail.com",
1189
+ "gmail.con": "gmail.com",
1190
+ "gmail.cim": "gmail.com",
1191
+ "gmail.comm": "gmail.com",
1192
+ "yahooo.com": "yahoo.com",
1193
+ "yaho.com": "yahoo.com",
1194
+ "yahoo.co": "yahoo.com",
1195
+ "yahoo.cm": "yahoo.com",
1196
+ "yahoo.con": "yahoo.com",
1197
+ "yahho.com": "yahoo.com",
1198
+ "hotmial.com": "hotmail.com",
1199
+ "hotmal.com": "hotmail.com",
1200
+ "hotmai.com": "hotmail.com",
1201
+ "hotmil.com": "hotmail.com",
1202
+ "hotmail.co": "hotmail.com",
1203
+ "hotmail.cm": "hotmail.com",
1204
+ "hotmail.con": "hotmail.com",
1205
+ "outlok.com": "outlook.com",
1206
+ "outloo.com": "outlook.com",
1207
+ "outlook.co": "outlook.com",
1208
+ "outlook.cm": "outlook.com",
1209
+ "protonmal.com": "protonmail.com",
1210
+ "protonmail.co": "protonmail.com",
1211
+ "icloud.co": "icloud.com",
1212
+ "icloud.cm": "icloud.com",
1213
+ "icoud.com": "icloud.com"
1214
+ };
1215
+ function invalidResult(reason, email) {
1216
+ return {
1217
+ valid: false,
1218
+ reason,
1219
+ suggestion: null,
1220
+ isFree: false,
1221
+ isDisposable: false,
1222
+ normalized: email
1223
+ };
1224
+ }
1225
+ function validateEmail(email, options = {}) {
1226
+ const {
1227
+ checkDisposable = true,
1228
+ suggestTypoFix = true,
1229
+ blockedDomains = [],
1230
+ allowedDomains = []
1231
+ } = options;
1232
+ const normalized = email.trim().toLowerCase();
1233
+ if (!normalized || normalized.length > MAX_EMAIL_LENGTH) {
1234
+ return invalidResult("invalid_syntax", normalized);
1235
+ }
1236
+ const atIndex = normalized.lastIndexOf("@");
1237
+ if (atIndex === -1) {
1238
+ return invalidResult("invalid_syntax", normalized);
1239
+ }
1240
+ const localPart = normalized.slice(0, atIndex);
1241
+ const domain = normalized.slice(atIndex + 1);
1242
+ if (localPart.length === 0 || localPart.length > MAX_LOCAL_LENGTH) {
1243
+ return invalidResult("invalid_syntax", normalized);
1244
+ }
1245
+ if (domain.length === 0 || domain.length > MAX_DOMAIN_LENGTH) {
1246
+ return invalidResult("invalid_syntax", normalized);
1247
+ }
1248
+ if (localPart.includes("..")) {
1249
+ return invalidResult("invalid_syntax", normalized);
1250
+ }
1251
+ if (localPart.startsWith(".") || localPart.endsWith(".")) {
1252
+ return invalidResult("invalid_syntax", normalized);
1253
+ }
1254
+ if (!EMAIL_SYNTAX.test(normalized)) {
1255
+ return invalidResult("invalid_syntax", normalized);
1256
+ }
1257
+ const allowedSet = new Set(allowedDomains.map((d) => d.toLowerCase()));
1258
+ if (allowedSet.has(domain)) {
1259
+ return {
1260
+ valid: true,
1261
+ reason: "valid",
1262
+ suggestion: null,
1263
+ isFree: FREE_PROVIDERS.has(domain),
1264
+ isDisposable: false,
1265
+ normalized
1266
+ };
1267
+ }
1268
+ const blockedSet = new Set(blockedDomains.map((d) => d.toLowerCase()));
1269
+ if (blockedSet.has(domain)) {
1270
+ return invalidResult("blocked", normalized);
1271
+ }
1272
+ const isDisposable = DISPOSABLE_DOMAINS.has(domain);
1273
+ if (checkDisposable && isDisposable) {
1274
+ return {
1275
+ valid: false,
1276
+ reason: "disposable",
1277
+ suggestion: null,
1278
+ isFree: false,
1279
+ isDisposable: true,
1280
+ normalized
1281
+ };
1282
+ }
1283
+ const isFree = FREE_PROVIDERS.has(domain);
1284
+ if (suggestTypoFix && DOMAIN_TYPOS[domain]) {
1285
+ const corrected = `${localPart}@${DOMAIN_TYPOS[domain]}`;
1286
+ return {
1287
+ valid: true,
1288
+ reason: "typo",
1289
+ suggestion: corrected,
1290
+ isFree: FREE_PROVIDERS.has(DOMAIN_TYPOS[domain]),
1291
+ isDisposable: false,
1292
+ normalized
1293
+ };
1294
+ }
1295
+ return {
1296
+ valid: true,
1297
+ reason: "valid",
1298
+ suggestion: null,
1299
+ isFree,
1300
+ isDisposable,
1301
+ normalized
1302
+ };
1303
+ }
1304
+
996
1305
  // src/logging/redactor.ts
997
1306
  var LOG_LEVELS = {
998
1307
  debug: 0,
@@ -1065,10 +1374,192 @@ function redactString(str, maxLength, patterns) {
1065
1374
  return safe;
1066
1375
  }
1067
1376
 
1377
+ // src/telemetry/client.ts
1378
+ var DEFAULT_BATCH_SIZE = 50;
1379
+ var MAX_BATCH_SIZE = 500;
1380
+ var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
1381
+ var MIN_FLUSH_INTERVAL_MS = 500;
1382
+ var FLUSH_TIMEOUT_MS = 1e4;
1383
+ var TelemetryClient = class {
1384
+ constructor(options) {
1385
+ this.queue = [];
1386
+ this.flushing = false;
1387
+ this.closed = false;
1388
+ if (!options.endpoint || typeof options.endpoint !== "string") {
1389
+ throw new TypeError("TelemetryClient: `endpoint` is required");
1390
+ }
1391
+ this.endpoint = options.endpoint;
1392
+ this.apiKey = options.apiKey;
1393
+ this.workspaceId = options.workspaceId;
1394
+ this.batchSize = clamp(
1395
+ options.batchSize ?? DEFAULT_BATCH_SIZE,
1396
+ 1,
1397
+ MAX_BATCH_SIZE
1398
+ );
1399
+ this.flushIntervalMs = Math.max(
1400
+ options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
1401
+ MIN_FLUSH_INTERVAL_MS
1402
+ );
1403
+ this.onError = options.onError ?? (() => {
1404
+ });
1405
+ this.startTimer();
1406
+ }
1407
+ /**
1408
+ * Enqueue an event. Fast, synchronous, cannot throw.
1409
+ * Triggers a flush if the queue has reached `batchSize`.
1410
+ */
1411
+ record(event) {
1412
+ if (this.closed) return;
1413
+ this.queue.push(event);
1414
+ if (this.queue.length >= this.batchSize) {
1415
+ void this.flush();
1416
+ }
1417
+ }
1418
+ /**
1419
+ * Manually flush the queue. Pulls up to `batchSize` events into a batch and
1420
+ * POSTs them. Returns a resolved promise on success OR on handled failure.
1421
+ * Never throws.
1422
+ */
1423
+ async flush() {
1424
+ if (this.flushing) return;
1425
+ if (this.queue.length === 0) return;
1426
+ this.flushing = true;
1427
+ try {
1428
+ const batch = this.queue.splice(0, this.batchSize);
1429
+ await this.send(batch);
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
+
1068
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
+ }
1069
1550
  function arcis(options = {}) {
1070
1551
  const middlewares = [];
1071
1552
  const cleanupFns = [];
1553
+ let telemetryClient;
1554
+ const telemetryOpts = options.telemetry?.endpoint ? options.telemetry : buildTelemetryFromEnv();
1555
+ if (telemetryOpts) {
1556
+ const client = new TelemetryClient(telemetryOpts);
1557
+ telemetryClient = client;
1558
+ middlewares.push(createTelemetryEmitter(client));
1559
+ cleanupFns.push(() => {
1560
+ void client.close();
1561
+ });
1562
+ }
1072
1563
  if (options.headers !== false) {
1073
1564
  const headerOpts = typeof options.headers === "object" ? options.headers : {};
1074
1565
  middlewares.push(createHeaders(headerOpts));
@@ -1081,7 +1572,8 @@ function arcis(options = {}) {
1081
1572
  }
1082
1573
  if (options.sanitize !== false) {
1083
1574
  const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
1084
- middlewares.push(createSanitizer(sanitizeOpts));
1575
+ const sanitizer = createSanitizer(sanitizeOpts);
1576
+ middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
1085
1577
  }
1086
1578
  const result = middlewares;
1087
1579
  result.close = () => {
@@ -1394,6 +1886,16 @@ function secureCookieDefaults(options = {}) {
1394
1886
  sameSite: options.sameSite ?? "Lax",
1395
1887
  path: options.path
1396
1888
  };
1889
+ if (resolved.sameSite === "None" && resolved.secure === false) {
1890
+ throw new Error(
1891
+ "[arcis] secureCookieDefaults: sameSite=None requires secure=true (modern browsers reject the cookie otherwise)"
1892
+ );
1893
+ }
1894
+ if (resolved.httpOnly === false && resolved.secure === false && isProduction) {
1895
+ console.warn(
1896
+ "[arcis] secureCookieDefaults: httpOnly and secure are both disabled in production \u2014 cookies will be readable by JS and sent over HTTP"
1897
+ );
1898
+ }
1397
1899
  return (_req, res, next) => {
1398
1900
  const originalSetHeader = res.setHeader.bind(res);
1399
1901
  res.setHeader = function patchedSetHeader(name, value) {
@@ -1526,7 +2028,16 @@ function detectBehavioralSignals(req) {
1526
2028
  }
1527
2029
  function detectBot(req) {
1528
2030
  const rawUa = req.headers["user-agent"] ?? "";
1529
- const ua = rawUa.length > 2048 ? rawUa.slice(0, 2048) : rawUa;
2031
+ if (rawUa.length > 2048) {
2032
+ return {
2033
+ isBot: true,
2034
+ category: "UNKNOWN",
2035
+ name: null,
2036
+ confidence: 0.9,
2037
+ signals: detectBehavioralSignals(req)
2038
+ };
2039
+ }
2040
+ const ua = rawUa;
1530
2041
  const signals = detectBehavioralSignals(req);
1531
2042
  if (!ua) {
1532
2043
  return {
@@ -1690,6 +2201,10 @@ function csrfProtection(options = {}) {
1690
2201
  if (!validateCsrfToken(cookieToken, requestToken)) {
1691
2202
  return onError(req, res, next);
1692
2203
  }
2204
+ if (options.rotateOnUse) {
2205
+ const freshToken = generateCsrfToken(tokenLength);
2206
+ setCsrfCookie(res, cookieName, freshToken, cookieOpts);
2207
+ }
1693
2208
  next();
1694
2209
  };
1695
2210
  }
@@ -1724,9 +2239,93 @@ function escapeRegex(str) {
1724
2239
  }
1725
2240
  var createCsrf = csrfProtection;
1726
2241
 
2242
+ // src/middleware/signup-protection.ts
2243
+ function checkSignup(req, options = {}) {
2244
+ const {
2245
+ emailField = "email",
2246
+ checkEmail = true,
2247
+ blockDisposable = true,
2248
+ checkBot = true,
2249
+ allowedBotCategories = [],
2250
+ allowedEmailDomains = [],
2251
+ blockedEmailDomains = []
2252
+ } = options;
2253
+ if (checkBot) {
2254
+ const bot = detectBot(req);
2255
+ if (bot.isBot && !allowedBotCategories.includes(bot.category)) {
2256
+ return {
2257
+ allowed: false,
2258
+ reason: "bot",
2259
+ details: { category: bot.category, name: bot.name, confidence: bot.confidence }
2260
+ };
2261
+ }
2262
+ }
2263
+ if (checkEmail) {
2264
+ const email = req.body?.[emailField];
2265
+ if (typeof email !== "string" || email.length === 0) {
2266
+ return { allowed: false, reason: "missing_email" };
2267
+ }
2268
+ const result = validateEmail(email, {
2269
+ checkDisposable: blockDisposable,
2270
+ allowedDomains: allowedEmailDomains,
2271
+ blockedDomains: blockedEmailDomains
2272
+ });
2273
+ if (!result.valid) {
2274
+ const reason = result.reason === "disposable" ? "disposable_email" : "invalid_email";
2275
+ return { allowed: false, reason, details: { emailReason: result.reason } };
2276
+ }
2277
+ }
2278
+ return { allowed: true, reason: "ok" };
2279
+ }
2280
+ function signupProtection(options = {}) {
2281
+ const rateLimitCfg = options.rateLimit;
2282
+ const limiter = rateLimitCfg === false ? null : createRateLimiter({
2283
+ max: rateLimitCfg?.max ?? 5,
2284
+ windowMs: rateLimitCfg?.windowMs ?? 6e4,
2285
+ message: "Too many signup attempts"
2286
+ });
2287
+ const handler = (req, res, next) => {
2288
+ const result = checkSignup(req, options);
2289
+ if (!result.allowed) {
2290
+ options.onBlocked?.(req, result);
2291
+ const status = result.reason === "bot" ? 403 : 400;
2292
+ res.status(status).json({ error: "signup_blocked", reason: result.reason });
2293
+ return;
2294
+ }
2295
+ if (limiter) {
2296
+ let rateLimited = false;
2297
+ const rateLimitNext = (err) => {
2298
+ if (err) return next(err);
2299
+ if (!rateLimited) next();
2300
+ };
2301
+ const patchedRes = new Proxy(res, {
2302
+ get(target, prop) {
2303
+ if (prop === "status") {
2304
+ return (code) => {
2305
+ if (code === 429) {
2306
+ rateLimited = true;
2307
+ options.onBlocked?.(req, { allowed: false, reason: "rate_limited" });
2308
+ }
2309
+ return target.status.call(target, code);
2310
+ };
2311
+ }
2312
+ return Reflect.get(target, prop);
2313
+ }
2314
+ });
2315
+ limiter(req, patchedRes, rateLimitNext);
2316
+ return;
2317
+ }
2318
+ next();
2319
+ };
2320
+ const middleware = handler;
2321
+ middleware.close = () => limiter?.close();
2322
+ return middleware;
2323
+ }
2324
+
1727
2325
  exports.arcis = arcis;
1728
2326
  exports.arcisFunction = arcisWithMethods;
1729
2327
  exports.botProtection = botProtection;
2328
+ exports.checkSignup = checkSignup;
1730
2329
  exports.createCors = createCors;
1731
2330
  exports.createCsrf = createCsrf;
1732
2331
  exports.createErrorHandler = createErrorHandler;
@@ -1745,6 +2344,7 @@ exports.rateLimit = rateLimit;
1745
2344
  exports.safeCors = safeCors;
1746
2345
  exports.secureCookieDefaults = secureCookieDefaults;
1747
2346
  exports.securityHeaders = securityHeaders;
2347
+ exports.signupProtection = signupProtection;
1748
2348
  exports.validateCsrfToken = validateCsrfToken;
1749
2349
  //# sourceMappingURL=index.js.map
1750
2350
  //# sourceMappingURL=index.js.map