npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tuain/form-exchange

v3.1.0

Published

Motor de intercambio de formularios abstractos de la plataforma Tuain: transforma la definición y los datos de un formulario entre el frontend y el backend según las acciones ejecutadas en cada lado. Software propietario.

Readme

@tuain/form-exchange

Motor de intercambio de formularios abstractos de la plataforma Tuain.

SOFTWARE PROPIETARIO. Copyright (c) 2022-2026 Imix Latam / TUAIN. Todos los derechos reservados. Este paquete no es software libre ni de código abierto. Su uso está sujeto a la licencia del archivo LICENSE: licencia de uso no exclusiva, no transferible y revocable, sin derecho a modificar, redistribuir, sublicenciar, descompilar ni aplicar ingeniería inversa.

El acceso público en el registro de npm no otorga licencia de uso. Que npm install funcione sin token no convierte a este paquete en permisivo: el derecho de uso proviene únicamente del contrato escrito con el Titular. Si llegaste aquí sin ese contrato, no estás autorizado a usarlo.

Qué es

Transforma la definición y los datos de un formulario abstracto entre el frontend y el backend, aplicando en cada lado las transformaciones que corresponden a la acción ejecutada. Se compone de dos piezas:

  • FormManager — administra el catálogo de definiciones de formularios, calcula sus firmas y despacha cada acción a la clase de gestión correspondiente.
  • Form — clase base que el servicio consumidor extiende por cada formulario, registrando el comportamiento con onAction(), onFieldValidation(), onTablePopulate() y onTableAction().

El paquete se distribuye como un único bundle CommonJS minificado (dist/index.js) con sus definiciones de tipos (dist/index.d.ts). El código fuente no se publica.

Instalación

El paquete tiene acceso público, así que no se necesita token de lectura ni .npmrc:

npm install @tuain/form-exchange
const { FormManager, Form } = require('@tuain/form-exchange');

Requiere Node 18 o superior.

Uso

El catálogo: FormManager

const { FormManager } = require('@tuain/form-exchange');

const formManager = new FormManager(
  rootPath, // raíz desde la que se resuelven las clases de formulario
  formsDefinitions, // catálogo de definiciones, indexado por formCode
  cacheManager, // reservado
  appLogger, // logger con método log({ level, label, action, message })
  errMgr, // gestor de errores con addModuleSet() y get()
  formClasses, // opcional: mapa formCode → clase, en lugar de carga por ruta
  debugMode, // opcional: si true, el detalle del error expone mensaje y stack
);

const [error, definition] = await formManager.getFormDefinition(requestContext, 'miFormulario');
const [errorAccion, respuesta] = await formManager.executeFormAction(requestContext, 'miFormulario', requestData);

Ambos métodos devuelven la tupla [error, resultado]: error es null cuando todo salió bien.

Las clases de formulario se resuelven de dos maneras, y conviene elegir una:

  • formClasses — un mapa { formCode: ClaseDelFormulario }. Explícito y compatible con bundlers.
  • Por ruta — si formClasses es null, se cargan con require desde path.resolve(rootPath, formsDefinitions.formClassDirectory, <archivo>), tomando el nombre del archivo de formsDefinitions.formMapping[formCode].class o de formsDefinitions[formCode].classFile.

El comportamiento: extender Form

const { Form } = require('@tuain/form-exchange');

class MiFormulario extends Form {
  constructor(logger, errMgr, requestContext, formDefinition, requestData) {
    super(logger, errMgr, requestContext, formDefinition, requestData);
    this.onAction('guardar', () => this.guardar());
    this.onFieldValidation('documento', (code, value) => this.validarDocumento(code, value));
    this.onTablePopulate('resultados', (tableCode) => this.poblarResultados(tableCode));
  }

  async startOperation() {
    this.showField('documento');
    this.demandField('documento');
  }

  async guardar() {
    const valores = this.getFieldsValues(['documento', 'nombre']);
    // ... lógica del servicio
    return null; // null = sin error
  }
}

module.exports = MiFormulario;

Un manejador devuelve null si todo salió bien, o un objeto de error construido con errMgr.get().

