Ctrl K
ComunidadGuías…

scripts/update-bot-patterns.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const fs = require("fs");
const path = require("path");

const SOURCE = "https://raw.githubusercontent.com/omrilotan/isbot/main/src/patterns.json";
const BROWSER_FIXTURES = "https://raw.githubusercontent.com/omrilotan/isbot/main/fixtures/browsers.yml";
const DIR = path.join(__dirname, "..", "backend", "src", "bot_detection");
const ENFORCED_FILE = path.join(DIR, "bot_patterns.txt");
const SHADOW_FILE = path.join(DIR, "bot_patterns_shadow.txt");
const LOCAL_FILE = path.join(DIR, "bot_patterns_local.txt");

function readPatterns(file) {
  return fs
    .readFileSync(file, "utf8")
    .split(/\r?\n/)
    .filter((line) => line && !line.startsWith("#"));
}

async function main() {
  const res = await fetch(SOURCE);
  if (!res.ok) throw new Error(`Failed to fetch patterns: ${res.status}`);
  const patterns = await res.json();

  const invalid = patterns.filter((p) => typeof p !== "string" || p.includes("\n") || p.startsWith("#"));
  if (invalid.length > 0) throw new Error(`Unsupported patterns: ${JSON.stringify(invalid)}`);

  // Rust's regex parser rejects a bare '-' following a class escape inside [...];
  // escaping the hyphen is semantics-preserving in both JS and Rust
  const upstream = patterns.map((p) => p.replace(/\\([wdsWDS])-/g, "\\$1\\-"));
  const upstreamSet = new Set(upstream);

  // Enforced patterns must still exist upstream
  const enforcedSet = new Set(readPatterns(ENFORCED_FILE));
  const gone = [...enforcedSet].filter((p) => !upstreamSet.has(p));
  if (gone.length > 0) {
    throw new Error(
      `Enforced patterns no longer exist upstream — remove them or move them to ${path.basename(LOCAL_FILE)}:\n` +
        gone.join("\n"),
    );
  }

  // Unpromoted upstream patterns go to shadow
  const enforced = upstream.filter((p) => enforcedSet.has(p));
  const shadow = upstream.filter((p) => !enforcedSet.has(p));
  const local = readPatterns(LOCAL_FILE);

  // Guard against upstream regressions and local mistakes: no pattern in any tier
  // may match a real browser UA from upstream's human-browser fixture corpus
  const fixturesRes = await fetch(BROWSER_FIXTURES);
  if (!fixturesRes.ok) throw new Error(`Failed to fetch browser fixtures: ${fixturesRes.status}`);
  const humanUas = (await fixturesRes.text())
    .split("\n")
    .filter((line) => line.startsWith("  - "))
    .map((line) => line.slice(4).trim())
    .filter(Boolean);
  if (humanUas.length < 100) throw new Error(`Browser fixture corpus suspiciously small: ${humanUas.length}`);

  for (const [tier, tierPatterns] of [["enforced", enforced], ["local", local], ["shadow", shadow]]) {
    const combined = new RegExp(tierPatterns.join("|"), "i");
    const falsePositives = humanUas.filter((ua) => combined.test(ua));
    if (falsePositives.length > 0) {
      throw new Error(
        `Refusing to write: ${falsePositives.length} human browser UAs match the ${tier} tier:\n` +
          falsePositives.slice(0, 10).join("\n"),
      );
    }
  }
  console.log(`Verified 0 false positives against ${humanUas.length} human browser UAs`);

  const date = new Date().toISOString().slice(0, 10);
  const enforcedHeader = [
    "# Enforced bot user-agent patterns: the curated subset of the vendored isbot list",
    "# (omrilotan/isbot, Unlicense) that names a specific product, vendor, or bot class.",
    "# The upstream remainder runs in shadow (bot_patterns_shadow.txt); promote a pattern",
    "# by moving its line here once bot_events shadow data shows it only hits bots.",
    `# Source: ${SOURCE}`,
    `# Generated: ${date} (${enforced.length} of ${upstream.length} upstream patterns enforced)`,
    "# Regenerate with: node scripts/update-bot-patterns.js",
  ];
  const shadowHeader = [
    "# Shadow bot user-agent patterns, generated by scripts/update-bot-patterns.js — do",
    "# not hand-edit. Upstream isbot patterns not promoted in bot_patterns.txt: format",
    "# heuristics and generic words that could conceivably match a real client. Hits are",
    "# recorded to bot_events as shadow:ua-heuristic; the event is processed as human.",
    `# Source: ${SOURCE}`,
    `# Generated: ${date} (${shadow.length} patterns)`,
  ];
  fs.writeFileSync(ENFORCED_FILE, enforcedHeader.join("\n") + "\n" + enforced.join("\n") + "\n");
  fs.writeFileSync(SHADOW_FILE, shadowHeader.join("\n") + "\n" + shadow.join("\n") + "\n");

  console.log(
    `Wrote ${enforced.length} enforced + ${shadow.length} shadow patterns (${local.length} local additions untouched)`,
  );
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});