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

@jantstack/adonis-audit

v1.0.0

Published

Audit trail engine for AdonisJS + Lucid: one transactional store plus fan-out sinks, a Lucid CRUD mixin, polymorphic actors and entities, and a safe-by-default snapshot where auditable ⊆ serializable — fields hidden from your API never reach the trail.

Readme

@jantstack/adonis-audit

Motor de auditoría para AdonisJS 7 + Lucid: un store transaccional, N sinks best-effort, un mixin CRUD para modelos, actores y entidades polimórficos, y un snapshot seguro por defecto donde lo que ocultas de tu API tampoco entra en el rastro.

npm i @jantstack/adonis-audit
node ace configure @jantstack/adonis-audit
node ace migration:run

La idea en una línea

auditable ⊆ serializable. Un campo marcado con @column({ serializeAs: null }) —el equivalente en Lucid al $hidden de Eloquent— no sale por tu API y tampoco llega al log de auditoría. Una declaración, hecha donde ya estabas pensando en qué es sensible, con dos efectos.

Suena a detalle y no lo es. La implementación ingenua de un mixin de auditoría lee model.$attributes, que es la copia que va hacia la base de datos — una capa por debajo de donde serializeAs actúa. El resultado es un modelo protegido en su JSON y desprotegido en su rastro, que es justo donde el dato dura más y donde más gente puede leerlo. Y el caso peor no es crear, es actualizar: un cambio de contraseña deja registrados el hash viejo y el nuevo, permanentemente, sobreviviendo incluso al borrado de la cuenta.

Este paquete aplica la misma frontera a las dos rutas, y encima añade una segunda red por nombre (password, *_token, *_secret, *_hash…) que se aplica incluso sobre un toLog() explícito — porque el fallo realista no es olvidar toLog(), es añadir una columna refresh_token seis meses después y no acordarse.

La segunda red recorre en profundidad, y eso importa más de lo que parece: el agujero real no está en la primera capa. Una columna json llamada settings es legítimamente serializable —el campo en sí no es secreto, así que nadie le pone serializeAs: null— y su nombre no casa con ninguna regla. Sin recorrer, un settings.api_key atraviesa las dos redes.

El recorrido tiene tres topes —profundidad 12, ciclos y un techo de 10 000 nodos— y los tres fallan cerrados: lo que no se puede inspeccionar se sustituye por [audit: no inspeccionado] en vez de emitirse tal cual. Rendirse devolviendo el valor sería fallar abierto justo donde alguien escondería algo, y el marcador deja constancia del recorte en lugar de truncar en silencio.

Lo que no cubre, dicho claramente: la redacción es por nombre de campo. Un secreto interpolado dentro de un texto libre —la description de un evento, el describe() de una transición— pasa sin tocarse. Esos campos los escribes tú; trátalos como salida pública.


Arquitectura: un store + N sinks

No son drivers intercambiables. La auditoría es un flujo append-only y en la práctica quieres varios destinos a la vez: la tabla para consultar, un SIEM para cumplimiento, stdout para el colector.

audit.log(...)
   ├─→ store   exactamente uno · el consultable · honra tu transacción
   └─→ sinks[] cero o más · best-effort · nunca reciben trx

Los dos niveles tienen criticidad distinta, y esa es toda la arquitectura:

| | si falla | |---|---| | store | el error propaga. Dentro de una transacción, revierte el negocio con él: una operación cuyo rastro no pudo escribirse no debería darse por buena. | | sink | se registra y la vida sigue. Un colector de logs caído no puede tumbar un login. |

Mezclar ambos niveles es cómo se pierden rastros sin que nadie se entere.

Dos asimetrías explícitas en el contrato

Transaccionalidad. AuditStore.supportsTransactions es parte del contrato. Pasarle una trx a un store que no la soporta lanza en vez de descartarla en silencio — devolver un "ok" sin dar la atomicidad que se pidió es la forma más dañina de fallar en auditoría, porque el rastro parece existir.

