@arcis/node 1.3.0 → 1.4.2

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 (139) hide show
  1. package/README.md +1 -1
  2. package/dist/core/{index.d.mts → constants.d.ts} +21 -70
  3. package/dist/core/constants.d.ts.map +1 -0
  4. package/dist/core/errors.d.ts +53 -0
  5. package/dist/core/errors.d.ts.map +1 -0
  6. package/dist/core/index.d.ts +6 -168
  7. package/dist/core/index.d.ts.map +1 -0
  8. package/dist/core/index.js +11 -3
  9. package/dist/core/index.js.map +1 -1
  10. package/dist/core/index.mjs +11 -3
  11. package/dist/core/index.mjs.map +1 -1
  12. package/dist/{types-BOkx5YJc.d.mts → core/types.d.ts} +27 -30
  13. package/dist/core/types.d.ts.map +1 -0
  14. package/dist/index.d.ts +71 -166
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +182 -48
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +182 -50
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/logging/index.d.ts +4 -36
  21. package/dist/logging/index.d.ts.map +1 -0
  22. package/dist/logging/index.js.map +1 -1
  23. package/dist/logging/index.mjs.map +1 -1
  24. package/dist/logging/{index.d.mts → redactor.d.ts} +5 -9
  25. package/dist/logging/redactor.d.ts.map +1 -0
  26. package/dist/middleware/bot-detection.d.ts +86 -0
  27. package/dist/middleware/bot-detection.d.ts.map +1 -0
  28. package/dist/middleware/cookies.d.ts +48 -0
  29. package/dist/middleware/cookies.d.ts.map +1 -0
  30. package/dist/middleware/cors.d.ts +65 -0
  31. package/dist/middleware/cors.d.ts.map +1 -0
  32. package/dist/middleware/csrf.d.ts +109 -0
  33. package/dist/middleware/csrf.d.ts.map +1 -0
  34. package/dist/middleware/error-handler.d.ts +43 -0
  35. package/dist/middleware/error-handler.d.ts.map +1 -0
  36. package/dist/middleware/headers.d.ts +29 -0
  37. package/dist/middleware/headers.d.ts.map +1 -0
  38. package/dist/middleware/hpp.d.ts +56 -0
  39. package/dist/middleware/hpp.d.ts.map +1 -0
  40. package/dist/middleware/index.d.ts +16 -3
  41. package/dist/middleware/index.d.ts.map +1 -0
  42. package/dist/middleware/index.js +68 -31
  43. package/dist/middleware/index.js.map +1 -1
  44. package/dist/middleware/index.mjs +69 -32
  45. package/dist/middleware/index.mjs.map +1 -1
  46. package/dist/middleware/main.d.ts +40 -0
  47. package/dist/middleware/main.d.ts.map +1 -0
  48. package/dist/middleware/rate-limit-sliding.d.ts +46 -0
  49. package/dist/middleware/rate-limit-sliding.d.ts.map +1 -0
  50. package/dist/middleware/rate-limit-token.d.ts +51 -0
  51. package/dist/middleware/rate-limit-token.d.ts.map +1 -0
  52. package/dist/middleware/rate-limit.d.ts +34 -0
  53. package/dist/middleware/rate-limit.d.ts.map +1 -0
  54. package/dist/sanitizers/command.d.ts +28 -0
  55. package/dist/sanitizers/command.d.ts.map +1 -0
  56. package/dist/sanitizers/encode.d.ts +46 -0
  57. package/dist/sanitizers/encode.d.ts.map +1 -0
  58. package/dist/sanitizers/headers.d.ts +46 -0
  59. package/dist/sanitizers/headers.d.ts.map +1 -0
  60. package/dist/sanitizers/index.d.ts +18 -22
  61. package/dist/sanitizers/index.d.ts.map +1 -0
  62. package/dist/sanitizers/index.js +90 -32
  63. package/dist/sanitizers/index.js.map +1 -1
  64. package/dist/sanitizers/index.mjs +88 -33
  65. package/dist/sanitizers/index.mjs.map +1 -1
  66. package/dist/sanitizers/jsonp.d.ts +34 -0
  67. package/dist/sanitizers/jsonp.d.ts.map +1 -0
  68. package/dist/sanitizers/ldap.d.ts +42 -0
  69. package/dist/sanitizers/ldap.d.ts.map +1 -0
  70. package/dist/sanitizers/nosql.d.ts +31 -0
  71. package/dist/sanitizers/nosql.d.ts.map +1 -0
  72. package/dist/sanitizers/path.d.ts +28 -0
  73. package/dist/sanitizers/path.d.ts.map +1 -0
  74. package/dist/sanitizers/pii.d.ts +80 -0
  75. package/dist/sanitizers/pii.d.ts.map +1 -0
  76. package/dist/sanitizers/prototype.d.ts +34 -0
  77. package/dist/sanitizers/prototype.d.ts.map +1 -0
  78. package/dist/sanitizers/sanitize.d.ts +51 -0
  79. package/dist/sanitizers/sanitize.d.ts.map +1 -0
  80. package/dist/sanitizers/sql.d.ts +28 -0
  81. package/dist/sanitizers/sql.d.ts.map +1 -0
  82. package/dist/sanitizers/ssti.d.ts +20 -0
  83. package/dist/sanitizers/ssti.d.ts.map +1 -0
  84. package/dist/sanitizers/utils.d.ts +19 -0
  85. package/dist/sanitizers/utils.d.ts.map +1 -0
  86. package/dist/sanitizers/xss.d.ts +35 -0
  87. package/dist/sanitizers/xss.d.ts.map +1 -0
  88. package/dist/sanitizers/xxe.d.ts +20 -0
  89. package/dist/sanitizers/xxe.d.ts.map +1 -0
  90. package/dist/stores/index.d.ts +6 -104
  91. package/dist/stores/index.d.ts.map +1 -0
  92. package/dist/stores/index.js +21 -1
  93. package/dist/stores/index.js.map +1 -1
  94. package/dist/stores/index.mjs +21 -1
  95. package/dist/stores/index.mjs.map +1 -1
  96. package/dist/stores/memory.d.ts +29 -0
  97. package/dist/stores/memory.d.ts.map +1 -0
  98. package/dist/stores/{index.d.mts → redis.d.ts} +6 -45
  99. package/dist/stores/redis.d.ts.map +1 -0
  100. package/dist/utils/duration.d.ts +34 -0
  101. package/dist/utils/duration.d.ts.map +1 -0
  102. package/dist/utils/fingerprint.d.ts +64 -0
  103. package/dist/utils/fingerprint.d.ts.map +1 -0
  104. package/dist/utils/index.d.ts +10 -0
  105. package/dist/utils/index.d.ts.map +1 -0
  106. package/dist/utils/index.js +188 -0
  107. package/dist/utils/index.js.map +1 -0
  108. package/dist/utils/index.mjs +182 -0
  109. package/dist/utils/index.mjs.map +1 -0
  110. package/dist/utils/ip.d.ts +70 -0
  111. package/dist/utils/ip.d.ts.map +1 -0
  112. package/dist/validation/email.d.ts +82 -0
  113. package/dist/validation/email.d.ts.map +1 -0
  114. package/dist/validation/file.d.ts +90 -0
  115. package/dist/validation/file.d.ts.map +1 -0
  116. package/dist/validation/index.d.ts +10 -3
  117. package/dist/validation/index.d.ts.map +1 -0
  118. package/dist/validation/index.js +38 -21
  119. package/dist/validation/index.js.map +1 -1
  120. package/dist/validation/index.mjs +38 -21
  121. package/dist/validation/index.mjs.map +1 -1
  122. package/dist/validation/redirect.d.ts +64 -0
  123. package/dist/validation/redirect.d.ts.map +1 -0
  124. package/dist/validation/schema.d.ts +36 -0
  125. package/dist/validation/schema.d.ts.map +1 -0
  126. package/dist/validation/url.d.ts +65 -0
  127. package/dist/validation/url.d.ts.map +1 -0
  128. package/package.json +8 -6
  129. package/dist/encode-CrQCGlBq.d.mts +0 -484
  130. package/dist/encode-jl9sOwmA.d.ts +0 -484
  131. package/dist/index-BAhgn9V2.d.ts +0 -532
  132. package/dist/index-BGNKspqH.d.ts +0 -340
  133. package/dist/index-Cd02z-0j.d.mts +0 -340
  134. package/dist/index-DgJtWMSj.d.mts +0 -532
  135. package/dist/index.d.mts +0 -175
  136. package/dist/middleware/index.d.mts +0 -3
  137. package/dist/sanitizers/index.d.mts +0 -24
  138. package/dist/types-BOkx5YJc.d.ts +0 -279
  139. package/dist/validation/index.d.mts +0 -3
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/constants.ts","../../src/stores/memory.ts","../../src/stores/redis.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @module @arcis/node/core/constants\n * Named constants for Arcis - no magic numbers\n */\n\n// =============================================================================\n// INPUT LIMITS\n// =============================================================================\nexport const INPUT = {\n /** Default maximum input size (1MB) */\n DEFAULT_MAX_SIZE: 1_000_000,\n /** Maximum recursion depth for nested objects */\n MAX_RECURSION_DEPTH: 10,\n} as const;\n\n// =============================================================================\n// RATE LIMITING\n// =============================================================================\nexport const RATE_LIMIT = {\n /** Default window size (1 minute) */\n DEFAULT_WINDOW_MS: 60_000,\n /** Default max requests per window */\n DEFAULT_MAX_REQUESTS: 100,\n /** Default HTTP status code for rate limited responses */\n DEFAULT_STATUS_CODE: 429,\n /** Default error message */\n DEFAULT_MESSAGE: 'Too many requests, please try again later.',\n /** Minimum window size (1 second) */\n MIN_WINDOW_MS: 1_000,\n /** Maximum window size (24 hours) */\n MAX_WINDOW_MS: 86_400_000,\n} as const;\n\n// =============================================================================\n// SECURITY HEADERS\n// =============================================================================\nexport const HEADERS = {\n /** Default Content Security Policy */\n DEFAULT_CSP: [\n \"default-src 'self'\",\n \"script-src 'self'\",\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: https:\",\n \"font-src 'self'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n ].join('; '),\n /** Default HSTS max age (1 year in seconds) */\n HSTS_MAX_AGE: 31_536_000,\n /** Default X-Frame-Options value */\n FRAME_OPTIONS: 'DENY' as const,\n /** Default X-Content-Type-Options value */\n CONTENT_TYPE_OPTIONS: 'nosniff',\n /** Default Referrer-Policy value */\n REFERRER_POLICY: 'strict-origin-when-cross-origin',\n /** Default Permissions-Policy value */\n PERMISSIONS_POLICY: 'geolocation=(), microphone=(), camera=()',\n /** Default Cache-Control value for security */\n CACHE_CONTROL: 'no-store, no-cache, must-revalidate, proxy-revalidate',\n} as const;\n\n// =============================================================================\n// XSS PATTERNS (ReDoS-safe)\n// =============================================================================\n\n/**\n * Detection patterns — used to flag whether a string contains XSS payloads.\n * Must stay in sync with XSS_REMOVE_PATTERNS below.\n */\nexport const XSS_PATTERNS = [\n /** Script tags (ReDoS-safe version) */\n /<script[^>]*>[\\s\\S]*?<\\/script>/gi,\n /** javascript: protocol (allow optional spaces before colon) */\n /javascript\\s*:/gi,\n /** vbscript: protocol */\n /vbscript\\s*:/gi,\n /** Event handlers (onclick, onerror, etc.) — any separator before attribute */\n /(?:[\\s/])on\\w+\\s*=/gi,\n /** iframe tags */\n /<iframe/gi,\n /** object tags */\n /<object/gi,\n /** embed tags */\n /<embed/gi,\n /** data: URIs (only dangerous ones, avoid false positives) */\n /(?:^|[\\s\"'=])data:/gi,\n /** URL-encoded script tags */\n /%3Cscript/gi,\n /** SVG with onload */\n /<svg[^>]*onload/gi,\n] as const;\n\n/**\n * Removal patterns — used by sanitizeXss() to strip dangerous content.\n * More targeted than XSS_PATTERNS: each pattern captures the full dangerous\n * substring (tag, attribute + value, protocol) so it can be replaced safely.\n * Must stay in sync with XSS_PATTERNS above.\n */\nexport const XSS_REMOVE_PATTERNS = [\n /** Full script blocks (content + tags) */\n /<script[^>]*>[\\s\\S]*?<\\/script>/gi,\n /** Standalone/unclosed script tags */\n /<script[^>]*>/gi,\n /** iframe — full block and partial/unclosed */\n /<iframe[^>]*>[\\s\\S]*?<\\/iframe>/gi,\n /<iframe[^>]*/gi,\n /** object — full block and partial/unclosed */\n /<object[^>]*>[\\s\\S]*?<\\/object>/gi,\n /<object[^>]*/gi,\n /** embed tags */\n /<embed[^>]*/gi,\n /** SVG with inline event handlers */\n /<svg[^>]*onload[^>]*>/gi,\n /** URL-encoded script tags */\n /%3Cscript/gi,\n /** Event handlers with quoted values: onclick=\"...\", onerror='...' */\n /(?:[\\s/])on\\w+\\s*=\\s*[\"'][^\"']*[\"']/gi,\n /** Event handlers with unquoted values: onload=value */\n /(?:[\\s/])on\\w+\\s*=\\s*[^\\s>]*/gi,\n /** javascript: and vbscript: protocols (allow optional spaces before colon) */\n /javascript\\s*:/gi,\n /vbscript\\s*:/gi,\n /** data: URIs with HTML/script content */\n /data\\s*:\\s*text\\/html[^>\\s]*/gi,\n] as const;\n\n// =============================================================================\n// SQL INJECTION PATTERNS\n// =============================================================================\nexport const SQL_PATTERNS = [\n /** SQL keywords */\n /(\\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)\\b)/gi,\n /** SQL comments: ANSI (--), C-style (slash-star ... star-slash), MySQL (#) */\n /(--|\\/\\*|\\*\\/|#)/g,\n /** SQL statement separators */\n /(;|\\|\\||&&)/g,\n /** Boolean injection: OR 1=1 */\n /\\bOR\\s+\\d+\\s*=\\s*\\d+/gi,\n /** Boolean injection: OR 'a'='a' or OR \"a\"=\"a\" (including mixed quotes) */\n /\\bOR\\s+(['\"])[^'\"]*\\1\\s*=\\s*(['\"])[^'\"]*\\2/gi,\n /\\bOR\\s+('[^']*'|\"[^\"]*\")\\s*=\\s*('[^']*'|\"[^\"]*\")/gi,\n /** Boolean injection: AND 1=1 */\n /\\bAND\\s+\\d+\\s*=\\s*\\d+/gi,\n /** Boolean injection: AND 'a'='a' or AND \"a\"=\"a\" (including mixed quotes) */\n /\\bAND\\s+(['\"])[^'\"]*\\1\\s*=\\s*(['\"])[^'\"]*\\2/gi,\n /\\bAND\\s+('[^']*'|\"[^\"]*\")\\s*=\\s*('[^']*'|\"[^\"]*\")/gi,\n /** Time-based blind: SLEEP() */\n /\\bSLEEP\\s*\\(\\s*\\d+\\s*\\)/gi,\n /** Time-based blind: BENCHMARK() */\n /\\bBENCHMARK\\s*\\(/gi,\n /** Time-based blind: PostgreSQL pg_sleep() */\n /\\bpg_sleep\\s*\\(/gi,\n /** Time-based blind: MSSQL WAITFOR DELAY */\n /\\bWAITFOR\\s+DELAY\\b/gi,\n] as const;\n\n// =============================================================================\n// PATH TRAVERSAL PATTERNS\n// =============================================================================\nexport const PATH_PATTERNS = [\n /** Unix path traversal */\n /\\.\\.\\//g,\n /** Windows path traversal */\n /\\.\\.\\\\/g,\n /** URL-encoded traversal (%2e%2e) */\n /%2e%2e/gi,\n /** Double URL-encoded traversal (%252e) */\n /%252e/gi,\n /** Mixed encoding: ..%2F */\n /\\.\\.%2F/gi,\n /** Mixed encoding: %2e./ and .%2e/ */\n /%2e\\.[\\\\/]/gi,\n /\\.%2e[\\\\/]/gi,\n /** Fully URL-encoded: %2e%2e%2f */\n /%2e%2e%2f/gi,\n /** Double URL-encoded forward slash: %252f */\n /%252f/gi,\n /** Dotdotslash bypass: ....// or ....\\\\ */\n /\\.{2,}[/\\\\]{2,}/g,\n /** Null byte injection in paths */\n /\\0/g,\n] as const;\n\n// =============================================================================\n// COMMAND INJECTION PATTERNS\n// =============================================================================\nexport const COMMAND_PATTERNS = [\n /**\n * Shell metacharacters that enable command chaining/substitution.\n * Bare ( and ) are excluded — they appear in common legitimate values\n * (function calls in code fields, math expressions, etc.).\n * Command substitution is caught by the $( combined pattern below.\n * NOTE: ';', '&', '|' may appear in legitimate URL query strings\n * and Markdown; consider disabling command checking (command: false)\n * for fields that intentionally allow those characters.\n */\n /[;&|`]/g,\n /** Command substitution: $( ... ) — matched as a pair to reduce false positives */\n /\\$\\(/g,\n /** URL-encoded newline/carriage-return injection (%0a, %0d) */\n /%0[ad]/gi,\n] as const;\n\n// =============================================================================\n// DANGEROUS KEYS\n// =============================================================================\n\n/**\n * Prototype pollution keys to block.\n * Stored lowercase — always compare with key.toLowerCase().\n *\n * Includes:\n * - __proto__: direct prototype assignment\n * - constructor: access to constructor.prototype chain\n * - prototype: direct prototype property\n * - __defineGetter__/__defineSetter__: legacy property definition (can override getters/setters)\n * - __lookupGetter__/__lookupSetter__: legacy property introspection\n */\nexport const DANGEROUS_PROTO_KEYS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n '__definegetter__',\n '__definesetter__',\n '__lookupgetter__',\n '__lookupsetter__',\n]);\n\n/** MongoDB operators to block */\nexport const NOSQL_DANGEROUS_KEYS = new Set([\n // Comparison\n '$gt', '$gte', '$lt', '$lte', '$ne', '$eq', '$in', '$nin',\n // Logical\n '$and', '$or', '$not', '$nor',\n // Element / evaluation\n '$exists', '$type', '$regex', '$where', '$expr', '$mod', '$text', '$jsonSchema',\n // Array\n '$elemMatch', '$all', '$size',\n // JavaScript execution (critical)\n '$function', '$accumulator',\n // Aggregation pipeline operators (injectable via $lookup etc.)\n '$lookup', '$match', '$project', '$group', '$sort', '$limit', '$skip',\n '$unwind', '$addFields', '$replaceRoot',\n]);\n\n// =============================================================================\n// REDACTION\n// =============================================================================\nexport const REDACTION = {\n /** Replacement text for redacted values */\n REPLACEMENT: '[REDACTED]',\n /** Truncation indicator */\n TRUNCATED: '[TRUNCATED]',\n /** Max depth indicator */\n MAX_DEPTH: '[MAX_DEPTH]',\n /** Default max message length */\n DEFAULT_MAX_LENGTH: 10_000,\n /** Default sensitive keys to redact */\n SENSITIVE_KEYS: new Set([\n 'password', 'passwd', 'pwd', 'secret', 'token', 'apikey',\n 'api_key', 'apiKey', 'auth', 'authorization', 'credit_card',\n 'creditcard', 'cc', 'ssn', 'social_security', 'private_key',\n 'privateKey', 'access_token', 'accessToken', 'refresh_token',\n 'refreshToken', 'bearer', 'jwt', 'session', 'cookie',\n 'credentials', 'x-api-key', 'x-auth-token',\n ]),\n} as const;\n\n// =============================================================================\n// VALIDATION PATTERNS\n// =============================================================================\nexport const VALIDATION = {\n /**\n * Email regex pattern.\n * Rejects consecutive dots in local part (e.g. test..foo@example.com),\n * leading/trailing dots, and other common invalid forms.\n */\n EMAIL: /^[^\\s@.][^\\s@]*(?:\\.[^\\s@.][^\\s@]*)*@[^\\s@]+\\.[^\\s@]+$/,\n /**\n * URL regex pattern.\n * Only allows http:// and https:// — explicitly rejects javascript:,\n * data:, vbscript:, and other dangerous URI schemes.\n */\n URL: /^https?:\\/\\/[^\\s/$.?#][^\\s]*$/,\n /** UUID regex pattern (v4) */\n UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,\n} as const;\n\n// =============================================================================\n// ERROR MESSAGES\n// =============================================================================\nexport const ERRORS = {\n /** Generic error message (production) */\n INTERNAL_SERVER_ERROR: 'Internal Server Error',\n /** Input too large error */\n INPUT_TOO_LARGE: (maxSize: number) => `Input exceeds maximum size of ${maxSize} bytes`,\n /** Validation error messages */\n VALIDATION: {\n REQUIRED: (field: string) => `${field} is required`,\n INVALID_TYPE: (field: string, type: string) => `${field} must be a ${type}`,\n MIN_LENGTH: (field: string, min: number) => `${field} must be at least ${min} characters`,\n MAX_LENGTH: (field: string, max: number) => `${field} must be at most ${max} characters`,\n MIN_VALUE: (field: string, min: number) => `${field} must be at least ${min}`,\n MAX_VALUE: (field: string, max: number) => `${field} must be at most ${max}`,\n INVALID_FORMAT: (field: string) => `${field} format is invalid`,\n INVALID_EMAIL: (field: string) => `${field} must be a valid email`,\n INVALID_URL: (field: string) => `${field} must be a valid URL`,\n INVALID_UUID: (field: string) => `${field} must be a valid UUID`,\n INVALID_ENUM: (field: string, values: unknown[]) => `${field} must be one of: ${values.join(', ')}`,\n MIN_ITEMS: (field: string, min: number) => `${field} must have at least ${min} items`,\n MAX_ITEMS: (field: string, max: number) => `${field} must have at most ${max} items`,\n },\n} as const;\n\n// =============================================================================\n// BLOCKED TEXT (for sanitizer replacements)\n// =============================================================================\nexport const BLOCKED = '[BLOCKED]' as const;\n","/**\n * @module @arcis/node/stores/memory\n * In-memory rate limit store\n */\n\nimport type { RateLimitStore, RateLimitEntry } from '../core/types';\nimport { RATE_LIMIT } from '../core/constants';\n\n/**\n * In-memory rate limit store.\n * Suitable for single-instance deployments.\n * For distributed systems, use RedisStore or a custom store.\n * \n * @example\n * const store = new MemoryStore(60000); // 1 minute window\n * const limiter = createRateLimiter({ store });\n */\nexport class MemoryStore implements RateLimitStore {\n private store: Map<string, RateLimitEntry> = new Map();\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n private windowMs: number;\n\n constructor(windowMs: number = RATE_LIMIT.DEFAULT_WINDOW_MS) {\n if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {\n throw new RangeError(\n `MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`\n );\n }\n this.windowMs = windowMs;\n this.startCleanup();\n }\n\n /**\n * Start the cleanup interval to remove expired entries.\n */\n private startCleanup(): void {\n // Clamp the cleanup interval between 30 s and 5 min regardless of windowMs.\n // Running it every windowMs is fine for typical windows but would fire every\n // second for short windows (e.g. windowMs: 1000), causing O(n) GC pressure.\n const CLEANUP_MIN_MS = 30_000;\n const CLEANUP_MAX_MS = 300_000;\n const cleanupMs = Math.min(Math.max(this.windowMs, CLEANUP_MIN_MS), CLEANUP_MAX_MS);\n\n this.cleanupInterval = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.store.entries()) {\n if (entry.resetTime < now) {\n this.store.delete(key);\n }\n }\n }, cleanupMs);\n\n // Prevent interval from keeping the process alive\n if (typeof this.cleanupInterval.unref === 'function') {\n this.cleanupInterval.unref();\n }\n }\n\n async get(key: string): Promise<RateLimitEntry | null> {\n const entry = this.store.get(key);\n if (!entry) return null;\n \n // Check if expired\n if (entry.resetTime < Date.now()) {\n this.store.delete(key);\n return null;\n }\n \n return entry;\n }\n\n async set(key: string, entry: RateLimitEntry): Promise<void> {\n this.store.set(key, entry);\n }\n\n async increment(key: string): Promise<number> {\n const now = Date.now();\n const entry = this.store.get(key);\n \n if (!entry || entry.resetTime < now) {\n // Start new window\n this.store.set(key, { count: 1, resetTime: now + this.windowMs });\n return 1;\n }\n \n entry.count++;\n return entry.count;\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.store.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n async reset(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n async close(): Promise<void> {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n this.store.clear();\n }\n\n /**\n * Get current store size (for monitoring).\n */\n get size(): number {\n return this.store.size;\n }\n}\n","/**\n * @module @arcis/node/stores/redis\n * Redis rate limit store\n * \n * Note: This is a reference implementation. You'll need to install\n * the 'ioredis' or 'redis' package and pass your client instance.\n */\n\nimport type { RateLimitStore, RateLimitEntry } from '../core/types';\nimport { RATE_LIMIT } from '../core/constants';\n\n/** Generic Redis client interface (works with ioredis, redis, etc.) */\nexport interface RedisClientLike {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, mode?: string, duration?: number): Promise<unknown>;\n setex(key: string, seconds: number, value: string): Promise<unknown>;\n expire(key: string, seconds: number): Promise<unknown>;\n incr(key: string): Promise<number>;\n decr(key: string): Promise<number>;\n del(key: string): Promise<number>;\n ttl(key: string): Promise<number>;\n quit?(): Promise<unknown>;\n disconnect?(): Promise<unknown>;\n}\n\nexport interface RedisStoreOptions {\n /** Redis client instance */\n client: RedisClientLike;\n /** Key prefix. Default: 'arcis:rl:' */\n prefix?: string;\n /** Window size in milliseconds. Default: 60000 */\n windowMs?: number;\n}\n\n/**\n * Redis rate limit store for distributed deployments.\n * \n * @example\n * import Redis from 'ioredis';\n * \n * const redis = new Redis();\n * const store = new RedisStore({ client: redis });\n * const limiter = createRateLimiter({ store });\n * \n * // Cleanup on shutdown\n * process.on('SIGTERM', async () => {\n * await store.close();\n * });\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClientLike;\n private prefix: string;\n private windowMs: number;\n private windowSec: number;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? 'arcis:rl:';\n this.windowMs = options.windowMs ?? RATE_LIMIT.DEFAULT_WINDOW_MS;\n this.windowSec = Math.ceil(this.windowMs / 1000);\n }\n\n private getKey(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n async get(key: string): Promise<RateLimitEntry | null> {\n const redisKey = this.getKey(key);\n \n const [countStr, ttl] = await Promise.all([\n this.client.get(redisKey),\n this.client.ttl(redisKey),\n ]);\n \n if (!countStr || ttl < 0) {\n return null;\n }\n \n const count = parseInt(countStr, 10);\n if (isNaN(count)) {\n // Corrupt value in Redis — treat as if key doesn't exist\n return null;\n }\n\n return {\n count,\n resetTime: Date.now() + (ttl * 1000),\n };\n }\n\n async set(key: string, entry: RateLimitEntry): Promise<void> {\n const redisKey = this.getKey(key);\n // Clamp to at least 1 second — Math.ceil can produce 0 or negative values\n // when entry.resetTime is in the past due to Redis latency or clock skew.\n const ttlSec = Math.max(1, Math.ceil((entry.resetTime - Date.now()) / 1000));\n await this.client.setex(redisKey, ttlSec, entry.count.toString());\n }\n\n async increment(key: string): Promise<number> {\n const redisKey = this.getKey(key);\n \n // INCR creates key with value 1 if it doesn't exist\n const count = await this.client.incr(redisKey);\n\n // Set expiry only on first increment using EXPIRE, which sets the TTL\n // without overwriting the value (unlike SET ... EX which would reset the\n // counter if two requests increment concurrently before expiry is set).\n if (count === 1) {\n await this.client.expire(redisKey, this.windowSec);\n }\n\n return count;\n }\n\n async decrement(key: string): Promise<void> {\n const redisKey = this.getKey(key);\n await this.client.decr(redisKey);\n }\n\n async reset(key: string): Promise<void> {\n const redisKey = this.getKey(key);\n await this.client.del(redisKey);\n }\n\n async close(): Promise<void> {\n // Don't close the client - it may be shared\n // The caller should manage the client lifecycle\n }\n}\n\n/**\n * Create a Redis store with the given options.\n * Convenience function for functional programming style.\n * \n * @example\n * const store = createRedisStore({ client: redisClient });\n */\nexport function createRedisStore(options: RedisStoreOptions): RedisStore {\n return new RedisStore(options);\n}\n"]}
1
+ {"version":3,"sources":["../../src/core/constants.ts","../../src/stores/memory.ts","../../src/stores/redis.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @module @arcis/node/core/constants\n * Named constants for Arcis - no magic numbers\n */\n\n// =============================================================================\n// INPUT LIMITS\n// =============================================================================\nexport const INPUT = {\n /** Default maximum input size (1MB) */\n DEFAULT_MAX_SIZE: 1_000_000,\n /** Maximum recursion depth for nested objects */\n MAX_RECURSION_DEPTH: 10,\n} as const;\n\n// =============================================================================\n// RATE LIMITING\n// =============================================================================\nexport const RATE_LIMIT = {\n /** Default window size (1 minute) */\n DEFAULT_WINDOW_MS: 60_000,\n /** Default max requests per window */\n DEFAULT_MAX_REQUESTS: 100,\n /** Default HTTP status code for rate limited responses */\n DEFAULT_STATUS_CODE: 429,\n /** Default error message */\n DEFAULT_MESSAGE: 'Too many requests, please try again later.',\n /** Minimum window size (1 second) */\n MIN_WINDOW_MS: 1_000,\n /** Maximum window size (24 hours) */\n MAX_WINDOW_MS: 86_400_000,\n} as const;\n\n// =============================================================================\n// SECURITY HEADERS\n// =============================================================================\nexport const HEADERS = {\n /** Default Content Security Policy */\n DEFAULT_CSP: [\n \"default-src 'self'\",\n \"script-src 'self'\",\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: https:\",\n \"font-src 'self'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n ].join('; '),\n /** Default HSTS max age (1 year in seconds) */\n HSTS_MAX_AGE: 31_536_000,\n /** Default X-Frame-Options value */\n FRAME_OPTIONS: 'DENY' as const,\n /** Default X-Content-Type-Options value */\n CONTENT_TYPE_OPTIONS: 'nosniff',\n /** Default Referrer-Policy value */\n REFERRER_POLICY: 'strict-origin-when-cross-origin',\n /** Default Permissions-Policy value */\n PERMISSIONS_POLICY: 'geolocation=(), microphone=(), camera=()',\n /** Default Cache-Control value for security */\n CACHE_CONTROL: 'no-store, no-cache, must-revalidate, proxy-revalidate',\n} as const;\n\n// =============================================================================\n// XSS PATTERNS (ReDoS-safe)\n// =============================================================================\n\n/**\n * Detection patterns — used to flag whether a string contains XSS payloads.\n * Must stay in sync with XSS_REMOVE_PATTERNS below.\n */\nexport const XSS_PATTERNS = [\n /** Script tags (ReDoS-safe version) */\n /<script[^>]*>[\\s\\S]*?<\\/script>/gi,\n /** javascript: protocol (allow optional spaces before colon) */\n /javascript\\s*:/gi,\n /** vbscript: protocol */\n /vbscript\\s*:/gi,\n /** Event handlers (onclick, onerror, etc.) — any separator before attribute */\n /(?:[\\s/])on\\w+\\s*=/gi,\n /** iframe tags */\n /<iframe/gi,\n /** object tags */\n /<object/gi,\n /** embed tags */\n /<embed/gi,\n /** data: URIs (only dangerous ones, avoid false positives) */\n /(?:^|[\\s\"'=])data:/gi,\n /** URL-encoded script tags */\n /%3Cscript/gi,\n /** SVG with onload */\n /<svg[^>]*onload/gi,\n /** form tags — phishing/credential harvesting via action= redirection */\n /<form[\\s>]/gi,\n /** meta tags — http-equiv refresh redirects or CSP bypass */\n /<meta[\\s>]/gi,\n /** base href hijacking — redirects all relative URLs to attacker domain */\n /<base[\\s>]/gi,\n /** link tag injection — stylesheet or preload CSRF attacks */\n /<link[\\s>]/gi,\n] as const;\n\n/**\n * Removal patterns — used by sanitizeXss() to strip dangerous content.\n * More targeted than XSS_PATTERNS: each pattern captures the full dangerous\n * substring (tag, attribute + value, protocol) so it can be replaced safely.\n * Must stay in sync with XSS_PATTERNS above.\n */\nexport const XSS_REMOVE_PATTERNS = [\n /** Full script blocks (content + tags) */\n /<script[^>]*>[\\s\\S]*?<\\/script>/gi,\n /** Standalone/unclosed script tags */\n /<script[^>]*>/gi,\n /** iframe — full block and partial/unclosed */\n /<iframe[^>]*>[\\s\\S]*?<\\/iframe>/gi,\n /<iframe[^>]*/gi,\n /** object — full block and partial/unclosed */\n /<object[^>]*>[\\s\\S]*?<\\/object>/gi,\n /<object[^>]*/gi,\n /** embed tags */\n /<embed[^>]*/gi,\n /** SVG with inline event handlers */\n /<svg[^>]*onload[^>]*>/gi,\n /** URL-encoded script tags */\n /%3Cscript/gi,\n /** Event handlers with quoted values: onclick=\"...\", onerror='...' */\n /(?:[\\s/])on\\w+\\s*=\\s*[\"'][^\"']*[\"']/gi,\n /** Event handlers with unquoted values: onload=value */\n /(?:[\\s/])on\\w+\\s*=\\s*[^\\s>]*/gi,\n /** javascript: and vbscript: protocols (allow optional spaces before colon) */\n /javascript\\s*:/gi,\n /vbscript\\s*:/gi,\n /** data: URIs with HTML/script content */\n /data\\s*:\\s*text\\/html[^>\\s]*/gi,\n /** form tag injection — phishing via action= redirection */\n /<form[\\s>][^>]*/gi,\n /** meta tag injection — http-equiv refresh or CSP bypass */\n /<meta[\\s>][^>]*/gi,\n /** base href hijacking */\n /<base[\\s>][^>]*/gi,\n /** link tag injection — stylesheet or preload attacks */\n /<link[\\s>][^>]*/gi,\n] as const;\n\n// =============================================================================\n// SQL INJECTION PATTERNS\n// =============================================================================\nexport const SQL_PATTERNS = [\n /** SQL keywords */\n /(\\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)\\b)/gi,\n /** SQL comments: ANSI (--), C-style (slash-star ... star-slash), MySQL (#) */\n /(--|\\/\\*|\\*\\/|#)/g,\n /** SQL statement separators */\n /(;|\\|\\||&&)/g,\n /** Boolean injection: OR 1=1 */\n /\\bOR\\s+\\d+\\s*=\\s*\\d+/gi,\n /** Boolean injection: OR 'a'='a' or OR \"a\"=\"a\" (including mixed quotes) */\n /\\bOR\\s+(['\"])[^'\"]*\\1\\s*=\\s*(['\"])[^'\"]*\\2/gi,\n /\\bOR\\s+('[^']*'|\"[^\"]*\")\\s*=\\s*('[^']*'|\"[^\"]*\")/gi,\n /** Boolean injection: AND 1=1 */\n /\\bAND\\s+\\d+\\s*=\\s*\\d+/gi,\n /** Boolean injection: AND 'a'='a' or AND \"a\"=\"a\" (including mixed quotes) */\n /\\bAND\\s+(['\"])[^'\"]*\\1\\s*=\\s*(['\"])[^'\"]*\\2/gi,\n /\\bAND\\s+('[^']*'|\"[^\"]*\")\\s*=\\s*('[^']*'|\"[^\"]*\")/gi,\n /** Time-based blind: SLEEP() */\n /\\bSLEEP\\s*\\(\\s*\\d+\\s*\\)/gi,\n /** Time-based blind: BENCHMARK() */\n /\\bBENCHMARK\\s*\\(/gi,\n /** Time-based blind: PostgreSQL pg_sleep() */\n /\\bpg_sleep\\s*\\(/gi,\n /** Time-based blind: MSSQL WAITFOR DELAY */\n /\\bWAITFOR\\s+DELAY\\b/gi,\n] as const;\n\n// =============================================================================\n// PATH TRAVERSAL PATTERNS\n// =============================================================================\nexport const PATH_PATTERNS = [\n /** Unix path traversal */\n /\\.\\.\\//g,\n /** Windows path traversal */\n /\\.\\.\\\\/g,\n /** URL-encoded traversal (%2e%2e) */\n /%2e%2e/gi,\n /** Double URL-encoded traversal (%252e) */\n /%252e/gi,\n /** Mixed encoding: ..%2F */\n /\\.\\.%2F/gi,\n /** Mixed encoding: %2e./ and .%2e/ */\n /%2e\\.[\\\\/]/gi,\n /\\.%2e[\\\\/]/gi,\n /** Fully URL-encoded: %2e%2e%2f */\n /%2e%2e%2f/gi,\n /** Double URL-encoded forward slash: %252f */\n /%252f/gi,\n /** Dotdotslash bypass: ....// or ....\\\\ */\n /\\.{2,}[/\\\\]{2,}/g,\n /** Null byte injection in paths */\n /\\0/g,\n] as const;\n\n// =============================================================================\n// COMMAND INJECTION PATTERNS\n// =============================================================================\nexport const COMMAND_PATTERNS = [\n /**\n * Shell metacharacters that enable command chaining/substitution.\n * Bare ( and ) are excluded — they appear in common legitimate values\n * (function calls in code fields, math expressions, etc.).\n * Command substitution is caught by the $( combined pattern below.\n * NOTE: ';', '&', '|' may appear in legitimate URL query strings\n * and Markdown; consider disabling command checking (command: false)\n * for fields that intentionally allow those characters.\n */\n /[;&|`]/g,\n /** Command substitution: $( ... ) — matched as a pair to reduce false positives */\n /\\$\\(/g,\n /** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */\n /%0[0-9a-f]/gi,\n] as const;\n\n// =============================================================================\n// DANGEROUS KEYS\n// =============================================================================\n\n/**\n * Prototype pollution keys to block.\n * Stored lowercase — always compare with key.toLowerCase().\n *\n * Includes:\n * - __proto__: direct prototype assignment\n * - constructor: access to constructor.prototype chain\n * - prototype: direct prototype property\n * - __defineGetter__/__defineSetter__: legacy property definition (can override getters/setters)\n * - __lookupGetter__/__lookupSetter__: legacy property introspection\n */\nexport const DANGEROUS_PROTO_KEYS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n '__definegetter__',\n '__definesetter__',\n '__lookupgetter__',\n '__lookupsetter__',\n]);\n\n/** MongoDB operators to block */\nexport const NOSQL_DANGEROUS_KEYS = new Set([\n // Comparison\n '$gt', '$gte', '$lt', '$lte', '$ne', '$eq', '$in', '$nin',\n // Logical\n '$and', '$or', '$not', '$nor',\n // Element / evaluation\n '$exists', '$type', '$regex', '$where', '$expr', '$mod', '$text', '$jsonSchema',\n // Array\n '$elemMatch', '$all', '$size',\n // JavaScript execution (critical)\n '$function', '$accumulator',\n // Aggregation pipeline operators (injectable via $lookup etc.)\n '$lookup', '$match', '$project', '$group', '$sort', '$limit', '$skip',\n '$unwind', '$addFields', '$replaceRoot',\n]);\n\n// =============================================================================\n// REDACTION\n// =============================================================================\nexport const REDACTION = {\n /** Replacement text for redacted values */\n REPLACEMENT: '[REDACTED]',\n /** Truncation indicator */\n TRUNCATED: '[TRUNCATED]',\n /** Max depth indicator */\n MAX_DEPTH: '[MAX_DEPTH]',\n /** Default max message length */\n DEFAULT_MAX_LENGTH: 10_000,\n /** Default sensitive keys to redact */\n SENSITIVE_KEYS: new Set([\n 'password', 'passwd', 'pwd', 'secret', 'token', 'apikey',\n 'api_key', 'apiKey', 'auth', 'authorization', 'credit_card',\n 'creditcard', 'cc', 'ssn', 'social_security', 'private_key',\n 'privateKey', 'access_token', 'accessToken', 'refresh_token',\n 'refreshToken', 'bearer', 'jwt', 'session', 'cookie',\n 'credentials', 'x-api-key', 'x-auth-token',\n ]),\n} as const;\n\n// =============================================================================\n// VALIDATION PATTERNS\n// =============================================================================\nexport const VALIDATION = {\n /**\n * Email regex pattern.\n * Rejects consecutive dots in local part (e.g. test..foo@example.com),\n * leading/trailing dots, and other common invalid forms.\n */\n EMAIL: /^[^\\s@.][^\\s@]*(?:\\.[^\\s@.][^\\s@]*)*@[^\\s@]+\\.[^\\s@]+$/,\n /**\n * URL regex pattern.\n * Only allows http:// and https:// — explicitly rejects javascript:,\n * data:, vbscript:, and other dangerous URI schemes.\n */\n URL: /^https?:\\/\\/[^\\s/$.?#][^\\s]*$/,\n /** UUID regex pattern (v4) */\n UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,\n} as const;\n\n// =============================================================================\n// ERROR MESSAGES\n// =============================================================================\nexport const ERRORS = {\n /** Generic error message (production) */\n INTERNAL_SERVER_ERROR: 'Internal Server Error',\n /** Input too large error */\n INPUT_TOO_LARGE: (maxSize: number) => `Input exceeds maximum size of ${maxSize} bytes`,\n /** Validation error messages */\n VALIDATION: {\n REQUIRED: (field: string) => `${field} is required`,\n INVALID_TYPE: (field: string, type: string) => `${field} must be a ${type}`,\n MIN_LENGTH: (field: string, min: number) => `${field} must be at least ${min} characters`,\n MAX_LENGTH: (field: string, max: number) => `${field} must be at most ${max} characters`,\n MIN_VALUE: (field: string, min: number) => `${field} must be at least ${min}`,\n MAX_VALUE: (field: string, max: number) => `${field} must be at most ${max}`,\n INVALID_FORMAT: (field: string) => `${field} format is invalid`,\n INVALID_EMAIL: (field: string) => `${field} must be a valid email`,\n INVALID_URL: (field: string) => `${field} must be a valid URL`,\n INVALID_UUID: (field: string) => `${field} must be a valid UUID`,\n INVALID_ENUM: (field: string, values: unknown[]) => `${field} must be one of: ${values.join(', ')}`,\n MIN_ITEMS: (field: string, min: number) => `${field} must have at least ${min} items`,\n MAX_ITEMS: (field: string, max: number) => `${field} must have at most ${max} items`,\n },\n} as const;\n\n// =============================================================================\n// BLOCKED TEXT (for sanitizer replacements)\n// =============================================================================\nexport const BLOCKED = '[BLOCKED]' as const;\n","/**\n * @module @arcis/node/stores/memory\n * In-memory rate limit store\n */\n\nimport type { RateLimitStore, RateLimitEntry } from '../core/types';\nimport { RATE_LIMIT } from '../core/constants';\n\n/**\n * In-memory rate limit store.\n * Suitable for single-instance deployments.\n * For distributed systems, use RedisStore or a custom store.\n * \n * @example\n * const store = new MemoryStore(60000); // 1 minute window\n * const limiter = createRateLimiter({ store });\n */\n/** Default maximum number of keys the in-memory store will hold. */\nconst DEFAULT_MAX_SIZE = 10_000;\n\nexport class MemoryStore implements RateLimitStore {\n private store: Map<string, RateLimitEntry> = new Map();\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n private windowMs: number;\n private maxSize: number;\n\n constructor(windowMs: number = RATE_LIMIT.DEFAULT_WINDOW_MS, maxSize: number = DEFAULT_MAX_SIZE) {\n if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {\n throw new RangeError(\n `MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`\n );\n }\n if (!Number.isFinite(maxSize) || maxSize < 1) {\n throw new RangeError(`MemoryStore: maxSize must be >= 1 (got ${maxSize})`);\n }\n this.windowMs = windowMs;\n this.maxSize = maxSize;\n this.startCleanup();\n }\n\n /**\n * Start the cleanup interval to remove expired entries.\n */\n private startCleanup(): void {\n // Clamp the cleanup interval between 30 s and 5 min regardless of windowMs.\n // Running it every windowMs is fine for typical windows but would fire every\n // second for short windows (e.g. windowMs: 1000), causing O(n) GC pressure.\n const CLEANUP_MIN_MS = 30_000;\n const CLEANUP_MAX_MS = 300_000;\n const cleanupMs = Math.min(Math.max(this.windowMs, CLEANUP_MIN_MS), CLEANUP_MAX_MS);\n\n this.cleanupInterval = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.store.entries()) {\n if (entry.resetTime < now) {\n this.store.delete(key);\n }\n }\n }, cleanupMs);\n\n // Prevent interval from keeping the process alive\n if (typeof this.cleanupInterval.unref === 'function') {\n this.cleanupInterval.unref();\n }\n }\n\n async get(key: string): Promise<RateLimitEntry | null> {\n const entry = this.store.get(key);\n if (!entry) return null;\n \n // Check if expired\n if (entry.resetTime < Date.now()) {\n this.store.delete(key);\n return null;\n }\n \n return entry;\n }\n\n async set(key: string, entry: RateLimitEntry): Promise<void> {\n if (!this.store.has(key) && this.store.size >= this.maxSize) {\n this.evictExpired();\n // If still at capacity after eviction, fail open — don't crash the app\n if (this.store.size >= this.maxSize) return;\n }\n this.store.set(key, entry);\n }\n\n async increment(key: string): Promise<number> {\n const now = Date.now();\n const entry = this.store.get(key);\n\n if (!entry || entry.resetTime < now) {\n // Start new window — check capacity first\n if (this.store.size >= this.maxSize) {\n this.evictExpired();\n if (this.store.size >= this.maxSize) return 1; // fail open\n }\n this.store.set(key, { count: 1, resetTime: now + this.windowMs });\n return 1;\n }\n\n entry.count++;\n return entry.count;\n }\n\n /** Eagerly remove expired entries to reclaim capacity. */\n private evictExpired(): void {\n const now = Date.now();\n for (const [key, entry] of this.store.entries()) {\n if (entry.resetTime < now) this.store.delete(key);\n }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.store.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n async reset(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n async close(): Promise<void> {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n this.store.clear();\n }\n\n /**\n * Get current store size (for monitoring).\n */\n get size(): number {\n return this.store.size;\n }\n}\n","/**\n * @module @arcis/node/stores/redis\n * Redis rate limit store\n * \n * Note: This is a reference implementation. You'll need to install\n * the 'ioredis' or 'redis' package and pass your client instance.\n */\n\nimport type { RateLimitStore, RateLimitEntry } from '../core/types';\nimport { RATE_LIMIT } from '../core/constants';\n\n/** Generic Redis client interface (works with ioredis, redis, etc.) */\nexport interface RedisClientLike {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, mode?: string, duration?: number): Promise<unknown>;\n setex(key: string, seconds: number, value: string): Promise<unknown>;\n expire(key: string, seconds: number): Promise<unknown>;\n incr(key: string): Promise<number>;\n decr(key: string): Promise<number>;\n del(key: string): Promise<number>;\n ttl(key: string): Promise<number>;\n quit?(): Promise<unknown>;\n disconnect?(): Promise<unknown>;\n}\n\nexport interface RedisStoreOptions {\n /** Redis client instance */\n client: RedisClientLike;\n /** Key prefix. Default: 'arcis:rl:' */\n prefix?: string;\n /** Window size in milliseconds. Default: 60000 */\n windowMs?: number;\n}\n\n/**\n * Redis rate limit store for distributed deployments.\n * \n * @example\n * import Redis from 'ioredis';\n * \n * const redis = new Redis();\n * const store = new RedisStore({ client: redis });\n * const limiter = createRateLimiter({ store });\n * \n * // Cleanup on shutdown\n * process.on('SIGTERM', async () => {\n * await store.close();\n * });\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClientLike;\n private prefix: string;\n private windowMs: number;\n private windowSec: number;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? 'arcis:rl:';\n this.windowMs = options.windowMs ?? RATE_LIMIT.DEFAULT_WINDOW_MS;\n this.windowSec = Math.ceil(this.windowMs / 1000);\n }\n\n private getKey(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n async get(key: string): Promise<RateLimitEntry | null> {\n const redisKey = this.getKey(key);\n \n const [countStr, ttl] = await Promise.all([\n this.client.get(redisKey),\n this.client.ttl(redisKey),\n ]);\n \n if (!countStr || ttl < 0) {\n return null;\n }\n \n const count = parseInt(countStr, 10);\n if (isNaN(count)) {\n // Corrupt value in Redis — treat as if key doesn't exist\n return null;\n }\n\n return {\n count,\n resetTime: Date.now() + (ttl * 1000),\n };\n }\n\n async set(key: string, entry: RateLimitEntry): Promise<void> {\n const redisKey = this.getKey(key);\n // Clamp to at least 1 second — Math.ceil can produce 0 or negative values\n // when entry.resetTime is in the past due to Redis latency or clock skew.\n const ttlSec = Math.max(1, Math.ceil((entry.resetTime - Date.now()) / 1000));\n await this.client.setex(redisKey, ttlSec, entry.count.toString());\n }\n\n async increment(key: string): Promise<number> {\n const redisKey = this.getKey(key);\n \n // INCR creates key with value 1 if it doesn't exist\n const count = await this.client.incr(redisKey);\n\n // Set expiry only on first increment using EXPIRE, which sets the TTL\n // without overwriting the value (unlike SET ... EX which would reset the\n // counter if two requests increment concurrently before expiry is set).\n if (count === 1) {\n await this.client.expire(redisKey, this.windowSec);\n }\n\n return count;\n }\n\n async decrement(key: string): Promise<void> {\n const redisKey = this.getKey(key);\n await this.client.decr(redisKey);\n }\n\n async reset(key: string): Promise<void> {\n const redisKey = this.getKey(key);\n await this.client.del(redisKey);\n }\n\n async close(): Promise<void> {\n // Don't close the client - it may be shared\n // The caller should manage the client lifecycle\n }\n}\n\n/**\n * Create a Redis store with the given options.\n * Convenience function for functional programming style.\n * \n * @example\n * const store = createRedisStore({ client: redisClient });\n */\nexport function createRedisStore(options: RedisStoreOptions): RedisStore {\n return new RedisStore(options);\n}\n"]}
@@ -6,8 +6,9 @@ var RATE_LIMIT = {
6
6
  MIN_WINDOW_MS: 1e3};
