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

@c80/types

v3.4.0

Published

Contrato de tipos compartido del ecosistema barman (enums + interfaces de dominio + DTOs de WebSocket). Fuente unica de verdad para zerath-back, vulcan-arm y las apps del monorepo ngx-c80 (kyron, midas, chillbox, kiosk).

Readme

@c80/types

Contrato de tipos compartido del ecosistema barman. Lib TS pura (framework-agnostica: sin Angular, sin Nest, cero dependencias de runtime). Describe EXACTAMENTE lo que cruza un proceso por HTTP y WS.

Consumidores:

  • zerath-back — backend NestJS (dueno del contrato); paquete npm con pin exacto.
  • vulcan-arm — controlador del brazo; paquete npm con pin exacto.
  • kyron, midas, chillbox — apps del monorepo, por path (tsconfig.base.json).

Las REGLAS del contrato (fechas ISO, DTO anclado, errores con code, nombres) estan en la cabecera de src/index.ts. Las reglas de NEGOCIO (redondeos, puntos, volumen, modos) NO son tipos: viven en @c80/domain.

Contenido

Solo interface + enum + aliases. El fuente esta partido por DOMINIO en src/lib/ y el barrel src/index.ts reexporta TODO (la API publica no depende del archivo en que viva cada tipo; los imports internos llevan extension .js, obligatoria para ESM):

  • common — IsoDateString, FK, ActionResponseDto, PaginatedResponse, ApiErrorResponse (body de error).
  • auth — Permission, Role, PublicUser/PublicBoxInfo, LoginResponse, GoogleLoginRequest, codigos de cuenta.
  • user — AdminUser, OperatorProfile (perfil fiscal/contractual del operador B2B) y el equipo del operador.
  • box — Box, BoxClientResponse, configuraciones OneToOne, Driver + DriverUpsertRequest, estado operativo y sesion de box (QR).
  • config — GlobalConfiguration, feature flags (FeatureFlagKey/FeatureFlag) y GameId.
  • catalog — insumos, dispensadores (+ calibracion y lecturas de nivel), Product/Recipe/Part y sus requests, ratings, favoritos.
  • task — Task, Movement (+Drivers), MovementStatus/StepType, TaskStats, updates de movement.
  • ws — salas/eventos del canal WS, ChatMessage, mantenimiento y jog, telemetria, BOX_HEALTH.
  • purchase — saga de servido, DTOs de canje (chillbox), PurchaseErrorCode y listado admin.
  • invoice — facturacion electronica ARCA (InvoiceStatus, comprobante publico/admin).
  • recharge — preferencia de pago (MercadoPago), historial, config global, admin.
  • wallet — billetera del operador B2B (movimientos, resumen de liquidacion, payouts).
  • stats — pasivo de puntos, estadisticas de ventas, economia de puntos y juegos.
  • terms — TyC versionados por audiencia (CLIENT/OPERATOR).
  • consumer-request — botones de arrepentimiento/baja (Disp. 954/2025).
  • worklog — bitacora de agentes (entregables con ciclo de vida).

3.4.0 — evidencia de lo servido y motivo de la pausa (2026-08-29)

MINOR aditivo: dos interfaces nuevas para la evidencia de la venta, una para el body del toggle de barra, un campo en AdminPurchase y otro (opcional) en BoxStatus. No rompe 3.3.0.

Cierra la ultima deuda de contrato viva: zerath implemento las dos medidas de la excepcion legal al congelamiento (D62) tipandolas LOCALMENTE, que es el "DTO gemelo sin ancla" que prohiben las reglas de la cabecera de src/index.ts. Aca vuelven a tener una sola declaracion.

Nuevo — composicion servida (zerath 961ed2c)

RecipeSnapshotPart y RecipeSnapshot (purchase.ts) + AdminPurchase.recipeSnapshot: RecipeSnapshot | null, que viaja en GET /api/purchase:

interface RecipeSnapshot {
  productId: string;
  productName: string;
  hasIce: boolean;
  parts: RecipeSnapshotPart[]; // { ingredientId, ingredientName, isAlcoholic, doses, unitMl }
}

La receta es mutable y la venta apunta al PRODUCTO, no a una version de la receta: sin snapshot una venta de marzo termina describiendo la receta de hoy. El art. 40 de la Ley 24.240 pide una constancia anterior al reclamo, asi que la composicion se congela al canjear y se lee tal cual (no se recalcula ni se completa desde el catalogo actual, que es de lo que justamente no hay que fiarse).

Dos decisiones que conviene no re-discutir:

  • No es Recipe recortada. Recipe es el catalogo VIVO (editable, con ids de parte y de dispensador); esto es evidencia congelada. Compartir el tipo ataria la evidencia a los cambios del catalogo.
  • isAlcoholic por PARTE, no solo Product.isAlcoholic. Ante una intoxicacion la pregunta es cuanto alcohol se sirvio; y el flag del insumo tambien se edita despues del canje.