Lectura. Append lo cumplen todos; query no. Por eso AuditReader es una capacidad opcional y separada, y el provider avisa al arrancar si tu store no sabe leer — no en la primera petición al endpoint de historial.


Uso

El mixin

import { compose } from '@adonisjs/core/helpers'
import { auditable } from '@jantstack/adonis-audit'

export default class Invoice extends compose(BaseModel, auditable({ eventPrefix: 'invoice' })) {
  @column() declare total: number

  // No sale por la API ⇒ no entra en el rastro. Sin más ceremonia.
  @column({ serializeAs: null }) declare internalMargin: number
}

Emite invoice.created, invoice.updated (solo con los campos que cambiaron) e invoice.deleted. Si el modelo está en una transacción, el rastro participa de ella.

Transiciones para no quedarte con diffs anónimos:

auditable({
  eventPrefix: 'invoice',
  transitions: [{
    when: (m) => m.$original.status === 'draft' && m.status === 'issued',
    event: 'invoice.issued',
  }],
})

Eventos a mano

import audit from '@jantstack/adonis-audit/services/main'

await audit.log('org.archived', organization, {
  previous: { archived: false },
  trx,                       // participa de tu transacción
})

// `actor: null` explícito ≠ omitirlo. Un login fallido no tiene actor, y eso ES el dato.
await audit.log('auth.login_failed', null, { actor: null, description: email })

config/audit.ts

export default defineAuditConfig({
  store: new DatabaseAuditStore(),
  sinks: [new StdoutAuditSink()],
  redact: ['salaryBand', /^interno_/],
  resolveActor: () => {
    const user = HttpContext.get()?.auth?.user
    return user ? { type: 'users', uuid: user.uuid } : null
  },
})

resolveActor lo aportas tú porque los guards son tuyos: el paquete no sabe si tienes users, admins, integrations o cualquier otra cosa.


Escribir tu propio store

Implementa AuditStore y, si sabes consultar, también AuditReader. Después júzgalo con el mismo juez que los que trae el paquete:

import { runAuditStoreContract } from '@jantstack/adonis-audit/testing'

test.group('mi store de ClickHouse', () => {
  runAuditStoreContract({
    test,
    makeStore: () => new ClickHouseAuditStore(),
    reset: async () => { /* ... */ },
  })
})

La suite es deliberadamente asimétrica: los casos de lectura se saltan solos si tu store no implementa AuditReader, y ese salto se anuncia — "no falló" no debe confundirse con "cumple".

Incluidos: DatabaseAuditStore (transaccional y consultable) y NullAuditStore. El segundo no es relleno: que la suite de contrato pase contra él es lo que demuestra que el contrato es real y no la implementación de la base de datos con otro nombre.


El rastro es de solo-inserción

ActivityLog rechaza save() y delete() sobre una fila existente: una auditoría que se puede reescribir no prueba nada.

Eso es resistencia dentro de la aplicación, no una garantía. El query builder no dispara hooks de modelo, así que ActivityLog.query().delete() sigue funcionando — a propósito, es como se purga y como limpian los tests. La garantía de verdad se pone en la base de datos:

REVOKE UPDATE, DELETE ON activity_logs FROM mi_rol_de_aplicacion;

Si necesitas anular un evento, registra uno nuevo que lo compense. Esa es la forma correcta en un libro de solo-inserción, y deja constancia de la anulación.

Compatibilidad

Node ≥ 20.6 · AdonisJS ^7 · Lucid ^22 · PostgreSQL, MySQL y SQLite.

Las columnas current/previous se serializan a mano porque los motores no coinciden: Postgres devuelve el json ya parseado, SQLite y MySQL devuelven la cadena. Sin eso el paquete "funcionaría" en Postgres y devolvería strings en los otros dos.

Licencia

MIT © Jose Tenorio