#!/usr/bin/env node

// src/security/redactor.ts
var DEFAULT_PATTERNS = [
  // password: xxx / password = xxx / password="xxx"
  // Covers YAML / JSON / shell-ish / env-file forms.
  { kind: "password", regex: /\b(password|passwd|pwd)\s*[:=]\s*["']?([^\s"',;{}]{4,200})["']?/gi },
  // PGPASSWORD=xxx (explicit bash env-var form, separate rule because no quotes usually)
  { kind: "pgpassword-env", regex: /\b(PGPASSWORD)=([^\s"']{4,200})/g },
  // JDBC/PG/MySQL/Mongo connection strings with inline credentials
  // postgresql://user:pass@host/db → redact pass
  { kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
  // Anthropic API keys
  { kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
  // L6 (v0.4.108): Zhipu / GLM API keys — `<24+ hex/base64-ish>.<32+>`
  // Two segments separated by a dot, each safely identifiable by length
  // and char class. Conservative on the lower bound so we don't eat
  // version strings like `1.0.0` or filenames.
  { kind: "zhipu-key", regex: /\b([a-zA-Z0-9]{24,}\.[a-zA-Z0-9]{32,})\b/g },
  // OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
  { kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
  // GitHub personal access tokens
  { kind: "github-pat", regex: /\b(ghp_[a-zA-Z0-9]{36})\b/g },
  { kind: "github-oauth", regex: /\b(gho_[a-zA-Z0-9]{36})\b/g },
  { kind: "github-install", regex: /\b(ghs_[a-zA-Z0-9]{36})\b/g },
  // Slack tokens
  { kind: "slack-bot", regex: /\b(xoxb-\d+-\d+-[a-zA-Z0-9]+)\b/g },
  { kind: "slack-user", regex: /\b(xoxp-\d+-\d+-\d+-[a-zA-Z0-9]+)\b/g },
  // AWS access key IDs (AKIA...) and secret access keys are context-dependent;
  // we only catch the ID because secret key alone is indistinguishable from random base64.
  { kind: "aws-access-key-id", regex: /\b(AKIA[0-9A-Z]{16})\b/g },
  // Google API keys
  { kind: "google-api-key", regex: /\b(AIza[0-9A-Za-z_-]{35})\b/g },
  // Generic "api_key": "..." / "apiKey": "..." / api-key=xxx
  { kind: "api-key", regex: /\b(api[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{16,200})["']?/gi },
  // Generic token: xxx (only when value looks token-shaped; avoids eating human prose)
  { kind: "token", regex: /\b(token|access[_-]?token|bearer[_-]?token)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{20,300})["']?/gi },
  // Bearer <token> in Authorization headers
  { kind: "bearer", regex: /\b(Authorization:\s*Bearer\s+)([a-zA-Z0-9_\-.=]{20,500})/g },
  // Private key PEM blocks — catch the header+footer together
  { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
];
var MAX_CUSTOM = 32;
var MAX_PATTERN_LEN = 500;
var SUSPICIOUS_REDOS = /\([^)]*[+*][^)]*\)\s*[+*{]/;
function render(placeholder, kind) {
  return placeholder.replace("{kind}", kind);
}
function redactString(input, options) {
  if (!options.enabled || !input) return { redacted: input, hits: [] };
  const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
  const customSrcs = (options.customRegexes ?? []).slice(0, MAX_CUSTOM);
  const patterns = [
    ...options.patterns ?? DEFAULT_PATTERNS,
    ...customSrcs.flatMap((src, i) => {
      if (typeof src !== "string" || src.length === 0 || src.length > MAX_PATTERN_LEN) return [];
      try {
        const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
        const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
        if (SUSPICIOUS_REDOS.test(body)) return [];
        const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
        return [{ kind: `custom-${i}`, regex }];
      } catch {
        return [];
      }
    })
  ];
  let redacted = input;
  const hits = [];
  for (const { kind, regex } of patterns) {
    const rx = new RegExp(regex.source, regex.flags);
    const captureCount = new RegExp(rx.source + "|").exec("").length - 1;
    redacted = redacted.replace(rx, (...args) => {
      const match = args[0];
      const g1 = captureCount >= 1 ? args[1] : void 0;
      const g2 = captureCount >= 2 ? args[2] : void 0;
      const offset = args[1 + captureCount];
      if (captureCount >= 2 && typeof g2 === "string") {
        hits.push({ kind, start: offset + (g1?.length ?? 0), length: g2.length, secret: g2 });
        return `${g1}${render(placeholder, kind)}`;
      }
      hits.push({ kind, start: offset, length: match.length, secret: g1 ?? match });
      return render(placeholder, kind);
    });
  }
  return { redacted, hits };
}
function redactJson(value, options) {
  if (!options.enabled) return { value, hits: [] };
  const allHits = [];
  function walk(v) {
    if (typeof v === "string") {
      const r = redactString(v, options);
      allHits.push(...r.hits);
      return r.redacted;
    }
    if (Array.isArray(v)) return v.map(walk);
    if (v && typeof v === "object") {
      const out = {};
      for (const [k, vv] of Object.entries(v)) {
        out[k] = walk(vv);
      }
      return out;
    }
    return v;
  }
  const redacted = walk(value);
  return { value: redacted, hits: allHits };
}
function scanString(input, options) {
  const { hits } = redactString(input, { ...options, enabled: true });
  return hits;
}

export {
  DEFAULT_PATTERNS,
  redactString,
  redactJson,
  scanString
};