El campo es requerido y nullable (RecipeSnapshot | null), no opcional: zerath lo emite siempre —una sola funcion de mapeo para staff y para operador scopeado— y vale null en las ventas anteriores a la columna. Que el null sea explicito obliga al consumidor a decidir que muestra; en kyron se informa "sin registro de composicion", no una lista vacia.

Nuevo — motivo de la pausa de barra (zerath fbff8f8)

ToggleBoxServingPausedRequest { reason?: string } (box.ts) — body OPCIONAL de PATCH /box/:id/serving-paused. El endpoint sigue aceptando {}, asi que sumarlo no rompe a quien ya lo llamaba sin cuerpo; quien reanuda tambien puede explicarse. Tope del server: 200 caracteres (pasarse es HTTP 400). Es una NOTA operativa corta ("sospecha de contaminacion en el dispensador 3"), no un descargo: acotarla evita que el campo se vuelva un cajon de texto libre con el riesgo de PII que eso trae. El motivo NO vuelve por ninguna respuesta: queda en el historial de pausas de zerath.

BoxStatus.servingPausedAt?: IsoDateString | null — desde cuando esta pausada la barra.

Va en BoxStatus y no en Box, por donde viaja de verdad el dato: Box refleja BoxResponseDto, que deja fuera a proposito lo operativo de la pausa (servingPausedBy tampoco esta ahi), y los paneles leen el estado de la barra del GET /chillbox/box/:id/status, no del box. Al estar en BoxStatus lo hereda BoxLiveStatus (Omit<BoxStatus, 'health'>), o sea el evento WS BOX_STATUS, que es como llega el estado en vivo a kyron y midas.

Es opcional porque hoy zerath no lo emite: la columna box.serving_paused_at existe y se escribe, pero ningun mapeo la copia al cable (ver migracion). Los paneles ya lo muestran cuando llegue; hasta entonces simplemente no pintan esa linea.

Lo que NO entra: el historial de pausas

box_serving_pause_event (la tabla con cada pausa, su motivo, su origen y su actor) no se tipa: zerath no la expone por ningun endpoint. El contrato tipa el CABLE, no el esquema. Cuando exista un GET de historial —para el reclamo o el siniestro que motivo la tabla— entra con su *Response y su paginado, no antes.

Notas de migracion — zerath

Compila sin cambios (los tipos son nuevos y los campos que agrega ya los emite). Lo que hay que hacer al pinear:

  1. Anclar la composicion: borrar src/common/types/recipe-snapshot.types.ts e importar RecipeSnapshot / RecipeSnapshotPart de @c80/types (entity Purchase, AdminPurchaseResponseDto, recipe-to-task-generator.service.ts, purchase.service.ts). AdminPurchaseResponseDto puede ademas dejar de re-listar recipeSnapshot: ya lo hereda de AdminPurchase.
  2. Anclar el body del toggle: ToggleBoxServingPausedDto implements ToggleBoxServingPausedRequest (el DTO con class-validator encima, como manda la regla del contrato). El MaxLength(200) se queda en zerath: es la validacion, no el tipo.
  3. Emitir servingPausedAt (falta hoy, es lo unico que bloquea la pantalla de midas): agregar servingPausedAt: true al select y servingPausedAt: toIso(box.servingPausedAt) al objeto de retorno en purchase-query.service.ts (getBoxStatusForCatalog) y en box-live-status.service.ts (snapshot de BOX_STATUS). Sin eso midas muestra la barra pausada pero no desde cuando.
  4. Pin: "@c80/types": "3.4.0". @c80/domain no cambia (sigue 1.1.0; su peer >=3.1.0 <4.0.0 acepta 3.4.0 sin republicar).

Del lado de las apps ya esta hecho (por path): kyron muestra la composicion en Ventas y midas manda el motivo al pausar.

3.3.0 — la contraseña regenerada del equipo entra al contrato (D55, 2026-08-29)

MINOR aditivo: una interfaz nueva, nada mas. No rompe 3.2.0.

Nuevo

OperatorTeamPasswordResetResponse (user.ts) — respuesta de POST /api/midas/team/:id/password (permiso OPERATOR_TEAM_MANAGE), el endpoint con el que el operador le regenera la contraseña inicial a un miembro que perdio la suya:

interface OperatorTeamPasswordResetResponse extends OperatorTeamMember {
  password: string;
}

Se define extendiendo OperatorTeamMember, no re-listando sus campos: el server devuelve el miembro completo y le adosa la contraseña, asi que si el miembro cambia, esta respuesta cambia con el.

password viaja UNA sola vez, en el cuerpo de esta respuesta, y no se persiste en claro en ningun lado: zerath guarda solo el hash y ningun GET la expone. Quien la recibe la muestra en el acto para que el operador se la entregue a la persona; si se pierde, el unico camino es regenerarla otra vez. No debe loguearse ni quedar en el estado de la app mas alla del momento en que se muestra.