7
7
 
8
8
  // src/stores/memory.ts
9
+ var DEFAULT_MAX_SIZE = 1e4;
9
10
  var MemoryStore = class {
10
- constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS) {
11
+ constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS, maxSize = DEFAULT_MAX_SIZE) {
11
12
  this.store = /* @__PURE__ */ new Map();
12
13
  this.cleanupInterval = null;
13
14
  if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {
@@ -15,7 +16,11 @@ var MemoryStore = class {
15
16
  `MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`
16
17
  );
17
18
  }
19
+ if (!Number.isFinite(maxSize) || maxSize < 1) {
20
+ throw new RangeError(`MemoryStore: maxSize must be >= 1 (got ${maxSize})`);
21
+ }
18
22
  this.windowMs = windowMs;
23
+ this.maxSize = maxSize;
19
24
  this.startCleanup();
20
25
  }
21
26
  /**
@@ -47,18 +52,33 @@ var MemoryStore = class {
47
52
  return entry;
48
53
  }
49
54
  async set(key, entry) {
55
+ if (!this.store.has(key) && this.store.size >= this.maxSize) {
56
+ this.evictExpired();
57
+ if (this.store.size >= this.maxSize) return;
58
+ }
50
59
  this.store.set(key, entry);
51
60
  }
52
61
  async increment(key) {
53
62
  const now = Date.now();
54
63
  const entry = this.store.get(key);
55
64
  if (!entry || entry.resetTime < now) {
65
+ if (this.store.size >= this.maxSize) {
66
+ this.evictExpired();
67
+ if (this.store.size >= this.maxSize) return 1;
68
+ }
56
69
  this.store.set(key, { count: 1, resetTime: now + this.windowMs });
57
70
  return 1;
58
71
  }
59
72
  entry.count++;
60
73
  return entry.count;
61
74
  }
75
+ /** Eagerly remove expired entries to reclaim capacity. */
76
+ evictExpired() {
77
+ const now = Date.now();
78
+ for (const [key, entry] of this.store.entries()) {
79
+ if (entry.resetTime < now) this.store.delete(key);
80
+ }
81
+ }
62
82
  async decrement(key) {
63
83
  const entry = this.store.get(key);
64
84
  if (entry && entry.count > 0) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/constants.ts","../../src/stores/memory.ts","../../src/stores/redis.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @module @arcis/node/core/constants\n * Named constants for Arcis - no magic numbers\n */\n\n// =============================================================================\n// INPUT LIMITS\n// =============================================================================\nexport const INPUT = {\n /** Default maximum input size (1MB) */\n DEFAULT_MAX_SIZE: 1_000_000,\n /** Maximum recursion depth for nested objects */\n MAX_RECURSION_DEPTH: 10,\n} as const;\n\n// =============================================================================\n// RATE LIMITING\n// =============================================================================\nexport const RATE_LIMIT = {\n /** Default window size (1 minute) */\n DEFAULT_WINDOW_MS: 60_000,\n /** Default max requests per window */\n DEFAULT_MAX_REQUESTS: 100,\n /** Default HTTP status code for rate limited responses */\n DEFAULT_STATUS_CODE: 429,\n /** Default error message */\n DEFAULT_MESSAGE: 'Too many requests, please try again later.',\n /** Minimum window size (1 second) */\n MIN_WINDOW_MS: 1_000,\n /** Maximum window size (24 hours) */\n MAX_WINDOW_MS: 86_400_000,\n} as const;\n\n// =============================================================================\n// SECURITY HEADERS\n// =============================================================================\nexport const HEADERS = {\n /** Default Content Security Policy */\n DEFAULT_CSP: [\n \"default-src 'self'\",\n \"script-src 'self'\",\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: https:\",\n \"font-src 'self'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n ].join('; '),\n /** Default HSTS max age (1 year in seconds) */\n HSTS_MAX_AGE: 31_536_000,\n /** Default X-Frame-Options value */\n FRAME_OPTIONS: 'DENY' as const,\n /** Default X-Content-Type-Options value */\n CONTENT_TYPE_OPTIONS: 'nosniff',\n /** Default Referrer-Policy value */\n REFERRER_POLICY: 'strict-origin-when-cross-origin',\n /** Default Permissions-Policy value */\n PERMISSIONS_POLICY: 'geolocation=(), microphone=(), camera=()',\n /** Default Cache-Control value for security */\n CACHE_CONTROL: 'no-store, no-cache, must-revalidate, proxy-revalidate',\n} as const;\n\n// =============================================================================\n// XSS PATTERNS (ReDoS-safe)\n// =============================================================================\n\n/**\n * Detection patterns — used to flag whether a string contains XSS payloads.\n * Must stay in sync with XSS_REMOVE_PATTERNS below.\n */\nexport const XSS_PATTERNS = [\n /** Script tags (ReDoS-safe version) */\n /<script[^>]*>[\\s\\S]*?<\\/script>/gi,\n /** javascript: protocol (allow optional spaces before colon) */\n /javascript\\s*:/gi,\n /** vbscript: protocol */\n /vbscript\\s*:/gi,\n /** Event handlers (onclick, onerror, etc.) — any separator before attribute */\n /(?:[\\s/])on\\w+\\s*=/gi,\n /** iframe tags */\n /<iframe/gi,\n /** object tags */\n /<object/gi,\n /** embed tags */\n /<embed/gi,\n /** data: URIs (only dangerous ones, avoid false positives) */\n /(?:^|[\\s\"'=])data:/gi,\n /** URL-encoded script tags */\n /%3Cscript/gi,\n /** SVG with onload */\n /<svg[^>]*onload/gi,\n] as const;\n\n/**\n * Removal patterns — used by sanitizeXss() to strip dangerous content.\n * More targeted than XSS_PATTERNS: each pattern captures the full dangerous\n * substring (tag, attribute + value, protocol) so it can be replaced safely.\n * Must stay in sync with XSS_PATTERNS above.\n */\nexport const XSS_REMOVE_PATTERNS = [\n /** Full script blocks (content + tags) */\n /<script[^>]*>[\\s\\S]*?<\\/script>/gi,\n /** Standalone/unclosed script tags */\n /<script[^>]*>/gi,\n /** iframe — full block and partial/unclosed */\n /<iframe[^>]*>[\\s\\S]*?<\\/iframe>/gi,\n /<iframe[^>]*/gi,\n /** object — full block and partial/unclosed */\n /<object[^>]*>[\\s\\S]*?<\\/object>/gi,\n /<object[^>]*/gi,\n /** embed tags */\n /<embed[^>]*/gi,\n /** SVG with inline event handlers */\n /<svg[^>]*onload[^>]*>/gi,\n /** URL-encoded script tags */\n /%3Cscript/gi,\n /** Event handlers with quoted values: onclick=\"...\", onerror='...' */\n /(?:[\\s/])on\\w+\\s*=\\s*[\"'][^\"']*[\"']/gi,\n /** Event handlers with unquoted values: onload=value */\n /(?:[\\s/])on\\w+\\s*=\\s*[^\\s>]*/gi,\n /** javascript: and vbscript: protocols (allow optional spaces before colon) */\n /javascript\\s*:/gi,\n /vbscript\\s*:/gi,\n /** data: URIs with HTML/script content */\n /data\\s*:\\s*text\\/html[^>\\s]*/gi,\n] as const;\n\n// =============================================================================\n// SQL INJECTION PATTERNS\n// =============================================================================\nexport const SQL_PATTERNS = [\n /** SQL keywords */\n /(\\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)\\b)/gi,\n /** SQL comments: ANSI (--), C-style (slash-star ... star-slash), MySQL (#) */\n /(--|\\/\\*|\\*\\/|#)/g,\n /** SQL statement separators */\n /(;|\\|\\||&&)/g,\n /** Boolean injection: OR 1=1 */\n /\\bOR\\s+\\d+\\s*=\\s*\\d+/gi,\n /** Boolean injection: OR 'a'='a' or OR \"a\"=\"a\" (including mixed quotes) */\n /\\bOR\\s+(['\"])[^'\"]*\\1\\s*=\\s*(['\"])[^'\"]*\\2/gi,\n /\\bOR\\s+('[^']*'|\"[^\"]*\")\\s*=\\s*('[^']*'|\"[^\"]*\")/gi,\n /** Boolean injection: AND 1=1 */\n /\\bAND\\s+\\d+\\s*=\\s*\\d+/gi,\n /** Boolean injection: AND 'a'='a' or AND \"a\"=\"a\" (including mixed quotes) */\n /\\bAND\\s+(['\"])[^'\"]*\\1\\s*=\\s*(['\"])[^'\"]*\\2/gi,\n /\\bAND\\s+('[^']*'|\"[^\"]*\")\\s*=\\s*('[^']*'|\"[^\"]*\")/gi,\n /** Time-based blind: SLEEP() */\n /\\bSLEEP\\s*\\(\\s*\\d+\\s*\\)/gi,\n /** Time-based blind: BENCHMARK() */\n /\\bBENCHMARK\\s*\\(/gi,\n /** Time-based blind: PostgreSQL pg_sleep() */\n /\\bpg_sleep\\s*\\(/gi,\n /** Time-based blind: MSSQL WAITFOR DELAY */\n /\\bWAITFOR\\s+DELAY\\b/gi,\n] as const;\n\n// =============================================================================\n// PATH TRAVERSAL PATTERNS\n// =============================================================================\nexport const PATH_PATTERNS = [\n /** Unix path traversal */\n /\\.\\.\\//g,\n /** Windows path traversal */\n /\\.\\.\\\\/g,\n /** URL-encoded traversal (%2e%2e) */\n /%2e%2e/gi,\n /** Double URL-encoded traversal (%252e) */\n /%252e/gi,\n /** Mixed encoding: ..%2F */\n /\\.\\.%2F/gi,\n /** Mixed encoding: %2e./ and .%2e/ */\n /%2e\\.[\\\\/]/gi,\n /\\.%2e[\\\\/]/gi,\n /** Fully URL-encoded: %2e%2e%2f */\n /%2e%2e%2f/gi,\n /** Double URL-encoded forward slash: %252f */\n /%252f/gi,\n /** Dotdotslash bypass: ....// or ....\\\\ */\n /\\.{2,}[/\\\\]{2,}/g,\n /** Null byte injection in paths */\n /\\0/g,\n] as const;\n\n// =============================================================================\n// COMMAND INJECTION PATTERNS\n// =============================================================================\nexport const COMMAND_PATTERNS = [\n /**\n * Shell metacharacters that enable command chaining/substitution.\n * Bare ( and ) are excluded — they appear in common legitimate values\n * (function calls in code fields, math expressions, etc.).\n * Command substitution is caught by the $( combined pattern below.\n * NOTE: ';', '&', '|' may appear in legitimate URL query strings\n * and Markdown; consider disabling command checking (command: false)\n * for fields that intentionally allow those characters.\n */\n /[;&|`]/g,\n /** Command substitution: $( ... ) — matched as a pair to reduce false positives */\n /\\$\\(/g,\n /** URL-encoded newline/carriage-return injection (%0a, %0d) */\n /%0[ad]/gi,\n] as const;\n\n// =============================================================================\n// DANGEROUS KEYS\n// =============================================================================\n\n/**\n * Prototype pollution keys to block.\n * Stored lowercase — always compare with key.toLowerCase().\n *\n * Includes:\n * - __proto__: direct prototype assignment\n * - constructor: access to constructor.prototype chain\n * - prototype: direct prototype property\n * - __defineGetter__/__defineSetter__: legacy property definition (can override getters/setters)\n * - __lookupGetter__/__lookupSetter__: legacy property introspection\n */\nexport const DANGEROUS_PROTO_KEYS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n '__definegetter__',\n '__definesetter__',\n '__lookupgetter__',\n '__lookupsetter__',\n]);\n\n/** MongoDB operators to block */\nexport const NOSQL_DANGEROUS_KEYS = new Set([\n // Comparison\n '$gt', '$gte', '$lt', '$lte', '$ne', '$eq', '$in', '$nin',\n // Logical\n '$and', '$or', '$not', '$nor',\n // Element / evaluation\n '$exists', '$type', '$regex', '$where', '$expr', '$mod', '$text', '$jsonSchema',\n // Array\n '$elemMatch', '$all', '$size',\n // JavaScript execution (critical)\n '$function', '$accumulator',\n // Aggregation pipeline operators (injectable via $lookup etc.)\n '$lookup', '$match', '$project', '$group', '$sort', '$limit', '$skip',\n '$unwind', '$addFields', '$replaceRoot',\n]);\n\n// =============================================================================\n// REDACTION\n// =============================================================================\nexport const REDACTION = {\n /** Replacement text for redacted values */\n REPLACEMENT: '[REDACTED]',\n /** Truncation indicator */\n TRUNCATED: '[TRUNCATED]',\n /** Max depth indicator */\n MAX_DEPTH: '[MAX_DEPTH]',\n /** Default max message length */\n DEFAULT_MAX_LENGTH: 10_000,\n /** Default sensitive keys to redact */\n SENSITIVE_KEYS: new Set([\n 'password', 'passwd', 'pwd', 'secret', 'token', 'apikey',\n 'api_key', 'apiKey', 'auth', 'authorization', 'credit_card',\n 'creditcard', 'cc', 'ssn', 'social_security', 'private_key',\n 'privateKey', 'access_token', 'accessToken', 'refresh_token',\n 'refreshToken', 'bearer', 'jwt', 'session', 'cookie',\n 'credentials', 'x-api-key', 'x-auth-token',\n ]),\n} as const;\n\n// =============================================================================\n// VALIDATION PATTERNS\n// =============================================================================\nexport const VALIDATION = {\n /**\n * Email regex pattern.\n * Rejects consecutive dots in local part (e.g. test..foo@example.com),\n * leading/trailing dots, and other common invalid forms.\n */\n EMAIL: /^[^\\s@.][^\\s@]*(?:\\.[^\\s@.][^\\s@]*)*@[^\\s@]+\\.[^\\s@]+$/,\n /**\n * URL regex pattern.\n * Only allows http:// and https:// — explicitly rejects javascript:,\n * data:, vbscript:, and other dangerous URI schemes.\n */\n URL: /^https?:\\/\\/[^\\s/$.?#][^\\s]*$/,\n /** UUID regex pattern (v4) */\n UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,\n} as const;\n\n// =============================================================================\n// ERROR MESSAGES\n// =============================================================================\nexport const ERRORS = {\n /** Generic error message (production) */\n INTERNAL_SERVER_ERROR: 'Internal Server Error',\n /** Input too large error */\n INPUT_TOO_LARGE: (maxSize: number) => `Input exceeds maximum size of ${maxSize} bytes`,\n /** Validation error messages */\n VALIDATION: {\n REQUIRED: (field: string) => `${field} is required`,\n INVALID_TYPE: (field: string, type: string) => `${field} must be a ${type}`,\n MIN_LENGTH: (field: string, min: number) => `${field} must be at least ${min} characters`,\n MAX_LENGTH: (field: string, max: number) => `${field} must be at most ${max} characters`,\n MIN_VALUE: (field: string, min: number) => `${field} must be at least ${min}`,\n MAX_VALUE: (field: string, max: number) => `${field} must be at most ${max}`,\n INVALID_FORMAT: (field: string) => `${field} format is invalid`,\n INVALID_EMAIL: (field: string) => `${field} must be a valid email`,\n INVALID_URL: (field: string) => `${field} must be a valid URL`,\n INVALID_UUID: (field: string) => `${field} must be a valid UUID`,\n INVALID_ENUM: (field: string, values: unknown[]) => `${field} must be one of: ${values.join(', ')}`,\n MIN_ITEMS: (field: string, min: number) => `${field} must have at least ${min} items`,\n MAX_ITEMS: (field: string, max: number) => `${field} must have at most ${max} items`,\n },\n} as const;\n\n// =============================================================================\n// BLOCKED TEXT (for sanitizer replacements)\n// =============================================================================\nexport const BLOCKED = '[BLOCKED]' as const;\n","/**\n * @module @arcis/node/stores/memory\n * In-memory rate limit store\n */\n\nimport type { RateLimitStore, RateLimitEntry } from '../core/types';\nimport { RATE_LIMIT } from '../core/constants';\n\n/**\n * In-memory rate limit store.\n * Suitable for single-instance deployments.\n * For distributed systems, use RedisStore or a custom store.\n * \n * @example\n * const store = new MemoryStore(60000); // 1 minute window\n * const limiter = createRateLimiter({ store });\n */\nexport class MemoryStore implements RateLimitStore {\n private store: Map<string, RateLimitEntry> = new Map();\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n private windowMs: number;\n\n constructor(windowMs: number = RATE_LIMIT.DEFAULT_WINDOW_MS) {\n if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {\n throw new RangeError(\n `MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`\n );\n }\n this.windowMs = windowMs;\n this.startCleanup();\n }\n\n /**\n * Start the cleanup interval to remove expired entries.\n */\n private startCleanup(): void {\n // Clamp the cleanup interval between 30 s and 5 min regardless of windowMs.\n // Running it every windowMs is fine for typical windows but would fire every\n // second for short windows (e.g. windowMs: 1000), causing O(n) GC pressure.\n const CLEANUP_MIN_MS = 30_000;\n const CLEANUP_MAX_MS = 300_000;\n const cleanupMs = Math.min(Math.max(this.windowMs, CLEANUP_MIN_MS), CLEANUP_MAX_MS);\n\n this.cleanupInterval = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.store.entries()) {\n if (entry.resetTime < now) {\n this.store.delete(key);\n }\n }\n }, cleanupMs);\n\n // Prevent interval from keeping the process alive\n if (typeof this.cleanupInterval.unref === 'function') {\n this.cleanupInterval.unref();\n }\n }\n\n async get(key: string): Promise<RateLimitEntry | null> {\n const entry = this.store.get(key);\n if (!entry) return null;\n \n // Check if expired\n if (entry.resetTime < Date.now()) {\n this.store.delete(key);\n return null;\n }\n \n return entry;\n }\n\n async set(key: string, entry: RateLimitEntry): Promise<void> {\n this.store.set(key, entry);\n }\n\n async increment(key: string): Promise<number> {\n const now = Date.now();\n const entry = this.store.get(key);\n \n if (!entry || entry.resetTime < now) {\n // Start new window\n this.store.set(key, { count: 1, resetTime: now + this.windowMs });\n return 1;\n }\n \n entry.count++;\n return entry.count;\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.store.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n async reset(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n async close(): Promise<void> {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n this.store.clear();\n }\n\n /**\n * Get current store size (for monitoring).\n */\n get size(): number {\n return this.store.size;\n }\n}\n","/**\n * @module @arcis/node/stores/redis\n * Redis rate limit store\n * \n * Note: This is a reference implementation. You'll need to install\n * the 'ioredis' or 'redis' package and pass your client instance.\n */\n\nimport type { RateLimitStore, RateLimitEntry } from '../core/types';\nimport { RATE_LIMIT } from '../core/constants';\n\n/** Generic Redis client interface (works with ioredis, redis, etc.) */\nexport interface RedisClientLike {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, mode?: string, duration?: number): Promise<unknown>;\n setex(key: string, seconds: number, value: string): Promise<unknown>;\n expire(key: string, seconds: number): Promise<unknown>;\n incr(key: string): Promise<number>;\n decr(key: string): Promise<number>;\n del(key: string): Promise<number>;\n ttl(key: string): Promise<number>;\n quit?(): Promise<unknown>;\n disconnect?(): Promise<unknown>;\n}\n\nexport interface RedisStoreOptions {\n /** Redis client instance */\n client: RedisClientLike;\n /** Key prefix. Default: 'arcis:rl:' */\n prefix?: string;\n /** Window size in milliseconds. Default: 60000 */\n windowMs?: number;\n}\n\n/**\n * Redis rate limit store for distributed deployments.\n * \n * @example\n * import Redis from 'ioredis';\n * \n * const redis = new Redis();\n * const store = new RedisStore({ client: redis });\n * const limiter = createRateLimiter({ store });\n * \n * // Cleanup on shutdown\n * process.on('SIGTERM', async () => {\n * await store.close();\n * });\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClientLike;\n private prefix: string;\n private windowMs: number;\n private windowSec: number;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? 'arcis:rl:';\n this.windowMs = options.windowMs ?? RATE_LIMIT.DEFAULT_WINDOW_MS;\n this.windowSec = Math.ceil(this.windowMs / 1000);\n }\n\n private getKey(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n async get(key: string): Promise<RateLimitEntry | null> {\n const redisKey = this.getKey(key);\n \n const [countStr, ttl] = await Promise.all([\n this.client.get(redisKey),\n this.client.ttl(redisKey),\n ]);\n \n if (!countStr || ttl < 0) {\n return null;\n }\n \n const count = parseInt(countStr, 10);\n if (isNaN(count)) {\n // Corrupt value in Redis — treat as if key doesn't exist\n return null;\n }\n\n return {\n count,\n resetTime: Date.now() + (ttl * 1000),\n };\n }\n\n async set(key: string, entry: RateLimitEntry): Promise<void> {\n const redisKey = this.getKey(key);\n // Clamp to at least 1 second — Math.ceil can produce 0 or negative values\n // when entry.resetTime is in the past due to Redis latency or clock skew.\n const ttlSec = Math.max(1, Math.ceil((entry.resetTime - Date.now()) / 1000));\n await this.client.setex(redisKey, ttlSec, entry.count.toString());\n }\n\n async increment(key: string): Promise<number> {\n const redisKey = this.getKey(key);\n \n // INCR creates key with value 1 if it doesn't exist\n const count = await this.client.incr(redisKey);\n\n // Set expiry only on first increment using EXPIRE, which sets the TTL\n // without overwriting the value (unlike SET ... EX which would reset the\n // counter if two requests increment concurrently before expiry is set).\n if (count === 1) {\n await this.client.expire(redisKey, this.windowSec);\n }\n\n return count;\n }\n\n async decrement(key: string): Promise<void> {\n const redisKey = this.getKey(key);\n await this.client.decr(redisKey);\n }\n\n async reset(key: string): Promise<void> {\n const redisKey = this.getKey(key);\n await this.client.del(redisKey);\n }\n\n async close(): Promise<void> {\n // Don't close the client - it may be shared\n // The caller should manage the client lifecycle\n }\n}\n\n/**\n * Create a Redis store with the given options.\n * Convenience function for functional programming style.\n * \n * @example\n * const store = createRedisStore({ client: redisClient });\n */\nexport function createRedisStore(options: RedisStoreOptions): RedisStore {\n return new RedisStore(options);\n}\n"]}
1
+ {"version":3,"sources":["../../src/core/constants.ts","../../src/stores/memory.ts","../../src/stores/redis.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @module @arcis/node/core/constants\n * Named constants for Arcis - no magic numbers\n */\n\n// =============================================================================\n// INPUT LIMITS\n// =============================================================================\nexport const INPUT = {\n /** Default maximum input size (1MB) */\n DEFAULT_MAX_SIZE: 1_000_000,\n /** Maximum recursion depth for nested objects */\n MAX_RECURSION_DEPTH: 10,\n} as const;\n\n// =============================================================================\n// RATE LIMITING\n// =============================================================================\nexport const RATE_LIMIT = {\n /** Default window size (1 minute) */\n DEFAULT_WINDOW_MS: 60_000,\n /** Default max requests per window */\n DEFAULT_MAX_REQUESTS: 100,\n /** Default HTTP status code for rate limited responses */\n DEFAULT_STATUS_CODE: 429,\n /** Default error message */\n DEFAULT_MESSAGE: 'Too many requests, please try again later.',\n /** Minimum window size (1 second) */\n MIN_WINDOW_MS: 1_000,\n /** Maximum window size (24 hours) */\n MAX_WINDOW_MS: 86_400_000,\n} as const;\n\n// =============================================================================\n// SECURITY HEADERS\n// =============================================================================\nexport const HEADERS = {\n /** Default Content Security Policy */\n DEFAULT_CSP: [\n \"default-src 'self'\",\n \"script-src 'self'\",\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: https:\",\n \"font-src 'self'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n ].join('; '),\n /** Default HSTS max age (1 year in seconds) */\n HSTS_MAX_AGE: 31_536_000,\n /** Default X-Frame-Options value */\n FRAME_OPTIONS: 'DENY' as const,\n /** Default X-Content-Type-Options value */\n CONTENT_TYPE_OPTIONS: 'nosniff',\n /** Default Referrer-Policy value */\n REFERRER_POLICY: 'strict-origin-when-cross-origin',\n /** Default Permissions-Policy value */\n PERMISSIONS_POLICY: 'geolocation=(), microphone=(), camera=()',\n /** Default Cache-Control value for security */\n CACHE_CONTROL: 'no-store, no-cache, must-revalidate, proxy-revalidate',\n} as const;\n\n// =============================================================================\n// XSS PATTERNS (ReDoS-safe)\n// =============================================================================\n\n/**\n * Detection patterns — used to flag whether a string contains XSS payloads.\n * Must stay in sync with XSS_REMOVE_PATTERNS below.\n */\nexport const XSS_PATTERNS = [\n /** Script tags (ReDoS-safe version) */\n /<script[^>]*>[\\s\\S]*?<\\/script>/gi,\n /** javascript: protocol (allow optional spaces before colon) */\n /javascript\\s*:/gi,\n /** vbscript: protocol */\n /vbscript\\s*:/gi,\n /** Event handlers (onclick, onerror, etc.) — any separator before attribute */\n /(?:[\\s/])on\\w+\\s*=/gi,\n /** iframe tags */\n /<iframe/gi,\n /** object tags */\n /<object/gi,\n /** embed tags */\n /<embed/gi,\n /** data: URIs (only dangerous ones, avoid false positives) */\n /(?:^|[\\s\"'=])data:/gi,\n /** URL-encoded script tags */\n /%3Cscript/gi,\n /** SVG with onload */\n /<svg[^>]*onload/gi,\n /** form tags — phishing/credential harvesting via action= redirection */\n /<form[\\s>]/gi,\n /** meta tags — http-equiv refresh redirects or CSP bypass */\n /<meta[\\s>]/gi,\n /** base href hijacking — redirects all relative URLs to attacker domain */\n /<base[\\s>]/gi,\n /** link tag injection — stylesheet or preload CSRF attacks */\n /<link[\\s>]/gi,\n] as const;\n\n/**\n * Removal patterns — used by sanitizeXss() to strip dangerous content.\n * More targeted than XSS_PATTERNS: each pattern captures the full dangerous\n * substring (tag, attribute + value, protocol) so it can be replaced safely.\n * Must stay in sync with XSS_PATTERNS above.\n */\nexport const XSS_REMOVE_PATTERNS = [\n /** Full script blocks (content + tags) */\n /<script[^>]*>[\\s\\S]*?<\\/script>/gi,\n /** Standalone/unclosed script tags */\n /<script[^>]*>/gi,\n /** iframe — full block and partial/unclosed */\n /<iframe[^>]*>[\\s\\S]*?<\\/iframe>/gi,\n /<iframe[^>]*/gi,\n /** object — full block and partial/unclosed */\n /<object[^>]*>[\\s\\S]*?<\\/object>/gi,\n /<object[^>]*/gi,\n /** embed tags */\n /<embed[^>]*/gi,\n /** SVG with inline event handlers */\n /<svg[^>]*onload[^>]*>/gi,\n /** URL-encoded script tags */\n /%3Cscript/gi,\n /** Event handlers with quoted values: onclick=\"...\", onerror='...' */\n /(?:[\\s/])on\\w+\\s*=\\s*[\"'][^\"']*[\"']/gi,\n /** Event handlers with unquoted values: onload=value */\n /(?:[\\s/])on\\w+\\s*=\\s*[^\\s>]*/gi,\n /** javascript: and vbscript: protocols (allow optional spaces before colon) */\n /javascript\\s*:/gi,\n /vbscript\\s*:/gi,\n /** data: URIs with HTML/script content */\n /data\\s*:\\s*text\\/html[^>\\s]*/gi,\n /** form tag injection — phishing via action= redirection */\n /<form[\\s>][^>]*/gi,\n /** meta tag injection — http-equiv refresh or CSP bypass */\n /<meta[\\s>][^>]*/gi,\n /** base href hijacking */\n /<base[\\s>][^>]*/gi,\n /** link tag injection — stylesheet or preload attacks */\n /<link[\\s>][^>]*/gi,\n] as const;\n\n// =============================================================================\n// SQL INJECTION PATTERNS\n// =============================================================================\nexport const SQL_PATTERNS = [\n /** SQL keywords */\n /(\\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)\\b)/gi,\n /** SQL comments: ANSI (--), C-style (slash-star ... star-slash), MySQL (#) */\n /(--|\\/\\*|\\*\\/|#)/g,\n /** SQL statement separators */\n /(;|\\|\\||&&)/g,\n /** Boolean injection: OR 1=1 */\n /\\bOR\\s+\\d+\\s*=\\s*\\d+/gi,\n /** Boolean injection: OR 'a'='a' or OR \"a\"=\"a\" (including mixed quotes) */\n /\\bOR\\s+(['\"])[^'\"]*\\1\\s*=\\s*(['\"])[^'\"]*\\2/gi,\n /\\bOR\\s+('[^']*'|\"[^\"]*\")\\s*=\\s*('[^']*'|\"[^\"]*\")/gi,\n /** Boolean injection: AND 1=1 */\n /\\bAND\\s+\\d+\\s*=\\s*\\d+/gi,\n /** Boolean injection: AND 'a'='a' or AND \"a\"=\"a\" (including mixed quotes) */\n /\\bAND\\s+(['\"])[^'\"]*\\1\\s*=\\s*(['\"])[^'\"]*\\2/gi,\n /\\bAND\\s+('[^']*'|\"[^\"]*\")\\s*=\\s*('[^']*'|\"[^\"]*\")/gi,\n /** Time-based blind: SLEEP() */\n /\\bSLEEP\\s*\\(\\s*\\d+\\s*\\)/gi,\n /** Time-based blind: BENCHMARK() */\n /\\bBENCHMARK\\s*\\(/gi,\n /** Time-based blind: PostgreSQL pg_sleep() */\n /\\bpg_sleep\\s*\\(/gi,\n /** Time-based blind: MSSQL WAITFOR DELAY */\n /\\bWAITFOR\\s+DELAY\\b/gi,\n] as const;\n\n// =============================================================================\n// PATH TRAVERSAL PATTERNS\n// =============================================================================\nexport const PATH_PATTERNS = [\n /** Unix path traversal */\n /\\.\\.\\//g,\n /** Windows path traversal */\n /\\.\\.\\\\/g,\n /** URL-encoded traversal (%2e%2e) */\n /%2e%2e/gi,\n /** Double URL-encoded traversal (%252e) */\n /%252e/gi,\n /** Mixed encoding: ..%2F */\n /\\.\\.%2F/gi,\n /** Mixed encoding: %2e./ and .%2e/ */\n /%2e\\.[\\\\/]/gi,\n /\\.%2e[\\\\/]/gi,\n /** Fully URL-encoded: %2e%2e%2f */\n /%2e%2e%2f/gi,\n /** Double URL-encoded forward slash: %252f */\n /%252f/gi,\n /** Dotdotslash bypass: ....// or ....\\\\ */\n /\\.{2,}[/\\\\]{2,}/g,\n /** Null byte injection in paths */\n /\\0/g,\n] as const;\n\n// =============================================================================\n// COMMAND INJECTION PATTERNS\n// =============================================================================\nexport const COMMAND_PATTERNS = [\n /**\n * Shell metacharacters that enable command chaining/substitution.\n * Bare ( and ) are excluded — they appear in common legitimate values\n * (function calls in code fields, math expressions, etc.).\n * Command substitution is caught by the $( combined pattern below.\n * NOTE: ';', '&', '|' may appear in legitimate URL query strings\n * and Markdown; consider disabling command checking (command: false)\n * for fields that intentionally allow those characters.\n */\n /[;&|`]/g,\n /** Command substitution: $( ... ) — matched as a pair to reduce false positives */\n /\\$\\(/g,\n /** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */\n /%0[0-9a-f]/gi,\n] as const;\n\n// =============================================================================\n// DANGEROUS KEYS\n// =============================================================================\n\n/**\n * Prototype pollution keys to block.\n * Stored lowercase — always compare with key.toLowerCase().\n *\n * Includes:\n * - __proto__: direct prototype assignment\n * - constructor: access to constructor.prototype chain\n * - prototype: direct prototype property\n * - __defineGetter__/__defineSetter__: legacy property definition (can override getters/setters)\n * - __lookupGetter__/__lookupSetter__: legacy property introspection\n */\nexport const DANGEROUS_PROTO_KEYS = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n '__definegetter__',\n '__definesetter__',\n '__lookupgetter__',\n '__lookupsetter__',\n]);\n\n/** MongoDB operators to block */\nexport const NOSQL_DANGEROUS_KEYS = new Set([\n // Comparison\n '$gt', '$gte', '$lt', '$lte', '$ne', '$eq', '$in', '$nin',\n // Logical\n '$and', '$or', '$not', '$nor',\n // Element / evaluation\n '$exists', '$type', '$regex', '$where', '$expr', '$mod', '$text', '$jsonSchema',\n // Array\n '$elemMatch', '$all', '$size',\n // JavaScript execution (critical)\n '$function', '$accumulator',\n // Aggregation pipeline operators (injectable via $lookup etc.)\n '$lookup', '$match', '$project', '$group', '$sort', '$limit', '$skip',\n '$unwind', '$addFields', '$replaceRoot',\n]);\n\n// =============================================================================\n// REDACTION\n// =============================================================================\nexport const REDACTION = {\n /** Replacement text for redacted values */\n REPLACEMENT: '[REDACTED]',\n /** Truncation indicator */\n TRUNCATED: '[TRUNCATED]',\n /** Max depth indicator */\n MAX_DEPTH: '[MAX_DEPTH]',\n /** Default max message length */\n DEFAULT_MAX_LENGTH: 10_000,\n /** Default sensitive keys to redact */\n SENSITIVE_KEYS: new Set([\n 'password', 'passwd', 'pwd', 'secret', 'token', 'apikey',\n 'api_key', 'apiKey', 'auth', 'authorization', 'credit_card',\n 'creditcard', 'cc', 'ssn', 'social_security', 'private_key',\n 'privateKey', 'access_token', 'accessToken', 'refresh_token',\n 'refreshToken', 'bearer', 'jwt', 'session', 'cookie',\n 'credentials', 'x-api-key', 'x-auth-token',\n ]),\n} as const;\n\n// =============================================================================\n// VALIDATION PATTERNS\n// =============================================================================\nexport const VALIDATION = {\n /**\n * Email regex pattern.\n * Rejects consecutive dots in local part (e.g. test..foo@example.com),\n * leading/trailing dots, and other common invalid forms.\n */\n EMAIL: /^[^\\s@.][^\\s@]*(?:\\.[^\\s@.][^\\s@]*)*@[^\\s@]+\\.[^\\s@]+$/,\n /**\n * URL regex pattern.\n * Only allows http:// and https:// — explicitly rejects javascript:,\n * data:, vbscript:, and other dangerous URI schemes.\n */\n URL: /^https?:\\/\\/[^\\s/$.?#][^\\s]*$/,\n /** UUID regex pattern (v4) */\n UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,\n} as const;\n\n// =============================================================================\n// ERROR MESSAGES\n// =============================================================================\nexport const ERRORS = {\n /** Generic error message (production) */\n INTERNAL_SERVER_ERROR: 'Internal Server Error',\n /** Input too large error */\n INPUT_TOO_LARGE: (maxSize: number) => `Input exceeds maximum size of ${maxSize} bytes`,\n /** Validation error messages */\n VALIDATION: {\n REQUIRED: (field: string) => `${field} is required`,\n INVALID_TYPE: (field: string, type: string) => `${field} must be a ${type}`,\n MIN_LENGTH: (field: string, min: number) => `${field} must be at least ${min} characters`,\n MAX_LENGTH: (field: string, max: number) => `${field} must be at most ${max} characters`,\n MIN_VALUE: (field: string, min: number) => `${field} must be at least ${min}`,\n MAX_VALUE: (field: string, max: number) => `${field} must be at most ${max}`,\n INVALID_FORMAT: (field: string) => `${field} format is invalid`,\n INVALID_EMAIL: (field: string) => `${field} must be a valid email`,\n INVALID_URL: (field: string) => `${field} must be a valid URL`,\n INVALID_UUID: (field: string) => `${field} must be a valid UUID`,\n INVALID_ENUM: (field: string, values: unknown[]) => `${field} must be one of: ${values.join(', ')}`,\n MIN_ITEMS: (field: string, min: number) => `${field} must have at least ${min} items`,\n MAX_ITEMS: (field: string, max: number) => `${field} must have at most ${max} items`,\n },\n} as const;\n\n// =============================================================================\n// BLOCKED TEXT (for sanitizer replacements)\n// =============================================================================\nexport const BLOCKED = '[BLOCKED]' as const;\n","/**\n * @module @arcis/node/stores/memory\n * In-memory rate limit store\n */\n\nimport type { RateLimitStore, RateLimitEntry } from '../core/types';\nimport { RATE_LIMIT } from '../core/constants';\n\n/**\n * In-memory rate limit store.\n * Suitable for single-instance deployments.\n * For distributed systems, use RedisStore or a custom store.\n * \n * @example\n * const store = new MemoryStore(60000); // 1 minute window\n * const limiter = createRateLimiter({ store });\n */\n/** Default maximum number of keys the in-memory store will hold. */\nconst DEFAULT_MAX_SIZE = 10_000;\n\nexport class MemoryStore implements RateLimitStore {\n private store: Map<string, RateLimitEntry> = new Map();\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n private windowMs: number;\n private maxSize: number;\n\n constructor(windowMs: number = RATE_LIMIT.DEFAULT_WINDOW_MS, maxSize: number = DEFAULT_MAX_SIZE) {\n if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {\n throw new RangeError(\n `MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`\n );\n }\n if (!Number.isFinite(maxSize) || maxSize < 1) {\n throw new RangeError(`MemoryStore: maxSize must be >= 1 (got ${maxSize})`);\n }\n this.windowMs = windowMs;\n this.maxSize = maxSize;\n this.startCleanup();\n }\n\n /**\n * Start the cleanup interval to remove expired entries.\n */\n private startCleanup(): void {\n // Clamp the cleanup interval between 30 s and 5 min regardless of windowMs.\n // Running it every windowMs is fine for typical windows but would fire every\n // second for short windows (e.g. windowMs: 1000), causing O(n) GC pressure.\n const CLEANUP_MIN_MS = 30_000;\n const CLEANUP_MAX_MS = 300_000;\n const cleanupMs = Math.min(Math.max(this.windowMs, CLEANUP_MIN_MS), CLEANUP_MAX_MS);\n\n this.cleanupInterval = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.store.entries()) {\n if (entry.resetTime < now) {\n this.store.delete(key);\n }\n }\n }, cleanupMs);\n\n // Prevent interval from keeping the process alive\n if (typeof this.cleanupInterval.unref === 'function') {\n this.cleanupInterval.unref();\n }\n }\n\n async get(key: string): Promise<RateLimitEntry | null> {\n const entry = this.store.get(key);\n if (!entry) return null;\n \n // Check if expired\n if (entry.resetTime < Date.now()) {\n this.store.delete(key);\n return null;\n }\n \n return entry;\n }\n\n async set(key: string, entry: RateLimitEntry): Promise<void> {\n if (!this.store.has(key) && this.store.size >= this.maxSize) {\n this.evictExpired();\n // If still at capacity after eviction, fail open — don't crash the app\n if (this.store.size >= this.maxSize) return;\n }\n this.store.set(key, entry);\n }\n\n async increment(key: string): Promise<number> {\n const now = Date.now();\n const entry = this.store.get(key);\n\n if (!entry || entry.resetTime < now) {\n // Start new window — check capacity first\n if (this.store.size >= this.maxSize) {\n this.evictExpired();\n if (this.store.size >= this.maxSize) return 1; // fail open\n }\n this.store.set(key, { count: 1, resetTime: now + this.windowMs });\n return 1;\n }\n\n entry.count++;\n return entry.count;\n }\n\n /** Eagerly remove expired entries to reclaim capacity. */\n private evictExpired(): void {\n const now = Date.now();\n for (const [key, entry] of this.store.entries()) {\n if (entry.resetTime < now) this.store.delete(key);\n }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.store.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n async reset(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n async close(): Promise<void> {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n this.store.clear();\n }\n\n /**\n * Get current store size (for monitoring).\n */\n get size(): number {\n return this.store.size;\n }\n}\n","/**\n * @module @arcis/node/stores/redis\n * Redis rate limit store\n * \n * Note: This is a reference implementation. You'll need to install\n * the 'ioredis' or 'redis' package and pass your client instance.\n */\n\nimport type { RateLimitStore, RateLimitEntry } from '../core/types';\nimport { RATE_LIMIT } from '../core/constants';\n\n/** Generic Redis client interface (works with ioredis, redis, etc.) */\nexport interface RedisClientLike {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, mode?: string, duration?: number): Promise<unknown>;\n setex(key: string, seconds: number, value: string): Promise<unknown>;\n expire(key: string, seconds: number): Promise<unknown>;\n incr(key: string): Promise<number>;\n decr(key: string): Promise<number>;\n del(key: string): Promise<number>;\n ttl(key: string): Promise<number>;\n quit?(): Promise<unknown>;\n disconnect?(): Promise<unknown>;\n}\n\nexport interface RedisStoreOptions {\n /** Redis client instance */\n client: RedisClientLike;\n /** Key prefix. Default: 'arcis:rl:' */\n prefix?: string;\n /** Window size in milliseconds. Default: 60000 */\n windowMs?: number;\n}\n\n/**\n * Redis rate limit store for distributed deployments.\n * \n * @example\n * import Redis from 'ioredis';\n * \n * const redis = new Redis();\n * const store = new RedisStore({ client: redis });\n * const limiter = createRateLimiter({ store });\n * \n * // Cleanup on shutdown\n * process.on('SIGTERM', async () => {\n * await store.close();\n * });\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClientLike;\n private prefix: string;\n private windowMs: number;\n private windowSec: number;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? 'arcis:rl:';\n this.windowMs = options.windowMs ?? RATE_LIMIT.DEFAULT_WINDOW_MS;\n this.windowSec = Math.ceil(this.windowMs / 1000);\n }\n\n private getKey(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n async get(key: string): Promise<RateLimitEntry | null> {\n const redisKey = this.getKey(key);\n \n const [countStr, ttl] = await Promise.all([\n this.client.get(redisKey),\n this.client.ttl(redisKey),\n ]);\n \n if (!countStr || ttl < 0) {\n return null;\n }\n \n const count = parseInt(countStr, 10);\n if (isNaN(count)) {\n // Corrupt value in Redis — treat as if key doesn't exist\n return null;\n }\n\n return {\n count,\n resetTime: Date.now() + (ttl * 1000),\n };\n }\n\n async set(key: string, entry: RateLimitEntry): Promise<void> {\n const redisKey = this.getKey(key);\n // Clamp to at least 1 second — Math.ceil can produce 0 or negative values\n // when entry.resetTime is in the past due to Redis latency or clock skew.\n const ttlSec = Math.max(1, Math.ceil((entry.resetTime - Date.now()) / 1000));\n await this.client.setex(redisKey, ttlSec, entry.count.toString());\n }\n\n async increment(key: string): Promise<number> {\n const redisKey = this.getKey(key);\n \n // INCR creates key with value 1 if it doesn't exist\n const count = await this.client.incr(redisKey);\n\n // Set expiry only on first increment using EXPIRE, which sets the TTL\n // without overwriting the value (unlike SET ... EX which would reset the\n // counter if two requests increment concurrently before expiry is set).\n if (count === 1) {\n await this.client.expire(redisKey, this.windowSec);\n }\n\n return count;\n }\n\n async decrement(key: string): Promise<void> {\n const redisKey = this.getKey(key);\n await this.client.decr(redisKey);\n }\n\n async reset(key: string): Promise<void> {\n const redisKey = this.getKey(key);\n await this.client.del(redisKey);\n }\n\n async close(): Promise<void> {\n // Don't close the client - it may be shared\n // The caller should manage the client lifecycle\n }\n}\n\n/**\n * Create a Redis store with the given options.\n * Convenience function for functional programming style.\n * \n * @example\n * const store = createRedisStore({ client: redisClient });\n */\nexport function createRedisStore(options: RedisStoreOptions): RedisStore {\n return new RedisStore(options);\n}\n"]}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @module @arcis/node/stores/memory
3
+ * In-memory rate limit store
4
+ */
5
+ import type { RateLimitStore, RateLimitEntry } from '../core/types';
6
+ export declare class MemoryStore implements RateLimitStore {
7
+ private store;
8
+ private cleanupInterval;
9
+ private windowMs;
10
+ private maxSize;
11
+ constructor(windowMs?: number, maxSize?: number);
12
+ /**
13
+ * Start the cleanup interval to remove expired entries.
14
+ */
15
+ private startCleanup;
16
+ get(key: string): Promise<RateLimitEntry | null>;
17
+ set(key: string, entry: RateLimitEntry): Promise<void>;
18
+ increment(key: string): Promise<number>;
19
+ /** Eagerly remove expired entries to reclaim capacity. */
20
+ private evictExpired;
21
+ decrement(key: string): Promise<void>;
22
+ reset(key: string): Promise<void>;
23
+ close(): Promise<void>;
24
+ /**
25
+ * Get current store size (for monitoring).
26
+ */
27
+ get size(): number;
28
+ }
29
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAepE,qBAAa,WAAY,YAAW,cAAc;IAChD,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,eAAe,CAA+C;IACtE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;gBAEZ,QAAQ,GAAE,MAAqC,EAAE,OAAO,GAAE,MAAyB;IAc/F;;OAEG;IACH,OAAO,CAAC,YAAY;IAuBd,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAahD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAStD,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAkB7C,0DAA0D;IAC1D,OAAO,CAAC,YAAY;IAOd,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -1,41 +1,3 @@
1
- import { g as RateLimitStore, R as RateLimitEntry } from '../types-BOkx5YJc.mjs';
2
- import 'express';
3
-
4
- /**
5
- * @module @arcis/node/stores/memory
6
- * In-memory rate limit store
7
- */
8
-
9
- /**
10
- * In-memory rate limit store.
11
- * Suitable for single-instance deployments.
12
- * For distributed systems, use RedisStore or a custom store.
13
- *
14
- * @example
15
- * const store = new MemoryStore(60000); // 1 minute window
16
- * const limiter = createRateLimiter({ store });
17
- */
18
- declare class MemoryStore implements RateLimitStore {
19
- private store;
20
- private cleanupInterval;
21
- private windowMs;
22
- constructor(windowMs?: number);
23
- /**
24
- * Start the cleanup interval to remove expired entries.
25
- */
26
- private startCleanup;
27
- get(key: string): Promise<RateLimitEntry | null>;
28
- set(key: string, entry: RateLimitEntry): Promise<void>;
29
- increment(key: string): Promise<number>;
30
- decrement(key: string): Promise<void>;
31
- reset(key: string): Promise<void>;
32
- close(): Promise<void>;
33
- /**
34
- * Get current store size (for monitoring).
35
- */
36
- get size(): number;
37
- }
38
-
39
1
  /**
40
2
  * @module @arcis/node/stores/redis
41
3
  * Redis rate limit store
@@ -43,9 +5,9 @@ declare class MemoryStore implements RateLimitStore {
43
5
  * Note: This is a reference implementation. You'll need to install
44
6
  * the 'ioredis' or 'redis' package and pass your client instance.
45
7
  */
46
-
8
+ import type { RateLimitStore, RateLimitEntry } from '../core/types';
47
9
  /** Generic Redis client interface (works with ioredis, redis, etc.) */
48
- interface RedisClientLike {
10
+ export interface RedisClientLike {
49
11
  get(key: string): Promise<string | null>;
50
12
  set(key: string, value: string, mode?: string, duration?: number): Promise<unknown>;
51
13
  setex(key: string, seconds: number, value: string): Promise<unknown>;
@@ -57,7 +19,7 @@ interface RedisClientLike {
57
19
  quit?(): Promise<unknown>;
58
20
  disconnect?(): Promise<unknown>;
59
21
  }
60
- interface RedisStoreOptions {
22
+ export interface RedisStoreOptions {
61
23
  /** Redis client instance */
62
24
  client: RedisClientLike;
63
25
  /** Key prefix. Default: 'arcis:rl:' */
@@ -80,7 +42,7 @@ interface RedisStoreOptions {
80
42
  * await store.close();
81
43
  * });
82
44
  */
83
- declare class RedisStore implements RateLimitStore {
45
+ export declare class RedisStore implements RateLimitStore {
84
46
  private client;
85
47
  private prefix;
86
48
  private windowMs;
@@ -101,6 +63,5 @@ declare class RedisStore implements RateLimitStore {
101
63
  * @example
102
64
  * const store = createRedisStore({ client: redisClient });
103
65
  */
104
- declare function createRedisStore(options: RedisStoreOptions): RedisStore;
105
-
106
- export { MemoryStore, type RedisClientLike, RedisStore, type RedisStoreOptions, createRedisStore };
66
+ export declare function createRedisStore(options: RedisStoreOptions): RedisStore;
67
+ //# sourceMappingURL=redis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/stores/redis.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpE,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpF,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1B,UAAU,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,4BAA4B;IAC5B,MAAM,EAAE,eAAe,CAAC;IACxB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,UAAW,YAAW,cAAc;IAC/C,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;gBAEd,OAAO,EAAE,iBAAiB;IAOtC,OAAO,CAAC,MAAM;IAIR,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAwBhD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtD,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBvC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAEvE"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @module @arcis/node/utils/duration
3
+ * Parse human-readable duration strings into milliseconds.
4
+ *
5
+ * Supports: ms, s, m, h, d
6
+ *
7
+ * @example
8
+ * parseDuration('5m') // 300000
9
+ * parseDuration('2h') // 7200000
10
+ * parseDuration(60000) // 60000 (passthrough)
11
+ * parseDuration('500ms') // 500
12
+ */
13
+ /**
14
+ * Parse a duration string or number into milliseconds.
15
+ *
16
+ * @param value - Duration string (e.g. "5m", "2h", "30s") or number (ms)
17
+ * @returns Duration in milliseconds
18
+ * @throws {Error} If the value is not a valid duration
19
+ *
20
+ * @example
21
+ * parseDuration('15m') // 900000
22
+ * parseDuration('1d') // 86400000
23
+ * parseDuration('500ms') // 500
24
+ * parseDuration(60000) // 60000
25
+ */
26
+ export declare function parseDuration(value: string | number): number;
27
+ /**
28
+ * Format milliseconds into a human-readable duration string.
29
+ *
30
+ * @param ms - Duration in milliseconds
31
+ * @returns Human-readable string (e.g. "5m", "2h 30m")
32
+ */
33
+ export declare function formatDuration(ms: number): string;
34
+ //# sourceMappingURL=duration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../../src/utils/duration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAeH;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CA4B5D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAiBjD"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @module @arcis/node/utils/fingerprint
3
+ * Deterministic request fingerprinting via SHA-256.
4
+ *
5
+ * Generates a stable hash from request characteristics for
6
+ * rate limiting keys, abuse detection, and analytics.
7
+ *
8
+ * @example
9
+ * const fp = await fingerprint(req);
10
+ * // "a3f2b8c1d4e5..."
11
+ */
12
+ import type { DetectIpOptions } from './ip';
13
+ export interface FingerprintOptions {
14
+ /** Include IP address in fingerprint. Default: true */
15
+ ip?: boolean;
16
+ /** Include User-Agent header. Default: true */
17
+ userAgent?: boolean;
18
+ /** Include Accept header. Default: true */
19
+ accept?: boolean;
20
+ /** Include Accept-Language header. Default: true */
21
+ acceptLanguage?: boolean;
22
+ /** Include Accept-Encoding header. Default: true */
23
+ acceptEncoding?: boolean;
24
+ /** Additional custom components to include */
25
+ custom?: string[];
26
+ /** IP detection options */
27
+ ipOptions?: DetectIpOptions;
28
+ }
29
+ interface RequestLike {
30
+ headers: Record<string, string | string[] | undefined>;
31
+ socket?: {
32
+ remoteAddress?: string;
33
+ };
34
+ connection?: {
35
+ remoteAddress?: string;
36
+ };
37
+ ip?: string;
38
+ }
39
+ /**
40
+ * Generate a deterministic fingerprint for a request.
41
+ *
42
+ * Creates a SHA-256 hash from configurable request components.
43
+ * The fingerprint is stable across requests from the same client
44
+ * (same IP, browser, language settings).
45
+ *
46
+ * @param req - HTTP request object
47
+ * @param options - Fingerprint configuration
48
+ * @returns Hex-encoded SHA-256 hash (64 characters)
49
+ *
50
+ * @example
51
+ * // Default fingerprint (IP + UA + Accept headers)
52
+ * const fp = fingerprint(req);
53
+ *
54
+ * @example
55
+ * // IP-only fingerprint (for simple rate limiting)
56
+ * const fp = fingerprint(req, { userAgent: false, accept: false, acceptLanguage: false, acceptEncoding: false });
57
+ *
58
+ * @example
59
+ * // With custom components
60
+ * const fp = fingerprint(req, { custom: [req.body?.userId] });
61
+ */
62
+ export declare function fingerprint(req: RequestLike, options?: FingerprintOptions): string;
63
+ export {};
64
+ //# sourceMappingURL=fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/utils/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAE5C,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,+CAA+C;IAC/C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oDAAoD;IACpD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oDAAoD;IACpD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,2BAA2B;IAC3B,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,UAAU,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAQD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,CAuCtF"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @module @arcis/node/utils
3
+ * Utility functions for Arcis
4
+ */
5
+ export { parseDuration, formatDuration } from './duration';
6
+ export { detectClientIp, isPrivateIp } from './ip';
7
+ export { fingerprint } from './fingerprint';
8
+ export type { Platform, DetectIpOptions } from './ip';
9
+ export type { FingerprintOptions } from './fingerprint';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AACtD,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC"}