KYC Embebido con JAAK Mosaic
Guía paso a paso para crear un sistema KYC embebido con configuración dinámica vía iframe segura
🎯 ¿Qué vas a construir?
Sistema KYC Embebido
Vas a crear una aplicación web completa para integración KYC con JAAK Mosaic:
- ✅ Backend Node.js con middleware seguro y headers de permisos
- ✅ Frontend responsivo con layout de dos columnas
- ✅ Configuración dinámica de pasos KYC con persistencia
- ✅ Comunicación PostMessage robusta con manejo de sesiones
- ✅ UI moderna con navy blue styling y progreso en tiempo real
- ✅ Gestión automática de shortKeys y configuraciones
🎬 Características Principales
Al finalizar tendrás un sistema en http://localhost:3000
con:
- Panel de configuración con selección de pasos KYC
- ShortKey readonly con botón de copia y obtención automática
- Iframe embebido con permisos de cámara y micrófono
- Logs en tiempo real y visualización de progreso
- Exportación de datos y persistencia de configuración

📦 Repositorio del proyecto
Puedes acceder al código del proyecto en el siguiente link: Ir a Github
🛠️ Preparación del entorno
📋 Requisitos Previos
✅ Checklist antes de empezar:
- Node.js 18+ instalado (
node --version
) - NPM funcionando (
npm --version
) - Editor de código (VS Code recomendado)
- Navegador moderno (Chrome/Firefox/Safari/Edge)
- Token de JAAK (solicitar a tu proveedor)
🔧 Instalación de Herramientas
1. Node.js (Obligatorio)
# Verificar si ya tienes Node.js
node --version
# Si no lo tienes, descarga desde:
# https://nodejs.org/ (versión LTS)
2. Editor de Código
# VS Code (recomendado)
# Descarga desde: https://code.visualstudio.com/
# Extensiones útiles:
# - ES6 String HTML
# - Prettier
# - Auto Rename Tag
📁 Estructura del proyecto
🏗️ Arquitectura del Sistema
kyc-embebido/
├── 📁 backend/ # 🔧 Servidor y API
│ ├── server.js # Servidor Express principal
│ ├── package.json # Dependencias del proyecto
│ ├── .env # Variables de entorno (SECRETO)
│ ├── config.json # Configuración KYC persistente
│ └── README.md # Documentación del backend
├── 📁 frontend/ # 🎨 Interfaz de usuario
│ ├── index.html # Página principal con iframe
│ ├── styles/
│ │ └── main.css # Estilos del demo
│ ├── scripts/
│ │ └── kyc-embed.js # Lógica de comunicación iframe
│ └── assets/ # Recursos estáticos (imágenes, etc.)
└── 📁 docs/ # 📚 Documentación
├── README.md # Documentación general
├── API.md # Documentación de API
└── DEPLOYMENT.md # Guía de despliegue
📂 Crear la Estructura Base
Copia y pega los siguientes comandos en una terminal
# 1. Crear directorio del proyecto
mkdir kyc-embebido
cd kyc-embebido
# 2. Crear estructura de carpetas organizadas
mkdir -p backend frontend/styles frontend/scripts frontend/assets docs
# 3. Crear archivos del backend
touch backend/server.js backend/package.json backend/.env backend/config.json
# 4. Crear archivos del frontend
touch frontend/index.html frontend/styles/main.css frontend/scripts/kyc-embed.js
# 5. Crear documentación
touch docs/README.md docs/API.md docs/DEPLOYMENT.md
⚙️ Configuración del backend
📦 Paso 1: package.json
Abre el archivo de dependencias que creaste backend/package.json
y pega los siguientes datos:
{
"name": "kyc-embebido",
"version": "1.0.0",
"description": "Sistema KYC embebido con JAAK Mosaic",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"axios": "^1.6.0",
"dotenv": "^16.3.1"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}
🔒 Paso 2: Variables de entorno (backend/.env)
# ⚠️ MANTENER ESTE ARCHIVO SECRETO - NO SUBIR A GIT
PORT=3000
JAAK_API_URL=https://sandbox.api.jaak.ai/api/v1/kyc/flow
JAAK_BEARER_TOKEN=tu_token_jaak_aqui
🔑 Obtener tu Token JAAK:
-
Inicia sesión en JAAK Platform
-
Ve a Menú Lateral → Ajustes → API Keys
-
Haz clic en "Generar nueva API key"
-
Completa los campos:
-
Nombre:
API-KYC-Produccion
(o el nombre que prefieras) -
Expira en: Selecciona "1 año" o "Sin vencimiento" según tu preferencia
-
-
Haz clic en "Generar"
⚙️ Paso 3: Configuración KYC (backend/config.json)
Abre el archivo de dependencias que creaste backend/config.json
y pega los siguientes datos:
{
"shortKey": "",
"steps": [
{"key": "WELCOME"},
{"key": "DOCUMENT_EXTRACT"},
{"key": "DOCUMENT_VERIFY"},
{"key": "BLACKLIST"},
{"key": "IVERIFICATION"},
{"key": "LOCATION_PERMISSIONS"},
{"key": "OTO"},
{"key": "FINISH"}
]
}
Nota: La configuración es persistente y se actualiza automáticamente cuando cambias los pasos en la UI.
🚀 Paso 4: Servidor Express (backend/server.js)
Servidor completo con headers de seguridad y gestión de configuración, copia y pega en el archivo backend/server.js
:
const express = require('express');
const cors = require('cors');
const axios = require('axios');
const path = require('path');
const fs = require('fs');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Configuración middleware
app.use(cors());
app.use(express.json());
// 🔒 Headers de seguridad para permisos de cámara y micrófono
app.use((req, res, next) => {
// Permissions Policy para permitir acceso a cámara, micrófono y geolocalización
res.setHeader('Permissions-Policy', 'camera=(*), microphone=(*), geolocation=(*)');
// Feature Policy (para compatibilidad con navegadores antiguos)
res.setHeader('Feature-Policy', 'camera \'*\'; microphone \'*\'; geolocation \'*\'');
// Permitir embebido en iframes
res.setHeader('X-Frame-Options', 'ALLOWALL');
// Content Security Policy que permite el acceso a medios
res.setHeader('Content-Security-Policy',
"default-src 'self' https:; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; " +
"style-src 'self' 'unsafe-inline' https:; " +
"img-src 'self' data: blob: https:; " +
"media-src 'self' blob: https:; " +
"frame-src 'self' https://mosaic.sandbox.jaak.ai; " +
"connect-src 'self' https://mosaic.sandbox.jaak.ai https://sandbox.api.jaak.ai;"
);
next();
});
// Servir archivos estáticos del frontend
app.use(express.static(path.join(__dirname, '../frontend')));
// 📋 Funciones de gestión de configuración
function readConfig() {
try {
const configPath = path.join(__dirname, 'config.json');
// Verificar si el archivo existe
if (!fs.existsSync(configPath)) {
console.log('📋 config.json no existe, creando archivo por defecto...');
const defaultConfig = {
shortKey: "",
steps: [
{ key: "DOCUMENT_EXTRACT" },
{ key: "DOCUMENT_VERIFY" },
{ key: "IVERIFICATION" }
]
};
saveConfig(defaultConfig);
return defaultConfig;
}
const data = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(data);
console.log(`📋 Configuración cargada: ${config.steps?.length || 0} pasos`);
return config;
} catch (error) {
console.error('❌ Error leyendo config.json:', error);
const fallbackConfig = { shortKey: "", steps: [] };
return fallbackConfig;
}
}
//Actualiza los valores de configuración en el archivo config.json --> Podría simular una gestión en base de datos
function saveConfig(config) {
try {
const configPath = path.join(__dirname, 'config.json');
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
console.log('✅ Config guardado exitosamente');
return true;
} catch (error) {
console.error('❌ Error guardando config.json:', error);
return false;
}
}
//Extrae la shortKey de la url que manda JAAK
function extractShortKeyFromUrl(sessionUrl) {
const urlParts = sessionUrl.split('/');
const lastPart = urlParts[urlParts.length - 1];
return lastPart.length >= 7 ? lastPart.slice(-7) : lastPart;
}
// 🔗 API Endpoints
// Endpoints expuestos en este proyecto
// GET /api/config - Obtener configuración actual
app.get('/api/config', (req, res) => {
const config = readConfig();
res.json(config);
});
// POST /api/config - Guardar configuración
app.post('/api/config', (req, res) => {
try {
const { steps } = req.body;
if (!steps || !Array.isArray(steps)) {
return res.status(400).json({
success: false,
message: 'El parámetro steps es requerido y debe ser un array'
});
}
const currentConfig = readConfig();
const updatedConfig = {
...currentConfig,
steps: steps.map(step => ({ key: step }))
};
if (saveConfig(updatedConfig)) {
console.log(`💾 Configuración actualizada: ${steps.length} pasos`);
res.json({
success: true,
message: 'Configuración guardada exitosamente',
config: updatedConfig
});
} else {
res.status(500).json({
success: false,
message: 'Error al guardar la configuración'
});
}
} catch (error) {
console.error('❌ Error guardando configuración:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor',
error: error.message
});
}
});
// POST /api/kyc/flow - Crear nuevo flujo KYC -> Llamada API a server de JAAK
// En este punto este server.js funciona como un middleware entre el frontend y la API de JAAK
app.post('/api/kyc/flow', async (req, res) => {
try {
console.log('🔄 Creando nuevo flujo KYC...');
const config = readConfig();
const requestBody = {
name: req.body.name || "Demo KYC Embebido",
flow: req.body.flow || "DEMO_FLOW",
redirectUrl: req.body.redirectUrl || "",
countryDocument: req.body.countryDocument || "MEX",
flowType: req.body.flowType || "KYC",
verificationType: req.body.verificationType || "",
verification: req.body.verification || {
EMAIL: "",
SMS: "",
WHATSAPP: ""
}
};
console.log('📤 Enviando a JAAK API:', requestBody);
// 🌐 Llamada a JAAK API
const response = await axios.post(process.env.JAAK_API_URL, requestBody, {
headers: {
'Authorization': `Bearer ${process.env.JAAK_BEARER_TOKEN}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
console.log('✅ Respuesta exitosa de JAAK API');
// 🔑 Extraer y guardar shortKey automáticamente
if (response.data && response.data.sessionUrl) {
const newShortKey = extractShortKeyFromUrl(response.data.sessionUrl);
console.log(`🔑 Nuevo shortKey extraído: ${newShortKey}`);
// Actualizar config.json automáticamente
const updatedConfig = {
...config,
shortKey: newShortKey
};
if (saveConfig(updatedConfig)) {
console.log(`💾 ShortKey actualizado en config: ${newShortKey}`);
}
}
res.json({
success: true,
data: response.data,
extractedShortKey: response.data?.sessionUrl ? extractShortKeyFromUrl(response.data.sessionUrl) : null
});
} catch (error) {
console.error('❌ Error en JAAK API:', error.response?.data || error.message);
res.status(error.response?.status || 500).json({
success: false,
error: error.response?.data || 'Error interno del servidor',
message: 'Error al consumir JAAK API',
statusCode: error.response?.status,
details: error.message
});
}
});
// 🏠 Ruta principal
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../frontend/index.html'));
});
// 🚀 Iniciar servidor
app.listen(PORT, () => {
console.log(`🚀 Servidor KYC ejecutándose en http://localhost:${PORT}`);
console.log(`📱 Demo embebido: http://localhost:${PORT}/index.html`);
console.log(`🔗 API endpoint: http://localhost:${PORT}/api/kyc/flow`);
});
📦 Instalar Dependencias
En una terminal accede a la raíz de tu proyecto y ejecuta en terminal:
# Navegar al directorio del backend
cd backend
# Instalar dependencias del servidor
npm install
🎨 Implementación del frontend
🎨 Paso 1: Estilos CSS (frontend/styles/main.css)
Estilos modernos con navy blue theme y layout responsivo, puedes copiar y pegar estos estilos en el archivo frontend/styles/main.css y ajustalos a tu diseño:
/* 🎨 Reset y configuración base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #2c3e50;
background: linear-gradient(135deg, #183583 0%, #030018 100%);
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* 📱 Header */
header {
text-align: center;
margin-bottom: 40px;
background: rgba(255, 255, 255, 0.95);
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
header h1 {
font-size: 2rem;
margin-bottom: 15px;
background: rgb(11, 11, 62);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
}
header p {
font-size: 1.2rem;
color: #7f8c8d;
max-width: 800px;
margin: 0 auto;
}
/* 🔧 Panel de Control */
.control-panels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
.config-panel,
.session-panel {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 30px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
height: fit-content;
margin-top: 1rem;
}
.session-panel {
max-width: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
overflow-x: hidden;
}
.config-section h4 {
margin-top: 15px;
margin-bottom: 15px;
color: #2c3e50;
font-size: 1.1rem;
border-bottom: 2px solid #e9ecef;
padding-bottom: 8px;
}
/* 🔑 Configuración de Short Key */
.shortkey-controls {
display: flex;
flex-direction: column;
gap: 15px;
}
.shortkey-controls .input-group {
display: flex;
align-items: end;
gap: 10px;
margin-bottom: 0;
}
.shortkey-controls .input-group input[readonly] {
background-color: #f8f9fa;
color: #6c757d;
}
/* 📋 Pasos KYC */
.steps-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.step-card {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 10px;
padding: 15px;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
}
.step-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.step-card.selected {
background: rgba(30, 58, 138, 0.1);
color: #1e3a8a;
border: 2px solid #1e3a8a;
font-weight: 600;
}
/* 🎮 Botones de acción */
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: #1e3a8a;
color: white;
}
.btn-primary:hover {
background: #1e40af;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(30, 58, 138, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn-icon {
padding: 8px 12px;
min-width: auto;
}
.btn-full-width {
width: 100%;
justify-content: center;
}
/* 📱 Contenedor principal */
.main-content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
/* 🖼️ Iframe container */
.iframe-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 20px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.iframe-container h3 {
margin-bottom: 20px;
color: #2c3e50;
display: flex;
align-items: center;
gap: 10px;
}
#kycIframe {
width: 100%;
height: 600px;
border: none;
border-radius: 10px;
background: #f8f9fa;
}
/* 📊 Panel lateral */
.side-panel {
display: flex;
flex-direction: column;
gap: 20px;
}
.panel-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 20px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.panel-section h4 {
margin-bottom: 15px;
color: #2c3e50;
display: flex;
align-items: center;
gap: 8px;
}
/* 📈 Barra de progreso */
.progress-bar {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
width: 0%;
transition: width 0.3s ease;
}
/* 📝 Logs */
.logs-content {
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.log-entry {
padding: 8px;
margin-bottom: 5px;
border-radius: 4px;
display: flex;
gap: 10px;
}
.log-entry.info {
background: #e7f3ff;
color: #0066cc;
}
.log-entry.success {
background: #d4edda;
color: #155724;
}
.log-entry.error {
background: #f8d7da;
color: #721c24;
}
.log-entry.warning {
background: #fff3cd;
color: #856404;
}
.log-time {
font-weight: bold;
min-width: 80px;
}
/* 📊 Datos */
.data-content {
max-height: 200px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
}
.data-content pre {
margin: 0;
white-space: pre-wrap;
}
.no-data {
text-align: center;
color: #6c757d;
font-style: italic;
}
/* 🔔 Notificaciones */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 8px;
color: white;
font-weight: 600;
z-index: 1000;
animation: slideInRight 0.3s ease-out;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.notification.success {
background: #28a745;
}
.notification.error {
background: #dc3545;
}
.notification.warning {
background: #ffc107;
color: #212529;
}
.notification.info {
background: #17a2b8;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* 📱 Responsive Design */
@media (max-width: 768px) {
.config-grid {
grid-template-columns: 1fr;
}
.main-content {
grid-template-columns: 1fr;
}
.action-buttons {
justify-content: center;
}
header h1 {
font-size: 2rem;
}
.container {
padding: 10px;
}
}
📱 Paso 2: Página Principal (frontend/index.html)
Estructura HTML actualizada con layout de dos columnas y iframe con permisos, copia y pega en el archivo frontend/index.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo KYC Embebido - JAAK Mosaic</title>
<link rel="stylesheet" href="styles/main.css">
</head>
<body>
<div class="container">
<!-- 📱 Header -->
<header>
<h1>🔒 Demo KYC Mosaic</h1>
<p>Sistema completo de verificación de identidad con JAAK Mosaic integrado via iframe</p>
</header>
<!-- 🔧 Panel de Control -->
<div class="control-panels">
<!-- 🔧 Panel de Configuración -->
<div class="config-panel">
<h3>⚙️ Configuración del Flujo KYC</h3>
<!-- 🔑 Configuración de Short Key -->
<div class="config-section">
<h4>🔑 Creación de Sesión KYC</h4>
<div class="shortkey-controls">
<div class="input-group">
<label for="shortKey">Short Key JAAK:</label>
<input type="text" id="shortKey" readonly placeholder="Se obtendrá automáticamente...">
<button class="btn btn-icon" id="copyShortKeyBtn" title="Copiar Short Key">
📋
</button>
</div>
<button class="btn btn-secondary btn-full-width" id="getShortKeyBtn">
🔄 Obtener Nuevo Short Key
</button>
</div>
</div>
<!-- 🎯 Pasos del Flujo KYC -->
<div class="config-section">
<h4>🎯 Seleccionar Pasos del Flujo KYC</h4>
<div class="steps-container" id="stepsContainer">
<!-- Los pasos se cargarán dinámicamente -->
</div>
<button class="btn btn-primary" id="saveConfigBtn">
💾 Guardar Cambios
</button>
</div>
</div>
<!-- 📊 Panel de Datos de Sesión -->
<div class="session-panel">
<h3>📊 Datos de Sesión KYC</h3>
<!-- 📈 Progreso -->
<div class="config-section">
<h4>📈 Progreso del Flujo</h4>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div id="progressText">Esperando inicio del flujo...</div>
</div>
<!-- 📊 Datos Capturados -->
<div class="config-section">
<h4>📊 Datos Capturados</h4>
<div class="data-content" id="dataContent">
<p class="no-data">No hay datos disponibles</p>
</div>
</div>
<!-- 📤 Botones de Exportación -->
<div class="config-section">
<div class="export-buttons">
<button class="btn btn-secondary" id="exportDataBtn" disabled>
📤 Exportar Resultados
</button>
<button class="btn btn-secondary" id="copyDataBtn" disabled>
📋 Copiar Datos
</button>
</div>
</div>
</div>
</div>
<!-- 📱 Vista del Usuario Final -->
<div class="user-view">
<!-- 🖼️ Iframe Container -->
<div class="iframe-container">
<div class="iframe-header">
<h3>🖼️ Vista del Usuario Final - KYC</h3>
<div class="iframe-controls">
<button class="btn btn-primary" id="startKycBtn">
🚀 Iniciar Flujo KYC
</button>
<button class="btn btn-warning" id="restartKycBtn">
🔄 Reiniciar
</button>
</div>
</div>
<iframe
id="kycIframe"
src="about:blank"
allow="camera; microphone; geolocation; fullscreen; web-share"
sandbox="allow-scripts allow-same-origin allow-forms allow-top-navigation allow-popups allow-popups-to-escape-sandbox"
></iframe>
</div>
<!-- 📝 Panel de Logs -->
<div class="logs-panel">
<div class="panel-section">
<h4>📝 Logs del Sistema</h4>
<div class="logs-content" id="logsContent">
<div class="log-entry info">
<span class="log-time">--:--:--</span>
<span class="log-message">Sistema iniciado - Listo para usar</span>
</div>
</div>
<button class="btn btn-secondary" id="clearLogsBtn" style="margin-top: 10px;">
🗑️ Limpiar Logs
</button>
</div>
</div>
</div>
</div>
<script src="scripts/kyc-embed.js"></script>
</body>
</html>
Características clave del HTML actualizado:
- ✅ Layout de dos columnas en la sección de control
- ✅ ShortKey readonly con botón de copia
- ✅ Iframe con permisos de cámara, micrófono y geolocalización
- ✅ Botón de guardar configuración para persistencia
- ✅ Controles separados para iniciar y reiniciar
🔗 Integración iframe-backend
⚡ Paso 3: Lógica JavaScript (frontend/scripts/kyc-embed.js)
JavaScript modular con gestión de sesiones y arquitectura limpia, puedes copiar y pegar este código en tu archivo frontend/scripts/kyc-embed.js:
/**
* 🌟 Sistema KYC Embebido - JAAK Mosaic
*
* Características principales:
* - Gestión de configuración persistente
* - Comunicación PostMessage robusta
* - Manejo de sesiones únicas
* - UI responsiva y moderna
*
* @version 2.0.0
* @author Sistema KYC Embebido
*/
// 📊 Variables globales para gestión de estado
let kycResults = {};
let currentSteps = [];
let completedSteps = [];
let currentSessionConfig = null;
let currentConfigurationId = null;
let messageListener = null;
// 🎯 Pasos disponibles del flujo KYC
const availableSteps = [
{ key: 'WELCOME', name: '👋 Bienvenida', description: 'Página de bienvenida al flujo' },
{ key: 'DOCUMENT_EXTRACT', name: '📄 Extracción', description: 'Extracción de datos del documento' },
{ key: 'DOCUMENT_VERIFY', name: '✅ Verificación', description: 'Verificación de documento de identidad' },
{ key: 'BLACKLIST', name: '🚫 Lista Negra', description: 'Verificación en listas negras' },
{ key: 'IVERIFICATION', name: '🤳 Verificación Facial', description: 'Verificación de identidad en vivo' },
{ key: 'LOCATION_PERMISSIONS', name: '📍 Ubicación', description: 'Permisos de geolocalización' },
{ key: 'OTO', name: '👤 One-To-One', description: 'Verificación facial One-To-One' },
{ key: 'FINISH', name: '🏁 Finalizar', description: 'Finalización explícita del flujo' }
];
/**
* 🚀 Inicializa el proceso KYC con configuración específica
* @param {Object} config - Configuración del flujo KYC
* @param {string} config.shortKey - Clave de sesión de JAAK
* @param {Array} config.steps - Pasos del flujo a ejecutar
*/
function initKYC(config) {
const iframe = document.getElementById('kycIframe');
// Validaciones de entrada
if (!config.shortKey) {
showNotification('❌ Short Key es requerido', 'error');
return;
}
if (!config.steps || config.steps.length === 0) {
showNotification('❌ Selecciona al menos un paso del flujo KYC', 'error');
return;
}
// Generar ID único para esta configuración
currentConfigurationId = generateUniqueId();
currentSessionConfig = { ...config };
// Resetear estado global
kycResults = {};
completedSteps = [];
currentSteps = config.steps.map(step => step.key || step);
// Configurar iframe con sandbox y permisos
iframe.src = 'https://mosaic.sandbox.jaak.ai/embed';
// Configurar comunicación PostMessage con cleanup previo
if (messageListener) {
window.removeEventListener('message', messageListener);
}
messageListener = function(event) {
// Verificación de seguridad del origen
if (event.origin !== 'https://mosaic.sandbox.jaak.ai') {
return;
}
if (event.source === iframe.contentWindow) {
handleKYCMessage(event.data);
}
};
window.addEventListener('message', messageListener);
addLog('info', `🚀 Iniciando flujo KYC con ${config.steps.length} pasos`);
addLog('info', `🔗 Pasos: ${currentSteps.join(' → ')}`);
addLog('info', `🆔 Sesión: ${currentConfigurationId}`);
updateProgress(0);
}
/**
* 📨 Maneja los mensajes recibidos del iframe JAAK Mosaic
* @param {Object} message - Mensaje recibido del iframe
*/
function handleKYCMessage(message) {
addLog('info', `📨 Evento recibido: ${message.type}`);
switch(message.type) {
case 'READY':
addLog('success', '✅ JAAK Mosaic listo para configuración');
sendKYCConfiguration();
break;
case 'STEP_COMPLETE':
const stepData = message.data;
addLog('success', `✅ Paso completado: ${stepData.stepKey}`);
// Almacenar datos del paso
kycResults[stepData.stepKey] = stepData.data;
// Agregar a pasos completados
if (!completedSteps.includes(stepData.stepKey)) {
completedSteps.push(stepData.stepKey);
}
updateProgress();
updateDataDisplay();
break;
case 'FLOW_COMPLETE':
addLog('success', '🎉 Flujo KYC completado exitosamente');
if (message.data) {
kycResults = { ...kycResults, ...message.data };
}
updateDataDisplay();
updateProgress(100);
showNotification('🎉 Verificación KYC completada exitosamente', 'success');
break;
case 'ERROR':
if (message.data.error === 'CANCELLED') {
addLog('warning', '❌ KYC cancelado por el usuario');
showNotification('Proceso KYC cancelado', 'warning');
} else {
addLog('error', `❌ Error: ${message.data.error || message.data.message}`);
showNotification(`❌ Error: ${message.data.error || message.data.message}`, 'error');
}
break;
default:
addLog('info', `📨 Evento no manejado: ${message.type}`);
break;
}
}
/**
* 📤 Envía la configuración al iframe de JAAK Mosaic
*/
function sendKYCConfiguration() {
const iframe = document.getElementById('kycIframe');
if (!currentSessionConfig || !currentConfigurationId) {
addLog('error', '❌ No hay configuración de sesión disponible');
return;
}
const steps = currentSessionConfig.steps.map(step => {
if (typeof step === 'string') {
return { key: step };
}
return step;
});
const kycConfig = {
steps: steps,
shortKey: currentSessionConfig.shortKey,
configurationId: currentConfigurationId
};
setTimeout(() => {
iframe.contentWindow.postMessage({
type: 'CONFIG',
data: kycConfig
}, 'https://mosaic.sandbox.jaak.ai');
addLog('success', `📤 Configuración enviada: ${steps.length} pasos`);
addLog('info', `🔑 Short Key: ${currentSessionConfig.shortKey}`);
}, 1000);
}
/**
* 🆔 Genera un ID único para la sesión
* @returns {string} ID único de 8 caracteres
*/
function generateUniqueId() {
return Math.random().toString(36).substring(2, 10);
}
/**
* 💾 Guarda la configuración en el servidor
* @param {Array} selectedSteps - Pasos seleccionados
*/
async function saveConfiguration(selectedSteps) {
try {
addLog('info', '💾 Guardando configuración...');
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
steps: selectedSteps
})
});
const result = await response.json();
if (result.success) {
addLog('success', `✅ Configuración guardada: ${selectedSteps.length} pasos`);
showNotification('✅ Configuración guardada exitosamente', 'success');
} else {
addLog('error', `❌ Error guardando: ${result.message}`);
showNotification(`❌ Error: ${result.message}`, 'error');
}
} catch (error) {
addLog('error', `❌ Error de conexión: ${error.message}`);
showNotification(`❌ Error de conexión: ${error.message}`, 'error');
}
}
// Event Listeners - Configurados al cargar la página
document.addEventListener('DOMContentLoaded', function() {
// Crear UI de pasos dinámicamente
createStepsUI();
// Botón para iniciar KYC
document.getElementById('startKycBtn').addEventListener('click', function() {
const shortKey = document.getElementById('shortKey').value.trim();
const selectedSteps = getSelectedSteps();
if (!shortKey) {
showNotification('❌ El Short Key es requerido', 'error');
return;
}
if (selectedSteps.length === 0) {
showNotification('❌ Selecciona al menos un paso del flujo KYC', 'error');
return;
}
const config = {
shortKey: shortKey,
steps: selectedSteps
};
initKYC(config);
});
// Botón para guardar configuración
document.getElementById('saveConfigBtn').addEventListener('click', async function() {
const selectedSteps = getSelectedSteps();
if (selectedSteps.length === 0) {
showNotification('❌ Selecciona al menos un paso para guardar', 'error');
return;
}
await saveConfiguration(selectedSteps);
});
// Botón para obtener Short Key
document.getElementById('getShortKeyBtn').addEventListener('click', async function() {
await fetchKYCFlowAndGetShortKey();
});
// Botón para copiar Short Key
document.getElementById('copyShortKeyBtn').addEventListener('click', function() {
const shortKey = document.getElementById('shortKey').value;
if (shortKey) {
navigator.clipboard.writeText(shortKey).then(() => {
showNotification('📋 Short Key copiado al portapapeles', 'success');
});
}
});
// Botón para reiniciar KYC
document.getElementById('restartKycBtn').addEventListener('click', function() {
kycResults = {};
completedSteps = [];
currentSteps = [];
updateDataDisplay();
updateProgress(0);
document.getElementById('kycIframe').src = 'about:blank';
addLog('info', '🔄 Sistema reiniciado');
showNotification('✅ Sistema reiniciado', 'success');
});
// Cargar configuración inicial del servidor
setTimeout(async () => {
await loadConfigFromServer();
addLog('success', '🎉 Sistema inicializado correctamente');
showNotification('🚀 Sistema KYC listo para usar', 'info');
}, 1000);
});
```
**Características clave del JavaScript actualizado:**
- ✅ **Arquitectura modular** con JSDoc documentation
- ✅ **Gestión de sesiones únicas** para evitar conflictos de configuración
- ✅ **Cleanup de eventos** PostMessage para prevenir memory leaks
- ✅ **Configuración persistente** con endpoint `/api/config`
- ✅ **Validación robusta** de datos y estados
- ✅ **Manejo de errores** comprehensivo
// 📊 Mostrar información específica del paso
function showStepSpecificInfo(stepData) {
if (!stepData.data) return;
switch(stepData.stepKey) {
case 'LOCATION_PERMISSIONS':
const location = stepData.data;
if (location.latitude && location.longitude) {
addLog('info', `📍 Ubicación: ${location.latitude}, ${location.longitude}`);
}
break;
case 'DOCUMENT_EXTRACT':
case 'DOCUMENT_VERIFY':
if (stepData.data.face || stepData.data.document) {
addLog('info', '📷 Imagen del documento capturada');
}
if (stepData.data.extractedText) {
addLog('info', '📝 Texto extraído del documento');
}
break;
case 'IVERIFICATION':
if (stepData.data.bestFrame || stepData.data.selfie) {
addLog('info', '🤳 Selfie de verificación capturado');
}
break;
case 'OTO':
if (stepData.data.similarity) {
addLog('info', `👤 Similitud facial: ${stepData.data.similarity}%`);
}
break;
}
}
// 🔄 Obtener Short Key automáticamente del backend
async function fetchKYCFlowAndGetShortKey() {
try {
addLog('info', '🔄 Obteniendo shortKey del backend...');
const response = await fetch('/api/kyc/flow', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: "Demo KYC Embebido",
flow: "DEMO_FLOW",
countryDocument: "MEX",
flowType: "KYC"
})
});
const result = await response.json();
if (result.success && result.extractedShortKey) {
const shortKey = result.extractedShortKey;
document.getElementById('shortKey').value = shortKey;
addLog('success', `🔑 ShortKey obtenido: ${shortKey}`);
showNotification(`✅ ShortKey obtenido: ${shortKey}`, 'success');
return shortKey;
} else {
addLog('error', `❌ Error obteniendo shortKey: ${result.message}`);
showNotification(`❌ Error: ${result.message}`, 'error');
return null;
}
} catch (error) {
addLog('error', `❌ Error de conexión: ${error.message}`);
showNotification(`❌ Error de conexión: ${error.message}`, 'error');
return null;
}
}
// 📋 Cargar configuración desde el backend
async function loadConfigFromServer() {
try {
const response = await fetch('/api/config');
const config = await response.json();
if (config.shortKey) {
document.getElementById('shortKey').value = config.shortKey;
addLog('info', `📋 Config cargado: shortKey = ${config.shortKey}`);
}
if (config.steps && config.steps.length > 0) {
const stepKeys = config.steps.map(step => step.key);
setSelectedSteps(stepKeys);
addLog('info', `📋 Pasos cargados: ${stepKeys.join(', ')}`);
}
} catch (error) {
addLog('error', `⚠️ Error cargando config: ${error.message}`);
}
}
// 🎯 Obtener pasos seleccionados
function getSelectedSteps() {
const checkboxes = document.querySelectorAll('.step-card input[type="checkbox"]:checked');
return Array.from(checkboxes).map(cb => cb.value);
}
// ✅ Establecer pasos seleccionados
function setSelectedSteps(stepKeys) {
// Limpiar selecciones
document.querySelectorAll('.step-card input[type="checkbox"]').forEach(cb => {
cb.checked = false;
cb.closest('.step-card').classList.remove('selected');
});
// Seleccionar pasos especificados
stepKeys.forEach(stepKey => {
const checkbox = document.querySelector(`input[value="${stepKey}"]`);
if (checkbox) {
checkbox.checked = true;
checkbox.closest('.step-card').classList.add('selected');
}
});
}
// 📊 Actualizar progreso visual
function updateProgress(percentage = null) {
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
if (percentage !== null) {
progressFill.style.width = `${percentage}%`;
} else if (currentSteps.length > 0) {
const completed = completedSteps.length;
const total = currentSteps.length;
const percent = (completed / total) * 100;
progressFill.style.width = `${percent}%`;
}
if (completedSteps.length > 0) {
progressText.innerHTML = `
Progreso: ${completedSteps.length}/${currentSteps.length} pasos completados<br>
<small>Últimos: ${completedSteps.slice(-3).join(', ')}</small>
`;
} else {
progressText.textContent = 'Esperando inicio del flujo...';
}
}
// 📊 Actualizar visualización de datos
function updateDataDisplay() {
const dataContent = document.getElementById('dataContent');
const exportBtn = document.getElementById('exportDataBtn');
const copyBtn = document.getElementById('copyDataBtn');
if (Object.keys(kycResults).length === 0) {
dataContent.innerHTML = '<p class="no-data">No hay datos disponibles</p>';
exportBtn.disabled = true;
copyBtn.disabled = true;
} else {
dataContent.innerHTML = `<pre>${JSON.stringify(kycResults, null, 2)}</pre>`;
exportBtn.disabled = false;
copyBtn.disabled = false;
}
}
// 📝 Agregar entrada de log
function addLog(type, message) {
const logsContent = document.getElementById('logsContent');
const time = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.innerHTML = `
<span class="log-time">${time}</span>
<span class="log-message">${message}</span>
`;
logsContent.appendChild(logEntry);
logsContent.scrollTop = logsContent.scrollHeight;
}
// 🔔 Mostrar notificación
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideInRight 0.3s ease-out reverse';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 4000);
}
// 🚀 Crear pasos dinámicamente
function createStepsUI() {
const stepsContainer = document.getElementById('stepsContainer');
stepsContainer.innerHTML = '';
availableSteps.forEach(step => {
const stepCard = document.createElement('div');
stepCard.className = 'step-card';
stepCard.innerHTML = `
<input type="checkbox" value="${step.key}" id="step-${step.key}">
<label for="step-${step.key}">
<strong>${step.name}</strong><br>
<small>${step.description}</small>
</label>
`;
const checkbox = stepCard.querySelector('input[type="checkbox"]');
checkbox.addEventListener('change', function() {
if (this.checked) {
stepCard.classList.add('selected');
} else {
stepCard.classList.remove('selected');
}
});
stepsContainer.appendChild(stepCard);
});
}
// 🎮 Event Listeners
document.addEventListener('DOMContentLoaded', function() {
// Crear UI de pasos
createStepsUI();
// Botón para iniciar KYC
document.getElementById('startKycBtn').addEventListener('click', function() {
const shortKey = document.getElementById('shortKey').value.trim();
const selectedSteps = getSelectedSteps();
if (!shortKey) {
showNotification('❌ El Short Key es requerido', 'error');
return;
}
if (selectedSteps.length === 0) {
showNotification('❌ Selecciona al menos un paso del flujo KYC', 'error');
return;
}
const config = {
shortKey: shortKey,
steps: selectedSteps
};
initKYC(config);
});
// Botón para obtener Short Key
document.getElementById('getShortKeyBtn').addEventListener('click', async function() {
await fetchKYCFlowAndGetShortKey();
});
// Botones de configuración rápida
document.querySelectorAll('[data-config]').forEach(btn => {
btn.addEventListener('click', function() {
const configType = this.getAttribute('data-config');
const steps = presetConfigurations[configType];
if (steps) {
setSelectedSteps(steps);
showNotification(`✅ Configuración "${configType}" aplicada`, 'success');
addLog('info', `Configuración aplicada: ${configType} (${steps.length} pasos)`);
}
});
});
// Botón para limpiar datos
document.getElementById('clearDataBtn').addEventListener('click', function() {
kycResults = {};
completedSteps = [];
currentSteps = [];
updateDataDisplay();
updateProgress(0);
document.getElementById('kycIframe').src = 'about:blank';
addLog('info', '🗑️ Datos limpiados');
showNotification('✅ Datos limpiados', 'success');
});
// Botón para limpiar logs
document.getElementById('clearLogsBtn').addEventListener('click', function() {
const logsContent = document.getElementById('logsContent');
logsContent.innerHTML = '<div class="log-entry info"><span class="log-time">--:--:--</span><span class="log-message">Logs limpiados</span></div>';
});
// Botón para exportar datos
document.getElementById('exportDataBtn').addEventListener('click', function() {
if (Object.keys(kycResults).length === 0) {
showNotification('No hay datos para exportar', 'warning');
return;
}
const dataStr = JSON.stringify(kycResults, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `kyc-results-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showNotification('📤 Datos exportados exitosamente', 'success');
addLog('success', 'Datos exportados a archivo JSON');
});
// Botón para copiar datos
document.getElementById('copyDataBtn').addEventListener('click', function() {
if (Object.keys(kycResults).length === 0) {
showNotification('No hay datos para copiar', 'warning');
return;
}
const dataStr = JSON.stringify(kycResults, null, 2);
navigator.clipboard.writeText(dataStr).then(() => {
showNotification('📋 Datos copiados al portapapeles', 'success');
addLog('success', 'Datos copiados al portapapeles');
}).catch(err => {
console.error('Error al copiar:', err);
showNotification('❌ Error al copiar al portapapeles', 'error');
});
});
// Cargar configuración inicial
setTimeout(async () => {
await loadConfigFromServer();
// Si no hay shortKey, obtener uno automáticamente
const currentShortKey = document.getElementById('shortKey').value.trim();
if (!currentShortKey) {
await fetchKYCFlowAndGetShortKey();
}
addLog('success', '🎉 Sistema inicializado correctamente');
showNotification('🚀 Sistema KYC listo para usar', 'info');
}, 1000);
});
🧪 Pruebas y validación
🔍 Paso 1: Verificación de la Instalación
Ejecuta en una terminal, navega a la raíz de tu proyecto y ejecuta:
# 1. Navegar al directorio del backend
cd backend
# 2. Verificar que las dependencias están instaladas
npm list
# 3. Iniciar el servidor
npm start
# 4. Verificar en el navegador
# Ve a: http://localhost:3000
✅ Checklist de Funcionalidades
Backend (servidor en http://localhost:3000):
- ✅ Servidor Express funciona
- ✅ Headers de seguridad para cámara/micrófono configurados
- ✅ Endpoint
/api/config
GET/POST funcionan - ✅ Endpoint
/api/kyc/flow
obtiene shortKey de JAAK - ✅ Configuración persistente en config.json
- ✅ Variables de entorno cargadas correctamente
Frontend:
- ✅ Layout de dos columnas funciona
- ✅ ShortKey readonly con botón de copia
- ✅ Botón "Obtener Nuevo Short Key" funciona
- ✅ Selección de pasos KYC funciona
- ✅ Botón "Guardar Configuración" persiste cambios
- ✅ Iframe se carga con permisos de cámara
- ✅ Botones de iniciar y reiniciar funcionan
Integración:
- ✅ PostMessage entre iframe y página funciona
- ✅ Gestión de sesiones únicas sin conflictos
- ✅ Logs aparecen en tiempo real
- ✅ Progreso se actualiza al completar pasos
- ✅ Datos se capturan y muestran correctamente
- ✅ Exportación y copia de datos funciona
- ✅ Configuración se carga automáticamente al iniciar
🧪 Pruebas Paso a Paso
Test 1: Verificar Headers de Seguridad
# Verificar que los headers de permisos están configurados
curl -I http://localhost:3000
# Deberías ver:
# Permissions-Policy: camera=(*), microphone=(*), geolocation=(*)
# Content-Security-Policy: frame-src 'self' https://mosaic.sandbox.jaak.ai
Test 2: Obtener Short Key
// En la consola del navegador:
fetch('/api/kyc/flow', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: "Test"})
}).then(r => r.json()).then(console.log);
Test 3: Configuración Persistente
// Probar guardar configuración
fetch('/api/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({steps: ['DOCUMENT_EXTRACT', 'IVERIFICATION']})
}).then(r => r.json()).then(console.log);
// Luego verificar que se guardó
fetch('/api/config').then(r => r.json()).then(console.log);
Test 4: Flujo Completo con Nueva UI
- Ve a la página
http://localhost:3000
- Haz clic en "🔄 Obtener Nuevo Short Key"
- Verifica que aparece en el campo readonly
- Haz clic en el botón de copia (📋) para copiar el shortKey
- Selecciona algunos pasos KYC en la columna izquierda
- Haz clic en "💾 Guardar Cambios"
- Haz clic en "🚀 Iniciar Flujo KYC"
- Verifica que el iframe carga con permisos de cámara
- Completa al menos un paso del flujo
- Verifica logs en tiempo real y progreso
- Prueba el botón "🔄 Reiniciar"
Test 5: Persistencia de Configuración
- Configura algunos pasos y guarda con "💾 Guardar Cambios"
- Recarga la página
- Verifica que los pasos siguen seleccionados
- Verifica que el shortKey se mantiene
🚀 Despliegue
🐳 Docker (Recomendado)
Crea un Dockerfile
:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Comandos para deployment:
# Construir imagen
docker build -t kyc-embebido .
# Ejecutar contenedor
docker run -p 3000:3000 --env-file .env kyc-embebido
☁️ Deployment en la Nube
Heroku
# Instalar Heroku CLI
npm install -g heroku
# Login y crear app
heroku login
heroku create tu-app-kyc
# Configurar variables de entorno
heroku config:set JAAK_BEARER_TOKEN=tu_token_aqui
heroku config:set JAAK_API_URL=https://sandbox.api.jaak.ai/api/v1/kyc/flow
# Deploy
git push heroku main
Vercel
# Instalar Vercel CLI
npm install -g vercel
# Deploy
vercel
# Configurar variables de entorno en Vercel dashboard
Railway
# Conectar con GitHub y hacer push
# Railway detectará automáticamente Node.js
🔧 Solución de problemas
❌ Errores Comunes y Soluciones
1. "Permissions Policy Violation: camera is not allowed"
Problema: El iframe no tiene permisos para acceder a la cámara.
Solución:
// Verificar que el iframe tenga los atributos correctos en index.html
<iframe
id="kycIframe"
allow="camera; microphone; geolocation; fullscreen; web-share"
sandbox="allow-scripts allow-same-origin allow-forms allow-top-navigation allow-popups allow-popups-to-escape-sandbox"
></iframe>
// Y que el servidor tenga los headers en server.js
res.setHeader('Permissions-Policy', 'camera=(*), microphone=(*), geolocation=(*)');
2. "Configuración no se guarda" o persiste
Problema: El endpoint POST /api/config
no funciona.
Solución:
// Verificar en Network tab que la petición se envía correctamente
// Revisar logs del servidor:
console.log('💾 Configuración actualizada: X pasos');
// Verificar que config.json se crea en backend/
ls -la backend/config.json
3. "Iframe recibe configuración incorrecta"
Problema: Conflicto entre sesiones o closure capturing old config.
Solución:
// Verificar que se usa currentSessionConfig en lugar de parámetros antiguos
function sendKYCConfiguration() {
if (!currentSessionConfig || !currentConfigurationId) {
addLog('error', '❌ No hay configuración de sesión disponible');
return;
}
// ... usar currentSessionConfig
}
4. "Layout roto" o elementos no se muestran
Problema: CSS de dos columnas no funciona.
Solución:
/* Verificar que existan estas clases en main.css */
.control-panels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.session-panel {
max-width: 100%;
overflow-wrap: break-word;
}
5. "ShortKey readonly no se puede copiar"
Problema: Botón de copia no funciona.
Solución:
// Verificar que el event listener esté configurado
document.getElementById('copyShortKeyBtn').addEventListener('click', function() {
const shortKey = document.getElementById('shortKey').value;
if (shortKey) {
navigator.clipboard.writeText(shortKey).then(() => {
showNotification('📋 Short Key copiado al portapapeles', 'success');
});
}
});
6. "Multiple configuration sends" en logs
Problema: Se envían múltiples configuraciones al iframe.
Solución:
// Verificar que se use cleanup de event listeners
if (messageListener) {
window.removeEventListener('message', messageListener);
}
// Y que se genere un ID único por sesión
currentConfigurationId = generateUniqueId();
7. "Cannot read property of undefined" en session config
Problema: currentSessionConfig es null cuando se intenta acceder.
Solución:
// Siempre verificar antes de usar
if (!currentSessionConfig || !currentConfigurationId) {
addLog('error', '❌ No hay configuración de sesión disponible');
return;
}
🔍 Herramientas de Debug
1. Console del Navegador
// Verificar estado actual
console.log('KYC Results:', kycResults);
console.log('Current Steps:', currentSteps);
console.log('Completed Steps:', completedSteps);
2. Network Tab
- Verificar que las llamadas a
/api/config
y/api/kyc/flow
sean exitosas - Revisar headers y responses de las peticiones
3. Server Logs
# Ver logs del servidor en tiempo real
npm start
# Deberías ver:
# 🚀 Servidor KYC ejecutándose en http://localhost:3000
# 📤 Enviando a JAAK API: {...}
# ✅ Respuesta exitosa de JAAK API
📞 Soporte y Recursos
Verificar Instalación Completa
# Checklist final (desde el directorio raíz)
node --version # Debe mostrar v18.x.x o superior
npm --version # Debe mostrar 9.x.x o superior
ls -la # Debe mostrar las carpetas: backend/, frontend/, docs/
cat backend/.env # Debe mostrar las variables (⚠️ no compartir el token)
cd backend && npm list # Debe mostrar todas las dependencias instaladas
Logs de Diagnóstico
# Si nada funciona, ejecutar diagnóstico completo:
echo "=== Diagnóstico KYC Embebido ==="
echo "Node.js version: $(node --version)"
echo "NPM version: $(npm --version)"
echo "Directorio actual: $(pwd)"
echo "Estructura del proyecto:"
ls -la
echo "Archivos del backend:"
ls -la backend/
echo "Archivos del frontend:"
ls -la frontend/
echo "Contenido package.json:"
cat backend/package.json
echo "Estado del servidor:"
cd backend && npm start
🎉 ¡Felicidades!
Has implementado exitosamente un sistema KYC embebido completo que replica la funcionalidad del Demo Embebido de JAAK Mosaic.
🚀 Lo que has logrado:
- ✅ Backend Node.js con headers de seguridad y gestión de permisos
- ✅ Frontend responsivo con layout de dos columnas y navy blue theme
- ✅ Comunicación PostMessage robusta con gestión de sesiones
- ✅ Configuración persistente con endpoints API y archivos JSON
- ✅ UI moderna con shortKey readonly, botones de copia y controles separados
- ✅ Arquitectura modular con JSDoc documentation y event cleanup
- ✅ Sistema robusto con manejo de errores y validaciones
💡 Características Avanzadas Implementadas:
- 🔒 Headers de seguridad para permisos de cámara y micrófono
- 🆔 Gestión de sesiones únicas para evitar conflictos de configuración
- 📱 Layout responsivo adaptado a diferentes tamaños de pantalla
- 💾 Persistencia automática de configuración entre sesiones
- 🎨 Tema navy blue consistente en toda la aplicación
- 📋 Funcionalidad de copia para shortKeys y datos
- 🔄 Reinicio limpio sin recarga de página
📚 Recursos Adicionales
- Documentación JAAK: Referencia oficial
- Express.js Guide: Framework backend
- PostMessage API: Comunicación iframe
- Modern JavaScript: Conceptos avanzados
🤝 Comunidad y Soporte
- GitHub Issues: Para reportar bugs específicos
- Stack Overflow: Para preguntas técnicas generales
- Discord/Slack: Comunidades de desarrolladores
Updated about 1 hour ago