Por que un minor por un tipo. El endpoint se implemento despues de 3.2.0 y quedo tipado LOCAL en los dos extremos — OperatorTeamPasswordResponse en midas y OperatorTeamPasswordResetResponseDto en zerath —: dos declaraciones del mismo shape de cable, que es exactamente el "DTO gemelo sin ancla" que prohiben las reglas del contrato (cabecera de src/index.ts). Y ya habian driftado: la de midas declaraba solo { password } mientras el server devolvia el miembro entero. Se cierra con el tipo aca y las dos declaraciones locales borradas.

Notas de migracion — zerath

Compila sin cambios (el tipo es nuevo y nadie lo referencia todavia). Lo que hay que hacer:

  1. Anclar el DTO: OperatorTeamPasswordResetResponseDto deja de ser un shape suelto y pasa a implements OperatorTeamPasswordResetResponse (o el objeto de retorno con satisfies, si no necesita ser una clase), sin re-listar los campos de OperatorTeamMember. Si el DTO hoy declara solo password, ampliarlo a lo que el endpoint realmente devuelve — el HTTP 200 ya manda { id, name, email, enabled, createdAt, lastLoginAt, password } — o achicar la respuesta; lo que no puede seguir es que el tipo y el cable digan cosas distintas.
  2. Pin: "@c80/types": "3.3.0". @c80/domain no cambia (sigue 1.1.0; su peer >=3.1.0 <4.0.0 acepta 3.3.0 sin republicar).

Del lado de midas ya esta hecho (por path): el servicio del equipo importa el tipo del contrato y la declaracion local se borro.

3.2.0 — permisos finos del box y equipo del operador (D53/D55, 2026-08-29)

MINOR aditivo: tres miembros nuevos del enum Permission y tres interfaces nuevas. No rompe 3.1.0 (un enum que crece y tipos nuevos no invalidan a ningun consumidor). Viaja con la oleada C-ui, que no toca el contrato.

Nuevo

D53 — BOX_SELLING_WINDOW_UPDATE y BOX_STAFF_PIN_UPDATE (auth.ts):

  • Permission.BOX_SELLING_WINDOW_UPDATE — lo exige PUT /api/box/:id/selling-window (UpdateBoxSellingWindowRequest, D50).
  • Permission.BOX_STAFF_PIN_UPDATE — lo exige PUT /api/box/:id/staff-pin (SetBoxStaffPinRequest, D51).

Quien los tiene: el staff de kyron (roles internos, alcance global) y el rol Operator, este acotado a SUS boxes por el filtro de scope de zerath, igual que BOX_SERVING_TOGGLE. El permiso habilita la ACCION; el box sigue decidiendolo el scope.

Por que no alcanzaba BOX_UPDATE: ese permiso abre el PUT /box/:id generico — renombrar, relocalizar, reasignar ownerId —, que es administracion de plataforma y no del local. Filtrar campos por rol dentro de un mismo DTO es fragil; un permiso por accion es la forma barata de minimo privilegio. PUT /box/:id sigue siendo interno (BOX_WRITE/BOX_UPDATE de staff global).

D55 — equipo self-service del operador B2B (auth.ts, user.ts):

  • Permission.OPERATOR_TEAM_MANAGE — lo exigen GET/POST/PATCH /api/midas/team. Es solo para el rol Operator; el staff de kyron administra usuarios con USER_*, no con este.
  • OperatorTeamMember { id, name, email, enabled, createdAt, lastLoginAt } — la vista del equipo en midas.
  • CreateOperatorTeamMemberRequest { name, email, password } y UpdateOperatorTeamMemberRequest { enabled?, name? }.

La invariante de seguridad: el rol y el scope NO son parametros. El alta de usuarios es la superficie clasica de escalacion de privilegios, asi que ninguno de los dos requests los lleva y el server no los acepta de ningun modo:

  1. El rol lo FUERZA el server: todo miembro se crea con OperatorStaff, el subconjunto operativo del rol Operator (ver su box, pausar/reanudar la barra, reponer insumos, catalogo y ventas) y sin USER_*/ROLE_*, sin billetera ni payouts, sin ventana horaria ni PIN, sin TyC.
  2. El scope lo FUERZA el server: los boxes del operador que llama, nunca los que venga pidiendo el cliente.
  3. Los permisos efectivos se intersecan con los del propio operador: nadie puede otorgar lo que no tiene, ni siquiera si el rol semilla creciera.
  4. El operador da de alta, desactiva y reactiva miembros de SU equipo, y nada mas: no cambia roles, no toca cuentas ajenas, no borra. PATCH solo acepta enabled y name; el email es la identidad de la cuenta y no se edita.
  5. Toda alta/baja queda auditada con el actor.