Las acciones start, startOperation, GETDATA, VALIDATE, TABLEACTION y GETTABLEDATA ya vienen registradas por la clase base: se personalizan sobrescribiendo startOperation() y getData(), no volviendo a registrarlas.

Datos inmutables: claros y oscuros

Un dato inmutable es estado que el servidor le confía al cliente para que se lo devuelva. Hay dos clases, y la diferencia es qué puede ver el frontend.

Claros (setImmutableElement / getImmutableElement). Viajan como { value, signature }. El frontend los lee con getImmutableElement(nombre). Sirven para lo que el cliente necesita mostrar y el servidor necesita recuperar sin volver a consultarlo.

this.setImmutableElement('cuenta', { numero: '123', titular: 'Ana' });
// en la petición siguiente
const cuenta = this.getImmutableElement('cuenta'); // null si fue alterado

Oscuros (setDarkImmutableElement / getDarkImmutableElement). Viajan como una sola cadena:

<base64url(JSON del valor)>.<HMAC-SHA256 del base64url>

El frontend la recibe, la guarda y la devuelve tal cual; la librería del cliente no expone ningún camino para leer su contenido. Sirven para lo que el cliente no debe ver: identificadores internos, montos de referencia, cualquier cosa cuyo valor no sea asunto del navegador.

this.setDarkImmutableElement('tarifa', { valor: 1500, origen: 'tabla-2024' });
// en la petición siguiente
const tarifa = this.getDarkImmutableElement('tarifa'); // null si no existe

El sello es HMAC con la clave del servidor, no un hash simple: un hash sin clave lo recalcularía cualquiera que edite el contenido, y el mecanismo no detectaría nada.

Un bloque oscuro alterado rechaza la petición completa. Se verifica al importar, no al leerse: la acción no se ejecuta, ningún manejador del formulario corre, y se devuelve el error 1006 (immutableTampered, HTTP 400). Es distinto de los claros, que devuelven null al leerse y dejan que el formulario decida. Un oscuro alterado no es un dato dudoso: es evidencia de manipulación.

El sello no depende del orden de las claves

Desde la 3.1.0 el texto que el HMAC cubre es canónico: las claves de cada objeto se ordenan antes de serializar, recursivamente. Los arreglos no se tocan —ahí la posición es dato—.

La razón es que el inmutable viaja por el cliente y vuelve, y en el camino hay capas que lo reconstruyen sin alterarlo: un puente nativo que parsea la respuesta a un diccionario del sistema y la vuelve a serializar, una caché que lo rehidrata, un proxy que lo normaliza. El caso que lo obligó fue un cliente Capacitor con CapacitorHttp activo: en iOS la capa nativa hace JSONSerialization sobre la respuesta —un diccionario que no tiene orden— y el JavaScript recibe una re-serialización de ese diccionario. El cliente devolvía fielmente lo recibido, con las claves en otro orden, y todos los inmutables de tipo objeto se rechazaban en cada ida y vuelta, como si los hubieran manipulado.

Lo que no cambia: un valor alterado, una clave de más y el orden de un arreglo siguen invalidando el sello. Y para un valor escalar el texto es idéntico al de antes, así que solo rotan los sellos de los valores de tipo objeto; los bloques oscuros no rotan ninguno, porque lo que sellan es una cadena base64 —y por eso mismo un inmutable oscuro es inmune a este problema y es la salida rápida cuando un cliente concreto reconstruye el JSON—.

Los sellos que el cliente ya tenía emitidos se siguen aceptando mientras drenan, y el veredicto lo reporta en legacyOrderAccepted para que se pueda medir cuándo dejaron de llegar. Esa tolerancia se apaga con TUAIN_FORM_ACCEPT_LEGACY_CONTENT_ORDER=false; no es una concesión de seguridad —se verifica con la misma clave y sobre el mismo dato— así que apagarla es higiene, no urgencia.

Campos inmutables

Un campo se puede marcar para que su valor lo fije solo el servidor:

this.setFieldValue('monto', montoCalculado);
this.markFieldImmutable('monto');

Efecto:

  • el campo llega a la respuesta —equivale además a un touchField— con immutable: true, y la librería del frontend impide modificarlo (avisa por consola, no lanza);
  • su valor se sella en el bloque oscuro reservado _fields, y al volver sobrescribe lo que el cliente haya enviado en formData.fields.

