Created
February 13, 2026 02:54
-
-
Save juliandavidmr/5ba4d0300a0753a5a221222a0a5962d6 to your computer and use it in GitHub Desktop.
Automated Link X-Ray n8n workflow to inspect any URL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "nodes": [ | |
| { | |
| "parameters": { | |
| "httpMethod": "POST", | |
| "path": "link-xray", | |
| "responseMode": "responseNode", | |
| "options": {} | |
| }, | |
| "id": "0d69f1d6-0c3a-412d-a874-860d62322843", | |
| "name": "Webhook POST", | |
| "type": "n8n-nodes-base.webhook", | |
| "typeVersion": 2, | |
| "position": [ | |
| -352, | |
| -160 | |
| ], | |
| "webhookId": "3735f41a-c109-41ac-a450-c0f2d9788b5c" | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "var input = $input.first().json;\nvar rawUrl = (input.body && input.body.url) ? input.body.url.toString() : (input.url ? input.url.toString() : '');\n\nif (!rawUrl || rawUrl.trim() === '') {\n return [{ json: { valid: false, error: 'No URL provided', originalUrl: rawUrl } }];\n}\n\nvar url = rawUrl.trim();\nurl = url.replace(/[\\x00-\\x1F\\x7F]/g, '');\n\nif (!/^https?:\\/\\//i.test(url)) {\n if (url.indexOf('//') === 0) {\n url = 'https:' + url;\n } else {\n url = 'https://' + url;\n }\n}\n\nvar protoMatch = url.match(/^(https?):\\/\\//i);\nif (!protoMatch) {\n return [{ json: { valid: false, error: 'Unsupported or missing protocol', originalUrl: rawUrl } }];\n}\nvar protocol = protoMatch[1].toLowerCase() + ':';\nvar rest = url.slice(protoMatch[0].length);\n\nvar slashIdx = rest.indexOf('/');\nvar hostPart = slashIdx === -1 ? rest : rest.slice(0, slashIdx);\nvar pathname = slashIdx === -1 ? '/' : rest.slice(slashIdx);\nvar hostname = hostPart.split(':')[0].toLowerCase();\n\nif (!hostname) {\n return [{ json: { valid: false, error: 'Empty hostname', originalUrl: rawUrl } }];\n}\nif (hostname.indexOf('.') === -1 && hostname !== 'localhost') {\n return [{ json: { valid: false, error: 'Invalid hostname - no TLD found', originalUrl: rawUrl } }];\n}\nif (/[\\s<>{}|\\\\^`]/.test(hostname)) {\n return [{ json: { valid: false, error: 'Hostname contains invalid characters', originalUrl: rawUrl } }];\n}\n\nvar cleanUrl = protocol + '//' + hostPart.toLowerCase() + pathname;\n\nreturn [{ json: {\n valid: true,\n url: cleanUrl,\n originalUrl: rawUrl,\n hostname: hostname,\n protocol: protocol,\n pathname: pathname\n}}];" | |
| }, | |
| "id": "383c14b6-573d-4590-9731-617b4d435818", | |
| "name": "Validate & Fix URL", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| -128, | |
| -160 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "conditions": { | |
| "options": { | |
| "caseSensitive": true, | |
| "leftValue": "", | |
| "typeValidation": "strict" | |
| }, | |
| "conditions": [ | |
| { | |
| "id": "valid-check", | |
| "leftValue": "={{ $json.valid }}", | |
| "rightValue": true, | |
| "operator": { | |
| "type": "boolean", | |
| "operation": "equals" | |
| } | |
| } | |
| ], | |
| "combinator": "and" | |
| }, | |
| "options": {} | |
| }, | |
| "id": "d052f251-9ff4-4ee3-92f0-aa485ebb20ce", | |
| "name": "URL Valid?", | |
| "type": "n8n-nodes-base.if", | |
| "typeVersion": 2.2, | |
| "position": [ | |
| 96, | |
| -160 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "respondWith": "json", | |
| "responseBody": "={{ JSON.stringify({ success: false, error: $json.error, originalUrl: $json.originalUrl, analysis: null }) }}", | |
| "options": { | |
| "responseCode": 400 | |
| } | |
| }, | |
| "id": "aa65cf77-5b8a-4849-bd36-200608477761", | |
| "name": "Respond: Invalid URL", | |
| "type": "n8n-nodes-base.respondToWebhook", | |
| "typeVersion": 1.1, | |
| "position": [ | |
| 320, | |
| -64 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "url": "={{ $json.url }}", | |
| "options": { | |
| "redirect": { | |
| "redirect": { | |
| "maxRedirects": 10 | |
| } | |
| }, | |
| "timeout": 15000 | |
| } | |
| }, | |
| "id": "15e9cf7b-c7fa-4bc1-a66c-abe4520f47e6", | |
| "name": "Fetch URL (3 retries, 10 redirects)", | |
| "type": "n8n-nodes-base.httpRequest", | |
| "typeVersion": 4.2, | |
| "position": [ | |
| 320, | |
| -256 | |
| ], | |
| "onError": "continueErrorOutput" | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "var input = $input.first().json;\nvar urlData = $('Validate & Fix URL').first().json;\n\nreturn [{ json: {\n url: urlData.url,\n hostname: urlData.hostname,\n originalUrl: urlData.originalUrl,\n protocol: urlData.protocol,\n reachable: true,\n httpStatusCode: input.statusCode || 200,\n headers: {\n contentType: (input.headers && input.headers['content-type']) ? input.headers['content-type'] : 'unknown',\n server: (input.headers && input.headers['server']) ? input.headers['server'] : 'unknown'\n }\n}}];" | |
| }, | |
| "id": "86204156-8e37-4f2c-b7c8-6ee91ba039b3", | |
| "name": "HTTP Success - Extract Meta", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| 544, | |
| -352 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "var urlData = $('Validate & Fix URL').first().json;\nvar errorInfo = $input.first().json;\n\nreturn [{ json: {\n url: urlData.url,\n hostname: urlData.hostname,\n originalUrl: urlData.originalUrl,\n protocol: urlData.protocol,\n reachable: false,\n httpError: errorInfo.message || errorInfo.error || 'Request failed after 3 retries',\n httpStatusCode: errorInfo.statusCode || null\n}}];" | |
| }, | |
| "id": "ea1d1285-4e88-494f-9164-3716898cf982", | |
| "name": "HTTP Error - Capture", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| 544, | |
| -160 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "var input = $input.first().json;\nreturn [{ json: input }];" | |
| }, | |
| "id": "ae9666b1-25a7-401c-a55c-0b3bcfc80c3d", | |
| "name": "Merge HTTP Results", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| 768, | |
| -256 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "assignments": { | |
| "assignments": [ | |
| { | |
| "id": "942fd261-01eb-446d-9a20-fc01c065b755", | |
| "name": "API_KEY", | |
| "value": "PUT_HERE_YOUR_API_KEY", | |
| "type": "string" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "id": "9757d3b1-51f4-4602-8100-b593b9224a48", | |
| "name": "Config: Google Web Risk API Key", | |
| "type": "n8n-nodes-base.set", | |
| "typeVersion": 3.4, | |
| "position": [ | |
| 992, | |
| -256 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "var httpData = $('Merge HTTP Results').first().json;\nvar apiKey = $input.first().json.API_KEY;\nvar targetUri = encodeURIComponent(httpData.url);\n\nvar endpoint = 'https://webrisk.googleapis.com/v1/uris:search'\n + '?threatTypes=MALWARE'\n + '&threatTypes=SOCIAL_ENGINEERING'\n + '&threatTypes=UNWANTED_SOFTWARE'\n + '&threatTypes=SOCIAL_ENGINEERING_EXTENDED_COVERAGE'\n + '&uri=' + targetUri\n + '&key=' + apiKey;\n\nreturn [{ json: {\n webRiskUrl: endpoint,\n checkedUrl: httpData.url\n}}];" | |
| }, | |
| "id": "eb1e3572-da0b-41fb-8692-656bd96ee161", | |
| "name": "Build Web Risk Request URL", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| 1216, | |
| -256 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "url": "={{ $json.webRiskUrl }}", | |
| "options": { | |
| "timeout": 10000 | |
| } | |
| }, | |
| "id": "6531512b-8883-4743-b299-a8bef1344801", | |
| "name": "Google Web Risk Lookup API", | |
| "type": "n8n-nodes-base.httpRequest", | |
| "typeVersion": 4.2, | |
| "position": [ | |
| 1440, | |
| -256 | |
| ], | |
| "onError": "continueErrorOutput" | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "var wrResponse = $input.first().json;\nvar httpData = $('Merge HTTP Results').first().json;\n\nvar threatTypes = [];\nvar expireTime = null;\nvar hasThreat = false;\n\nif (wrResponse.threat && wrResponse.threat.threatTypes && wrResponse.threat.threatTypes.length > 0) {\n hasThreat = true;\n threatTypes = wrResponse.threat.threatTypes;\n expireTime = wrResponse.threat.expireTime || null;\n}\n\nvar isSafe = !hasThreat;\n\nvar threatDescriptions = {\n 'MALWARE': 'This site distributes malware that can infect your device.',\n 'SOCIAL_ENGINEERING': 'This site uses phishing or social engineering to trick users into revealing sensitive information.',\n 'UNWANTED_SOFTWARE': 'This site distributes unwanted software that may alter your browser or system settings.',\n 'SOCIAL_ENGINEERING_EXTENDED_COVERAGE': 'This site was flagged under extended social engineering coverage for deceptive content.'\n};\n\nvar threatDetails = threatTypes.map(function(tt) {\n return {\n type: tt,\n description: threatDescriptions[tt] || 'Flagged for: ' + tt\n };\n});\n\nvar riskScore = 0;\nif (hasThreat) {\n var weights = {\n 'MALWARE': 90,\n 'SOCIAL_ENGINEERING': 85,\n 'SOCIAL_ENGINEERING_EXTENDED_COVERAGE': 80,\n 'UNWANTED_SOFTWARE': 60\n };\n var scores = threatTypes.map(function(tt) { return weights[tt] || 50; });\n riskScore = Math.min(100, Math.max.apply(null, scores));\n}\n\nvar heuristics = [];\nif (httpData.protocol === 'http:') {\n heuristics.push({ flag: 'NO_HTTPS', severity: 'medium', message: 'Site does not use HTTPS encryption.' });\n riskScore = Math.min(100, riskScore + 15);\n}\nif (!httpData.reachable) {\n heuristics.push({ flag: 'UNREACHABLE', severity: 'high', message: 'Site is unreachable: ' + (httpData.httpError || 'unknown error') });\n riskScore = Math.min(100, riskScore + 20);\n}\n\nvar verdict = 'SAFE';\nif (riskScore >= 70) verdict = 'DANGEROUS';\nelse if (riskScore >= 40) verdict = 'SUSPICIOUS';\nelse if (riskScore > 0) verdict = 'CAUTION';\n\nreturn [{ json: {\n success: true,\n analysis: {\n url: httpData.url,\n originalUrl: httpData.originalUrl,\n hostname: httpData.hostname,\n verdict: verdict,\n riskScore: riskScore,\n isSafe: isSafe,\n threats: threatDetails,\n heuristics: heuristics,\n connectivity: {\n reachable: httpData.reachable,\n httpStatusCode: httpData.httpStatusCode || null,\n httpError: httpData.httpError || null,\n serverHeader: (httpData.headers && httpData.headers.server) ? httpData.headers.server : null,\n contentType: (httpData.headers && httpData.headers.contentType) ? httpData.headers.contentType : null\n },\n webRisk: {\n threatTypesFound: threatTypes,\n cacheExpireTime: expireTime,\n checkedAt: new Date().toISOString()\n }\n }\n}}];" | |
| }, | |
| "id": "14e52627-21b4-43fa-a0ec-1ca1efca7dde", | |
| "name": "Process Results & Verdict", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| 1664, | |
| -352 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "var httpData = $('Merge HTTP Results').first().json;\nvar errorInfo = $input.first().json;\n\nreturn [{ json: {\n success: false,\n error: 'Google Web Risk API request failed',\n apiError: errorInfo.message || errorInfo.error || 'Unknown API error',\n partialAnalysis: {\n url: httpData.url,\n originalUrl: httpData.originalUrl,\n hostname: httpData.hostname,\n connectivity: {\n reachable: httpData.reachable,\n httpStatusCode: httpData.httpStatusCode || null,\n httpError: httpData.httpError || null\n },\n note: 'Web Risk check could not be completed. Connectivity data is still valid.'\n }\n}}];" | |
| }, | |
| "id": "c0e81355-af69-41a3-b896-512af962306b", | |
| "name": "Web Risk API Error", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| 1664, | |
| -160 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "respondWith": "json", | |
| "responseBody": "={{ JSON.stringify($json) }}", | |
| "options": { | |
| "responseCode": 200 | |
| } | |
| }, | |
| "id": "1e28769c-9ef8-4447-a1b7-487a10c55753", | |
| "name": "Respond: Analysis Complete", | |
| "type": "n8n-nodes-base.respondToWebhook", | |
| "typeVersion": 1.1, | |
| "position": [ | |
| 1888, | |
| -352 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "respondWith": "json", | |
| "responseBody": "={{ JSON.stringify($json) }}", | |
| "options": { | |
| "responseCode": 502 | |
| } | |
| }, | |
| "id": "a7cb9e1b-279a-407e-abe1-94c24b5f61e1", | |
| "name": "Respond: API Error", | |
| "type": "n8n-nodes-base.respondToWebhook", | |
| "typeVersion": 1.1, | |
| "position": [ | |
| 1888, | |
| -160 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "path": "xray", | |
| "options": { | |
| "responseData": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Link X-Ray | Threat Intelligence</title>\n \n <!-- Tailwind CSS -->\n <script src=\"https://cdn.tailwindcss.com\"></script>\n \n <!-- Google Fonts (Inter & JetBrains Mono) -->\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap\" rel=\"stylesheet\">\n \n <!-- Phosphor Icons -->\n <script src=\"https://unpkg.com/@phosphor-icons/web\"></script>\n\n <!-- Configuración de Tailwind -->\n <script>\n tailwind.config = {\n darkMode: 'media', // Automático según sistema\n theme: {\n extend: {\n fontFamily: {\n sans: ['Inter', 'sans-serif'],\n mono: ['JetBrains Mono', 'monospace'],\n },\n colors: {\n brand: {\n light: '#f8fafc', // Slate 50\n dark: '#0f172a', // Slate 900\n cardLight: '#ffffff',\n cardDark: '#1e293b',\n accent: '#0ea5e9', // Sky 500\n accentHover: '#0284c7', // Sky 600\n }\n },\n animation: {\n 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',\n 'fade-in': 'fadeIn 0.5s ease-out',\n 'blob': 'blob 7s infinite',\n },\n keyframes: {\n fadeIn: {\n '0%': { opacity: '0', transform: 'translateY(10px)' },\n '100%': { opacity: '1', transform: 'translateY(0)' },\n },\n blob: {\n '0%': { transform: 'translate(0px, 0px) scale(1)' },\n '33%': { transform: 'translate(30px, -50px) scale(1.1)' },\n '66%': { transform: 'translate(-20px, 20px) scale(0.9)' },\n '100%': { transform: 'translate(0px, 0px) scale(1)' },\n }\n }\n }\n }\n }\n </script>\n\n <!-- Lógica de Alpine.js -->\n <script>\n document.addEventListener('alpine:init', () => {\n Alpine.data('scannerApp', () => ({\n url: '',\n isLoading: false,\n result: null,\n error: null,\n showRaw: false,\n lang: navigator.language.startsWith('es') ? 'es' : 'en',\n \n // Diccionario de textos\n texts: {\n es: {\n title: \"Link X-Ray\",\n subtitle: \"Inteligencia de amenazas para URLs en tiempo real.\",\n label: \"URL Objetivo\",\n placeholder: \"https://ejemplo-sospechoso.com\",\n scanBtn: \"Escanear Enlace\",\n scanning: \"Analizando...\",\n errorTitle: \"Error de Análisis\",\n invalidUrl: \"La URL ingresada no es válida (requiere http:// o https://).\",\n connectionError: \"Error de conexión con el servicio de escaneo.\",\n reportTitle: \"Reporte de Análisis\",\n rawJson: \"JSON RAW\",\n adTitle: \"Potenciado por Travos.ai\",\n adText: \"Soluciones de IA Empresarial de Siguiente Generación\",\n adBtn: \"Conoce más\",\n riskScore: \"PUNTAJE DE RIESGO\",\n threatsFound: \"Amenazas Detectadas\",\n connectivity: \"Conectividad\",\n reachable: \"Accesible\",\n unreachable: \"Inaccesible\",\n safeVerdict: \"ENLACE SEGURO\",\n dangerVerdict: \"PELIGROSO\",\n toggleRaw: \"Ver Respuesta Cruda\"\n },\n en: {\n title: \"Link X-Ray\",\n subtitle: \"Real-time threat intelligence for suspicious URLs.\",\n label: \"Target URL\",\n placeholder: \"https://suspicious-example.com\",\n scanBtn: \"Scan Link\",\n scanning: \"Analyzing...\",\n errorTitle: \"Scan Error\",\n invalidUrl: \"Invalid URL format (requires http:// or https://).\",\n connectionError: \"Connection error with scan service.\",\n reportTitle: \"Analysis Report\",\n rawJson: \"RAW JSON\",\n adTitle: \"Powered by Travos.ai\",\n adText: \"Next-Gen Enterprise AI Solutions\",\n adBtn: \"Learn more\",\n riskScore: \"RISK SCORE\",\n threatsFound: \"Threats Detected\",\n connectivity: \"Connectivity\",\n reachable: \"Reachable\",\n unreachable: \"Unreachable\",\n safeVerdict: \"SAFE LINK\",\n dangerVerdict: \"DANGEROUS\",\n toggleRaw: \"View Raw Response\"\n }\n },\n\n // Helper para traducir\n t(key) {\n return this.texts[this.lang][key] || key;\n },\n\n async scanUrl() {\n this.isLoading = true;\n this.error = null;\n this.result = null;\n this.showRaw = false;\n\n if (!this.isValidUrl(this.url)) {\n this.error = this.t('invalidUrl');\n this.isLoading = false;\n return;\n }\n\n try {\n // URL Actualizada a producción\n const response = await fetch('https://tawmzfke.travos.ai/webhook/link-xray', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json' \n },\n body: JSON.stringify({ url: this.url })\n });\n\n if (!response.ok) {\n throw new Error(`Status: ${response.status}`);\n }\n\n const data = await response.json();\n this.result = data;\n\n } catch (err) {\n console.error(err);\n this.error = this.t('connectionError');\n } finally {\n this.isLoading = false;\n }\n },\n\n isValidUrl(string) {\n try {\n new URL(string);\n return true;\n } catch (_) {\n return false;\n }\n },\n\n // Determina si es seguro basado en el análisis\n isSafe() {\n return this.result?.analysis?.verdict === 'SAFE';\n },\n\n syntaxHighlight(json) {\n if (!json) return '';\n if (typeof json !== 'string') {\n json = JSON.stringify(json, undefined, 2);\n }\n json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n return json.replace(/(\"(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\\"])*\"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function (match) {\n let cls = 'text-purple-600 dark:text-purple-400';\n if (/^\"/.test(match)) {\n if (/:$/.test(match)) {\n cls = 'text-sky-600 dark:text-sky-400 font-bold';\n } else {\n cls = 'text-green-600 dark:text-green-400';\n }\n } else if (/true|false/.test(match)) {\n cls = 'text-blue-600 dark:text-blue-400 font-bold';\n } else if (/null/.test(match)) {\n cls = 'text-gray-500';\n }\n return '<span class=\"' + cls + '\">' + match + '</span>';\n });\n }\n }))\n })\n </script>\n\n <!-- Alpine.js Core -->\n <script src=\"https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js\"></script>\n\n <style>\n [x-cloak] { display: none !important; }\n \n .bg-grid-pattern {\n background-size: 40px 40px;\n background-image: linear-gradient(to right, rgba(100, 116, 139, 0.05) 1px, transparent 1px),\n linear-gradient(to bottom, rgba(100, 116, 139, 0.05) 1px, transparent 1px);\n }\n \n /* Scrollbar elegante */\n .custom-scrollbar::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n .custom-scrollbar::-webkit-scrollbar-track {\n background: transparent; \n }\n .custom-scrollbar::-webkit-scrollbar-thumb {\n background: #cbd5e1; \n border-radius: 4px;\n }\n .dark .custom-scrollbar::-webkit-scrollbar-thumb {\n background: #475569; \n }\n .custom-scrollbar::-webkit-scrollbar-thumb:hover {\n background: #94a3b8; \n }\n </style>\n</head>\n<!-- Se cambió overflow-hidden a overflow-x-hidden para permitir scroll vertical en móviles -->\n<body class=\"bg-brand-light dark:bg-brand-dark text-slate-800 dark:text-slate-200 font-sans min-h-screen flex flex-col transition-colors duration-300 relative overflow-x-hidden\">\n\n <!-- Fondo decorativo sutil y animado -->\n <div class=\"fixed inset-0 bg-grid-pattern pointer-events-none z-0\"></div>\n <div class=\"fixed inset-0 w-full h-full overflow-hidden -z-10 pointer-events-none\">\n <!-- Orbe 1: Azul (Top Left) -->\n <div class=\"absolute top-0 left-[-10%] w-[500px] h-[500px] bg-blue-400/30 dark:bg-blue-500/20 rounded-full blur-[100px] mix-blend-multiply dark:mix-blend-normal animate-blob\"></div>\n \n <!-- Orbe 2: Púrpura (Top Right) -->\n <div class=\"absolute top-0 right-[-10%] w-[500px] h-[500px] bg-purple-400/30 dark:bg-purple-500/20 rounded-full blur-[100px] mix-blend-multiply dark:mix-blend-normal animate-blob\" style=\"animation-delay: 2s\"></div>\n \n <!-- Orbe 3: Rosa/Cyan (Bottom Center) -->\n <div class=\"absolute -bottom-32 left-[20%] w-[500px] h-[500px] bg-cyan-400/30 dark:bg-indigo-500/20 rounded-full blur-[100px] mix-blend-multiply dark:mix-blend-normal animate-blob\" style=\"animation-delay: 4s\"></div>\n </div>\n\n <!-- Contenido Principal -->\n <main class=\"flex-grow flex flex-col items-center justify-center p-4 z-10\"\n x-data=\"scannerApp()\">\n \n <div class=\"w-full max-w-2xl space-y-8 my-auto\">\n \n <!-- Cabecera -->\n <div class=\"text-center space-y-3\">\n <div class=\"inline-flex items-center justify-center p-3 bg-white dark:bg-brand-cardDark border border-slate-200 dark:border-slate-700 rounded-2xl shadow-sm mb-2 transition-colors\">\n <i class=\"ph ph-shield-check text-4xl text-brand-accent\"></i>\n </div>\n <h1 class=\"text-4xl md:text-5xl font-bold tracking-tight text-slate-900 dark:text-white transition-colors\" x-text=\"t('title')\">\n </h1>\n <p class=\"text-slate-500 dark:text-slate-400 text-lg\" x-text=\"t('subtitle')\">\n </p>\n </div>\n\n <!-- Tarjeta del Formulario -->\n <div class=\"bg-white/80 dark:bg-brand-cardDark/60 backdrop-blur-xl border border-slate-200 dark:border-slate-700/50 rounded-2xl p-6 md:p-8 shadow-xl relative overflow-hidden transition-all duration-300\">\n \n <form @submit.prevent=\"scanUrl\" class=\"space-y-6\">\n <div>\n <label for=\"url\" class=\"block text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2 ml-1\" x-text=\"t('label')\">\n </label>\n <div class=\"relative group\">\n <div class=\"absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none\">\n <i class=\"ph ph-link text-slate-400 dark:text-slate-500 text-xl group-focus-within:text-brand-accent transition-colors\"></i>\n </div>\n <input \n type=\"url\" \n id=\"url\" \n x-model=\"url\" \n :placeholder=\"t('placeholder')\"\n required\n class=\"w-full bg-slate-50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-600 text-slate-900 dark:text-white rounded-xl py-4 pl-12 pr-4 focus:outline-none focus:ring-2 focus:ring-brand-accent/50 focus:border-brand-accent transition-all placeholder-slate-400 font-mono text-sm\"\n >\n </div>\n </div>\n\n <button \n type=\"submit\" \n :disabled=\"isLoading || !url\"\n class=\"w-full bg-brand-accent hover:bg-brand-accentHover text-white font-semibold rounded-xl py-3.5 shadow-lg shadow-sky-500/20 hover:shadow-sky-500/40 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none transition-all duration-200 flex items-center justify-center space-x-2\"\n >\n <span x-show=\"!isLoading\" x-text=\"t('scanBtn')\"></span>\n <i x-show=\"!isLoading\" class=\"ph ph-scan text-xl\"></i>\n \n <!-- Spinner -->\n <div x-show=\"isLoading\" x-cloak class=\"flex items-center space-x-2\">\n <svg class=\"animate-spin h-5 w-5 text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n <span x-text=\"t('scanning')\"></span>\n </div>\n </button>\n </form>\n\n <!-- Mensaje de Error -->\n <div x-show=\"error\" x-cloak class=\"mt-6 p-4 rounded-lg bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 flex items-start space-x-3 animate-fade-in\">\n <i class=\"ph ph-warning-circle text-red-500 text-xl mt-0.5\"></i>\n <div>\n <h3 class=\"text-sm font-bold text-red-700 dark:text-red-400\" x-text=\"t('errorTitle')\"></h3>\n <p class=\"text-xs text-red-600 dark:text-red-300 mt-1\" x-text=\"error\"></p>\n </div>\n </div>\n\n </div>\n\n <!-- Panel de Resultados (Dashboard) -->\n <div x-show=\"result\" x-cloak class=\"mt-8 animate-fade-in space-y-6\">\n \n <!-- Tarjeta Principal de Veredicto -->\n <div class=\"bg-white dark:bg-brand-cardDark border rounded-2xl overflow-hidden shadow-lg transition-colors\"\n :class=\"isSafe() ? 'border-green-500/30' : 'border-red-500/30'\">\n \n <!-- Cabecera Veredicto -->\n <div class=\"p-6 flex items-center justify-between\"\n :class=\"isSafe() ? 'bg-green-500/10' : 'bg-red-500/10'\">\n <div class=\"flex items-center space-x-3\">\n <div class=\"p-2 rounded-full\"\n :class=\"isSafe() ? 'bg-green-500 text-white' : 'bg-red-500 text-white'\">\n <i class=\"ph text-2xl\" :class=\"isSafe() ? 'ph-check-circle' : 'ph-warning'\"></i>\n </div>\n <div>\n <h2 class=\"text-xl font-bold tracking-tight\" \n :class=\"isSafe() ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400'\"\n x-text=\"isSafe() ? t('safeVerdict') : t('dangerVerdict')\">\n </h2>\n <p class=\"text-xs font-mono opacity-80\" \n :class=\"isSafe() ? 'text-green-800 dark:text-green-300' : 'text-red-800 dark:text-red-300'\"\n x-text=\"result?.analysis?.hostname\">\n </p>\n </div>\n </div>\n <div class=\"text-right\">\n <div class=\"text-3xl font-black\"\n :class=\"isSafe() ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'\"\n x-text=\"result?.analysis?.riskScore\">\n </div>\n <div class=\"text-[10px] font-bold uppercase tracking-widest text-slate-500\" x-text=\"t('riskScore')\"></div>\n </div>\n </div>\n\n <!-- Detalles Grid -->\n <div class=\"p-6 grid grid-cols-1 md:grid-cols-2 gap-6 bg-white/50 dark:bg-transparent\">\n \n <!-- Conectividad -->\n <div>\n <h3 class=\"text-xs font-bold uppercase tracking-wider text-slate-500 mb-3 flex items-center\">\n <i class=\"ph ph-globe mr-2\"></i> <span x-text=\"t('connectivity')\"></span>\n </h3>\n <div class=\"flex items-center space-x-2\">\n <span class=\"relative flex h-3 w-3\">\n <span class=\"animate-ping absolute inline-flex h-full w-full rounded-full opacity-75\"\n :class=\"result?.analysis?.connectivity?.reachable ? 'bg-green-400' : 'bg-red-400'\"></span>\n <span class=\"relative inline-flex rounded-full h-3 w-3\"\n :class=\"result?.analysis?.connectivity?.reachable ? 'bg-green-500' : 'bg-red-500'\"></span>\n </span>\n <span class=\"font-medium text-slate-700 dark:text-slate-300\"\n x-text=\"result?.analysis?.connectivity?.reachable ? t('reachable') : t('unreachable')\">\n </span>\n </div>\n </div>\n\n <!-- Amenazas -->\n <div>\n <h3 class=\"text-xs font-bold uppercase tracking-wider text-slate-500 mb-3 flex items-center\">\n <i class=\"ph ph-skull mr-2\"></i> <span x-text=\"t('threatsFound')\"></span>\n </h3>\n <template x-if=\"result?.analysis?.threats?.length > 0\">\n <ul class=\"space-y-2\">\n <template x-for=\"threat in result.analysis.threats\" :key=\"threat.type\">\n <li class=\"text-sm bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 px-3 py-2 rounded border border-red-100 dark:border-red-900/30 flex items-start\">\n <i class=\"ph ph-warning-octagon mt-0.5 mr-2 flex-shrink-0\"></i>\n <span x-text=\"threat.type\"></span>\n </li>\n </template>\n </ul>\n </template>\n <template x-if=\"!result?.analysis?.threats?.length\">\n <p class=\"text-sm text-slate-400 italic\">None</p>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Toggle JSON Raw -->\n <div class=\"text-center\">\n <button @click=\"showRaw = !showRaw\" \n class=\"text-xs font-medium text-slate-500 hover:text-brand-accent transition-colors underline decoration-dotted underline-offset-4\">\n <span x-text=\"showRaw ? 'Hide JSON' : t('toggleRaw')\"></span>\n </button>\n </div>\n\n <!-- Visor JSON (Oculto por defecto) -->\n <div x-show=\"showRaw\" x-cloak class=\"animate-fade-in bg-slate-50 dark:bg-black/80 backdrop-blur rounded-xl border border-slate-200 dark:border-slate-700 shadow-xl overflow-hidden\">\n <div class=\"bg-slate-100 dark:bg-slate-800/50 px-4 py-2 border-b border-slate-200 dark:border-slate-700 flex space-x-2 items-center\">\n <div class=\"w-3 h-3 rounded-full bg-red-400\"></div>\n <div class=\"w-3 h-3 rounded-full bg-amber-400\"></div>\n <div class=\"w-3 h-3 rounded-full bg-green-400\"></div>\n <span class=\"ml-auto text-[10px] text-slate-400 font-mono\" x-text=\"t('rawJson')\"></span>\n </div>\n \n <div class=\"p-4 overflow-x-auto max-h-64 custom-scrollbar\">\n <pre class=\"font-mono text-xs md:text-sm text-slate-700 dark:text-slate-300 leading-relaxed\" x-html=\"syntaxHighlight(result)\"></pre>\n </div>\n </div>\n </div>\n\n <!-- Texto Legal Eliminado -->\n\n </div>\n\n <!-- Ad / Footer para Travos.ai -->\n <footer class=\"w-full max-w-2xl mt-12 mb-4 animate-fade-in\">\n <a href=\"https://travos.ai/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"group block relative overflow-hidden rounded-2xl bg-gradient-to-br from-slate-900 to-slate-800 dark:from-slate-800 dark:to-slate-900 p-[1px] shadow-lg hover:shadow-brand-accent/20 transition-all\">\n \n <!-- Borde brillante en hover -->\n <div class=\"absolute inset-0 bg-gradient-to-r from-brand-accent via-purple-500 to-brand-accent opacity-0 group-hover:opacity-100 transition-opacity duration-500\" style=\"filter: blur(8px);\"></div>\n \n <div class=\"relative bg-white dark:bg-slate-900 rounded-2xl p-4 flex flex-col sm:flex-row items-center justify-between gap-4 h-full\">\n <div class=\"flex items-center space-x-4\">\n <div class=\"p-2 bg-indigo-50 dark:bg-indigo-900/30 rounded-lg\">\n <i class=\"ph ph-brain text-3xl text-indigo-600 dark:text-indigo-400\"></i>\n </div>\n <div class=\"text-center sm:text-left\">\n <h3 class=\"text-sm font-bold text-slate-900 dark:text-white group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors\" x-text=\"t('adTitle')\"></h3>\n <p class=\"text-xs text-slate-500 dark:text-slate-400\" x-text=\"t('adText')\"></p>\n </div>\n </div>\n \n <div class=\"flex items-center text-xs font-semibold text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-900/20 px-4 py-2 rounded-full group-hover:bg-indigo-600 group-hover:text-white transition-colors duration-300\">\n <span x-text=\"t('adBtn')\"></span>\n <i class=\"ph ph-arrow-right ml-1\"></i>\n </div>\n </div>\n </a>\n </footer>\n\n </main>\n</body>\n</html>" | |
| } | |
| }, | |
| "type": "n8n-nodes-base.webhook", | |
| "typeVersion": 2.1, | |
| "position": [ | |
| -352, | |
| 160 | |
| ], | |
| "id": "dab4b66c-fb89-4b2e-b111-7d0c2f98c097", | |
| "name": "Webhook", | |
| "webhookId": "26d74b4a-0018-443b-8140-9fb7515a00d6" | |
| } | |
| ], | |
| "connections": { | |
| "Webhook POST": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Validate & Fix URL", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Validate & Fix URL": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "URL Valid?", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "URL Valid?": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Fetch URL (3 retries, 10 redirects)", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ], | |
| [ | |
| { | |
| "node": "Respond: Invalid URL", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Fetch URL (3 retries, 10 redirects)": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "HTTP Success - Extract Meta", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ], | |
| [ | |
| { | |
| "node": "HTTP Error - Capture", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "HTTP Success - Extract Meta": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Merge HTTP Results", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "HTTP Error - Capture": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Merge HTTP Results", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Merge HTTP Results": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Config: Google Web Risk API Key", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Config: Google Web Risk API Key": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Build Web Risk Request URL", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Build Web Risk Request URL": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Google Web Risk Lookup API", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Google Web Risk Lookup API": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Process Results & Verdict", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ], | |
| [ | |
| { | |
| "node": "Web Risk API Error", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Process Results & Verdict": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Respond: Analysis Complete", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Web Risk API Error": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Respond: API Error", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| } | |
| }, | |
| "pinData": {}, | |
| "meta": { | |
| "instanceId": "62d8fdb413e534b7ac4853913aef6c8330020d1c81641fa6cc3dd9e55e67803f" | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment