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:

  1. Inicia sesión en JAAK Platform

  2. Ve a Menú Lateral → Ajustes → API Keys

  3. Haz clic en "Generar nueva API key"

  4. 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

  5. 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

  1. Ve a la página http://localhost:3000
  2. Haz clic en "🔄 Obtener Nuevo Short Key"
  3. Verifica que aparece en el campo readonly
  4. Haz clic en el botón de copia (📋) para copiar el shortKey
  5. Selecciona algunos pasos KYC en la columna izquierda
  6. Haz clic en "💾 Guardar Cambios"
  7. Haz clic en "🚀 Iniciar Flujo KYC"
  8. Verifica que el iframe carga con permisos de cámara
  9. Completa al menos un paso del flujo
  10. Verifica logs en tiempo real y progreso
  11. Prueba el botón "🔄 Reiniciar"

Test 5: Persistencia de Configuración

  1. Configura algunos pasos y guarda con "💾 Guardar Cambios"
  2. Recarga la página
  3. Verifica que los pasos siguen seleccionados
  4. 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:

  1. Backend Node.js con headers de seguridad y gestión de permisos
  2. Frontend responsivo con layout de dos columnas y navy blue theme
  3. Comunicación PostMessage robusta con gestión de sesiones
  4. Configuración persistente con endpoints API y archivos JSON
  5. UI moderna con shortKey readonly, botones de copia y controles separados
  6. Arquitectura modular con JSDoc documentation y event cleanup
  7. 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

🤝 Comunidad y Soporte

  • GitHub Issues: Para reportar bugs específicos
  • Stack Overflow: Para preguntas técnicas generales
  • Discord/Slack: Comunidades de desarrolladores