@arcis/node 1.5.2 → 1.6.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.
Files changed (80) hide show
  1. package/README.md +48 -7
  2. package/dist/astro/index.js.map +1 -1
  3. package/dist/astro/index.mjs.map +1 -1
  4. package/dist/bun/index.js.map +1 -1
  5. package/dist/bun/index.mjs.map +1 -1
  6. package/dist/core/constants.d.ts +2 -2
  7. package/dist/core/constants.d.ts.map +1 -1
  8. package/dist/core/index.js +19 -1
  9. package/dist/core/index.js.map +1 -1
  10. package/dist/core/index.mjs +19 -1
  11. package/dist/core/index.mjs.map +1 -1
  12. package/dist/fastify/index.js.map +1 -1
  13. package/dist/fastify/index.mjs.map +1 -1
  14. package/dist/hono/index.js.map +1 -1
  15. package/dist/hono/index.mjs.map +1 -1
  16. package/dist/index.d.ts +3 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +407 -8
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs +407 -9
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/koa/index.js.map +1 -1
  23. package/dist/koa/index.mjs.map +1 -1
  24. package/dist/logging/index.js.map +1 -1
  25. package/dist/logging/index.mjs.map +1 -1
  26. package/dist/middleware/astro.d.ts +6 -1
  27. package/dist/middleware/astro.d.ts.map +1 -1
  28. package/dist/middleware/bun.d.ts +8 -1
  29. package/dist/middleware/bun.d.ts.map +1 -1
  30. package/dist/middleware/correlation.d.ts +87 -0
  31. package/dist/middleware/correlation.d.ts.map +1 -0
  32. package/dist/middleware/graphql.d.ts.map +1 -1
  33. package/dist/middleware/hono.d.ts +6 -0
  34. package/dist/middleware/hono.d.ts.map +1 -1
  35. package/dist/middleware/index.d.ts +3 -1
  36. package/dist/middleware/index.d.ts.map +1 -1
  37. package/dist/middleware/index.js +366 -8
  38. package/dist/middleware/index.js.map +1 -1
  39. package/dist/middleware/index.mjs +366 -9
  40. package/dist/middleware/index.mjs.map +1 -1
  41. package/dist/middleware/koa.d.ts +5 -0
  42. package/dist/middleware/koa.d.ts.map +1 -1
  43. package/dist/middleware/nextjs.d.ts +9 -1
  44. package/dist/middleware/nextjs.d.ts.map +1 -1
  45. package/dist/middleware/nuxt.d.ts +6 -1
  46. package/dist/middleware/nuxt.d.ts.map +1 -1
  47. package/dist/middleware/protect.d.ts +32 -0
  48. package/dist/middleware/protect.d.ts.map +1 -1
  49. package/dist/middleware/sveltekit.d.ts +6 -1
  50. package/dist/middleware/sveltekit.d.ts.map +1 -1
  51. package/dist/nestjs/index.js +55 -2
  52. package/dist/nestjs/index.js.map +1 -1
  53. package/dist/nestjs/index.mjs +55 -2
  54. package/dist/nestjs/index.mjs.map +1 -1
  55. package/dist/nextjs/index.js.map +1 -1
  56. package/dist/nextjs/index.mjs.map +1 -1
  57. package/dist/nuxt/index.js.map +1 -1
  58. package/dist/nuxt/index.mjs.map +1 -1
  59. package/dist/sanitizers/deserialization.d.ts +30 -0
  60. package/dist/sanitizers/deserialization.d.ts.map +1 -0
  61. package/dist/sanitizers/graphql.d.ts +20 -3
  62. package/dist/sanitizers/graphql.d.ts.map +1 -1
  63. package/dist/sanitizers/index.d.ts +2 -0
  64. package/dist/sanitizers/index.d.ts.map +1 -1
  65. package/dist/sanitizers/index.js +150 -7
  66. package/dist/sanitizers/index.js.map +1 -1
  67. package/dist/sanitizers/index.mjs +149 -8
  68. package/dist/sanitizers/index.mjs.map +1 -1
  69. package/dist/sanitizers/prompt-injection.d.ts.map +1 -1
  70. package/dist/sanitizers/sanitize.d.ts +0 -20
  71. package/dist/sanitizers/sanitize.d.ts.map +1 -1
  72. package/dist/stores/index.js.map +1 -1
  73. package/dist/stores/index.mjs.map +1 -1
  74. package/dist/sveltekit/index.js.map +1 -1
  75. package/dist/sveltekit/index.mjs.map +1 -1
  76. package/dist/validation/index.js +55 -2
  77. package/dist/validation/index.js.map +1 -1
  78. package/dist/validation/index.mjs +55 -2
  79. package/dist/validation/index.mjs.map +1 -1
  80. package/package.json +11 -11