Por eso OperatorTeamMember no expone rol, permisos ni boxes: si viajaran de ida, el front los mandaria de vuelta en el PATCH y la invariante duraria hasta el primer refactor. El nombre del rol (OperatorStaff) no entra al contrato: los nombres de rol del seed nunca vivieron aca (Role.name es string libre) y agregar uno solo seria una fuente de verdad a medias.

Sin canal de email todavia, el alta fija una contraseña inicial que el miembro debe cambiar al primer ingreso.

Notas de migracion — zerath (F-zerath)

Compila sin cambios (el enum solo suma). Lo que hay que implementar:

  1. Seed del rol Operator (src/common/constant/operator-role.constants.ts): sumar BOX_SELLING_WINDOW_UPDATE, BOX_STAFF_PIN_UPDATE y OPERATOR_TEAM_MANAGE a la lista (hoy tiene BOX_READ, BOX_SERVING_TOGGLE y los BOX_INGREDIENT_*). El seed es UPSERT: el redeploy los agrega a los roles ya creados, sin migracion de datos.
  2. Endpoints del box: PUT /box/:id/selling-window exige BOX_SELLING_WINDOW_UPDATE y PUT /box/:id/staff-pin exige BOX_STAFF_PIN_UPDATE — no el BOX_UPDATE que anticipaba la nota de 3.1.0 (punto 4). El scope del operador se aplica igual que en el resto de /box.
  3. Rol OperatorStaff en el seed (D55), junto a OPERATOR_ROLE_PERMISSIONS y con el mismo criterio con que se derivo aquel: los permisos REALES de los endpoints que va a llamar la pantalla, no un set "documentado". Punto de partida — ver su box (BOX_READ), pausar/reanudar la barra (BOX_SERVING_TOGGLE), reponer (BOX_INGREDIENT_*, INGREDIENT_SLOT_READ/UPDATE, INGREDIENT_SLOT_CONFIGURATION_READ) y mirar catalogo y ventas (PRODUCT_READ, RECIPE_READ, PURCHASE_READ). FUERA, por decision de D55: USER_*, ROLE_*, WALLET_*, PRICE_*, STATS_READ, BOX_SELLING_WINDOW_UPDATE, BOX_STAFF_PIN_UPDATE, TERMS_MANAGE y OPERATOR_TEAM_MANAGE (un miembro del equipo no se clona a si mismo). Es un rol de seed como los demas: su NOMBRE no viaja por el contrato (Role.name es string libre).
  4. Endpoints del equipo (D55, default-deny + authz-check): GET/POST/PATCH /api/midas/team con OPERATOR_TEAM_MANAGE, aplicando las 5 reglas de la invariante de arriba (rol forzado, scope forzado, interseccion de permisos, solo alta/desactivar/reactivar sobre el propio equipo, auditoria con el actor). El GET devuelve OperatorTeamMember[] solo del equipo de quien llama; un id ajeno en el PATCH es 404, no 403 (no confirmar que la cuenta existe).
  5. Pin: "@c80/types": "3.2.0". @c80/domain no cambia (sigue 1.1.0).

La UI de midas para el equipo (D55) y el backend van en otra tanda: 3.2.0 solo fija el contrato.

Del lado de D53, las apps ya estan adaptadas (por path): midas abre el modal si tiene cualquiera de los dos permisos y kyron ofrece la accion "Horario y PIN" de la tabla de Cajas con el mismo criterio; dentro del modal (@c80/panel) cada seccion se muestra por SU permiso, asi un operador con uno solo no ve un boton que le va a rebotar con 403. El editor de roles de kyron ya los lista (y de paso BOX_SERVING_TOGGLE, CONSUMER_REQUEST_MANAGE y AGENT_WORKLOG_MANAGE, que el contrato declaraba y la pantalla no ofrecia: no habia forma de componer un rol con ellos).

3.1.0 — ventana horaria, pausa desde el kiosco y clausula D26 (D50/D51/D52, 2026-08-29)

MINOR aditivo: no rompe nada de 3.0.0. Agrupa las tres decisiones de la oleada F. Se propaga a zerath (F-zerath) y vulcan (F-vulcan) con pin exacto 3.1.0 + @c80/domain 1.1.0; las apps del monorepo ya estan adaptadas (por path).

Nuevo

