@hostwebhook/platform-contracts
v0.18.0
Published
Contratos compartidos entre los servicios de HostWebhook: addons del plan, identidad interna, y las formas que cruzan una frontera de red
Maintainers
Readme
@hostwebhook/platform-contracts
Contratos compartidos entre los servicios de HostWebhook: lo que cruza una frontera y no puede estar escrito dos veces.
Cero dependencias de runtime, a propósito — el dashboard lo carga en el navegador.
Addons
Los complementos que se venden aparte del plan. La api decide si una petición pasa; el dashboard decide si pinta la pantalla o nada en absoluto. Los dos leen de aquí.
import { ADDONS, tieneAddon, normalizarAddons } from '@hostwebhook/platform-contracts';
tieneAddon(user.plan.addons, 'social'); // → boolean, falla CERRADO
normalizarAddons(['social', 'social']); // → ['social']⚠️ tieneAddon falla cerrado: ante una entrada que no entiende, dice que
no. Es lo contrario que llevaValor de abajo, y la diferencia es deliberada —
lo seguro al conceder acceso es negar; lo seguro al pintar un formulario es
enseñar el campo.
Operadores
Las dos listas de operadores, con los tipos derivados de ellas.
import {
FILTER_OPERATORS, // 30 — los que evalúa `filter-utils` del node-sdk
ROUTER_OPERATORS, // 9 — los que evalúa `RoutersService` de la api
llevaValor,
type FilterOperator,
type RouterOperator,
} from '@hostwebhook/platform-contracts';
llevaValor('exists'); // → false: el formulario esconde el campo del valor
llevaValor('eq'); // → true⚠️ ROUTER_OPERATORS no es FILTER_OPERATORS recortada. Tiene in y
not_in, que filter-utils no sabe resolver, y le faltan los otros 23. Son
dos evaluadores distintos. Darle los 30 al router permitiría guardar reglas que
su switch no resuelve: caerían en el default: false y la regla nunca
casaría — sin dar error, que es lo que lo hace difícil de ver.
Por qué las listas están aquí y no junto a sus evaluadores
La regla natural sería «la lista vive pegada al switch que la resuelve». Pero
el dashboard también las necesita, para pintar los desplegables, y el evaluador
de filtros vive en @hostwebhook/node-sdk, que arrastra re2 y medio runtime
de nodos. Importarlo desde el navegador para leer treinta cadenas sería pagar
un bundle entero por una constante.
Así que la regla se afina:
la definición vive aquí, donde no hay dependencias de runtime; el test que la ata a su evaluador vive con el evaluador.
Quién ata cada una:
| lista | quién la ata | contra qué |
|---|---|---|
| FILTER_OPERATORS | node-sdk/__tests__/una-sola-lista-de-operadores | los case de filter-utils.ts |
| ROUTER_OPERATORS | api/src/nodes/una-sola-lista-de-operadores.spec | los case de routers.service.ts |
⚠️ Si un evaluador se muda, su test se muda con él. Una lista aquí sin nadie que la ate al otro lado es una lista que vuelve a divergir — que es exactamente de donde se venía: había seis copias de los operadores de filtro y ninguna igual a otra.
Precios de los modelos
Lo que los proveedores de LLM cobran por millón de tokens. Entrada y salida se tarifan por separado, con precios que suelen llevar un 5x entre ellos.
import {
calculateCost,
findPricingKey,
isModelPriced,
MODEL_PRICING,
} from '@hostwebhook/platform-contracts';
calculateCost('claude-sonnet-4-6', 1_000_000, 1_000_000); // → 18 USD
findPricingKey('claude-sonnet-4-6-20260101'); // → 'claude-sonnet-4-6'
findPricingKey('modelo-que-no-existe'); // → null⚠️ null no es cero. findPricingKey devuelve null para un modelo que la
tabla no conoce, y entonces calculateCost da 0. Ese 0 significa «no sé
cuánto vale», no «es gratis». Quien vaya a decidir algo con el número —un tope
de gasto, una factura, una pantalla— tiene que preguntar antes con
isModelPriced(). Tratar el 0 como gratis deja pasar sin límite justo los
modelos recién salidos, que son los caros.
La resolución no adivina: sólo vale una clave de la tabla que sea prefijo
del modelo pedido, y hasta un separador. Así claude-sonnet-4-6-20260101
encuentra su base, y claude-opus-9 no hereda la tarifa de un vecino.
🔴 Hay dos copias de esta tabla
Ésta es la fuente de verdad desde el 2026-08-31.
hw-llm-traces/src/common/utils/pricing.ts es la copia que hay que retirar; el
PR que la hace consumir este paquete va en ese repo y todavía no está hecho.
Mientras tanto, un cambio de tarifa se hace aquí y se copia allí.
Por qué aquí y no en la api
Los topes de gasto del Chat Trigger se comprueban en el camino caliente del
chat, una vez por turno. Preguntar la tarifa por red a hw-llm-traces metería
latencia en cada turno y, si ese servicio está caído o sin configurar, el tope
se caería abierto — que es lo mismo que no tenerlo.
Qué no va aquí
- Precios de venta e ids de Stripe: viven en la configuración de la api, que
es la única que habla con Stripe. Cambiarlos no puede obligar a publicar un
paquete. (No confundir con
precios/, que es lo que los proveedores de LLM nos cobran a nosotros — ver arriba.) - Límites por plan:
plan.constants.tsen la api. - Etiquetas de pantalla: cómo se llama un operador para el usuario es cosa del
dashboard (
lib/operadores.ts). Aquí van las claves, no los textos. - Cualquier cosa que sólo lea un servicio.