Las dos protecciones son independientes a propósito: la del frontend evita el cambio accidental, y el sello hace que el cambio deliberado no sirva de nada. Marcar un campo no lo deshabilita ni lo oculta: inmutable y editable son banderas distintas, y hay campos inmutables que igual deben verse activos. Para deshabilitarlo, disableField aparte.

El bloque se arma al exportar, así que el orden no importa: markFieldImmutable antes o después de setFieldValue da lo mismo, y lo que queda sellado es el último valor que puso el servidor. La marca sobrevive las idas y vueltas sola: no hay que volver a declararla en cada petición. Para liberar el campo, unmarkFieldImmutable, que lo devuelve con immutable: false para que el frontend lo desbloquee.

El servidor sí puede cambiar el valor de un campo inmutable en cualquier momento: la inmutabilidad rige frente al cliente.

this.isFieldImmutable('monto');      // true
this.getImmutableFieldNames();       // ['monto']
this.markFieldsImmutable(['a', 'b']);

_fields es un nombre reservado: setDarkImmutableElement('_fields', …) se rechaza y se registra.

Versiones pareadas

Los oscuros exigen @tuain/ng-forms ≥ 22.2.0 en el frontend. Un frontend anterior no reenvía darkImmutables, y como el backend no tiene estado no puede notar la ausencia: el bloque se pierde en cada vuelta y los campos inmutables se quedan sin su valor confiable, en silencio. El protocolo de intercambio sigue siendo TUAINEXCHANGE_1.0: el cambio es aditivo y un backend anterior ignora el atributo nuevo sin romperse.

La prueba de integridad de los registros de tabla exige ≥ 22.3.0. Con un frontend anterior la prueba no vuelve, y con recordProofPolicy: 'enforce' toda acción sobre un registro se rechaza; en 'warn', que es como se publica, opera igual y solo se registra.

Rastro del recorrido: qué se envió, qué volvió, qué leyó el formulario

Un inmutable que no llega, que vuelve distinto o que el formulario lee como null se manifiesta mucho más arriba como un campo vacío. Desde la 3.0.5 las tres estaciones del viaje quedan en el log del servicio bajo la etiqueta immutableTrace, con una action por estación —enviados, recibidos, entregado, rechazado—, de modo que un grep por el nombre del elemento reconstruye el viaje sin tocar el código del consumidor.

Lo gobierna la política immutableTrace, por variable de entorno o por inyección:

TUAIN_FORM_IMMUTABLE_TRACE=summary   # off (publicado) | summary | full
new FormManager(root, defs, cache, logger, errMgr, clases, false, {
  securityPolicies: { immutableTrace: 'summary' },
});
  • off — nada del recorrido. Es como se publica.
  • summary — inventario por estación y, por elemento, tipo, tamaño, huella, prefijo del sello y si el sello recibido coincide con el esperado. Sin contenido. La huella es un hash corto del contenido: el mismo inmutable la tiene igual al salir y al volver, así que comparar dos líneas responde «¿volvió igual?» sin que el dato aparezca en ninguna parte.
  • full — además, el contenido serializado, el sello completo (selloGenerado a la salida, selloRecibido y selloEsperado a la entrada) y, para los valores de tipo objeto, el contenidoFirmado: el texto canónico exacto que el HMAC cubre. Es lo que resuelve el caso clásico —un objeto que se reconstruye con las claves en otro orden y deja de verificar sin que nadie lo haya tocado—, porque dos hashes distintos no dicen dónde está la diferencia y dos textos sí.

⚠️ full es para reproducir un problema, no para dejarlo encendido. Vuelca datos de negocio en el log, y el selloEsperado de un contenido que el cliente acaba de enviar es un sello válido para ese contenido: quien lea el log puede forjar. FormManager lo advierte en warn al arrancar.

Los rechazos no dependen de esta política. Un sello que no coincide, un inmutable que llega sin firma o un bloque mal formado se registran siempre, en error, incluso con el rastro en off: son evidencia de manipulación, no diagnóstico opcional. La línea dice qué se rechazó, por qué y qué consecuencia tuvo, pero no vuelca el contenido rechazado —es contenido que el servidor no avala—: deja su huella, que basta para reconocer el mismo intento repitiéndose.