D50 — ventana horaria de expendio por box (box.ts):

  • SellingWindow { start: string; end: string } — HH:mm 24 h en la hora LOCAL de la box (APP_TIMEZONE, unica para toda la red). start > end = la ventana CRUZA la medianoche (20:00 -> 04:00, el caso normal del canal nocturno). start === end es INVALIDO (sin restriccion se expresa con null).
  • Box.sellingWindow: SellingWindow | null (null = sin restriccion) y UpdateBoxSellingWindowRequest (PUT /box/:id/selling-window).
  • BoxStatus.sellingWindow + BoxStatus.closedBySchedule: boolean — lo calcula ZERATH con la hora del server (unica fuente confiable: el reloj del telefono del cliente no lo es). BoxLiveStatus (evento WS BOX_STATUS) los hereda por su Omit<BoxStatus, 'health'>.
  • PurchaseErrorCode.BOX_CLOSED — rechazo del canje ANTES de debitar, distinto de BOX_PAUSED (decision manual): este cierre es automatico y tiene hora de apertura conocida.
  • El calculo (cruce de medianoche incluido) vive en @c80/domain: isWithinSellingWindow, nextOpeningAt, formatSellingWindow, isValidSellingWindow.

D51 — pausa de la barra desde el kiosco con PIN (box.ts):

  • Box.staffPinSet: boolean (el PIN NUNCA viaja: zerath guarda el hash) y SetBoxStaffPinRequest (PUT /box/:id/staff-pin, 4 a 6 digitos = STAFF_PIN de @c80/domain).
  • ServingPauseOrigin = 'OPERATOR' | 'KIOSK' | 'STAFF' y BoxStatus.servingPausedBy (auditoria del origen de la pausa; null si la barra no esta pausada). La pausa sigue siendo UNA (Box.servingPaused): esto no agrega un segundo estado.
  • KioskServingPauseRequest / KioskServingPauseResponse (kiosk -> vulcan POST /api/kiosk/serving-pause -> zerath) y KioskErrorCode (INVALID_PIN, PIN_NOT_SET, TOO_MANY_ATTEMPTS).

D52 — clausula D26 en la app (consumer-request.ts, config.ts, auth.ts):

  • ConsumerRequestType.CHARGEBACK_APPEAL + CreateChargebackAppealRequest { message } (<= 2000 caracteres, CHARGEBACK_APPEAL de @c80/domain; uno abierto por vez).
  • GlobalConfiguration.chargebackPolicySince: IsoDateString | null (no retroactividad: se fija al ENCENDER el enforcement) y ChargebackPolicy { enabled, threshold, highAmountArs, since } (respuesta publica de GET /chillbox/policies/chargeback).
  • AccountSuspension { since, reason: 'CHARGEBACKS', appealOpen } y PublicUser.suspension? — solo en GET /chillbox/user/me, con motivo GENERICO. El detalle antifraude (contador, payment_ids) sigue siendo exclusivo de AdminUser (staff de kyron), como en 3.0.0.

Notas de migracion — zerath (F-zerath)

Compila sin cambios (todo lo nuevo es aditivo) salvo los shapes que zerath ancla y que ganaron campos REQUERIDOS: Box (sellingWindow, staffPinSet), BoxStatus y BoxLiveStatus (servingPausedBy, sellingWindow, closedBySchedule). Lo que hay que implementar:

  1. Esquema: box.selling_window (jsonb o dos columnas time; null = sin restriccion), box.staff_pin_hash (bcrypt, nullable), box.serving_paused_by (enum/varchar, nullable), global_configuration.chargeback_policy_since (timestamptz, nullable) y el tipo CHARGEBACK_APPEAL en consumer_request.
  2. Enforcement BOX_CLOSED: en el canje, ANTES de debitar (junto a BOX_PAUSED/BOX_OFFLINE), con isWithinSellingWindow(box.sellingWindow, new Date()) de @c80/domain; el message deberia citar la apertura (nextOpeningAt). Idem Product.availability si se quiere apagar la carta.
  3. BoxStatus/BoxLiveStatus: completar los tres campos en box-live-status.service.ts y en el DTO del GET. closedBySchedule se recalcula al emitir (no se cachea) y conviene re-emitir el snapshot al abrir/cerrar la ventana.
  4. Endpoints nuevos (default-deny, authz-check): PUT /box/:id/selling-window (UpdateBoxSellingWindowRequest, valida con isValidSellingWindow) y PUT /box/:id/staff-pin (SetBoxStaffPinRequest, hash bcrypt, sin lectura) — el permiso que exigen NO es BOX_UPDATE: 3.2.0 (D53) les da uno propio, ver esa seccion; PUT /box/serving-pause (API key del box: valida el PIN, aplica servingPaused con la MISMA semantica que el toggle de midas y setea servingPausedBy: KIOSK, rate-limit 5/10 min por box con KioskErrorCode), POST /chillbox/consumer-request/chargeback-appeal (JWT; 409 si ya hay uno abierto) y GET /chillbox/policies/chargeback (PUBLICO, @Public()).
  5. servingPausedBy: el toggle de midas escribe OPERATOR, kyron STAFF, el kiosco KIOSK; al reanudar vuelve a null.
  6. suspension en /me: toPublicUser no cambia; el controller de /chillbox/user/me agrega suspension (fecha + CHARGEBACKS + si hay descargo abierto) solo para el titular.
  7. No retroactividad (D52c): al pasar chargebackSuspensionEnabled de false a true, fijar chargebackPolicySince = now() y neutralizar los eventos previos (forgiven = count, evidencia intacta); el gatillo solo cuenta contracargos posteriores.
  8. Pins: "@c80/types": "3.1.0", "@c80/domain": "1.1.0".

