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

@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

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.ts en 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.