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

@setu-ts/audit-plugin

v0.11.0

Published

Immutable audit trail over memory, log, database, or file storage

Readme

@setu-ts/audit-plugin

Immutable audit-trail logging for Setu-TS. Registers an IAuditLogger under CAPABILITIES.AUDIT, backed by a pluggable storage backend. Each entry is stamped with an internally assigned id (runtime.uuid()) and wall-clock timestamp (runtime.now()), deep-frozen for immutability, then appended to the selected storage.

Storage backends:

| Backend | storage id | Persistence | Dependency | | ---------------------- | ------------ | ------------------------------------------ | -------------------------------------------------- | | MemoryAuditStorage | memory | in-process array (non-durable) | none (every runtime, incl. Cloudflare Workers) | | LogAuditStorage | log | routed to the resolved ILogger | the logger capability (LoggerPlugin) | | DatabaseAuditStorage | database | rows via an injected IAuditDbClient | an injected client facade (inject-only, no driver) | | FileAuditStorage | file | JSONL via runtime.fs (read-modify-write) | writable runtime.fs (Node/Deno/Bun only) |

The default backend is memory — zero-dependency and portable, but non-durable: it is lost on restart. Select log, database, or file for production. No database driver is ever a hard dependency; the database backend takes an injected client facade and never touches the database capability token.

Installation

import { AuditPlugin } from '@setu-ts/audit-plugin';

The memory, log, and file backends need nothing beyond the framework. The database backend needs an injected client that adapts your driver to the IAuditDbClient shape (insert(table, row) / select(table, criteria?)).

Usage

import { createApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
import { AuditPlugin } from '@setu-ts/audit-plugin';
import { CAPABILITIES } from '@setu-ts/common';
import type { IAuditLogger } from '@setu-ts/common';

const app = createApplication({
  plugins: [
    RuntimePlugin(),
    // In-memory (default — non-durable)
    AuditPlugin(),
  ],
});
await app.start();

const audit = app.services.get<IAuditLogger>(CAPABILITIES.AUDIT);
await audit.log({
  action: 'user.delete',
  resource: 'user',
  resourceId: '123',
  userId: currentUser.id,
  result: 'success',
  before: { active: true },
  after: { active: false },
});

IAuditLogger is write-only (like ILogger). AuditEntry is the write shape and carries no id/timestamp; those are assigned internally on the stored record, which is immutable once written.

Backend configuration

// Route audit records through the resolved logger.
AuditPlugin({ storage: 'log', options: { level: 'info' } });

// Persist to a database via an injected client (inject-only).
AuditPlugin({ storage: 'database', options: { client: myDbClient, table: 'audit_logs' } });

// Append JSONL to a file (Node/Deno/Bun only — requires runtime.fs).
AuditPlugin({ storage: 'file', options: { path: './audit.log' } });

| Option | Backend | Default | Notes | | ---------------- | ---------- | --------------- | --------------------------------------------------------------------- | | storage | — | 'memory' | 'memory' | 'log' | 'database' | 'file'. Unknown ids throw. | | options.level | log | 'info' | 'info' | 'warn' | 'error'. | | options.logger | log | ctx.logger | Injected ILogger; throws at registration when neither is present. | | options.client | database | — | Injected IAuditDbClient; required (throws when absent). | | options.table | database | 'audit_logs' | Table for insert/select. | | options.path | file | './audit.log' | Throws at registration when runtime.fs is absent (Workers/edge). |

Database row shape

The default table is audit_logs. Supply an IAuditDbClient that preserves this row shape; before, after, and metadata are JSON strings, not database JSON objects. A jsonb column decoded to an object is read as undefined. timestamp is epoch milliseconds:

| Column | PostgreSQL type | Value | | ----------------------------- | ------------------ | -------------------------------------- | | id | text PRIMARY KEY | Assigned UUID | | timestamp | bigint NOT NULL | Epoch milliseconds | | action, resource | text NOT NULL | Entry action and resource | | resource_id, user_id | text | Optional identifiers, otherwise null | | result | text NOT NULL | 'success' or 'failure' | | before, after, metadata | text | JSON-encoded objects, otherwise null |

CREATE TABLE audit_logs (
  id text PRIMARY KEY,
  timestamp bigint NOT NULL,
  action text NOT NULL,
  resource text NOT NULL,
  resource_id text,
  user_id text,
  result text NOT NULL CHECK (result IN ('success', 'failure')),
  before text,
  after text,
  metadata text
);

Reading the trail

IAuditLogger has no read method. An owner constructs the exported storage class over the same client and table used by the plugin, then calls query(criteria):

import { AuditPlugin, DatabaseAuditStorage, type IAuditDbClient } from '@setu-ts/audit-plugin';

app.register(
  AuditPlugin({ storage: 'database', options: { client: myDbClient, table: 'audit_logs' } }),
);

export function readTrail(client: IAuditDbClient) {
  const reader = new DatabaseAuditStorage({ client, table: 'audit_logs' });
  return reader.query({ resource: 'order', result: 'success', limit: 100 });
}

Only equality fields (action, resource, result, userId, resourceId) reach select. from/to are inclusive filters applied in memory over every matched row; there is no offset or paging. limit keeps the newest N records, returned in ascending timestamp order. For a large trail, query the table directly so the database can bound the scan.

Health

Registers an audit health indicator reporting { storage, reachable }. It carries BOTH signals: isReady() is lifecycle (constructed and accepting writes), and a cached, bounded probe is reachability (the sink answers right now).

| Backend | Probe | Reports | | ---------- | ------------------------------------------------------------------ | ----------------- | | memory | none needed — an in-process array | reachable: true | | log | none needed — the sink is the resolved in-process ILogger | reachable: true | | database | select on the primary key against a sentinel that matches no row | true / false | | file | the last append's outcome, then stat of the target directory | true / false |

A ready backend whose sink does not answer is down. A backend that cannot probe reports reachable: 'unknown' — never a falsely affirmative true. Outcomes are cached for 5 s and each probe is bounded at 2 s on the runtime's own clock and timers, so scraping /health never turns into sink load.

The database probe READS and never writes: an insert would put a fabricated record into the trail this plugin exists to keep trustworthy. The file probe does not write either, for the same reason — which leaves one gap it is honest about: a directory that is readable but not writable reports true until the first append proves otherwise, and that is why the append outcome is tracked.

Runtime portability

  • memory and log run on every target, including Cloudflare Workers.
  • file requires a writable runtime.fs; it throws at registration on runtimes without one (Workers/edge). The committed IFileSystem has no native append, so writes are read-modify-write and concurrent appends are serialized; on shutdown the plugin's onClose drains any in-flight write. The target file's parent directory is created recursively on first write, so a configured path in a not-yet-existing directory (e.g. ./var/log/audit.log) does not fail with ENOENT.
  • database is inject-only — there is no canonical SQL driver to lazy-load. Equality filters (action/resource/result/userId/resourceId) are delegated to the client's select WHERE; time-range (from/to), ordering, and limit are applied in-process.

Immutability

Every stored record is deep-frozen — including nested before/after/metadata — so it cannot be mutated after it is written, and records reconstructed on read (database/file) are frozen too.

Redaction

Pass redaction as a RedactionPolicy or IRedactionService to filter before, after, and metadata before an entry is frozen and stored. Omitting the option preserves the existing audit record shape and values.

License

MIT

Exports

| Export | Kind | | ---------------------- | --------- | | AuditPlugin | function | | AuditService | class | | DatabaseAuditStorage | class | | FileAuditStorage | class | | LogAuditStorage | class | | MemoryAuditStorage | class | | AuditEntry | interface | | AuditPluginOptions | interface | | AuditQuery | interface | | AuditStorageOptions | interface | | IAuditDbClient | interface | | IAuditLogger | interface | | StoredAuditEntry | interface | | AuditStorageType | type |

Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it drifts.

Full API

Every export and option is documented in PUBLIC_API.md.