Notas de migracion — vulcan (F-vulcan)

Aditivo y FUERA del nucleo (core:check debe seguir verde):

  1. Proxy POST /api/kiosk/serving-pause en el listener local del kiosco (hoy solo GET /config y /state): recibe KioskServingPauseRequest, lo reenvia a zerath (PUT /api/box/serving-pause) con la API key del box y devuelve KioskServingPauseResponse o el error con su KioskErrorCode. Sin PIN en logs.
  2. GET /state suma (aditivo, la app kiosk ya tolera su ausencia): servingPaused: boolean, servingPausedBy: ServingPauseOrigin | null y closedBySchedule: boolean (+ opcional sellingWindow), tomados del ultimo BOX_STATUS recibido por WS. Sin ellos el kiosco no muestra "pausada"/"cerrado por horario", pero el gesto y el PIN funcionan igual.
  3. Pins: "@c80/types": "3.1.0", "@c80/domain": "1.1.0".

3.0.0 — contrato "de precision" (D44, 2026-08-29)

Un solo major que agrupa todo el cambio de contrato de la reestructuracion pre-produccion. Se propaga a zerath (F2) y vulcan (F3) en la misma tanda que los errores tipados y las rutas canonicas de zerath (D47); las apps del monorepo ya estan adaptadas. Las rutas citadas en los docs son las CANONICAS de D47d (/api/user, /api/recipe, /api/ingredient-slot, ...; F2 las aplica).

BREAKING (2.x -> 3.0.0)

  1. Fechas: IsoDateString unico. Se eliminan Timestamp (string | Date) y NullableTimestamp, y los 27 campos Date puros + 3 Date | string pasan a IsoDateString (= string, ISO 8601 UTC). Afecta a: Role, Box, PriceConfiguration, GlobalPoint, MotorConfiguration, IngredientSlotConfiguration, Driver, GlobalConfiguration, FeatureFlag.updatedAt, BoxIngredient, TaskStats (6 campos), Movement (inProgressAt/completedAt/failedAt/cancelledAt), ChatMessage.date, BoxHealthPayload.timestamp y a todo lo que era Timestamp (createdAt/updatedAt/servedAt/... de purchase, recharge, invoice, wallet, terms, worklog, consumer-request, user, box-session). NullableTimestamp (string | Date | null | undefined) queda como IsoDateString | null en campos ya opcionales.
  2. Fuera los shapes de ENTITY que ningun consumidor usaba y exponian internals (tabla de uso 2026-08-29: 0 imports en zerath, vulcan y las 4 apps): User (con password? y relaciones), Purchase, Recharge, Favorite, Rating. Derivados: Product.ratings? fuera (zerath nunca lo envio; queda averageRating/userRating); Task.recipe? y Task.purchase? fuera (no viajan); Task.product?: FK | null; Box.owner?: FK | null (era User | FK | null); Box.apiKey? fuera (la key completa solo viaja en la respuesta de regenerarla); OperatorProfile.user es { id, name, email }.
  3. Recipe/Part dicen la verdad (lo que zerath sirve en GET /recipe, /recipe/product/:id y Product.recipe): Recipe.parts: Part[] requerido, Recipe.product?: RecipeProductRef; Part.unitMl: number requerido, Part.boxIngredient: RecipeIngredientRef, sin Part.recipe?. Product.recipe?: Recipe | null (ya no FK | Recipe | null). Product.image?: string | null (el seed manda null). Los paneles borraron su Recipe redeclarada.
  4. Opcionales que ya eran obligatorios: IngredientSlotConfiguration.cupCapacityMl: number y BoxStatus.servingPaused: boolean (zerath los envia siempre desde 2.x).
  5. ChatMessage sin index signature ([key: string]: unknown): un campo que un lado necesite transportar se declara. date?: IsoDateString (el emisor usa new Date().toISOString()).
  6. QueryParams y PaginationParams se mudan a @c80/core (tipos de front: 0 usos en zerath/vulcan). PaginationParams pierde boxId (nadie lo leia por ese tipo).
  7. Permission pierde 15 miembros fantasma (ningun endpoint los exigia ni ningun rol los otorgaba): INGREDIENT_SLOT_CONFIGURATION_TOGGLE, FAVORITE_READ/WRITE/DELETE, RATING_WRITE, PART_READ/WRITE/UPDATE/DELETE, RECHARGE_WRITE/UPDATE/DELETE/APPROVE/REJECT, PURCHASE_DELETE. Una fila role.permissions con esos strings en una DB vieja es inofensiva (nunca gatearon nada).
  8. CreatePurchaseResponse.code fuera: nunca viajo en un 200. Los codigos de rechazo viajan en ApiErrorResponse.code del HTTP 400 (asi lo hace zerath desde 2.7.0 con INSUFFICIENT_STOCK).

