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

@ahrowe/audit-log

v0.2.0

Published

Framework-agnostic audit-log recorder: hooks a DB event bus, records who/when/what (field-level diff + redacted snapshots) with AsyncLocalStorage request context.

Downloads

578

Readme

@ahrowe/audit-log

Framework-agnostic audit trail for debugging: hook a database event bus and get one immutable row per mutation — who changed what, when, and in which request — with a field-level diff on updates and redacted snapshots on create/delete.

It carries no database dependency. You inject any service that satisfies a tiny structural interface (on / insert / createIndex), so it works with any createService(...)-style db wrapper that emits create/update/remove events.

How it works

The db wrapper you already use emits created / updated / removed events. registerAudit listens, diffs the docs, and writes one idempotent audit row. Context (actor, tenantId, requestId, …) is captured at mutation time and carried on the event payload, so an outbox relay can deliver it safely after the request ends.

request → context middleware (requestId, ip, …)
          → your auth sets actor → your code mutates a doc
            → db emits 'updated' { prevDoc, doc, context, eventId }
              → recorder diffs → writes one audit row (idempotent via eventId)

Install

pnpm add @ahrowe/audit-log
# koa is an OPTIONAL peer — only needed for the `/koa` request-context middleware
pnpm add koa

Usage

1. Bind the recorder to your audit collection

import { createAuditLog, AUDIT_LOG_COLLECTION } from '@ahrowe/audit-log';
import db from 'your-db-layer';

// You create the service (the db binding); the package owns the indexes.
// `db.createService` here is any wrapper that returns an `AuditDbService`-compatible
// object (emits created/updated/removed, has insert + createIndex).
const store = db.createService(AUDIT_LOG_COLLECTION, null, { addUpdatedOnField: false });

export const { registerAudit, recordEvent } = createAuditLog(store, { ttlDays: 30 });

Audit rows are built internally and are correct by construction, so no write schema is shipped. If your db layer supports a validator and you want belt-and- suspenders write validation, pass your own in place of null.

2. Register the collections you want audited

registerAudit(invoiceService, 'invoices');
registerAudit(userService, 'users', { redact: ['resetToken'] }); // + per-service secrets

registerAudit requires the service to emit created / updated / removed as AuditDbEvent<T> (see AuditDbService). The context and eventId fields are optional — services that don't emit them still work. passwordHash, passwordSalt and updatedOn are always redacted; add more per service via redact.

3. Open a request context so rows get an actor

import { requestContext } from '@ahrowe/audit-log/koa';
import { auditContext, AuditActorType } from '@ahrowe/audit-log';

app.use(requestContext); // FIRST middleware

// after you authenticate the user:
auditContext.setActor({ type: AuditActorType.User, _id: user._id, email: user.email });
auditContext.setTenantId(company._id);

No request context (cron/webhook) → rows are recorded with actor: { type: 'system' }.

Not on Koa? Skip /koa and open the store yourself with auditContext.run(store, next).

4. Record custom (non-CRUD) events

For domain events that aren't a raw mutation — and carry intent a diff can't:

await recordEvent({
  action: 'invoice.finalized',     // any string; CRUD verbs autocomplete
  entityType: 'invoices',
  entityId: invoice._id,           // optional — omit for events with no single subject
  metaData: { number: invoice.invoiceNumber, total: invoice.total },
});
// actor, requestId, tenantId, method/route/ip are filled from ALS context.

recordEvent returns the write promise — await it for delivery confirmation, or ignore it for fire-and-forget. Recording only: there's no query/replay layer (that's an event store, a different tool).

What gets stored

| Source | action | changes | snapshot | metaData | |---|---|---|---|---| | CRUD created | created | — | redacted full doc | — | | CRUD updated | updated | field-level before/after diff | — | — | | CRUD removed | removed | — | redacted full doc | — | | recordEvent | any string | — | — | your payload |

Rows are immutable and expire automatically ttlDays (default 30) after createdOn.

API

  • createAuditLog(store, { ttlDays?, logger?, tenantField? }) → { registerAudit, recordEvent, recordAudit }
  • registerAudit(service, entityType, { redact? }) — auto-capture CRUD off the db bus
  • recordEvent({ action, entityType, entityId?, metaData?, tenantId? }) — custom events
  • auditContextrun / get / setActor / setTenantId
  • requestContext (from @ahrowe/audit-log/koa)
  • AUDIT_LOG_COLLECTION
  • diffDocs, redactDoc, REDACT — pure helpers
  • types: AuditLog, AuditAction, AuditActor, AuditActorType, AuditChange, AuditDbService, AuditDbEvent