'use strict';

var dns = require('dns');

function _interopNamespace(e) {
  if (e && e.__esModule) return e;
  var n = Object.create(null);
  if (e) {
    Object.keys(e).forEach(function (k) {
      if (k !== 'default') {
        var d = Object.getOwnPropertyDescriptor(e, k);
        Object.defineProperty(n, k, d.get ? d : {
          enumerable: true,
          get: function () { return e[k]; }
        });
      }
    });
  }
  n.default = e;
  return Object.freeze(n);
}

var dns__namespace = /*#__PURE__*/_interopNamespace(dns);

// src/core/constants.ts
var INPUT = {
  /** Default maximum input size (1MB) */
  DEFAULT_MAX_SIZE: 1e6};
var XSS_REMOVE_PATTERNS = [
  /** Full script blocks (content + tags) */
  /<script[^>]*>[\s\S]*?<\/script>/gi,
  /** Standalone/unclosed script tags */
  /<script[^>]*>/gi,
  /** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
  /<style[^>]*>[\s\S]*?<\/style>/gi,
  /<style[^>]*/gi,
  /** iframe — full block and partial/unclosed */
  /<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
  /<iframe[^>]*/gi,
  /** object — full block and partial/unclosed */
  /<object[^>]*>[\s\S]*?<\/object>/gi,
  /<object[^>]*/gi,
  /** embed tags */
  /<embed[^>]*/gi,
  /** SVG with inline event handlers */
  /<svg[^>]*onload[^>]*>/gi,
  /** URL-encoded script tags */
  /%3Cscript/gi,
  /** Event handlers with quoted values: onclick="...", onerror='...' */
  /(?:[\s/])on\w+\s*=\s*["'][^"']*["']/gi,
  /** Event handlers with unquoted values: onload=value */
  /(?:[\s/])on\w+\s*=\s*[^\s>]*/gi,
  /** javascript: and vbscript: protocols (allow optional spaces before colon) */
  /javascript\s*:/gi,
  /vbscript\s*:/gi,
  /** data: URIs with HTML or SVG content (SVG can run JS via inline event handlers) */
  /data\s*:\s*(?:text\/html|image\/svg)[^>\s]*/gi,
  /** form tag injection — phishing via action= redirection */
  /<form[\s>][^>]*/gi,
  /** meta tag injection — http-equiv refresh or CSP bypass */
  /<meta[\s>][^>]*/gi,
  /** base href hijacking */
  /<base[\s>][^>]*/gi,
  /** link tag injection — stylesheet or preload attacks */
  /<link[\s>][^>]*/gi
];
var SQL_PATTERNS = [
  /** SQL keywords */
  /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)\b)/gi,
  /** SQL comments: ANSI (--), C-style (slash-star ... star-slash), MySQL (#) */
  /(--|\/\*|\*\/|#)/g,
  /** SQL statement separators */
  /(;|\|\||&&)/g,
  /** Boolean injection: OR 1=1 */
  /\bOR\s+\d+\s*=\s*\d+/gi,
  /** Boolean injection: OR 'a'='a' or OR "a"="a" (including mixed quotes) */
  /\bOR\s+(['"])[^'"]*\1\s*=\s*(['"])[^'"]*\2/gi,
  /\bOR\s+('[^']*'|"[^"]*")\s*=\s*('[^']*'|"[^"]*")/gi,
  /** Boolean injection: AND 1=1 */
  /\bAND\s+\d+\s*=\s*\d+/gi,
  /** Boolean injection: AND 'a'='a' or AND "a"="a" (including mixed quotes) */
  /\bAND\s+(['"])[^'"]*\1\s*=\s*(['"])[^'"]*\2/gi,
  /\bAND\s+('[^']*'|"[^"]*")\s*=\s*('[^']*'|"[^"]*")/gi,
  /** Time-based blind: SLEEP() */
  /\bSLEEP\s*\(\s*\d+\s*\)/gi,
  /** Time-based blind: BENCHMARK() */
  /\bBENCHMARK\s*\(/gi,
  /** Time-based blind: PostgreSQL pg_sleep() */
  /\bpg_sleep\s*\(/gi,
  /** Time-based blind: MSSQL WAITFOR DELAY */
  /\bWAITFOR\s+DELAY\b/gi,
  /**
   * Oracle DBMS_* stdlib packages used for time-based blind SQLi
   * (DBMS_LOCK.SLEEP, DBMS_PIPE.RECEIVE_MESSAGE) and other Oracle
   * abuse paths. No legitimate user input contains these. Mirrors
   * `sqli-oracle-dbms-packages` in packages/core/patterns.json —
   * improvements.md §1.1.e Q3. Must stay in sync until Node
   * migrates to patterns.json-at-runtime (planned v1.7).
   */
  /\bDBMS_(?:LOCK|PIPE|UTILITY|XSLPROCESSOR|JAVA|OUTPUT|SCHEDULER)\b/gi
];
var PATH_PATTERNS = [
  /** Unix path traversal */
  /\.\.\//g,
  /** Windows path traversal */
  /\.\.\\/g,
  /** URL-encoded traversal (%2e%2e) */
  /%2e%2e/gi,
  /** Double URL-encoded traversal (%252e) */
  /%252e/gi,
  /** Mixed encoding: ..%2F */
  /\.\.%2F/gi,
  /** Mixed encoding: %2e./ and .%2e/ */
  /%2e\.[\\/]/gi,
  /\.%2e[\\/]/gi,
  /** Fully URL-encoded: %2e%2e%2f */
  /%2e%2e%2f/gi,
  /** Double URL-encoded forward slash: %252f */
  /%252f/gi,
  /** Dotdotslash bypass: ....// or ....\\ */
  /\.{2,}[/\\]{2,}/g,
  /** Null byte injection in paths */
  /\0/g
];
var COMMAND_PATTERNS = [
  /**
   * Shell metacharacters that enable command chaining/substitution.
   * Bare ( and ) are excluded — they appear in common legitimate values
   * (function calls in code fields, math expressions, etc.).
   * Command substitution is caught by the $( combined pattern below.
   * NOTE: ';', '&', '|' may appear in legitimate URL query strings
   * and Markdown; consider disabling command checking (command: false)
   * for fields that intentionally allow those characters.
   */
  /[;&|`]/g,
  /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
  /\$\(/g,
  /**
   * POSIX shell IFS-substitution: ${IFS} or ${IFS%??}.
   * Attackers use this to inject spaces past metacharacter filters
   * in payloads like `;cat${IFS}/etc/passwd`. Mirrors
   * `cmdi-ifs-bypass` in packages/core/patterns.json — improvements.md
   * §1.1.e Q5. Must stay in sync until Node migrates to
   * patterns.json-at-runtime (planned v1.7).
   */
  /\$\{IFS(?:%[^}]*)?\}/g,
  /** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
  /%0[0-9a-f]/gi
];
var VALIDATION = {
  /**
   * Email regex pattern.
   * Rejects consecutive dots in local part (e.g. test..foo@example.com),
   * leading/trailing dots, and other common invalid forms.
   */
  EMAIL: /^[^\s@.][^\s@]*(?:\.[^\s@.][^\s@]*)*@[^\s@]+\.[^\s@]+$/,
  /**
   * URL regex pattern.
   * Only allows http:// and https:// (case-insensitive scheme per
   * RFC 3986); explicitly rejects javascript:, data:, vbscript:, and
   * other dangerous URI schemes.
   */
  URL: /^https?:\/\/[^\s/$.?#][^\s]*$/i,
  /** UUID regex pattern (v4) */
  UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
};
var ERRORS = {
  /** Validation error messages */
  VALIDATION: {
    REQUIRED: (field) => `${field} is required`,
    INVALID_TYPE: (field, type) => `${field} must be a ${type}`,
    MIN_LENGTH: (field, min) => `${field} must be at least ${min} characters`,
    MAX_LENGTH: (field, max) => `${field} must be at most ${max} characters`,
    MIN_VALUE: (field, min) => `${field} must be at least ${min}`,
    MAX_VALUE: (field, max) => `${field} must be at most ${max}`,
    INVALID_FORMAT: (field) => `${field} format is invalid`,
    INVALID_EMAIL: (field) => `${field} must be a valid email`,
    INVALID_URL: (field) => `${field} must be a valid URL`,
    INVALID_UUID: (field) => `${field} must be a valid UUID`,
    INVALID_ENUM: (field, values) => `${field} must be one of: ${values.join(", ")}`,
    MIN_ITEMS: (field, min) => `${field} must have at least ${min} items`,
    MAX_ITEMS: (field, max) => `${field} must have at most ${max} items`
  }
};

// src/core/errors.ts
var ArcisError = class extends Error {
  constructor(message, statusCode = 500, code = "ARCIS_ERROR") {
    super(message);
    this.name = "ArcisError";
    this.statusCode = statusCode;
    this.code = code;
    this.expose = statusCode < 500;
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
};
var InputTooLargeError = class extends ArcisError {
  constructor(maxSize, actualSize) {
    super(`Input exceeds maximum size of ${maxSize} bytes`, 413, "INPUT_TOO_LARGE");
    this.name = "InputTooLargeError";
    this.maxSize = maxSize;
    this.actualSize = actualSize;
  }
};
var SecurityThreatError = class extends ArcisError {
  constructor(threatType, pattern) {
    super("Request blocked for security reasons", 400, "SECURITY_THREAT");
    this.name = "SecurityThreatError";
    this.threatType = threatType;
    this.pattern = pattern;
  }
};

// src/sanitizers/utils.ts
function encodeHtmlEntities(str) {
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
}

// src/sanitizers/xss.ts
function sanitizeXss(input, collectThreats = false, htmlEncode = false) {
  if (typeof input !== "string") {
    return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
  }
  const threats = [];
  let value = input;
  let wasSanitized = false;
  for (const pattern of XSS_REMOVE_PATTERNS) {
    pattern.lastIndex = 0;
    if (pattern.test(value)) {
      pattern.lastIndex = 0;
      if (collectThreats) {
        const matches = value.match(pattern);
        if (matches) {
          for (const match of matches) {
            threats.push({
              type: "xss",
              pattern: pattern.source,
              original: match
            });
          }
        }
      }
      value = value.replace(pattern, "");
      wasSanitized = true;
    }
  }
  if (htmlEncode) {
    const encoded = encodeHtmlEntities(value);
    if (encoded !== value) {
      wasSanitized = true;
    }
    value = encoded;
  }
  if (collectThreats) {
    return { value, wasSanitized, threats };
  }
  return value;
}

// src/sanitizers/sql.ts
function sanitizeSql(input, collectThreats = false) {
  if (typeof input !== "string") {
    return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
  }
  const threats = [];
  let value = input;
  let wasSanitized = false;
  for (const pattern of SQL_PATTERNS) {
    pattern.lastIndex = 0;
    if (pattern.test(value)) {
      pattern.lastIndex = 0;
      if (collectThreats) {
        const matches = value.match(pattern);
        if (matches) {
          for (const match of matches) {
            threats.push({
              type: "sql_injection",
              pattern: pattern.source,
              original: match
            });
          }
        }
      }
      value = value.replace(pattern, " ");
      wasSanitized = true;
    }
  }
  if (collectThreats) {
    return { value, wasSanitized, threats };
  }
  return value;
}
function detectSql(input) {
  if (typeof input !== "string") return false;
  for (const pattern of SQL_PATTERNS) {
    pattern.lastIndex = 0;
    if (pattern.test(input)) {
      return true;
    }
  }
  return false;
}

// src/sanitizers/path.ts
function sanitizePath(input, collectThreats = false) {
  if (typeof input !== "string") {
    return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
  }
  const threats = [];
  let value = input;
  let wasSanitized = false;
  value = value.normalize("NFKC");
  let prev;
  do {
    prev = value;
    for (const pattern of PATH_PATTERNS) {
      pattern.lastIndex = 0;
      if (pattern.test(value)) {
        pattern.lastIndex = 0;
        if (collectThreats) {
          const matches = value.match(pattern);
          if (matches) {
            for (const match of matches) {
              threats.push({
                type: "path_traversal",
                pattern: pattern.source,
                original: match
              });
            }
          }
        }
        value = value.replace(pattern, "");
        wasSanitized = true;
      }
    }
  } while (value !== prev);
  if (collectThreats) {
    return { value, wasSanitized, threats };
  }
  return value;
}

// src/sanitizers/command.ts
function sanitizeCommand(input, collectThreats = false) {
  if (typeof input !== "string") {
    return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
  }
  const threats = [];
  let value = input;
  let wasSanitized = false;
  for (const pattern of COMMAND_PATTERNS) {
    pattern.lastIndex = 0;
    if (pattern.test(value)) {
      pattern.lastIndex = 0;
      if (collectThreats) {
        const matches = value.match(pattern);
        if (matches) {
          for (const match of matches) {
            threats.push({
              type: "command_injection",
              pattern: pattern.source,
              original: match
            });
          }
        }
      }
      value = value.replace(pattern, " ");
      wasSanitized = true;
    }
  }
  if (collectThreats) {
    return { value, wasSanitized, threats };
  }
  return value;
}
function detectCommandInjection(input) {
  if (typeof input !== "string") return false;
  for (const pattern of COMMAND_PATTERNS) {
    pattern.lastIndex = 0;
    if (pattern.test(input)) {
      return true;
    }
  }
  return false;
}

// src/sanitizers/sanitize.ts
function multiDecode(value, maxPasses = 4) {
  for (let i = 0; i < maxPasses; i++) {
    const prev = value;
    try {
      value = decodeURIComponent(value);
    } catch {
    }
    value = htmlEntityDecode(value);
    if (value === prev) break;
  }
  return value;
}
function htmlEntityDecode(s) {
  s = s.replace(/&#(\d+);/g, (_m, n) => {
    const code = parseInt(n, 10);
    return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
  });
  s = s.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => {
    const code = parseInt(h, 16);
    return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
  });
  const named = {
    "&lt;": "<",
    "&gt;": ">",
    "&amp;": "&",
    "&quot;": '"',
    "&apos;": "'",
    "&nbsp;": " "
  };
  for (const [entity, ch] of Object.entries(named)) {
    s = s.split(entity).join(ch);
  }
  return s;
}
function sanitizeString(value, options = {}) {
  if (typeof value !== "string") return value;
  const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
  if (value.length > maxSize) {
    throw new InputTooLargeError(maxSize, value.length);
  }
  const reject = options.mode === "reject";
  let result = value.normalize("NFKC");
  result = multiDecode(result);
  if (options.sql !== false) {
    if (reject) {
      if (detectSql(result)) {
        throw new SecurityThreatError("sql_injection", "SQL pattern detected in input");
      }
    } else {
      result = sanitizeSql(result);
    }
  }
  if (options.path !== false) {
    result = sanitizePath(result);
  }
  if (options.command !== false) {
    if (reject) {
      if (detectCommandInjection(result)) {
        throw new SecurityThreatError("command_injection", "Shell metacharacter detected in input");
      }
    } else {
      result = sanitizeCommand(result);
    }
  }
  if (options.xss !== false) {
    result = sanitizeXss(result, false, options.htmlEncode ?? false);
  }
  return result;
}

// src/validation/schema.ts
function validate(schema, source = "body") {
  return (req, res, next) => {
    const data = req[source] || {};
    const errors = [];
    const validated = {};
    for (const [field, rules] of Object.entries(schema)) {
      const value = data[field];
      const result = validateField(field, value, rules);
      if (result.errors.length > 0) {
        errors.push(...result.errors);
      } else if (result.value !== void 0) {
        validated[field] = result.value;
      }
    }
    if (errors.length > 0) {
      res.status(400).json({ errors });
      return;
    }
    Object.defineProperty(req, source, {
      value: validated,
      writable: true,
      configurable: true,
      enumerable: true
    });
    next();
  };
}
function validateField(field, value, rules) {
  const errors = [];
  if (rules.required && (value === void 0 || value === null || value === "")) {
    errors.push(ERRORS.VALIDATION.REQUIRED(field));
    return { errors };
  }
  if (value === void 0 || value === null) {
    return { errors: [] };
  }
  let typedValue = value;
  let isValid = true;
  switch (rules.type) {
    case "string":
      if (typeof value !== "string") {
        errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "string"));
        isValid = false;
        break;
      }
      if (rules.min !== void 0 && value.length < rules.min) {
        errors.push(ERRORS.VALIDATION.MIN_LENGTH(field, rules.min));
        isValid = false;
      }
      if (rules.max !== void 0 && value.length > rules.max) {
        errors.push(ERRORS.VALIDATION.MAX_LENGTH(field, rules.max));
        isValid = false;
      }
      if (rules.pattern && !rules.pattern.test(value)) {
        errors.push(ERRORS.VALIDATION.INVALID_FORMAT(field));
        isValid = false;
      }
      if (isValid && rules.enum && !rules.enum.includes(value)) {
        errors.push(ERRORS.VALIDATION.INVALID_ENUM(field, rules.enum));
        isValid = false;
      }
      if (isValid && rules.sanitize !== false) {
        typedValue = sanitizeString(value);
      }
      break;
    case "number":
      typedValue = Number(value);
      if (isNaN(typedValue)) {
        errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "number"));
        isValid = false;
        break;
      }
      if (rules.min !== void 0 && typedValue < rules.min) {
        errors.push(ERRORS.VALIDATION.MIN_VALUE(field, rules.min));
        isValid = false;
      }
      if (rules.max !== void 0 && typedValue > rules.max) {
        errors.push(ERRORS.VALIDATION.MAX_VALUE(field, rules.max));
        isValid = false;
      }
      break;
    case "boolean":
      if (value === "true" || value === true || value === 1 || value === "1") {
        typedValue = true;
      } else if (value === "false" || value === false || value === 0 || value === "0") {
        typedValue = false;
      } else {
        errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "boolean"));
        isValid = false;
      }
      break;
    case "email":
      if (!VALIDATION.EMAIL.test(String(value))) {
        errors.push(ERRORS.VALIDATION.INVALID_EMAIL(field));
        isValid = false;
      }
      if (isValid) {
        typedValue = sanitizeString(String(value).toLowerCase().trim());
      }
      break;
    case "url":
      if (!VALIDATION.URL.test(String(value))) {
        errors.push(ERRORS.VALIDATION.INVALID_URL(field));
        isValid = false;
      }
      if (isValid) {
        typedValue = sanitizeString(String(value));
      }
      break;
    case "uuid":
      if (!VALIDATION.UUID.test(String(value))) {
        errors.push(ERRORS.VALIDATION.INVALID_UUID(field));
        isValid = false;
      }
      break;
    case "array":
      if (!Array.isArray(value)) {
        errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "array"));
        isValid = false;
        break;
      }
      if (rules.min !== void 0 && value.length < rules.min) {
        errors.push(ERRORS.VALIDATION.MIN_ITEMS(field, rules.min));
        isValid = false;
      }
      if (rules.max !== void 0 && value.length > rules.max) {
        errors.push(ERRORS.VALIDATION.MAX_ITEMS(field, rules.max));
        isValid = false;
      }
      break;
    case "object":
      if (typeof value !== "object" || Array.isArray(value) || value === null) {
        errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "object"));
        isValid = false;
      }
      break;
  }
  if (isValid && rules.enum && rules.type !== "string" && !rules.enum.includes(typedValue)) {
    errors.push(ERRORS.VALIDATION.INVALID_ENUM(field, rules.enum));
    isValid = false;
  }
  if (isValid && rules.custom) {
    const customResult = rules.custom(typedValue);
    if (customResult === void 0) {
      throw new TypeError(
        `Custom validator for field "${field}" returned undefined. Return true to pass, false to fail, or a string error message.`
      );
    }
    if (customResult !== true) {
      errors.push(typeof customResult === "string" && customResult.length > 0 ? customResult : `${field} is invalid`);
      isValid = false;
    }
  }
  return {
    value: isValid ? typedValue : void 0,
    errors
  };
}
var createValidator = validate;

// src/validation/file.ts
var MAGIC_BYTES = {
  // Images
  "image/jpeg": [Buffer.from([255, 216, 255])],
  "image/png": [Buffer.from([137, 80, 78, 71])],
  "image/gif": [Buffer.from("GIF87a"), Buffer.from("GIF89a")],
  "image/webp": [Buffer.from("RIFF")],
  // RIFF....WEBP
  "image/bmp": [Buffer.from([66, 77])],
  "image/svg+xml": [],
  // text-based, check separately
  // Documents
  "application/pdf": [Buffer.from("%PDF")],
  "application/zip": [Buffer.from([80, 75, 3, 4])],
  // Audio/Video
  "audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])],
  "video/mp4": []
  // ftyp at offset 4
};
var DANGEROUS_EXTENSIONS = /* @__PURE__ */ new Set([
  // Scripts
  ".exe",
  ".bat",
  ".cmd",
  ".com",
  ".msi",
  ".scr",
  ".pif",
  ".vbs",
  ".vbe",
  ".js",
  ".jse",
  ".ws",
  ".wsf",
  ".wsc",
  ".wsh",
  ".ps1",
  ".ps1xml",
  ".ps2",
  ".ps2xml",
  ".psc1",
  ".psc2",
  ".sh",
  ".bash",
  ".csh",
  ".ksh",
  // Server-side
  ".php",
  ".php3",
  ".php4",
  ".php5",
  ".phtml",
  ".pht",
  ".asp",
  ".aspx",
  ".ashx",
  ".asmx",
  ".cer",
  ".jsp",
  ".jspx",
  ".jsw",
  ".jsv",
  ".cgi",
  ".pl",
  ".py",
  ".rb",
  // Java
  ".jar",
  ".war",
  ".ear",
  ".class",
  // Config that can execute
  ".htaccess",
  ".htpasswd",
  // Template engines
  ".ejs",
  ".pug",
  ".hbs",
  ".handlebars",
  ".njk",
  ".twig",
  // Shortcuts/links
  ".lnk",
  ".inf",
  ".reg",
  ".url",
  // Office macros
  ".docm",
  ".xlsm",
  ".pptm",
  ".dotm"
]);
var DEFAULT_MAX_SIZE = 5 * 1024 * 1024;
function sanitizeFilename(filename) {
  let name = filename;
  name = name.replace(/\0/g, "");
  name = name.replace(/^.*[/\\]/, "");
  name = name.replace(/[\x00-\x1F\x7F]/g, "");
  name = name.replace(/[<>:"/\\|?*]/g, "");
  name = name.replace(/[\s()]+/g, "_");
  name = name.replace(/^\.+/, "");
  name = name.replace(/_{2,}/g, "_");
  name = name.replace(/\.{2,}/g, ".");
  name = name.replace(/_+\./g, ".");
  name = name.replace(/^_+|_+$/g, "");
  if (!name || name === ".") {
    name = "unnamed";
  }
  return name;
}
function matchesMagicBytes(buffer, mimetype) {
  const signatures = MAGIC_BYTES[mimetype];
  if (!signatures || signatures.length === 0) return true;
  return signatures.some((sig) => {
    if (buffer.length < sig.length) return false;
    return buffer.subarray(0, sig.length).equals(sig);
  });
}
function getExtension(filename) {
  const lastDot = filename.lastIndexOf(".");
  if (lastDot < 1) return "";
  return filename.slice(lastDot).toLowerCase();
}
function hasDoubleExtension(filename) {
  const parts = filename.split(".");
  if (parts.length < 3) return false;
  for (let i = 1; i < parts.length - 1; i++) {
    const ext = "." + parts[i].toLowerCase();
    if (DANGEROUS_EXTENSIONS.has(ext)) return true;
  }
  return false;
}
function validateFile(file, options = {}) {
  const {
    maxSize = DEFAULT_MAX_SIZE,
    allowedTypes,
    allowedExtensions,
    blockExecutables = true,
    validateMagicBytes = true,
    blockNoExtension = true,
    blockDoubleExtensions = true
  } = options;
  const errors = [];
  const sanitizedFilename = sanitizeFilename(file.filename);
  const extension = getExtension(sanitizedFilename);
  if (file.size > maxSize) {
    errors.push(`File size ${file.size} exceeds maximum ${maxSize} bytes`);
  }
  if (file.size === 0) {
    errors.push("File is empty");
  }
  if (blockNoExtension && !extension) {
    errors.push("File has no extension");
  }
  if (blockExecutables && extension && DANGEROUS_EXTENSIONS.has(extension)) {
    errors.push(`Executable extension "${extension}" is not allowed`);
  }
  if (blockDoubleExtensions && hasDoubleExtension(sanitizedFilename)) {
    errors.push("Double extensions with executable types are not allowed");
  }
  if (allowedExtensions && extension) {
    const normalizedAllowed = allowedExtensions.map((e) => e.toLowerCase());
    if (!normalizedAllowed.includes(extension)) {
      errors.push(`Extension "${extension}" is not allowed. Allowed: ${normalizedAllowed.join(", ")}`);
    }
  }
  if (allowedTypes && !allowedTypes.includes(file.mimetype)) {
    errors.push(`MIME type "${file.mimetype}" is not allowed. Allowed: ${allowedTypes.join(", ")}`);
  }
  if (validateMagicBytes && file.buffer && file.buffer.length > 0) {
    if (!matchesMagicBytes(file.buffer, file.mimetype)) {
      errors.push(`File content does not match claimed MIME type "${file.mimetype}"`);
    }
  }
  return {
    valid: errors.length === 0,
    errors,
    sanitizedFilename
  };
}
function isDangerousExtension(filename) {
  const ext = getExtension(filename);
  return ext !== "" && DANGEROUS_EXTENSIONS.has(ext);
}

// src/validation/url.ts
function validateUrl(url, options = {}) {
  const {
    allowedProtocols = ["http:", "https:"],
    blockedHosts = [],
    allowedHosts = [],
    allowLocalhost = false,
    allowPrivate = false
  } = options;
  if (typeof url !== "string" || url.trim() === "") {
    return { safe: false, reason: "invalid URL: empty or not a string" };
  }
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    return { safe: false, reason: "invalid URL: failed to parse" };
  }
  if (!allowedProtocols.includes(parsed.protocol)) {
    return { safe: false, reason: `disallowed protocol: ${parsed.protocol}` };
  }
  if (parsed.username || parsed.password) {
    return { safe: false, reason: "URL contains credentials" };
  }
  const hostname = parsed.hostname.toLowerCase();
  if (allowedHosts.some((h) => hostname === h.toLowerCase())) {
    return { safe: true };
  }
  if (blockedHosts.some((h) => hostname === h.toLowerCase())) {
    return { safe: false, reason: `blocked host: ${hostname}` };
  }
  if (!allowLocalhost) {
    if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1" || hostname === "0.0.0.0" || hostname.endsWith(".localhost")) {
      return { safe: false, reason: "loopback address" };
    }
    if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
      return { safe: false, reason: "loopback address" };
    }
  }
  if (!allowLocalhost || !allowPrivate) {
    const decimalCheck = checkDecimalIp(hostname, allowLocalhost, allowPrivate);
    if (decimalCheck) {
      return { safe: false, reason: decimalCheck };
    }
  }
  if (!allowLocalhost || !allowPrivate) {
    const octalCheck = checkOctalIp(hostname, allowLocalhost, allowPrivate);
    if (octalCheck) {
      return { safe: false, reason: octalCheck };
    }
  }
  if (!allowPrivate) {
    const privateCheck = checkPrivateIp(hostname);
    if (privateCheck) {
      return { safe: false, reason: privateCheck };
    }
  }
  return { safe: true };
}
function isUrlSafe(url, options = {}) {
  return validateUrl(url, options).safe;
}
function checkPrivateIp(hostname) {
  if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
    return "private address (10.0.0.0/8)";
  }
  const match172 = hostname.match(/^172\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/);
  if (match172) {
    const second = parseInt(match172[1], 10);
    if (second >= 16 && second <= 31) {
      return "private address (172.16.0.0/12)";
    }
  }
  if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
    return "private address (192.168.0.0/16)";
  }
  if (/^169\.254\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
    return "link-local address (169.254.0.0/16)";
  }
  if (/^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
    return "current network address (0.0.0.0/8)";
  }
  if (hostname === "metadata.google.internal" || hostname === "metadata.internal" || hostname === "metadata.azure.internal") {
    return "cloud metadata endpoint";
  }
  let ipv6 = hostname.replace(/^\[|\]$/g, "");
  const zoneIdx = ipv6.indexOf("%");
  if (zoneIdx !== -1) {
    ipv6 = ipv6.slice(0, zoneIdx);
  }
  if (ipv6 === "::1" || ipv6 === "::" || /^fc[0-9a-f]{2}:/i.test(ipv6) || /^fd[0-9a-f]{2}:/i.test(ipv6) || /^fe80:/i.test(ipv6) || /^ff[0-9a-f]{2}:/i.test(ipv6)) {
    return "private IPv6 address";
  }
  const mappedDotted = ipv6.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
  if (mappedDotted) {
    const mappedIp = mappedDotted[1];
    if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(mappedIp)) {
      return "IPv6-mapped loopback address";
    }
    const mappedCheck = checkPrivateIp(mappedIp);
    if (mappedCheck) {
      return `IPv6-mapped ${mappedCheck}`;
    }
  }
  const mappedHex = ipv6.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
  if (mappedHex) {
    const hi = parseInt(mappedHex[1], 16);
    const lo = parseInt(mappedHex[2], 16);
    const a = hi >> 8 & 255;
    const b = hi & 255;
    const c = lo >> 8 & 255;
    const d = lo & 255;
    const dotted = `${a}.${b}.${c}.${d}`;
    if (a === 127) {
      return "IPv6-mapped loopback address";
    }
    const hexCheck = checkPrivateIp(dotted);
    if (hexCheck) {
      return `IPv6-mapped ${hexCheck}`;
    }
  }
  return null;
}
function checkDecimalIp(hostname, allowLocalhost, allowPrivate) {
  if (!/^\d+$/.test(hostname)) return null;
  const num = parseInt(hostname, 10);
  if (isNaN(num) || num < 0 || num > 4294967295) return null;
  const a = num >>> 24 & 255;
  const b = num >>> 16 & 255;
  const c = num >>> 8 & 255;
  const d = num & 255;
  const dotted = `${a}.${b}.${c}.${d}`;
  if (!allowLocalhost && a === 127) {
    return `loopback address (decimal IP: ${dotted})`;
  }
  if (!allowPrivate) {
    const privateCheck = checkPrivateIp(dotted);
    if (privateCheck) {
      return `${privateCheck} (decimal IP: ${dotted})`;
    }
  }
  return null;
}
function checkOctalIp(hostname, allowLocalhost, allowPrivate) {
  const parts = hostname.split(".");
  if (parts.length !== 4) return null;
  const hasAlternateNotation = parts.some((p) => /^0[0-7]+$/.test(p) || /^0x[0-9a-fA-F]+$/i.test(p));
  if (!hasAlternateNotation) return null;
  const octets = [];
  for (const part of parts) {
    let val;
    if (/^0x[0-9a-fA-F]+$/i.test(part)) {
      val = parseInt(part, 16);
    } else if (/^0[0-7]*$/.test(part)) {
      val = parseInt(part, 8);
    } else if (/^\d+$/.test(part)) {
      val = parseInt(part, 10);
    } else {
      return null;
    }
    if (val < 0 || val > 255) return null;
    octets.push(val);
  }
  const dotted = octets.join(".");
  if (!allowLocalhost && octets[0] === 127) {
    return `loopback address (octal IP: ${dotted})`;
  }
  if (!allowPrivate) {
    const privateCheck = checkPrivateIp(dotted);
    if (privateCheck) {
      return `${privateCheck} (octal IP: ${dotted})`;
    }
  }
  return null;
}
var defaultLookup = (hostname) => new Promise((resolve, reject) => {
  dns__namespace.lookup(hostname, { all: true }, (err, addresses) => {
    if (err) reject(err);
    else resolve(addresses);
  });
});
function checkResolvedIp(ip, options) {
  const isIpv6 = ip.includes(":");
  const host = isIpv6 ? `[${ip}]` : ip;
  const { allowedProtocols, allowLocalhost, allowPrivate } = options;
  return validateUrl(`http://${host}/`, {
    allowedProtocols,
    allowLocalhost,
    allowPrivate
  });
}
async function validateUrlAsync(url, options = {}) {
  const sync = validateUrl(url, options);
  if (!sync.safe) return sync;
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    return { safe: false, reason: "invalid URL: failed to parse" };
  }
  const host = parsed.hostname.replace(/^\[|\]$/g, "");
  if (isLiteralIp(host)) return { safe: true };
  if (options.allowedHosts?.some((h) => host.toLowerCase() === h.toLowerCase())) {
    return { safe: true };
  }
  const lookup2 = options.lookup ?? defaultLookup;
  let addresses;
  try {
    addresses = await lookup2(host);
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    return { safe: false, reason: `DNS lookup failed: ${msg}` };
  }
  if (addresses.length === 0) {
    return { safe: false, reason: "DNS returned no addresses" };
  }
  const ips = addresses.map((a) => a.address);
  const acceptFirstPublic = options.acceptFirstPublic === true;
  let firstPublic;
  for (const ip of ips) {
    const ipResult = checkResolvedIp(ip, options);
    if (ipResult.safe) {
      if (firstPublic === void 0) firstPublic = ip;
    } else if (!acceptFirstPublic) {
      return {
        safe: false,
        reason: `resolved IP ${ip} is unsafe: ${ipResult.reason}`,
        resolvedIps: ips
      };
    }
  }
  if (firstPublic === void 0) {
    return {
      safe: false,
      reason: "all resolved IPs are private/loopback",
      resolvedIps: ips
    };
  }
  return { safe: true, resolvedIp: firstPublic, resolvedIps: ips };
}
function pinnedDnsLookup(ip) {
  if (typeof ip !== "string" || ip.trim() === "") {
    throw new TypeError("pinnedDnsLookup: ip must be a non-empty string");
  }
  const family = ip.includes(":") ? 6 : 4;
  return (_hostname, _options, callback) => {
    callback(null, ip, family);
  };
}
async function safeFollowRedirect(prev, location, options = {}) {
  if (typeof location !== "string" || location.trim() === "") {
    return { safe: false, reason: "redirect Location is empty" };
  }
  let absolute;
  try {
    absolute = new URL(location, prev);
  } catch {
    return { safe: false, reason: "invalid redirect URL" };
  }
  return validateUrlAsync(absolute.toString(), options);
}
function isLiteralIp(host) {
  if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
  if (/^\d+$/.test(host)) return true;
  if (/^0[0-7x]/.test(host) && /^[0-9a-fx.]+$/i.test(host) && host.includes(".")) return true;
  if (host.includes(":") && !/[/\s]/.test(host)) return true;
  return false;
}

// src/validation/redirect.ts
var DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript|blob):/i;
var CONTROL_CHARS = /[\t\n\r]/g;
function validateRedirect(url, options = {}) {
  const {
    allowedHosts = [],
    allowProtocolRelative = false,
    allowedProtocols = ["http:", "https:"]
  } = options;
  if (typeof url !== "string" || url.trim() === "") {
    return { safe: false, reason: "invalid redirect: empty or not a string" };
  }
  const cleaned = url.replace(CONTROL_CHARS, "");
  const proto = cleaned.match(DANGEROUS_PROTOCOLS);
  if (proto) {
    return { safe: false, reason: `dangerous protocol: ${proto[0]}` };
  }
  if (cleaned.startsWith("\\")) {
    return { safe: false, reason: "backslash-prefixed URL (browser treats as protocol-relative)" };
  }
  if (cleaned.startsWith("//")) {
    if (!allowProtocolRelative) {
      const host2 = extractHost(cleaned);
      if (host2 && allowedHosts.some((h) => host2 === h.toLowerCase())) {
        return { safe: true };
      }
      return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
    }
    const host = extractHost(cleaned);
    if (host && allowedHosts.length > 0 && !allowedHosts.some((h) => host === h.toLowerCase())) {
      return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
    }
    return { safe: true };
  }
  let parsed;
  try {
    parsed = new URL(cleaned);
  } catch {
    return { safe: true };
  }
  if (!allowedProtocols.includes(parsed.protocol)) {
    return { safe: false, reason: `disallowed protocol: ${parsed.protocol}` };
  }
  const hostname = parsed.hostname.toLowerCase();
  const hostWithPort = parsed.port ? `${hostname}:${parsed.port}` : hostname;
  if (allowedHosts.length === 0) {
    return { safe: false, reason: "absolute URL not in allowed hosts" };
  }
  if (!allowedHosts.some((h) => h.toLowerCase() === hostWithPort)) {
    return { safe: false, reason: `host not allowed: ${hostWithPort}` };
  }
  return { safe: true };
}
function isRedirectSafe(url, options = {}) {
  return validateRedirect(url, options).safe;
}
function extractHost(url) {
  const match = url.match(/^\/\/([^/?#]+)/);
  if (!match) return null;
  const authority = match[1].includes("@") ? match[1].slice(match[1].indexOf("@") + 1) : match[1];
  return authority.toLowerCase();
}
var MAX_EMAIL_LENGTH = 254;
var MAX_LOCAL_LENGTH = 64;
var MAX_DOMAIN_LENGTH = 255;
var EMAIL_SYNTAX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
var FREE_PROVIDERS = /* @__PURE__ */ new Set([
  "gmail.com",
  "yahoo.com",
  "hotmail.com",
  "outlook.com",
  "aol.com",
  "protonmail.com",
  "proton.me",
  "icloud.com",
  "mail.com",
  "zoho.com",
  "yandex.com",
  "gmx.com",
  "gmx.net",
  "live.com",
  "msn.com",
  "me.com",
  "mac.com",
  "fastmail.com",
  "tutanota.com",
  "hey.com"
]);
var DISPOSABLE_DOMAINS = /* @__PURE__ */ new Set([
  // Popular disposable services
  "guerrillamail.com",
  "guerrillamail.net",
  "guerrillamail.org",
  "tempmail.com",
  "temp-mail.org",
  "temp-mail.io",
  "throwaway.email",
  "throwaway.com",
  "mailinator.com",
  "mailinator.net",
  "yopmail.com",
  "yopmail.fr",
  "yopmail.net",
  "sharklasers.com",
  "grr.la",
  "guerrillamail.info",
  "guerrillamail.biz",
  "guerrillamail.de",
  "trashmail.com",
  "trashmail.me",
  "trashmail.net",
  "dispostable.com",
  "maildrop.cc",
  "mailnesia.com",
  "tempail.com",
  "mohmal.com",
  "getnada.com",
  "emailondeck.com",
  "discard.email",
  "fakeinbox.com",
  "mailcatch.com",
  "mintemail.com",
  "tempr.email",
  "tempinbox.com",
  "burnermail.io",
  "mailsac.com",
  "harakirimail.com",
  "tempmailo.com",
  "emailfake.com",
  "crazymailing.com",
  "armyspy.com",
  "dayrep.com",
  "einrot.com",
  "fleckens.hu",
  "gustr.com",
  "jourrapide.com",
  "rhyta.com",
  "superrito.com",
  "teleworm.us",
  "10minutemail.com",
  "10minutemail.net",
  "minutemail.com",
  "tempsky.com",
  "spamgourmet.com",
  "mytrashmail.com",
  "mailexpire.com",
  "safetymail.info",
  "filzmail.com",
  "trashymail.com",
  "sharkmail.com",
  "jetable.org",
  "nospam.ze.tc",
  "trash-me.com",
  "dodgit.com",
  "mailmoat.com",
  "spamfree24.org",
  "incognitomail.org",
  "tempomail.fr",
  "ephemail.net",
  "hidemail.de",
  "spaml.de",
  "uggsrock.com",
  "binkmail.com",
  "suremail.info",
  "bugmenot.com"
]);
var DOMAIN_TYPOS = {
  "gmial.com": "gmail.com",
  "gmaill.com": "gmail.com",
  "gmai.com": "gmail.com",
  "gamil.com": "gmail.com",
  "gnail.com": "gmail.com",
  "gmal.com": "gmail.com",
  "gmil.com": "gmail.com",
  "gmail.co": "gmail.com",
  "gmail.cm": "gmail.com",
  "gmail.om": "gmail.com",
  "gmail.con": "gmail.com",
  "gmail.cim": "gmail.com",
  "gmail.comm": "gmail.com",
  "yahooo.com": "yahoo.com",
  "yaho.com": "yahoo.com",
  "yahoo.co": "yahoo.com",
  "yahoo.cm": "yahoo.com",
  "yahoo.con": "yahoo.com",
  "yahho.com": "yahoo.com",
  "hotmial.com": "hotmail.com",
  "hotmal.com": "hotmail.com",
  "hotmai.com": "hotmail.com",
  "hotmil.com": "hotmail.com",
  "hotmail.co": "hotmail.com",
  "hotmail.cm": "hotmail.com",
  "hotmail.con": "hotmail.com",
  "outlok.com": "outlook.com",
  "outloo.com": "outlook.com",
  "outlook.co": "outlook.com",
  "outlook.cm": "outlook.com",
  "protonmal.com": "protonmail.com",
  "protonmail.co": "protonmail.com",
  "icloud.co": "icloud.com",
  "icloud.cm": "icloud.com",
  "icoud.com": "icloud.com"
};
function invalidResult(reason, email) {
  return {
    valid: false,
    reason,
    suggestion: null,
    isFree: false,
    isDisposable: false,
    normalized: email
  };
}
function validateEmail(email, options = {}) {
  const {
    checkDisposable = true,
    suggestTypoFix = true,
    blockedDomains = [],
    allowedDomains = []
  } = options;
  const normalized = email.trim().toLowerCase();
  if (!normalized || normalized.length > MAX_EMAIL_LENGTH) {
    return invalidResult("invalid_syntax", normalized);
  }
  const atIndex = normalized.lastIndexOf("@");
  if (atIndex === -1) {
    return invalidResult("invalid_syntax", normalized);
  }
  const localPart = normalized.slice(0, atIndex);
  const domain = normalized.slice(atIndex + 1);
  if (localPart.length === 0 || localPart.length > MAX_LOCAL_LENGTH) {
    return invalidResult("invalid_syntax", normalized);
  }
  if (domain.length === 0 || domain.length > MAX_DOMAIN_LENGTH) {
    return invalidResult("invalid_syntax", normalized);
  }
  if (localPart.includes("..")) {
    return invalidResult("invalid_syntax", normalized);
  }
  if (localPart.startsWith(".") || localPart.endsWith(".")) {
    return invalidResult("invalid_syntax", normalized);
  }
  if (!EMAIL_SYNTAX.test(normalized)) {
    return invalidResult("invalid_syntax", normalized);
  }
  const allowedSet = new Set(allowedDomains.map((d) => d.toLowerCase()));
  if (allowedSet.has(domain)) {
    return {
      valid: true,
      reason: "valid",
      suggestion: null,
      isFree: FREE_PROVIDERS.has(domain),
      isDisposable: false,
      normalized
    };
  }
  const blockedSet = new Set(blockedDomains.map((d) => d.toLowerCase()));
  if (blockedSet.has(domain)) {
    return invalidResult("blocked", normalized);
  }
  const isDisposable = DISPOSABLE_DOMAINS.has(domain);
  if (checkDisposable && isDisposable) {
    return {
      valid: false,
      reason: "disposable",
      suggestion: null,
      isFree: false,
      isDisposable: true,
      normalized
    };
  }
  const isFree = FREE_PROVIDERS.has(domain);
  if (suggestTypoFix && DOMAIN_TYPOS[domain]) {
    const corrected = `${localPart}@${DOMAIN_TYPOS[domain]}`;
    return {
      valid: true,
      reason: "typo",
      suggestion: corrected,
      isFree: FREE_PROVIDERS.has(DOMAIN_TYPOS[domain]),
      isDisposable: false,
      normalized
    };
  }
  return {
    valid: true,
    reason: "valid",
    suggestion: null,
    isFree,
    isDisposable,
    normalized
  };
}
async function verifyEmailMx(email) {
  if (!isValidEmailSyntax(email)) return false;
  const atIndex = email.lastIndexOf("@");
  const domain = email.slice(atIndex + 1).trim().toLowerCase();
  if (!domain) return false;
  try {
    const records = await dns.promises.resolveMx(domain);
    return records.length > 0;
  } catch {
    return false;
  }
}
function isValidEmailSyntax(email) {
  const normalized = email.trim().toLowerCase();
  if (!normalized || normalized.length > MAX_EMAIL_LENGTH) return false;
  const atIndex = normalized.lastIndexOf("@");
  if (atIndex === -1) return false;
  const localPart = normalized.slice(0, atIndex);
  if (localPart.includes("..") || localPart.startsWith(".") || localPart.endsWith(".")) return false;
  return EMAIL_SYNTAX.test(normalized);
}

exports.createValidator = createValidator;
exports.isDangerousExtension = isDangerousExtension;
exports.isRedirectSafe = isRedirectSafe;
exports.isUrlSafe = isUrlSafe;
exports.isValidEmailSyntax = isValidEmailSyntax;
exports.pinnedDnsLookup = pinnedDnsLookup;
exports.safeFollowRedirect = safeFollowRedirect;
exports.sanitizeFilename = sanitizeFilename;
exports.validate = validate;
exports.validateEmail = validateEmail;
exports.validateFile = validateFile;
exports.validateRedirect = validateRedirect;
exports.validateUrl = validateUrl;
exports.validateUrlAsync = validateUrlAsync;
exports.verifyEmailMx = verifyEmailMx;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map