¿Alguna vez has necesitado que una extensión de Google Chrome interactúe directamente con el hardware de la máquina anfitriona, imprima en impresoras térmicas de tickets, lea puertos serie o extraiga telemetría en tiempo real del sistema operativo?
Si lo has intentado, probablemente te hayas topado con la jaula de cristal: el sandbox del navegador. Por motivos evidentes de seguridad, una extensión web no puede invocar libremente comandos del sistema ni acceder a la memoria de la máquina.
Para romper este aislamiento de forma controlada, Google provee la API Chrome Native Messaging. Sin embargo, cualquiera que haya intentado implementarla en un entorno de producción para Windows sabe que es un campo minado de errores:
- Un simple
console.logen el host nativo corrompe el socket binario y el navegador desconecta la extensión al instante sin dar pistas. - Obligar al usuario final a tener Node.js o Python preinstalado en su máquina arruina la adopción.
- Exigir al usuario que abra
regedit.exepara registrar claves en Windows es inviable para usuarios no técnicos. - En la era de la IA, pedirle a un LLM que construya este sistema sin una metodología estricta casi siempre termina en alucinaciones y código incompatible entre subsistemas.
En este artículo veremos cómo resolvimos este desafío de ingeniería aplicando Spec-Driven Development (SDD): diseñamos un sistema completo de Monitoreo de Hardware (CPU, RAM y Uptime) compuesto por una extensión en Manifest V3, un ejecutable nativo .exe autocontenido en Node.js y un instalador automatizado en Inno Setup 6, con cero dependencias externas y 100% de pruebas automatizadas.

