{"version":3,"sources":["../../src/utils/duration.ts","../../src/utils/ip.ts","../../src/utils/fingerprint.ts"],"names":["xff","getHeader"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @module @arcis/node/utils/duration\n * Parse human-readable duration strings into milliseconds.\n *\n * Supports: ms, s, m, h, d\n *\n * @example\n * parseDuration('5m')    // 300000\n * parseDuration('2h')    // 7200000\n * parseDuration(60000)   // 60000 (passthrough)\n * parseDuration('500ms') // 500\n */\n\n/** Maximum duration: ~49.7 days (uint32 max in ms) */\nconst MAX_DURATION_MS = 4_294_967_295;\n\nconst DURATION_REGEX = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)$/i;\n\nconst UNIT_TO_MS: Record<string, number> = {\n  ms: 1,\n  s: 1_000,\n  m: 60_000,\n  h: 3_600_000,\n  d: 86_400_000,\n};\n\n/**\n * Parse a duration string or number into milliseconds.\n *\n * @param value - Duration string (e.g. \"5m\", \"2h\", \"30s\") or number (ms)\n * @returns Duration in milliseconds\n * @throws {Error} If the value is not a valid duration\n *\n * @example\n * parseDuration('15m')   // 900000\n * parseDuration('1d')    // 86400000\n * parseDuration('500ms') // 500\n * parseDuration(60000)   // 60000\n */\nexport function parseDuration(value: string | number): number {\n  if (typeof value === 'number') {\n    if (!Number.isFinite(value) || value < 0) {\n      throw new Error(`Invalid duration: ${value}. Must be a non-negative finite number.`);\n    }\n    return Math.min(Math.floor(value), MAX_DURATION_MS);\n  }\n\n  if (typeof value !== 'string' || value.trim() === '') {\n    throw new Error(`Invalid duration: \"${value}\". Expected a duration string (e.g. \"5m\", \"2h\") or number.`);\n  }\n\n  const match = value.trim().match(DURATION_REGEX);\n  if (!match) {\n    throw new Error(\n      `Invalid duration: \"${value}\". Expected format: <number><unit> where unit is ms, s, m, h, or d.`\n    );\n  }\n\n  const amount = parseFloat(match[1]);\n  const unit = match[2].toLowerCase();\n  const ms = Math.floor(amount * UNIT_TO_MS[unit]);\n\n  if (ms < 0 || ms > MAX_DURATION_MS) {\n    throw new Error(`Duration \"${value}\" exceeds maximum allowed (${MAX_DURATION_MS}ms / ~49.7 days).`);\n  }\n\n  return ms;\n}\n\n/**\n * Format milliseconds into a human-readable duration string.\n *\n * @param ms - Duration in milliseconds\n * @returns Human-readable string (e.g. \"5m\", \"2h 30m\")\n */\nexport function formatDuration(ms: number): string {\n  if (!Number.isFinite(ms) || ms < 0) return '0ms';\n\n  if (ms < 1000) return `${ms}ms`;\n\n  const days = Math.floor(ms / 86_400_000);\n  const hours = Math.floor((ms % 86_400_000) / 3_600_000);\n  const minutes = Math.floor((ms % 3_600_000) / 60_000);\n  const seconds = Math.floor((ms % 60_000) / 1_000);\n\n  const parts: string[] = [];\n  if (days > 0) parts.push(`${days}d`);\n  if (hours > 0) parts.push(`${hours}h`);\n  if (minutes > 0) parts.push(`${minutes}m`);\n  if (seconds > 0) parts.push(`${seconds}s`);\n\n  return parts.join(' ') || '0ms';\n}\n","/**\n * @module @arcis/node/utils/ip\n * Platform-aware client IP detection.\n *\n * Prevents IP spoofing by reading platform-specific headers\n * instead of blindly trusting X-Forwarded-For.\n *\n * @example\n * // Auto-detect platform from environment\n * const ip = detectClientIp(req);\n *\n * // Explicit platform\n * const ip = detectClientIp(req, { platform: 'cloudflare' });\n */\n\nimport type { IncomingMessage } from 'http';\n\nexport type Platform =\n  | 'auto'\n  | 'cloudflare'\n  | 'vercel'\n  | 'flyio'\n  | 'render'\n  | 'firebase'\n  | 'aws-alb'\n  | 'generic';\n\nexport interface DetectIpOptions {\n  /** Platform to use for header selection. Default: 'auto' */\n  platform?: Platform;\n  /** Number of trusted proxies (for X-Forwarded-For parsing). Default: 1 */\n  trustedProxyCount?: number;\n}\n\ninterface RequestLike {\n  headers: Record<string, string | string[] | undefined>;\n  socket?: { remoteAddress?: string };\n  connection?: { remoteAddress?: string };\n  ip?: string;\n}\n\n/**\n * Platform-specific header configurations.\n * Each platform sets a trusted header that cannot be spoofed by the client.\n */\nconst PLATFORM_HEADERS: Record<Exclude<Platform, 'auto' | 'generic'>, string> = {\n  cloudflare: 'cf-connecting-ip',\n  vercel: 'x-real-ip',\n  flyio: 'fly-client-ip',\n  render: 'x-render-client-ip',\n  firebase: 'x-appengine-user-ip',\n  'aws-alb': 'x-forwarded-for',\n};\n\n/**\n * Auto-detect the platform from environment variables.\n */\nfunction detectPlatform(): Platform {\n  const env = typeof process !== 'undefined' ? process.env : {};\n\n  if (env.CF_PAGES || env.CF_WORKERS) return 'cloudflare';\n  if (env.VERCEL) return 'vercel';\n  if (env.FLY_APP_NAME) return 'flyio';\n  if (env.RENDER) return 'render';\n  if (env.FIREBASE_CONFIG || env.GCLOUD_PROJECT) return 'firebase';\n  if (env.AWS_EXECUTION_ENV || env.AWS_LAMBDA_FUNCTION_NAME) return 'aws-alb';\n\n  return 'generic';\n}\n\n// Cache the detected platform — it won't change during process lifetime\nlet _cachedPlatform: Platform | null = null;\n\nfunction getCachedPlatform(): Platform {\n  if (_cachedPlatform === null) {\n    _cachedPlatform = detectPlatform();\n  }\n  return _cachedPlatform;\n}\n\n/** Max IP string length (IPv6 max = 45 chars) */\nconst MAX_IP_LENGTH = 45;\n\n/**\n * Sanitize an IP string: trim, truncate, strip control characters.\n * Prevents unbounded strings from being used as map keys.\n */\nfunction sanitizeIp(ip: string): string {\n  const trimmed = ip.trim();\n  if (trimmed.length > MAX_IP_LENGTH) return trimmed.slice(0, MAX_IP_LENGTH);\n  return trimmed;\n}\n\n/**\n * Get a header value from the request, handling string arrays.\n */\nfunction getHeader(req: RequestLike, name: string): string | undefined {\n  const val = req.headers[name];\n  if (Array.isArray(val)) return val[0];\n  return val;\n}\n\n/**\n * Parse the rightmost trusted IP from X-Forwarded-For.\n * Reading from the right prevents client spoofing — the rightmost entry\n * is the one added by the closest trusted proxy.\n */\nfunction parseForwardedFor(header: string, trustedProxyCount: number): string | undefined {\n  const ips = header.split(',').map(ip => ip.trim()).filter(Boolean);\n  if (ips.length === 0) return undefined;\n\n  // The client IP is at position (length - trustedProxyCount)\n  const clientIndex = Math.max(0, ips.length - trustedProxyCount);\n  return ips[clientIndex] || undefined;\n}\n\n/**\n * Detect the real client IP address from a request.\n *\n * Uses platform-specific headers when available to prevent IP spoofing.\n * Falls back to X-Forwarded-For (parsed from the right) and then\n * the socket remote address.\n *\n * @param req - HTTP request object (Express, raw http, etc.)\n * @param options - Detection options\n * @returns Client IP address, or 'unknown' if unresolvable\n *\n * @example\n * // Auto-detect platform\n * app.use((req, res, next) => {\n *   const clientIp = detectClientIp(req);\n *   console.log('Client IP:', clientIp);\n *   next();\n * });\n *\n * @example\n * // Behind Cloudflare\n * const ip = detectClientIp(req, { platform: 'cloudflare' });\n *\n * @example\n * // Behind 2 proxies (e.g. CDN + load balancer)\n * const ip = detectClientIp(req, { trustedProxyCount: 2 });\n */\nexport function detectClientIp(\n  req: RequestLike | IncomingMessage,\n  options: DetectIpOptions = {}\n): string {\n  const { platform = 'auto', trustedProxyCount = 1 } = options;\n  const r = req as RequestLike;\n\n  const resolvedPlatform = platform === 'auto' ? getCachedPlatform() : platform;\n\n  // 1. Try platform-specific header (most trusted)\n  if (resolvedPlatform !== 'generic' && resolvedPlatform in PLATFORM_HEADERS) {\n    const headerName = PLATFORM_HEADERS[resolvedPlatform as keyof typeof PLATFORM_HEADERS];\n    if (headerName) {\n      if (resolvedPlatform === 'aws-alb') {\n        // AWS ALB: parse X-Forwarded-For from the right\n        const xff = getHeader(r, 'x-forwarded-for');\n        if (xff) {\n          const ip = parseForwardedFor(xff, trustedProxyCount);\n          if (ip) return sanitizeIp(ip);\n        }\n      } else {\n        const ip = getHeader(r, headerName);\n        if (ip) return sanitizeIp(ip);\n      }\n    }\n  }\n\n  // 2. Try Express req.ip (respects trust proxy setting)\n  if (r.ip) return sanitizeIp(r.ip);\n\n  // 3. Try X-Forwarded-For (parsed from the right for safety)\n  const xff = getHeader(r, 'x-forwarded-for');\n  if (xff) {\n    const ip = parseForwardedFor(xff, trustedProxyCount);\n    if (ip) return sanitizeIp(ip);\n  }\n\n  // 4. Try X-Real-IP\n  const realIp = getHeader(r, 'x-real-ip');\n  if (realIp) return sanitizeIp(realIp);\n\n  // 5. Socket remote address\n  const socketIp = r.socket?.remoteAddress ?? r.connection?.remoteAddress;\n  if (socketIp) return sanitizeIp(socketIp);\n\n  return 'unknown';\n}\n\n/**\n * Check if an IP address is a private/internal address.\n *\n * Detects: loopback, private ranges (RFC 1918), link-local, IPv6 equivalents.\n */\nexport function isPrivateIp(ip: string): boolean {\n  // Strip IPv4-mapped IPv6 prefix (::ffff:127.0.0.1 -> 127.0.0.1)\n  const normalized = ip.startsWith('::ffff:') ? ip.slice(7) : ip;\n\n  // IPv4 private ranges\n  if (/^127\\./.test(normalized)) return true;                          // Loopback\n  if (/^10\\./.test(normalized)) return true;                           // Class A private\n  if (/^172\\.(1[6-9]|2\\d|3[01])\\./.test(normalized)) return true;     // Class B private\n  if (/^192\\.168\\./.test(normalized)) return true;                     // Class C private\n  if (/^169\\.254\\./.test(normalized)) return true;                     // Link-local\n  if (/^0\\./.test(normalized)) return true;                            // Current network\n\n  // IPv6\n  if (ip === '::1') return true;                               // Loopback\n  if (/^fe80:/i.test(ip)) return true;                         // Link-local\n  if (/^fc00:/i.test(ip)) return true;                         // Unique local\n  if (/^fd/i.test(ip)) return true;                            // Unique local\n\n  return false;\n}\n\n/** Reset cached platform (for testing). */\nexport function _resetPlatformCache(): void {\n  _cachedPlatform = null;\n}\n","/**\n * @module @arcis/node/utils/fingerprint\n * Deterministic request fingerprinting via SHA-256.\n *\n * Generates a stable hash from request characteristics for\n * rate limiting keys, abuse detection, and analytics.\n *\n * @example\n * const fp = await fingerprint(req);\n * // \"a3f2b8c1d4e5...\"\n */\n\nimport { createHash } from 'crypto';\nimport { detectClientIp } from './ip';\nimport type { DetectIpOptions } from './ip';\n\nexport interface FingerprintOptions {\n  /** Include IP address in fingerprint. Default: true */\n  ip?: boolean;\n  /** Include User-Agent header. Default: true */\n  userAgent?: boolean;\n  /** Include Accept header. Default: true */\n  accept?: boolean;\n  /** Include Accept-Language header. Default: true */\n  acceptLanguage?: boolean;\n  /** Include Accept-Encoding header. Default: true */\n  acceptEncoding?: boolean;\n  /** Additional custom components to include */\n  custom?: string[];\n  /** IP detection options */\n  ipOptions?: DetectIpOptions;\n}\n\ninterface RequestLike {\n  headers: Record<string, string | string[] | undefined>;\n  socket?: { remoteAddress?: string };\n  connection?: { remoteAddress?: string };\n  ip?: string;\n}\n\nfunction getHeader(req: RequestLike, name: string): string {\n  const val = req.headers[name];\n  if (Array.isArray(val)) return val[0] ?? '';\n  return val ?? '';\n}\n\n/**\n * Generate a deterministic fingerprint for a request.\n *\n * Creates a SHA-256 hash from configurable request components.\n * The fingerprint is stable across requests from the same client\n * (same IP, browser, language settings).\n *\n * @param req - HTTP request object\n * @param options - Fingerprint configuration\n * @returns Hex-encoded SHA-256 hash (64 characters)\n *\n * @example\n * // Default fingerprint (IP + UA + Accept headers)\n * const fp = fingerprint(req);\n *\n * @example\n * // IP-only fingerprint (for simple rate limiting)\n * const fp = fingerprint(req, { userAgent: false, accept: false, acceptLanguage: false, acceptEncoding: false });\n *\n * @example\n * // With custom components\n * const fp = fingerprint(req, { custom: [req.body?.userId] });\n */\nexport function fingerprint(req: RequestLike, options: FingerprintOptions = {}): string {\n  const {\n    ip = true,\n    userAgent = true,\n    accept = true,\n    acceptLanguage = true,\n    acceptEncoding = true,\n    custom = [],\n    ipOptions,\n  } = options;\n\n  const components: string[] = [];\n\n  if (ip) {\n    components.push(`ip:${detectClientIp(req, ipOptions)}`);\n  }\n  if (userAgent) {\n    components.push(`ua:${getHeader(req, 'user-agent')}`);\n  }\n  if (accept) {\n    components.push(`accept:${getHeader(req, 'accept')}`);\n  }\n  if (acceptLanguage) {\n    components.push(`lang:${getHeader(req, 'accept-language')}`);\n  }\n  if (acceptEncoding) {\n    components.push(`enc:${getHeader(req, 'accept-encoding')}`);\n  }\n\n  for (const c of custom) {\n    if (c !== null && c !== undefined) components.push(`custom:${c}`);\n  }\n\n  // Sort for deterministic ordering\n  components.sort();\n\n  const hash = createHash('sha256');\n  hash.update(components.join('|'));\n  return hash.digest('hex');\n}\n"]}