Nuevo (no rompe)

  • IsoDateString, ApiErrorResponse ({ statusCode, timestamp, path, method, error, message, code? }, el body del filtro global de zerath).
  • Lo que cruza proceso y vivia como tipo local: GoogleLoginRequest (chillbox), BoxClientResponse (chillbox), CreateRatingRequest (chillbox), CreateRecipeRequest/UpdateRecipeRequest (paneles), DriverUpsertRequest (vulcan), IngredientSlotWithSensor (vulcan), IngredientSlotCalibrationRequest (vulcan), LevelSensorReadingUpdate + LevelSensorReadingsBatchRequest (vulcan), RecipeProductRef/RecipeIngredientRef.
  • PurchaseErrorCode suma BOX_PAUSED, BOX_OFFLINE, QUEUE_FULL, INSUFFICIENT_POINTS (hoy zerath los rechaza con 400 SIN code; F2 los emite). Candidatos para un minor futuro, si el front los quiere ramificar: limite de alcohol (D.3), producto sin puntos, receta no servible.
  • CreatePurchaseDto.idempotencyKey?: string (uuid v4 por toque de "Canjear"; F2 lo implementa).
  • LoginResponse y BoxRoomSubscribeResponse se quedan: zerath los implementa en F2.

Notas de migracion — zerath (F2)

Compilacion de zerath 2d1945c contra el .d.ts de 3.0.0 (sin tocar el repo): 42 errores, TODOS de fechas (Date no asignable a IsoDateString); cero errores por los tipos podados, los permisos, ChatMessage, Task, Recipe o Product (zerath no anclaba esos shapes: por eso el punto B).

A. Fechas (obligatorio para compilar). El runtime NO cambia (Nest ya serializaba Date a ISO); solo el tipo pasa a decir la verdad. Regla: la capa que arma la respuesta hace toISOString() (helper sugerido toIso(d: Date | null | undefined): IsoDateString | null), y los DTOs de respuesta declaran IsoDateString. Sitios (archivo:linea de 2d1945c):

  • DTOs de respuesta que extends/implements el contrato con Date: billing/dto/invoice-response.dto.ts (8, 32: emittedAt, lastAttemptAt, createdAt, updatedAt), box-session/dto/box-session-response.dto.ts (10-11: expiresAt, serverNow), favorite/dto/favorite-response.dto.ts (29), purchase/dto/admin-purchase-response.dto.ts (9), purchase/dto/box-status-response.dto.ts (9: lastHealthAt), purchase/dto/purchase-history.dto.ts (8), purchase/dto/purchase-status-response.dto.ts (10), recharge/dto/admin-recharge-response.dto.ts (9), wallet/dto/wallet-response.dto.ts (11-12, 27).
  • Servicios que mapean entidades al contrato: agent-worklog.service.ts (208-210), billing.service.ts (559), box.service.ts (190, 204, 214, 225: priceConfiguration/motorConfiguration/ingredientSlotConfiguration/ drivers con fechas Date), consumer-request.service.ts (124-125), movement.service.ts (534-535, 607, 669-671), box-live-status.service.ts (131: lastHealthAt), recharge.service.ts (1619-1620), task.service.ts (342-343, 703), terms.service.ts (66, 260-261), user/utils/user.helper.ts (65: Role con fechas dentro de PublicUser; 83: suspendedAt), websocket.gateway.ts (403-404).

B. Anclar lo que entro al contrato (regla "prohibido el DTO gemelo sin ancla").

  • auth/dto/google-login.dto.ts: GoogleLoginDto implements GoogleLoginRequest.
  • rating/dto/create-rating.dto.ts: CreateRatingDto implements CreateRatingRequest.
  • box/dto/box-response.dto.ts: BoxClientResponseDto -> satisfies/implements BoxClientResponse.
  • driver/dto/upsert-driver.dto.ts: UpsertDriverDto implements DriverUpsertRequest.
  • ingredient-slot/dto/update-sensor-calibration.dto.ts: implements IngredientSlotCalibrationRequest; batch-sensor-reading.dto.ts: SensorReadingItemDto implements LevelSensorReadingUpdate y BatchSensorReadingDto implements LevelSensorReadingsBatchRequest; findByBoxWithSensors(): Promise<IngredientSlotWithSensor[]>.
  • recipe/dto/create-recipe.dto.ts / update-recipe.dto.ts: implements CreateRecipeRequest / UpdateRecipeRequest; recipe-response.dto.ts: RecipeResponseDto/PartResponseDto son gemelos sin ancla de Recipe/Part (mismos campos, fechas Date): pasar a satisfies con fechas ISO y borrar ProductFK/BoxIngredientFK locales (son RecipeProductRef/RecipeIngredientRef).
  • common/filters/global-exception.filter.ts: el errorResponse satisfies ApiErrorResponse.
  • LoginResponse (auth) y BoxRoomSubscribeResponse (gateway): implementarlos.
  • websocket/dto/chat-message.dto.ts: ChatMessageDto puede conservar su index signature propia (una clase puede tener mas que la interfaz), pero el contrato ya no la exige: es el momento de encender la whitelist del relay.

