Spec-Driven Development (SDD) en Acción: Conectando una Extensión de Chrome (MV3) con Windows usando Node.js

작성자

카테고리:

← 피드로
DEV Community · David Bernardo · 2026-09-20 개발(SW)

¿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.log en 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.exe para 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.

System Monitor Extension Popup en Tiempo Real
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:

  1. Protocolo Estricto de 4 Bytes: Toda comunicación por stdin/stdout debe anteponer un prefijo little-endian UInt32LE. Prohibido emitir texto plano o logs a stdout.
  2. Cero Dependencia de Node.js en el Cliente: El host debe compilarse como binario .exe x64 independiente.
  3. Instalación Transparente y Automatizada: Registro en Windows (HKCU\Software\Google\Chrome\NativeMessagingHosts) gestionado 100% por el instalador.
  4. Compatibilidad Estricta con Manifest V3: Operación exclusiva mediante Service Workers asíncronos sin background pages obsoletas.
  5. Aislamiento de Errores y Resiliencia: Captura de desconexiones y timeouts con degradación visual elegante sin bloquear el navegador.
  6. Desarrollo Guiado por Especificación (SDD): Trazabilidad documental de cada requisito mediante sintaxis EARS (Easy Approach to Requirements Syntax).
  7. Arquitectura Hexagonal (Puertos y Adaptadores): El dominio puro debe estar aislado de node:os, process.stdin o chrome.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

Enter fullscreen mode Exit fullscreen mode

🔌 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:

  1. 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.
  2. 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) ──────┘

Enter fullscreen mode Exit fullscreen mode

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 stdout se 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]));
  }
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

⚡ 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/"
]

Enter fullscreen mode Exit fullscreen mode

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..."
}

Enter fullscreen mode Exit fullscreen mode

Gracias a esto, Chrome genera siempre el mismo ID determinista e inmutable:

Tarjeta de la Extensión System Monitor en Chrome
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:

  1. Envía { "action": "GET_METRICS" } a través del puerto chrome.runtime.connectNative.
  2. Espera la respuesta (con un timeout de seguridad de 5000 ms).
  3. Tras procesar la respuesta, programa un reposo de 1000 ms mediante setTimeout.
  4. 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"
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. rcedit expande la sección .rsrc de la cabecera PE.
  2. Esto desplaza físicamente los bytes del overlay hacia adelante.
  3. 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

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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%

Enter fullscreen mode Exit fullscreen mode

Además, el script de PowerShell tests/installer/verify-install.ps1 ejecuta un ciclo completo desatendido:

  1. Instala el host en modo silencioso (/VERYSILENT).
  2. Valida que el archivo JSON esté en UTF-8 sin BOM y con rutas dobles (\\).
  3. Consulta el registro de Windows con Get-ItemProperty verificando la subclave en HKCU.
  4. 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!

원문에서 계속 ↗