const WILDCARD_RE = /[xX#]/g const ACCESS_HASH = "28db23491950da0d3033cbde13930e06004d033d4b3b4d6f9f10e11ccb2e1663" const AUTH_KEY = "ng_auth" async function sha256Hex(text) { const data = new TextEncoder().encode(text) const hashBuf = await crypto.subtle.digest("SHA-256", data) const bytes = new Uint8Array(hashBuf) let hex = "" for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, "0") return hex } function countWildcards(pattern) { const matches = pattern.match(WILDCARD_RE) return matches ? matches.length : 0 } function wildcardPositions(pattern) { const positions = [] for (let i = 0; i < pattern.length; i++) { const ch = pattern[i] if (ch === "x" || ch === "X" || ch === "#") positions.push(i) } return positions } function pow10(n) { let v = 1 for (let i = 0; i < n; i++) v *= 10 return v } function clampInt(n, min, max) { if (!Number.isFinite(n)) return min n = Math.trunc(n) if (n < min) return min if (n > max) return max return n } function toPaddedDigits(n, width) { const s = String(n) if (s.length >= width) return s return "0".repeat(width - s.length) + s } function computeRange(pattern, rawStart, rawEnd) { const wildcards = countWildcards(pattern) const total = pow10(wildcards) const maxIndex = Math.max(0, total - 1) const start = clampInt(rawStart, 0, maxIndex) const end = rawEnd === "" || rawEnd === null || rawEnd === undefined ? maxIndex : clampInt(Number(rawEnd), 0, maxIndex) if (end < start) return { wildcards, total, start, end: start, error: "El valor “Hasta” no puede ser menor que “Desde”." } return { wildcards, total, start, end, error: null } } function buildLine(patternChars, positions, digits) { for (let j = 0; j < positions.length; j++) patternChars[positions[j]] = digits[j] return patternChars.join("") } function generateChunks(pattern, start, end, format) { const positions = wildcardPositions(pattern) const wildcards = positions.length const patternChars = pattern.split("") const chunks = [] if (format === "csv") chunks.push("numero\r\n") if (wildcards === 0) { const line = format === "csv" ? `${pattern}\r\n` : `${pattern}\r\n` chunks.push(line) return { chunks, count: 1 } } let count = 0 for (let i = start; i <= end; i++) { const digits = toPaddedDigits(i, wildcards) const value = buildLine(patternChars.slice(), positions, digits) chunks.push(format === "csv" ? `${value}\r\n` : `${value}\r\n`) count++ } return { chunks, count } } function downloadBlob(filename, mime, blobParts) { const blob = new Blob(blobParts, { type: mime }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = filename document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } function formatInt(n) { return new Intl.NumberFormat("es-MX").format(n) } function previewFromChunks(chunks, maxLines) { const text = chunks.join("") const lines = text.split(/\r?\n/) lines.pop() const head = lines.slice(0, maxLines).join("\n") const remaining = lines.length - Math.min(lines.length, maxLines) return remaining > 0 ? `${head}\n\n... (${formatInt(remaining)} líneas más)` : head } const els = { app: document.getElementById("app"), authGate: document.getElementById("authGate"), gatePassword: document.getElementById("gatePassword"), gateSubmit: document.getElementById("gateSubmit"), gateError: document.getElementById("gateError"), pattern: document.getElementById("pattern"), rangeStart: document.getElementById("rangeStart"), rangeEnd: document.getElementById("rangeEnd"), maxResults: document.getElementById("maxResults"), wildcards: document.getElementById("wildcards"), totalCombos: document.getElementById("totalCombos"), toGenerate: document.getElementById("toGenerate"), preview: document.getElementById("preview"), error: document.getElementById("error"), format: document.getElementById("format"), filename: document.getElementById("filename"), btnGenerate: document.getElementById("btnGenerate"), btnClear: document.getElementById("btnClear"), btnDownload: document.getElementById("btnDownload"), btnLogout: document.getElementById("btnLogout"), } const state = { last: null, } function setError(msg) { if (!msg) { els.error.hidden = true els.error.textContent = "" return } els.error.hidden = false els.error.textContent = msg } function setGateError(msg) { if (!msg) { els.gateError.hidden = true els.gateError.textContent = "" return } els.gateError.hidden = false els.gateError.textContent = msg } function isAuthed() { return sessionStorage.getItem(AUTH_KEY) === "1" } function baseNameFromPattern(pattern) { const p = String(pattern || "").trim() if (!p) return "resultados" const firstWildcard = p.search(/[xX#]/) const prefix = firstWildcard === -1 ? p : p.slice(0, firstWildcard) const digits = prefix.replace(/\D+/g, "") if (digits) return digits const safe = prefix.replace(/[^a-zA-Z0-9_-]+/g, "_") return safe || "resultados" } function setAppControlsDisabled(disabled) { const controls = els.app.querySelectorAll("input,button,select,textarea") for (const el of controls) { if (disabled) { el.dataset.wasDisabled = el.disabled ? "1" : "0" el.disabled = true } else { el.disabled = el.dataset.wasDisabled === "1" delete el.dataset.wasDisabled } } } function setLocked(locked) { document.body.classList.toggle("locked", locked) els.btnLogout.hidden = locked if (locked) { els.app.setAttribute("inert", "") els.app.setAttribute("aria-hidden", "true") } else { els.app.removeAttribute("inert") els.app.removeAttribute("aria-hidden") } setAppControlsDisabled(locked) els.btnDownload.disabled = locked || !state.last if (locked) { setGateError(null) els.gatePassword.value = "" setTimeout(() => els.gatePassword.focus(), 0) } } async function tryUnlock() { const pass = els.gatePassword.value.normalize("NFKC").trim() if (!pass) { setGateError("Escribe la clave.") return } if (!globalThis.crypto || !crypto.subtle) { setGateError("Para validar la clave abre la app desde http://localhost o https.") return } const hash = await sha256Hex(pass) if (hash !== ACCESS_HASH) { setGateError("Clave incorrecta.") return } sessionStorage.setItem(AUTH_KEY, "1") setLocked(false) } function updateStats() { const pattern = els.pattern.value.trim() const { wildcards, total, start, end, error } = computeRange(pattern, Number(els.rangeStart.value), els.rangeEnd.value) const toGenerate = end - start + 1 els.filename.value = baseNameFromPattern(pattern) els.wildcards.textContent = String(wildcards) els.totalCombos.textContent = formatInt(total) els.toGenerate.textContent = formatInt(Math.max(0, toGenerate)) if (error) setError(error) else setError(null) } function generate() { const pattern = els.pattern.value.trim() if (!pattern) { setError("Escribe una serie / patrón.") return } const maxResults = clampInt(Number(els.maxResults.value), 1, 50_000_000) const { wildcards, total, start, end, error } = computeRange(pattern, Number(els.rangeStart.value), els.rangeEnd.value) if (error) { setError(error) return } const count = end - start + 1 if (count > maxResults) { setError(`El rango genera ${formatInt(count)} resultados, que excede el límite de ${formatInt(maxResults)}.`) return } if (wildcards > 10 && count > 500_000) { setError("Demasiados comodines/resultados para el navegador. Reduce el rango o la cantidad de x.") return } setError(null) const format = els.format.value const { chunks, count: generated } = generateChunks(pattern, start, end, format) state.last = { pattern, start, end, format, chunks, count: generated } els.filename.value = baseNameFromPattern(pattern) els.btnDownload.disabled = false const preview = previewFromChunks(chunks, 200) els.preview.value = preview } function clearAll() { els.preview.value = "" els.btnDownload.disabled = true state.last = null setError(null) } function download() { if (!state.last) return const base = baseNameFromPattern(state.last.pattern) const format = els.format.value const ext = format === "csv" ? "csv" : "txt" const filename = `${base}.${ext}` const mime = format === "csv" ? "text/csv;charset=utf-8" : "text/plain;charset=utf-8" downloadBlob(filename, mime, state.last.chunks) } els.pattern.addEventListener("input", updateStats) els.rangeStart.addEventListener("input", updateStats) els.rangeEnd.addEventListener("input", updateStats) els.maxResults.addEventListener("input", updateStats) els.format.addEventListener("change", () => { updateStats() if (state.last) { const pattern = els.pattern.value.trim() const { start, end, error } = computeRange(pattern, Number(els.rangeStart.value), els.rangeEnd.value) if (!error) generate() } }) els.gateSubmit.addEventListener("click", () => { tryUnlock() }) els.gatePassword.addEventListener("keydown", (e) => { if (e.key === "Enter") tryUnlock() }) els.btnLogout.addEventListener("click", () => { sessionStorage.removeItem(AUTH_KEY) clearAll() setLocked(true) }) els.btnGenerate.addEventListener("click", generate) els.btnClear.addEventListener("click", clearAll) els.btnDownload.addEventListener("click", download) updateStats() setLocked(!isAuthed())