C. Errores tipados y canje. purchase.service.ts: assertBoxOperational -> BOX_PAUSED / BOX_OFFLINE, resolveQueueSlot -> QUEUE_FULL, saldo -> INSUFFICIENT_POINTS (como ya hace INSUFFICIENT_STOCK: 400 con { code, message }); CreatePurchaseDto.idempotencyKey (cache por usuario+clave; un reintento devuelve la compra ya creada). chillbox YA lee code del body de error.

D. Adoptar @c80/domain (pin exacto 1.0.0). common/utils/money.util.ts -> re-export o reemplazo por round2/roundArs/roundTo/ceil2; point-calculation.util.ts -> calculatePoints; cup-capacity.util.ts -> calculateRecipeVolume + recipeFitsCup (el mensaje del 400 se arma con el desglose); level-sensor.util.ts -> calculateCurrentMl; datetime.constants.ts -> APP_TIMEZONE; error.util.ts -> getErrorMessage; websocket.gateway.ts:672 (validModes literal) -> isSystemMode. Semantica identica (verificada en runtime contra los valores de zerath).

E. Pins: "@c80/types": "3.0.0", "@c80/domain": "1.0.0" (exactos, D46).

Notas de migracion — vulcan (F3)

Compilacion de vulcan HEAD contra el .d.ts de 3.0.0: 2 errores, ninguno en el nucleo.

  • src/shared/types/index.ts:6 (TS2308): el barrel re-exporta LevelSensorReadingUpdate desde core.types (contrato) y desde level-sensor.types.ts (local, lineas 76-90). Borrar el local (mismo shape); idem IngredientSlotSensor -> IngredientSlotWithSensor del contrato.
  • src/modules/connection/websocket.service.ts:295: date: new Date() -> new Date().toISOString() (ChatMessage.date es IsoDateString). BoxHealthPayload.timestamp ya viaja ISO (box-health-heartbeat.service.ts:87): sin cambio.
  • src/modules/api/driver.service.ts:11: DriverUpsertRequest local -> el del contrato (y el re-export de modules/api/index.ts). ingredient-slot.service.ts: tipar los bodies con IngredientSlotCalibrationRequest y LevelSensorReadingsBatchRequest.
  • shared/utils/logging.utils.ts getErrorMessage -> @c80/domain (cambio de semantica deliberado: un objeto SIN message cae al fallback en lugar de JSON.stringify; con message suma el cause). SYSTEM_MODES/isSystemMode si vulcan valida modos por el cable.
  • ChatMessage sin index signature: vulcan compila sin cambios (verificado). Los permisos podados y los tipos de entidad eliminados no lo tocan.
  • Pins exactos: "@c80/types": "3.0.0", "@c80/domain": "1.0.0". Nada de esto entra al nucleo (core:check debe seguir verde).

Que NO vive aca

Tipos propios de un solo repo (UI de un front, Dispenser* de vulcan, estado de formularios como RecipePart de chillbox o PartForm de panel) quedan locales en cada repo.

Uso

import { Box, Movement, RoomEvent } from '@c80/types';

Build dual CJS/ESM

scripts/build-dual-lib.js types compila el mismo fuente dos veces:

  • dist/libs/types/esm/ — ESM (con stub {"type":"module"}); condicion import.
  • dist/libs/types/cjs/ — CommonJS (el package.json raiz no declara type); condicion require y main (consumidores NestJS).

El package.json de esta carpeta es el PUBLICABLE (se copia tal cual al dist). Regla: los imports internos del fuente llevan extension .js (sin ella la resolucion ESM de Node revienta en runtime aunque el gate compile).

Build / release (desde la raiz del workspace)

pnpm build:types                                            # arma dist/libs/types (esm + cjs)
npx nx release --projects=types --dry-run --skip-publish    # previsualizar (no toca nada)
pnpm release:types                                          # gate + version + CHANGELOG + tag [email protected] + publish

El bump lo deciden los conventional commits desde el ultimo tag [email protected] (feat = minor, fix = patch, BREAKING CHANGE/! = major). Detalle del flujo en el README raiz.

Antes de publicar, validar el runtime del dist: require('@c80/types') e import('@c80/types') deben cargar y exponer los enums (el gate NO lo detecta; probar con el dist montado en un node_modules suelto).