Skip to content

Package reference

Mirrors the package README (single source). Install @basaltkit/audit-viewer v3.0.2 — npm · source.

<p align="center"> <a href="https://basaltkit-docs.pages.dev"> <img src="https://basaltkit-docs.pages.dev/social-card.png" alt="Basalt" width="440"> </a> </p>

@basaltkit/audit-viewer ​

Read-only viewer for the audit trail produced by @basaltkit/audit: per-tenant, filterable and paginated queries, with aggregated statistics, and a self-contained HTML page to browse it. You need this module when you want to give admins (or yourself) a way to review who did what — for support, compliance, or debugging.

What this module solves ​

@basaltkit/audit writes an append-only (immutable) trail of everything that happens. This module is the lens for reading it: filter by event/actor/period/source, paginate, view totals and distributions — plus a page ready to open in the browser. It never writes to or alters the trail.

Installation ​

bash
pnpm add @basaltkit/audit-viewer @basaltkit/audit

Depends on @basaltkit/core, @basaltkit/audit, and @basaltkit/fastify. Requires the auditPlugin to be registered (that's where the trail comes from).

Get started in 5 minutes ​

ts
import { createApp } from '@basaltkit/core'
import { auditPlugin } from '@basaltkit/audit'
import { auditViewerPlugin, auditViewerRoutes, AUDIT_VIEWER } from '@basaltkit/audit-viewer'
import { fastifyPlugin } from '@basaltkit/fastify'

const app = await createApp({
  plugins: [
    auditPlugin(),
    auditViewerPlugin(),
    // plus authPlugin + the plugin behind your guard (permissions, teams, ...)
    fastifyPlugin({ routes: [...auditViewerRoutes({ meta: { can: 'audit:read' } })] }),
  ],
}).boot()

// programmatically
const viewer = app.container.get(AUDIT_VIEWER)
const page = await viewer.page({ tenantId: 'acme', event: 'auth:**', limit: 50 })
const stats = await viewer.stats({ tenantId: 'acme' })

Routes ​

auditViewerRoutes({ meta }) — every route requires login and the guard you pass as meta (e.g. { can: 'audit:read' } or { teamRole: 'admin' }). Without a guard it throws AuditViewerUnguardedError, unless you opt out explicitly with allowAnyAuthenticated: true. A meta whose only keys authorize nobody (rateLimit, tags, central, …) or whose guard value is empty ('', null, false, []) counts as no guard:

RouteDescription
GET /audit?event=&actorId=&source=&since=&until=&limit=&offset=Page of entries (most recent first) + total.
GET /audit/stats?…Aggregates: by event, by actor, by source, timeline.
GET /audit/:idA single entry.
GET /audit/viewHTML page for browsing (filters + table + pagination).

All are tenant-isolated (the tenant comes from the request context; inside a tenant context an explicit tenantId must match it).

The HTML page ​

GET /audit/view serves a vanilla page (no build step, no dependencies) that calls the JSON routes and shows a filterable table with pagination. Customize the title/base path:

ts
auditViewerRoutes({ meta: { can: 'audit:read' }, title: 'Audit — Acme', apiBase: '/admin' })

API reference ​

auditViewerPlugin({ bucketMs?, topN?, maxScan? }) ​

Registers the AUDIT_VIEWER token.

OptionTypeDefaultPurpose
bucketMsnumber86_400_000 (1 day)Timeline bucket size.
topNnumber20Rows returned by the per-event / per-actor breakdowns.
maxScannumber10_000Upper bound on rows read from the store per call.

maxScan exists because the trail is unbounded and these routes forward client input: an unbounded read is an OOM vector. When a call hits the bound, the result carries truncated: true and total means "matches within the window", not a grand total.

class AuditViewer ​

MethodDescription
page(query){ entries, total, limit, offset, truncated }.
stats(query){ total, truncated, byEvent, byActor, bySource, timeline }.
get(id, tenantId?)A single entry, or null.

ViewerQuery: event (wildcard), actorId, tenantId, source (hook/event/manual), since, until, limit, offset. Without tenantId, it uses ctx().tenant.id (otherwise AuditTenantRequiredError). Inside a tenant context an explicit tenantId must equal it, otherwise AuditTenantMismatchError (403).

> Note: the extra filtering (source/until) and the aggregation happen in memory over the result of Audit.trail, bounded by maxScan. truncated: true means the trail had more matches than the window — raise maxScan, narrow the query (since/until/event), or use an AuditStore with richer database querying.

Content-Security-Policy ​

The route sets a route-scoped CSP by default: everything locked down and the page's inline script allowed only by sha256 hash (exported as auditViewerCsp). It works under securityPlugin's strict app-wide CSP — do not disable CSP globally. Override with csp: '…' or opt out with csp: false; if you serve the raw HTML string yourself, set the matching CSP header on that route. Server-side inputs are HTML-escaped and embedded state cannot terminate the script block.

How it connects to other modules ​

  • @basaltkit/audit — the immutable source of the trail (this module only reads it).
  • @basaltkit/permissions — adds a guard (meta.can: 'audit:read') to restrict access to admins.
  • @basaltkit/exports — exports a query's result to CSV for compliance reports.

Released under the MIT License.