package/dist/index.mjs CHANGED
@@ -142,7 +142,16 @@ var SQL_PATTERNS = [
142
142
  /** Time-based blind: PostgreSQL pg_sleep() */
143
143
  /\bpg_sleep\s*\(/gi,
144
144
  /** Time-based blind: MSSQL WAITFOR DELAY */
145
- /\bWAITFOR\s+DELAY\b/gi
145
+ /\bWAITFOR\s+DELAY\b/gi,
146
+ /**
147
+ * Oracle DBMS_* stdlib packages used for time-based blind SQLi
148
+ * (DBMS_LOCK.SLEEP, DBMS_PIPE.RECEIVE_MESSAGE) and other Oracle
149
+ * abuse paths. No legitimate user input contains these. Mirrors
150
+ * `sqli-oracle-dbms-packages` in packages/core/patterns.json —
151
+ * improvements.md §1.1.e Q3. Must stay in sync until Node
152
+ * migrates to patterns.json-at-runtime (planned v1.7).
153
+ */
154
+ /\bDBMS_(?:LOCK|PIPE|UTILITY|XSLPROCESSOR|JAVA|OUTPUT|SCHEDULER)\b/gi
146
155
  ];
147
156
  var PATH_PATTERNS = [
148
157
  /** Unix path traversal */
@@ -180,6 +189,15 @@ var COMMAND_PATTERNS = [
180
189
  /[;&|`]/g,
181
190
  /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
182
191
  /\$\(/g,
192
+ /**
193
+ * POSIX shell IFS-substitution: ${IFS} or ${IFS%??}.
194
+ * Attackers use this to inject spaces past metacharacter filters
195
+ * in payloads like `;cat${IFS}/etc/passwd`. Mirrors
196
+ * `cmdi-ifs-bypass` in packages/core/patterns.json — improvements.md
197
+ * §1.1.e Q5. Must stay in sync until Node migrates to
198
+ * patterns.json-at-runtime (planned v1.7).
199
+ */
200
+ /\$\{IFS(?:%[^}]*)?\}/g,
183
201
  /** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
184
202
  /%0[0-9a-f]/gi
185
203
  ];
@@ -1124,6 +1142,40 @@ function detectHeaderInjection(input) {
1124
1142
  }
1125
1143
 
1126
1144
  // src/sanitizers/sanitize.ts
1145
+ function multiDecode(value, maxPasses = 4) {
1146
+ for (let i = 0; i < maxPasses; i++) {
1147
+ const prev = value;
1148
+ try {
1149
+ value = decodeURIComponent(value);
1150
+ } catch {
1151
+ }
1152
+ value = htmlEntityDecode(value);
1153
+ if (value === prev) break;
1154
+ }
1155
+ return value;
1156
+ }
1157
+ function htmlEntityDecode(s) {
1158
+ s = s.replace(/&#(\d+);/g, (_m, n) => {
1159
+ const code = parseInt(n, 10);
1160
+ return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
1161
+ });
1162
+ s = s.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => {
1163
+ const code = parseInt(h, 16);
1164
+ return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
1165
+ });
1166
+ const named = {
1167
+ "&lt;": "<",
1168
+ "&gt;": ">",
1169
+ "&amp;": "&",
1170
+ "&quot;": '"',
1171
+ "&apos;": "'",
1172
+ "&nbsp;": " "
1173
+ };
1174
+ for (const [entity, ch] of Object.entries(named)) {
1175
+ s = s.split(entity).join(ch);
1176
+ }
1177
+ return s;
1178
+ }
1127
1179
  function sanitizeString(value, options = {}) {
1128
1180
  if (typeof value !== "string") return value;
1129
1181
  const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
@@ -1131,7 +1183,8 @@ function sanitizeString(value, options = {}) {
1131
1183
  throw new InputTooLargeError(maxSize, value.length);
1132
1184
  }
1133
1185
  const reject = options.mode === "reject";
1134
- let result = value;
1186
+ let result = value.normalize("NFKC");
1187
+ result = multiDecode(result);
1135
1188
  if (options.sql !== false) {
1136
1189
  if (reject) {
1137
1190
  if (detectSql(result)) {
@@ -1578,7 +1631,9 @@ function encodeForCss(value) {
1578
1631
  var DEFAULTS = {
1579
1632
  maxDepth: 10,
1580
1633
  maxLength: 1e4,
1581
- blockIntrospection: true
1634
+ blockIntrospection: true,
1635
+ maxAliases: 50,
1636
+ blockFragmentCycles: true
1582
1637
  };
1583
1638
  var INTROSPECTION_PATTERN = /\b__(schema|type|typeKind|directive)\b/;
1584
1639
  function computeDepth(query) {
@@ -1595,22 +1650,87 @@ function computeDepth(query) {
1595
1650
  }
1596
1651
  return max;
1597
1652
  }
1653
+ var ALIAS_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
1654
+ var FRAGMENT_DEF_PATTERN = /\bfragment\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+on\s+[a-zA-Z_][a-zA-Z0-9_]*\s*\{/g;
1655
+ var FRAGMENT_SPREAD_PATTERN = /\.\.\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
1656
+ function countAliases(query) {
1657
+ let n = 0;
1658
+ ALIAS_PATTERN.lastIndex = 0;
1659
+ while (ALIAS_PATTERN.exec(query) !== null) n++;
1660
+ return n;
1661
+ }
1662
+ function hasFragmentCycle(query) {
1663
+ const deps = /* @__PURE__ */ new Map();
1664
+ FRAGMENT_DEF_PATTERN.lastIndex = 0;
1665
+ let match;
1666
+ while ((match = FRAGMENT_DEF_PATTERN.exec(query)) !== null) {
1667
+ const name = match[1];
1668
+ const bodyStart = match.index + match[0].length;
1669
+ let depth = 1;
1670
+ let i = bodyStart;
1671
+ while (i < query.length && depth > 0) {
1672
+ const ch = query[i];
1673
+ if (ch === "{") depth++;
1674
+ else if (ch === "}") depth--;
1675
+ i++;
1676
+ }
1677
+ const bodyEnd = depth === 0 ? i - 1 : i;
1678
+ const body = query.slice(bodyStart, bodyEnd);
1679
+ const spreads = /* @__PURE__ */ new Set();
1680
+ FRAGMENT_SPREAD_PATTERN.lastIndex = 0;
1681
+ let sm;
1682
+ while ((sm = FRAGMENT_SPREAD_PATTERN.exec(body)) !== null) {
1683
+ spreads.add(sm[1]);
1684
+ }
1685
+ deps.set(name, spreads);
1686
+ }
1687
+ if (deps.size === 0) return false;
1688
+ const WHITE = 0;
1689
+ const GRAY = 1;
1690
+ const BLACK = 2;
1691
+ const color = /* @__PURE__ */ new Map();
1692
+ for (const name of deps.keys()) color.set(name, WHITE);
1693
+ function visit(name) {
1694
+ if (color.get(name) === GRAY) return true;
1695
+ if (color.get(name) === BLACK) return false;
1696
+ if (!deps.has(name)) return false;
1697
+ color.set(name, GRAY);
1698
+ for (const child of deps.get(name)) {
1699
+ if (visit(child)) return true;
1700
+ }
1701
+ color.set(name, BLACK);
1702
+ return false;
1703
+ }
1704
+ for (const name of deps.keys()) {
1705
+ if (visit(name)) return true;
1706
+ }
1707
+ return false;
1708
+ }
1598
1709
  function inspectGraphqlQuery(query, options = {}) {
1599
1710
  const maxDepth = options.maxDepth ?? DEFAULTS.maxDepth;
1600
1711
  const maxLength = options.maxLength ?? DEFAULTS.maxLength;
1601
1712
  const blockIntrospection = options.blockIntrospection ?? DEFAULTS.blockIntrospection;
1713
+ const maxAliases = options.maxAliases ?? DEFAULTS.maxAliases;
1714
+ const blockFragmentCycles = options.blockFragmentCycles ?? DEFAULTS.blockFragmentCycles;
1602
1715
  const length = query.length;
1603
1716
  const depth = computeDepth(query);
1717
+ const aliases = countAliases(query);
1604
1718
  if (depth > maxDepth) {
1605
- return { blocked: true, reason: "depth", depth, length };
1719
+ return { blocked: true, reason: "depth", depth, length, aliases };
1606
1720
  }
1607
1721
  if (blockIntrospection && INTROSPECTION_PATTERN.test(query)) {
1608
- return { blocked: true, reason: "introspection", depth, length };
1722
+ return { blocked: true, reason: "introspection", depth, length, aliases };
1723
+ }
1724
+ if (aliases > maxAliases) {
1725
+ return { blocked: true, reason: "aliases", depth, length, aliases };
1726
+ }
1727
+ if (blockFragmentCycles && hasFragmentCycle(query)) {
1728
+ return { blocked: true, reason: "fragment_cycle", depth, length, aliases };
1609
1729
  }
1610
1730
  if (length > maxLength) {
1611
- return { blocked: true, reason: "length", depth, length };
1731
+ return { blocked: true, reason: "length", depth, length, aliases };
1612
1732
  }
1613
- return { blocked: false, depth, length };
1733
+ return { blocked: false, depth, length, aliases };
1614
1734
  }
1615
1735
  function detectGraphqlAbuse(query, options) {
1616
1736
  if (typeof query !== "string" || query.length === 0) return false;
@@ -9946,6 +10066,46 @@ function signupProtection(options = {}) {
9946
10066
  }
9947
10067
 
9948
10068
  // src/middleware/protect.ts
10069
+ function getClientIp(req) {
10070
+ const xff = req?.headers?.["x-forwarded-for"] ?? req?.headers?.["X-Forwarded-For"];
10071
+ if (typeof xff === "string" && xff.length > 0) {
10072
+ const first = xff.split(",")[0]?.trim();
10073
+ if (first) return first;
10074
+ }
10075
+ if (typeof req?.ip === "string") return req.ip;
10076
+ const remote = req?.socket?.remoteAddress;
10077
+ return typeof remote === "string" ? remote : "";
10078
+ }
10079
+ function correlationMiddleware(opts) {
10080
+ const vector = opts.vector ?? "request";
10081
+ const usernameField = opts.usernameField ?? "username";
10082
+ const statusCode = opts.statusCode ?? 429;
10083
+ const message = opts.message ?? "Suspicious request pattern detected.";
10084
+ return function(req, res, next) {
10085
+ const ip = getClientIp(req);
10086
+ if (!ip) return next();
10087
+ const route = opts.route ?? (req.path || req.url || "/");
10088
+ const username = req?.body?.[usernameField];
10089
+ const distinctValue = typeof username === "string" && username.length > 0 ? username : void 0;
10090
+ const detections = opts.window.record(
10091
+ ip,
10092
+ vector,
10093
+ route,
10094
+ req.method || "GET",
10095
+ distinctValue
10096
+ );
10097
+ if (detections.scanner || detections.credentialStuffing || detections.raceWindow) {
10098
+ res.status(statusCode).json({
10099
+ error: message,
10100
+ scanner: detections.scanner,
10101
+ credential_stuffing: detections.credentialStuffing,
10102
+ race_window: detections.raceWindow
10103
+ });
10104
+ return;
10105
+ }
10106
+ next();
10107
+ };
10108
+ }
9949
10109
  function resolve(override, defaults) {
9950
10110
  if (override === false) return null;
9951
10111
  if (override === void 0) return defaults;
@@ -9965,6 +10125,11 @@ function protectLogin(options = {}) {
9965
10125
  if (csrf) middlewares.push(csrfProtection(csrf));
9966
10126
  const sanitize = resolve(options.sanitize, {});
9967
10127
  if (sanitize) middlewares.push(createSanitizer(sanitize));
10128
+ if (options.correlation) {
10129
+ middlewares.push(
10130
+ correlationMiddleware({ vector: "login", ...options.correlation })
10131
+ );
10132
+ }
9968
10133
  return middlewares;
9969
10134
  }
9970
10135
  function protectSignup(options = {}) {
@@ -9981,6 +10146,11 @@ function protectSignup(options = {}) {
9981
10146
  if (sanitize) middlewares.push(createSanitizer(sanitize));
9982
10147
  const signup = resolve(options.signup, {});
9983
10148
  if (signup) middlewares.push(signupProtection(signup));
10149
+ if (options.correlation) {
10150
+ middlewares.push(
10151
+ correlationMiddleware({ vector: "signup", ...options.correlation })
10152
+ );
10153
+ }
9984
10154
  return middlewares;
9985
10155
  }
9986
10156
  function protectApi(options = {}) {
@@ -9991,6 +10161,11 @@ function protectApi(options = {}) {
9991
10161
  if (cors) middlewares.push(safeCors(cors));
9992
10162
  const sanitize = resolve(options.sanitize, {});
9993
10163
  if (sanitize) middlewares.push(createSanitizer(sanitize));
10164
+ if (options.correlation) {
10165
+ middlewares.push(
10166
+ correlationMiddleware({ vector: "api", ...options.correlation })
10167
+ );
10168
+ }
9994
10169
  return middlewares;
9995
10170
  }
9996
10171
 
@@ -9998,7 +10173,9 @@ function protectApi(options = {}) {
9998
10173
  var DEFAULT_MESSAGES = {
9999
10174
  depth: "Query exceeds maximum nesting depth",
10000
10175
  length: "Query exceeds maximum length",
10001
- introspection: "Introspection queries are disabled"
10176
+ introspection: "Introspection queries are disabled",
10177
+ aliases: "Query exceeds maximum alias count (alias-bomb protection)",
10178
+ fragment_cycle: "Query contains a cyclic fragment definition"
10002
10179
  };
10003
10180
  function extractQuery(req) {
10004
10181
  const bodyQuery = typeof req.body === "object" && req.body !== null ? req.body.query : void 0;
@@ -10034,6 +10211,186 @@ function graphqlGuard(options = {}) {
10034
10211
  };
10035
10212
  }
10036
10213
 
10214
+ // src/middleware/correlation.ts
10215
+ var EMPTY_DETECTIONS = Object.freeze({
10216
+ scanner: false,
10217
+ credentialStuffing: false,
10218
+ raceWindow: false,
10219
+ distinctVectors: 0,
10220
+ distinctValues: 0,
10221
+ requestsInWindow: 0
10222
+ });
10223
+ function normalizePair(a, b) {
10224
+ return a < b ? `${a}${b}` : `${b}${a}`;
10225
+ }
10226
+ var CorrelationWindow = class {
10227
+ constructor(options = {}) {
10228
+ // Map iteration order in JS is insertion order, so re-inserting on
10229
+ // access gives us LRU behaviour without a separate linked list.
10230
+ this.buckets = /* @__PURE__ */ new Map();
10231
+ const {
10232
+ windowSeconds = 60,
10233
+ maxIps = 1e4,
10234
+ maxEventsPerIp = 200,
10235
+ scannerDistinctVectors = 3,
10236
+ scannerMinRequests = 20,
10237
+ credentialStuffingDistinctValues = 10,
10238
+ raceWindowMs = 200,
10239
+ racePairs
10240
+ } = options;
10241
+ if (windowSeconds <= 0) throw new Error("windowSeconds must be > 0");
10242
+ if (maxIps < 1) throw new Error("maxIps must be >= 1");
10243
+ if (maxEventsPerIp < 1) throw new Error("maxEventsPerIp must be >= 1");
10244
+ this.windowSeconds = windowSeconds;
10245
+ this.maxIps = maxIps;
10246
+ this.maxEventsPerIp = maxEventsPerIp;
10247
+ this.scannerDistinctVectors = scannerDistinctVectors;
10248
+ this.scannerMinRequests = scannerMinRequests;
10249
+ this.csDistinctValues = credentialStuffingDistinctValues;
10250
+ this.raceWindowSeconds = raceWindowMs / 1e3;
10251
+ this.racePairKeys = /* @__PURE__ */ new Set();
10252
+ this.racePairTuples = [];
10253
+ if (racePairs) {
10254
+ for (const [a, b] of racePairs) {
10255
+ const key = normalizePair(a, b);
10256
+ if (!this.racePairKeys.has(key)) {
10257
+ this.racePairKeys.add(key);
10258
+ const sorted = a < b ? [a, b] : [b, a];
10259
+ this.racePairTuples.push(sorted);
10260
+ }
10261
+ }
10262
+ }
10263
+ }
10264
+ record(ip, vector, route, method = "GET", distinctValue, now) {
10265
+ if (!ip) return EMPTY_DETECTIONS;
10266
+ const ts = now ?? Date.now() / 1e3;
10267
+ const event = {
10268
+ timestamp: ts,
10269
+ vector,
10270
+ route,
10271
+ method,
10272
+ distinctValue
10273
+ };
10274
+ let bucket = this.buckets.get(ip);
10275
+ if (bucket === void 0) {
10276
+ bucket = { events: [] };
10277
+ this.buckets.set(ip, bucket);
10278
+ while (this.buckets.size > this.maxIps) {
10279
+ const oldest = this.buckets.keys().next().value;
10280
+ if (oldest === void 0) break;
10281
+ this.buckets.delete(oldest);
10282
+ }
10283
+ } else {
10284
+ this.buckets.delete(ip);
10285
+ this.buckets.set(ip, bucket);
10286
+ }
10287
+ bucket.events.push(event);
10288
+ this.evictStale(bucket, ts);
10289
+ return this.evaluate(bucket, route);
10290
+ }
10291
+ detectScanner(ip, now) {
10292
+ const bucket = this.buckets.get(ip);
10293
+ if (bucket === void 0) return false;
10294
+ this.evictStale(bucket, now ?? Date.now() / 1e3);
10295
+ return this.isScanner(bucket);
10296
+ }
10297
+ detectCredentialStuffing(ip, route, now) {
10298
+ const bucket = this.buckets.get(ip);
10299
+ if (bucket === void 0) return false;
10300
+ this.evictStale(bucket, now ?? Date.now() / 1e3);
10301
+ return this.isCredentialStuffing(bucket, route);
10302
+ }
10303
+ detectRaceWindow(ip, routePair, now) {
10304
+ const bucket = this.buckets.get(ip);
10305
+ if (bucket === void 0) return false;
10306
+ this.evictStale(bucket, now ?? Date.now() / 1e3);
10307
+ const sorted = routePair[0] < routePair[1] ? routePair : [routePair[1], routePair[0]];
10308
+ return this.racePairInBucket(bucket, sorted);
10309
+ }
10310
+ reset(ip) {
10311
+ if (ip === void 0) {
10312
+ this.buckets.clear();
10313
+ } else {
10314
+ this.buckets.delete(ip);
10315
+ }
10316
+ }
10317
+ stats() {
10318
+ let events = 0;
10319
+ for (const b of this.buckets.values()) events += b.events.length;
10320
+ return { trackedIps: this.buckets.size, eventsInWindow: events };
10321
+ }
10322
+ // -------------------------------------------------------- internals
10323
+ evictStale(bucket, now) {
10324
+ const cutoff = now - this.windowSeconds;
10325
+ let drop = 0;
10326
+ while (drop < bucket.events.length && bucket.events[drop].timestamp < cutoff) {
10327
+ drop++;
10328
+ }
10329
+ if (drop > 0) bucket.events.splice(0, drop);
10330
+ if (bucket.events.length > this.maxEventsPerIp) {
10331
+ bucket.events.splice(0, bucket.events.length - this.maxEventsPerIp);
10332
+ }
10333
+ }
10334
+ evaluate(bucket, route) {
10335
+ const vectors = /* @__PURE__ */ new Set();
10336
+ const values = /* @__PURE__ */ new Set();
10337
+ for (const e of bucket.events) {
10338
+ vectors.add(e.vector);
10339
+ if (e.route === route && e.distinctValue !== void 0) {
10340
+ values.add(e.distinctValue);
10341
+ }
10342
+ }
10343
+ return {
10344
+ scanner: this.isScanner(bucket),
10345
+ credentialStuffing: this.isCredentialStuffing(bucket, route),
10346
+ raceWindow: this.isRaceAny(bucket),
10347
+ distinctVectors: vectors.size,
10348
+ distinctValues: values.size,
10349
+ requestsInWindow: bucket.events.length
10350
+ };
10351
+ }
10352
+ isScanner(bucket) {
10353
+ if (bucket.events.length < this.scannerMinRequests) return false;
10354
+ const vectors = /* @__PURE__ */ new Set();
10355
+ for (const e of bucket.events) vectors.add(e.vector);
10356
+ return vectors.size >= this.scannerDistinctVectors;
10357
+ }
10358
+ isCredentialStuffing(bucket, route) {
10359
+ const values = /* @__PURE__ */ new Set();
10360
+ for (const e of bucket.events) {
10361
+ if (e.route === route && e.distinctValue !== void 0) {
10362
+ values.add(e.distinctValue);
10363
+ }
10364
+ }
10365
+ return values.size >= this.csDistinctValues;
10366
+ }
10367
+ racePairInBucket(bucket, sorted) {
10368
+ const [a, b] = sorted;
10369
+ const aTs = [];
10370
+ const bTs = [];
10371
+ for (const e of bucket.events) {
10372
+ if (e.route === a) aTs.push(e.timestamp);
10373
+ else if (e.route === b) bTs.push(e.timestamp);
10374
+ }
10375
+ if (aTs.length === 0 || bTs.length === 0) return false;
10376
+ let ai = 0;
10377
+ let bi = 0;
10378
+ while (ai < aTs.length && bi < bTs.length) {
10379
+ const diff = aTs[ai] - bTs[bi];
10380
+ if (Math.abs(diff) <= this.raceWindowSeconds) return true;
10381
+ if (diff < 0) ai++;
10382
+ else bi++;
10383
+ }
10384
+ return false;
10385
+ }
10386
+ isRaceAny(bucket) {
10387
+ for (const pair of this.racePairTuples) {
10388
+ if (this.racePairInBucket(bucket, pair)) return true;
10389
+ }
10390
+ return false;
10391
+ }
10392
+ };
10393
+
10037
10394
  // src/sanitizers/prompt-injection.ts
10038
10395
  var SIGNATURES = [
10039
10396
  // --- HIGH severity: clear override / jailbreak attempts ---
@@ -10163,6 +10520,47 @@ var SIGNATURES = [
10163
10520
  severity: "medium",
10164
10521
  description: "ROT13 / Caesar-cipher decode hint"
10165
10522
  },
10523
+ // ── V32: AI agent toolcall injection (improvements.md §1.2) ────────
10524
+ // Modern LLM agents (Claude tool-use, OpenAI function-calling,
10525
+ // ReAct loops) read tool definitions from the system prompt and
10526
+ // JSON-shaped requests from the model. A malicious user can embed
10527
+ // those shapes in their input to make the host think they invoked
10528
+ // a tool, or to trick the model into echoing a synthesized
10529
+ // tool_call that the runtime then executes.
10530
+ //
10531
+ // Narrow patterns — match the literal JSON keys and inline
10532
+ // tool-name shapes. Won't false-positive on plain English text
10533
+ // discussing tools.
10534
+ {
10535
+ rule: "agent-toolcall-marker",
10536
+ pattern: /"(?:tool_call|function_call|call_tool|tool_use|toolUse)"\s*:\s*\{/i,
10537
+ severity: "high",
10538
+ description: 'Injected agent tool-call JSON shape (e.g. {"tool_call":{...}})'
10539
+ },
10540
+ {
10541
+ rule: "agent-tool-name-spoof",
10542
+ pattern: /"name"\s*:\s*"(?:exec|shell|run_command|system|bash|cmd|python|eval|read_file|write_file|delete_file)"/i,
10543
+ severity: "high",
10544
+ description: "Forged tool-name attempting privileged tool invocation"
10545
+ },
10546
+ {
10547
+ rule: "agent-tool-result-marker",
10548
+ pattern: /"(?:tool_result|function_result|tool_output)"\s*:\s*[\{\["]/i,
10549
+ severity: "high",
10550
+ description: "Injected fake tool-result block (trick agent into trusting fabricated output)"
10551
+ },
10552
+ {
10553
+ rule: "ansi-escape-sequence",
10554
+ pattern: /\x1b\[/,
10555
+ severity: "medium",
10556
+ description: "ANSI escape sequence (terminal hijack / output spoofing on CLI agents)"
10557
+ },
10558
+ {
10559
+ rule: "claude-tool-use-tags",
10560
+ pattern: /<\/?\s*(?:tool_use|tool_result|invoke|function_calls?|parameter)\b/i,
10561
+ severity: "high",
10562
+ description: "Claude/OpenAI tool-use XML-style tag forgery"
10563
+ },
10166
10564
  // --- LOW severity: ambiguous but worth flagging in strict mode ---
10167
10565
  {
10168
10566
  rule: "from-now-on",
@@ -10716,6 +11114,6 @@ function createRedisStore(options) {
10716
11114
  return new RedisStore(options);
10717
11115
  }
10718
11116
 
10719
- export { ArcisError, ValidationError as ArcisValidationError, BLOCKED, ERRORS, Guards, HEADERS, INPUT, InputTooLargeError, MemoryStore, RATE_LIMIT, REDACTION, RateLimitError, RedisStore, ResponseSplittingError, 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, detectGraphqlAbuse, detectHeaderInjection, detectJsonpInjection, detectNoSqlInjection, detectPathTraversal, detectPii, detectPromptInjection, detectPrototypePollution, detectResponseSplitting, detectSql, detectSsti, detectXss, detectXxe, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, eventLoopProtection, fingerprint, formatDuration, generateCsrfToken, graphqlGuard, hpp, inspectGraphqlQuery, isDangerousExtension, isDangerousNoSqlKey, isDangerousProtoKey, isPrivateIp, isRedirectSafe, isUrlSafe, isValidEmailSyntax, massAssign, methodAllowlist, parseDuration, pinnedDnsLookup, protectApi, protectLogin, protectSignup, rateLimit, redactObjectPii, redactPii, responseSplittingGuard, safeCors, safeFollowRedirect, safeLog, sanitizeCommand, sanitizeFilename, sanitizeHeaderValue, sanitizeHeaders, sanitizeJsonpCallback, sanitizeObject, sanitizePath, sanitizePromptInjection, sanitizeResponseHeader, sanitizeSql, sanitizeSsti, sanitizeString, sanitizeXss, sanitizeXxe, scanObjectPii, scanPii, secureCookieDefaults, securityHeaders, signupProtection, tokenBudget, validate, validateCsrfToken, validateEmail, validateFile, validateRedirect, validateUrl, validateUrlAsync, verifyEmailMx };
11117
+ export { ArcisError, ValidationError as ArcisValidationError, BLOCKED, CorrelationWindow, ERRORS, Guards, HEADERS, INPUT, InputTooLargeError, MemoryStore, RATE_LIMIT, REDACTION, RateLimitError, RedisStore, ResponseSplittingError, 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, detectGraphqlAbuse, detectHeaderInjection, detectJsonpInjection, detectNoSqlInjection, detectPathTraversal, detectPii, detectPromptInjection, detectPrototypePollution, detectResponseSplitting, detectSql, detectSsti, detectXss, detectXxe, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, eventLoopProtection, fingerprint, formatDuration, generateCsrfToken, graphqlGuard, hpp, inspectGraphqlQuery, isDangerousExtension, isDangerousNoSqlKey, isDangerousProtoKey, isPrivateIp, isRedirectSafe, isUrlSafe, isValidEmailSyntax, massAssign, methodAllowlist, parseDuration, pinnedDnsLookup, protectApi, protectLogin, protectSignup, rateLimit, redactObjectPii, redactPii, responseSplittingGuard, safeCors, safeFollowRedirect, safeLog, sanitizeCommand, sanitizeFilename, sanitizeHeaderValue, sanitizeHeaders, sanitizeJsonpCallback, sanitizeObject, sanitizePath, sanitizePromptInjection, sanitizeResponseHeader, sanitizeSql, sanitizeSsti, sanitizeString, sanitizeXss, sanitizeXxe, scanObjectPii, scanPii, secureCookieDefaults, securityHeaders, signupProtection, tokenBudget, validate, validateCsrfToken, validateEmail, validateFile, validateRedirect, validateUrl, validateUrlAsync, verifyEmailMx };
10720
11118
  //# sourceMappingURL=index.mjs.map
10721
11119
  //# sourceMappingURL=index.mjs.map