Figura 1: Popup de la extensión System Monitor en Google Chrome mostrando telemetría de hardware en tiempo real (UI oscura con glassmorphism).
🏛️ 1. Spec-Driven Development (SDD): El Antídoto contra el Caos
Cuando desarrollas un proyecto con tecnologías heterogéneas (Frontend Web MV3 + Backend nativo PE x64 + Scripts de instalación en Pascal y claves del Registro de Windows), empezar tirando código es la receta garantizada para el fracaso.
En lugar de improvisar, adoptamos Spec-Driven Development (SDD). La regla de oro es simple:
"Ningún archivo de código fuente se crea ni se modifica si el cambio no está previamente especificado en un contrato formal."
La Constitución del Proyecto
Todo el proyecto está gobernado por una constitución innegociable (docs/constitution.md) que define 7 principios:
-
Protocolo Estricto de 4 Bytes: Toda comunicación por
stdin/stdoutdebe anteponer un prefijo little-endianUInt32LE. Prohibido emitir texto plano o logs astdout. -
Cero Dependencia de Node.js en el Cliente: El host debe compilarse como binario
.exex64 independiente. -
Instalación Transparente y Automatizada: Registro en Windows (
HKCU\Software\Google\Chrome\NativeMessagingHosts) gestionado 100% por el instalador. - Compatibilidad Estricta con Manifest V3: Operación exclusiva mediante Service Workers asíncronos sin background pages obsoletas.
- Aislamiento de Errores y Resiliencia: Captura de desconexiones y timeouts con degradación visual elegante sin bloquear el navegador.
- Desarrollo Guiado por Especificación (SDD): Trazabilidad documental de cada requisito mediante sintaxis EARS (Easy Approach to Requirements Syntax).
-
Arquitectura Hexagonal (Puertos y Adaptadores): El dominio puro debe estar aislado de
node:os,process.stdinochrome.runtime.
La Topología del Monorepo
Siguiendo las directrices constitucionales, el repositorio se organizó en 3 subsistemas independientes:
chrome-native-messaging-sdd/
├── host/ # SUBSISTEMA 1: Host Nativo en Node.js
│ ├── src/ # Código fuente (core, ports, adapters)
│ ├── scripts/set-metadata.js # Inyector PE que preserva el overlay de pkg
│ ├── dist/ # Binario compilado: system_monitor_host.exe
│ └── tests/ # Pruebas unitarias, integración y benchmarks
│
├── extension/ # SUBSISTEMA 2: Extensión Chrome MV3
│ ├── manifest.json # Manifiesto V3 con clave RSA determinista
│ ├── background.js # Service Worker (Composition Root)
│ ├── core/ # Formateadores matemáticos y máquina de estados
│ ├── popup/ # Interfaz visual oscura (HTML/CSS/JS)
│ └── tests/ # Suite de pruebas con simulación JSDOM
│
├── installer/ # SUBSISTEMA 3: Instalador de Windows
│ ├── setup.iss # Script de Inno Setup 6 (Modo Usuario)
│ ├── manifest-template.json # Plantilla JSON del host
│ └── output/ # Artefacto final: Setup_SystemMonitor.exe
│
├── specs/ # ESPECIFICACIONES SDD FORMALES
│ ├── 001-native-protocol-host/ # Fase 1: Protocolo y Host
│ ├── 002-chrome-extension-ui/ # Fase 2: Extensión y UI
│ └── 003-windows-installer-pkg/ # Fase 3: Empaquetado e Instalador
└── docs/constitution.md # Constitución y principios innegociables
🔌 2. El Protocolo Binario: 4 Bytes UInt32LE y la Trampa de stdout
Chromium se comunica con los ejecutables nativos mediante tuberías estándar (process.stdin y process.stdout). La especificación oficial exige que cada mensaje tenga un formato exacto:
-
Prefijo de Longitud (4 bytes): Un entero sin signo de 32 bits en formato little-endian (
UInt32LE) con la longitud $L$ del mensaje en bytes. - Payload JSON ($L$ bytes): El string JSON serializado en UTF-8 (tamaño máximo: 1 MB).
Estructura Binaria del Stream:
[ Byte 0 ] [ Byte 1 ] [ Byte 2 ] [ Byte 3 ] [ Byte 4 ... Byte 4 + L - 1 ]
└─────────────── UInt32LE (L) ──────────────┘└────── Payload JSON (UTF-8) ──────┘
La Trampa Mortal: ¿Por qué console.log Rompe Todo?
En Node.js, la primera reacción de cualquier desarrollador para depurar es escribir console.log("Mensaje recibido").
En Chrome Native Messaging, hacer esto es fatal: console.log emite texto plano por process.stdout sin la cabecera de 4 bytes. Chromium lee los primeros 4 caracteres ASCII como si fueran un entero binario (por ejemplo, el texto "Hola" equivale al número 0x616C6F48 = 1.634.553.672 bytes). Como el tamaño supera el límite de 1 MB o agota el stream, Chrome destruye el socket y cierra la extensión inmediatamente.
Nuestra Solución en SDD:
- Todo el diagnóstico del sistema se redirige estrictamente a
process.stderr, el cual Chrome captura en sus logs internos sin tocar el socket de datos. - La salida por
stdoutse gestiona a través de un adaptador exclusivo:
// host/src/adapters/outbound/binary-stream-writer.js
export class BinaryStreamWriter {
constructor(writableStream = process.stdout) {
this.stream = writableStream;
}
write(payload) {
const jsonString = JSON.stringify(payload);
const payloadBuffer = Buffer.from(jsonString, 'utf8');
const lengthBuffer = Buffer.alloc(4);
lengthBuffer.writeUInt32LE(payloadBuffer.length, 0);
// Emisión atómica de cabecera + payload
this.stream.write(Buffer.concat([lengthBuffer, payloadBuffer]));
}
}
Tolerancia a Fragmentación de Chunks
En Windows, las tuberías de consola pueden entregar los datos fragmentados (por ejemplo, 2 bytes de longitud en un paquete y el resto en el siguiente). Nuestro lector acumula los fragmentos en memoria hasta completar exactamente la longitud esperada antes de intentar deserializar el JSON:
> npm --prefix host test
# Subtest: Inbound Adapter: BinaryStreamReader
ok 1 - debe deserializar un mensaje completo en un único chunk
ok 2 - debe tolerar fragmentación en múltiples chunks contiguos
ok 3 - debe procesar múltiples mensajes consecutivos en un mismo buffer
ok 4 - debe emitir error si la longitud supera el límite de 1 MB
ok 5 - debe emitir error ante JSON corrupto o malformado
⚡ 3. La Extensión Chrome MV3: Determinismo y Sondeo Secuencial
En Google Chrome Manifest V3, las extensiones ya no tienen páginas de fondo (background pages) persistentes; ahora usan Service Workers efímeros que se suspenden cuando no hay actividad.
El Desafío del Extension ID
Para que el host nativo acepte comunicarse con una extensión, debe declararla explícitamente en el arreglo "allowed_origins" de su manifiesto JSON:
"allowed_origins": [
"chrome-extension://nknhjeknlgdehgjeoddeiibmpoicbgdc/"
]
Normalmente, al cargar una extensión descomprimida en desarrollo, Chrome calcula un ID dinámico basado en la ruta absoluta de la carpeta en disco. Si mueves el proyecto de carpeta, el ID cambia y la conexión falla.
¿Cómo lo resolvimos?
Generamos un par de claves RSA e inyectamos la clave pública en el campo "key" de extension/manifest.json:
{
"manifest_version": 3,
"name": "System Monitor",
"version": "1.0.0",
"permissions": ["nativeMessaging"],
"background": {
"service_worker": "background.js",
"type": "module"
},
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv4RuKyfoD6m..."
}
Gracias a esto, Chrome genera siempre el mismo ID determinista e inmutable:

Figura 2: La extensión cargada en chrome://extensions/ con su ID determinista garantizado por la clave pública RSA.
Sondeo Secuencial Anti-Solapamiento
En aplicaciones de monitoreo es común caer en la tentación de usar setInterval(fetchMetrics, 1000). Si una llamada tarda 1.2 segundos, las peticiones comienzan a encolarse en vuelo, saturando la tubería de Windows.
En su lugar, nuestro Service Worker implementa un sondeo secuencial con control de ciclo de vida:
- Envía
{ "action": "GET_METRICS" }a través del puertochrome.runtime.connectNative. - Espera la respuesta (con un timeout de seguridad de 5000 ms).
- Tras procesar la respuesta, programa un reposo de 1000 ms mediante
setTimeout. - Si el usuario cierra el popup, el Service Worker llama a
port.disconnect()y Windows termina el proceso del host nativo de inmediato, liberando los recursos de la máquina.
📦 4. De Script a Binario Independiente (.exe) y el Secreto del Overlay
El Principio 2 de nuestra constitución prohíbe exigir Node.js en la máquina del usuario. Para compilar el código de Node.js a un único binario x64 utilizamos @yao-pkg/pkg:
"scripts": {
"build:exe": "pkg . --targets node18-win-x64 --output dist/system_monitor_host.exe"
}
El "Gotcha" de Ingeniería: La Corrupción del Overlay PE
Cuando compilas un script con pkg, este empaqueta el runtime de Node.js junto con un archivo virtual que contiene tu código JS anexado al final del ejecutable (lo que en la estructura PE de Windows se conoce como Overlay).
Al intentar inyectar el icono de la aplicación (icon.ico) y los metadatos de versión utilizando la herramienta estándar rcedit:
-
rceditexpande la sección.rsrcde la cabecera PE. - Esto desplaza físicamente los bytes del overlay hacia adelante.
- Al arrancar el
.exe, el bootstrap de Node.js busca su código en las posiciones originales y arroja:Cannot find entry point / Invalid payload offset.
Para solucionarlo, construimos un script automatizado (host/scripts/set-metadata.js) que opera quirúrgicamente sobre el binario:
flowchart LR
A["system_monitor_host.exe"] --> B["1. Extraer Overlay (Payload JS)"]
B --> C["2. Ejecutar rcedit (Inyectar Icono en .rsrc)"]
C --> D["3. Re-anexar Overlay al final del binario"]
D --> E["4. Recalcular y parchar PAYLOAD_POSITION y PRELUDE_POSITION"]
El resultado es un ejecutable nativo de ~40 MB, con icono personalizado de Windows y arranque instantáneo.
🛠️ 5. Instalador Inno Setup: Cero Clics en el Registro y UTF-8 sin BOM
El último eslabón de la suite es la experiencia de instalación. Nadie quiere obligar a sus usuarios a abrir regedit.exe para registrar un archivo JSON.
Creamos un instalador con Inno Setup 6 (installer/setup.iss) configurado bajo premisas estrictas:
Despliegue en Modo Usuario (Sin UAC)
Configuramos PrivilegesRequired=lowest y desplegamos en {localappdata}\Programs\SystemMonitorHost. Cualquier usuario estándar puede instalar el software sin requerir permisos de Administrador ni ver advertencias de UAC.
Registro Automatizado en Windows
El instalador crea la clave de integración en la colmena del usuario actual:
[Registry]
Root: HKCU; Subkey: "Software\Google\Chrome\NativeMessagingHosts\com.tuempresa.systemmonitor"; \
ValueType: string; ValueData: "{app}\com.tuempresa.systemmonitor.json"; Flags: uninsdeletekey
El Detalle Crítico: UTF-8 sin BOM
Chromium exige que el archivo de manifiesto JSON esté codificado en UTF-8 estricto sin BOM (Byte Order Mark). Si se guarda con los 3 bytes iniciales de BOM (\xEF\xBB\xBF), el parser C++ de Chrome falla en silencio y arroja Specified native messaging host not found.
En el script Pascal del instalador forzamos la codificación limpia:
SaveStringsToUTF8FileWithoutBOM(ManifestPath, Lines, False);
Terminación Atómica de Procesos
Si la extensión está abierta mientras el instalador se ejecuta, Windows bloquea el archivo .exe con un handle abierto. Las rutinas PrepareToInstall y CurUninstallStepChanged ejecutan de fondo taskkill /F /IM system_monitor_host.exe antes de reemplazar archivos o desinstalar, garantizando una instalación y remoción 100% limpia.
🧪 6. Resultados y Verificación de Calidad
Gracias a la metodología SDD, cada requisito funcional especificado cuenta con su correspondiente prueba automatizada:
> npm test
✔ Host Nativo (26 tests)
- Deserialización de stream binario y tolerancia a fragmentación
- Enrutamiento de comandos y envelopes estructurados
- Rendimiento y latencia de respuesta (<50 ms)
✔ Extensión Chrome MV3 (61 tests)
- Puerto de mensajería y ciclo de vida del Service Worker
- Formateadores matemáticos de RAM (GB binarios 1024^3) y Uptime
- Máquina de estados finita (LOADING, DASHBOARD, ERROR)
✔ Binario Independiente Windows (3 tests)
- Validación de cabeceras PE x64
- Ejecución aislada sin Node.js en variables de entorno
- Respuesta a GET_METRICS vía stdio directo
----------------------------------------------------------------------
Total: 90 tests pasando al 100%
Además, el script de PowerShell tests/installer/verify-install.ps1 ejecuta un ciclo completo desatendido:
- Instala el host en modo silencioso (
/VERYSILENT). - Valida que el archivo JSON esté en UTF-8 sin BOM y con rutas dobles (
\\). - Consulta el registro de Windows con
Get-ItemPropertyverificando la subclave enHKCU. - Desinstala la aplicación comprobando que no quede ningún rastro en el sistema.
🚀 Conclusiones y Código Fuente
Conectar la web con el sistema operativo anfitrión a través de Chrome Native Messaging es una herramienta con un potencial inmenso para arquitecturas híbridas, aplicaciones empresariales y herramientas de desarrollo.
La diferencia entre un prototipo frágil que se rompe con cualquier log y una solución de grado de producción radica en la disciplina metodológica:
- Spec-Driven Development (SDD) proporcionó el marco para planificar contratos binarios antes de escribir código.
- La Arquitectura Hexagonal aisló la lógica del DOM y del sistema operativo, permitiendo probar el 100% de la lógica sin mocks complejos.
- La automatización de empaquetado e instalación convirtió un conjunto de scripts en un producto listo para el usuario final.
Todo el código fuente, especificaciones EARS y scripts de compilación están disponibles de forma abierta:
👉 Repositorio en GitHub: https://github.com/dhbernardo/chrome-native-messaging-sdd
¿Has trabajado antes con Chrome Native Messaging o aplicado Spec-Driven Development en tus proyectos? ¡Cuéntame tus experiencias y dudas en los comentarios!
Top comments (0)