Prueba de integridad de los registros de tabla

El registro sobre el que opera una acción de tabla viaja por el cliente. Sin prueba, nada lo ata al que el servidor envió: un cliente puede cambiarle el saldo y el callback de negocio opera sobre el dato falseado.

setTableRecords y fillTable emiten en cada fila un recordProof HMAC, y en la tabla la lista sealedColumns de las columnas que cubre. El cliente devuelve las dos cosas y el servidor recalcula.

Se verifica un subconjunto, no el registro completo, y eso es deliberado: el recordData que vuelve no es el que se envió ni siquiera sin manipulación —las aplicaciones le agregan contexto, le borran claves del widget y alternan la columna de selección—, así que exigir igualdad exacta rechazaría flujos vivos.

// El callback recibe el objeto del cliente, adiciones incluidas.
this.onTableAction('creditos', 'aprobar', (tableCode, actionCode, recordId, recordData) => {
  // Esto es entrada del cliente, sin garantía:
  const filtroDeLaPantalla = recordData.filtroAplicado;
  // Esto es lo que la prueba avala; null si no está cubierto o si la prueba no cuadró:
  const saldo = this.getTrustedRecordValue('saldo');
  if (!this.tableRecordTrusted) {
    return this.errMgr.get(/* … */);
  }
});

La política se publica en 'warn': registra y no rechaza, así que todavía no protege. Ver el CHANGELOG de la 2.2.0, sección "Cómo activar la protección", y también "Lo que la prueba no resuelve" —replay de un registro viejo, y que la prueba no autoriza: el callback sigue teniendo que verificar pertenencia—.

Configuración requerida: clave de firma

Los datos inmutables —claros y oscuros— y el sello de los campos inmutables usan todos la misma firma HMAC. Esa firma solo sirve si la clave es un secreto del servidor.

La clave se resuelve en este orden:

  1. requestContext.signatureKey
  2. requestContext.deviceKey
  3. La variable de entorno TUAIN_FORM_SIGNATURE_KEY
  4. Una clave heredada que viaja en el paquete publicado y debe considerarse comprometida

Si tu requestContext no puebla las dos primeras —lo habitual en las rutas web—, exporta la variable:

TUAIN_FORM_SIGNATURE_KEY=<secreto propio, largo y aleatorio>

Debe ser el mismo valor en todas las instancias del servicio: el dato inmutable viaja por el cliente y se verifica en cualquiera de ellas. Trátala como secreto de runtime, no la hornees en la imagen.

Mientras no la configures, la librería firma con la clave heredada y registra un error de nivel error una vez por proceso, con el texto de remediación. Al configurarla, los datos nuevos quedan protegidos de inmediato y los que el cliente traiga firmados con la clave anterior se siguen aceptando durante la transición, de modo que desplegar no rompe los formularios en vuelo.

Licencia de ejecución

Desde la 2.0.0 el paquete incluye el mecanismo de verificación de licencia de ejecución, inerte en esta versión: no verifica nada y no cambia el comportamiento de ningún consumidor. Cuando el Titular lo active, FormManager exigirá una licencia firmada, que el consumidor publica en la variable de entorno TUAIN_FORM_LICENSE:

TUAIN_FORM_LICENSE=<token entregado por el Titular>

También se puede pasar por opciones al construir el manager, para casos donde el secreto no viaja por entorno:

new FormManager(rootPath, defs, cache, logger, errMgr, formClasses, false, { license: token });

La licencia se entrega como secreto de runtime, no horneada en la imagen de contenedor: dentro de la imagen queda atada a su ciclo de vida y renovarla exigiría reconstruir y redesplegar.

Migración desde tuain-form-manager

El paquete cambia de nombre, el require cambia de cadena y ahora se publican tipos donde antes no había ninguno. El comportamiento en runtime no cambia. El documento de migración lo entrega el Titular junto con la versión.

Documentación adicional

  • docs/CONTRATO-CABLE.md — los vectores golden que congelan el contrato de cable con el gateway (firma de peticiones, cookie de sesión y sello por sesión), paso previo a migrar ese kernel a la librería.

Soporte

Titular: Imix Latam / TUAIN — [email protected]