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

hazo_audit

v2.0.1

Published

Field-level audit trail for hazo_connect-backed apps. Attach to any adapter via wrapWithAudit() to auto-capture writes; ships worker, Next.js routes, and React UI primitives.

Readme

hazo_audit

Field-level audit trail for hazo_connect-backed apps. Attach to any adapter with wrapWithAudit() and every write to a captured table auto-produces an outbox row, drained by an in-process worker into per-field events the UI can render via <FieldAuditIcon />.

Zero changes to hazo_connect. Zero per-route boilerplate. Add a new audited table with an INI block, not a code change.

npm i hazo_audit
npx hazo-audit-migrate --adapter=pg --connection-string=$DATABASE_URL

What it does

// instrumentation.ts
import { wrapWithAudit, startAuditWorker } from 'hazo_audit/server'
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup'

const base = getHazoConnectSingleton()
const adapter = wrapWithAudit(base, { resolvers: { wp: wpResolver } })
startAuditWorker({ app_adapter: base })
// middleware (or request handler)
import { runWithAuditContext } from 'hazo_audit/server'

await runWithAuditContext(
  { actor_kind: 'user', actor_user_id: session.user.id, scope_id: session.scope_id },
  () => next(),
)
// any field render site
import { FieldAuditIcon } from 'hazo_audit/client'

<FieldAuditIcon subjectKind="client_folder" subjectId={folder.id} fieldPath="client_name" />

That's it. Insert/update/delete on any app_* or hazo_* table is captured, diffed by microdiff for JSONB columns, and made queryable at /api/audit/events.

Architecture

caller writes
     │
     ▼
wrapWithAudit(adapter)         ─── reads INI ───►  capture decision
     │                                              ├─ pipeline tables → skip
     │                                              ├─ exclude_* matches → skip
     │                                              ├─ include_* matches → capture
     │                                              └─ default → skip
     │
     ▼ (captured)
hazo_audit_outbox  ◄────────────  claimRows()  ────► startAuditWorker
     │
     │ worker resolves before_row (caller hint, or chain prior outbox)
     │ microdiff(before, after) for JSONB columns
     │
     ▼
hazo_audit_field  ◄────────────  /api/audit/events  ────► <FieldAuditTrail>
                                                          (scope filter, mask sensitive)

Entries

| Import | What | |---|---| | hazo_audit | Client-safe types and permission constants (AUDIT_VIEW_HISTORY, etc.). | | hazo_audit/server | wrapWithAudit, startAuditWorker, runWithAuditContext, createAuditedCrudService, emitIntentEvent. Server-only. | | hazo_audit/nextjs | createAuditEventsRoutes — Next.js GET + reveal POST handlers. | | hazo_audit/client | <FieldAuditIcon />, <FieldAuditTrail />. Safe in client components. |

Tailwind v4 setup (required for the client components)

The client components use Tailwind utility classes. With Tailwind v4's JIT, classes inside node_modules/ are invisible unless you opt in. Add this to your globals.css (or wherever you @import "tailwindcss";):

@import "tailwindcss";
@source "../node_modules/hazo_audit/dist";

Without the @source line, the popover and trail will render without styling.

Capture rules — config/hazo_audit_config.ini

INI is the primary source of truth. The only TypeScript escape hatch is resolvers: {...} on wrapWithAudit (for project-specific JSONB path canonicalisation).

[capture]
include_table_prefixes = app_, hazo_
exclude_table_prefixes = _
exclude_tables = hazo_chat_typing_indicators

[overrides.app_clients]
subject_kind = client
sensitive_columns = ssn, dob

[overrides.app_client_folders]
subject_kind = client_folder
jsonb_columns = working_paper
jsonb_path_exclude = working_paper.__prefill_data
resolver = wp

Capture decision (deterministic, PRD §6.1): pipeline tables → exclude → include → default deny.

Public API (R1)

wrapWithAudit(adapter, opts?)HazoConnectAdapter

Proxies query() and claimRows(). For writes to captured tables, enqueues an outbox row after the underlying call succeeds. Best-effort: any audit failure fires opts.onAuditFailure(err, ctx) but never throws into the user write path.

wrapWithAudit(base, {
  resolvers: { wp: wpResolver },
  onAuditFailure: (err, ctx) => log.error('audit_failed', { ...ctx, err }),
})

startAuditWorker({ app_adapter, ... })WorkerHandle

Drains the outbox via claimRows() (concurrency-safe), runs microdiff, inserts field events, and dead-letters after max_attempts failures.

const worker = startAuditWorker({ app_adapter: base, poll_ms: 2000 })
// later
await worker.stop()
// or for tests
const { processed, failed } = await worker.drainOnce()

runWithAuditContext(ctx, fn)

AsyncLocalStorage-backed actor identity. The wrap stamps these fields onto each outbox row. Missing context → actor_kind: 'background_job'.

createAuditedCrudService(adapter, table)AuditedCrudService<T>

Drop-in for hazo_connect's createCrudService plus an opts.audit hook for supplying before_row (so the worker doesn't have to chain) and piggybacking a Layer-1 intent_event onto the write.

await svc.updateById(folderId, patch, {
  audit: {
    before_row: previousRow,
    intent_event: 'folder_field_edited',
    intent_payload: { field: 'client_name' },
  },
})

emitIntentEvent({ event_name, payload?, subject_kind?, subject_id? })

Direct Layer-1 intent emission (no piggyback). withAudit(name, payload, fn) ergonomic is Phase 2.

createAuditEventsRoutes({ audit_adapter, getAuth }){ GET, reveal: { POST } }

Next.js route factory.

  • GET /api/audit/events — requires view_audit_history. Default-filters by caller's scope_id unless they hold view_audit_across_scopes. Sensitive rows' values masked to null unless caller holds view_sensitive_audit.
  • POST /api/audit/events/[id]/reveal — additionally requires view_sensitive_audit. Side effect: writes an audit_reveal intent row.
createAuditEventsRoutes({
  audit_adapter,
  getAuth: async (req) => {
    const a = await hazo_get_tenant_auth(req)
    if (!a.authenticated) return null
    return {
      user_id: a.user.id,
      scope_id: a.selected_scope_id,
      permissions: a.permissions,
      user_label: a.user.full_name,
    }
  },
})

<FieldAuditIcon subjectKind subjectId fieldPath />

Renders a clock icon that opens a popover with the field's audit trail. Self-positioning, no Radix dep.

<FieldAuditTrail subjectKind subjectId fieldPath? />

Renders the trail directly (use in a sidebar, modal, etc.).

Schema

Three tables (see migrations/001_init.sql for both Postgres + SQLite):

  • hazo_audit_outbox — pre-drain queue.
  • hazo_audit_field — per-leaf field events (UI reads from this).
  • hazo_audit_intent — Layer-1 named events.

CLI — hazo-audit-migrate

npx hazo-audit-migrate --adapter=pg     --connection-string=$DATABASE_URL
npx hazo-audit-migrate --adapter=sqlite --path=./data/app.db
npx hazo-audit-migrate --adapter=pg     --connection-string=$DATABASE_URL --dry-run

PostgREST cannot run DDL; use --adapter=pg against the same Postgres instance.

Limitations (R1)

| Shape | Behavior | |---|---| | Single-row insert/update/delete with eq-on-id WHERE | ✅ captured | | Bulk insert | skipped + warning (opt-in fanout_bulk_writes) | | Bulk update / delete (non-id WHERE) | skipped + warning | | Composite primary key | skipped + warning (Phase 2) | | Insert without Prefer: return=representation | skipped + warning | | Raw writes (adapter.rawQuery) | pass-through, never audited |

See PRD §15 for the deferred-feature list.

Test-app

The test-app/ directory contains a Next.js demo:

npm run dev:test-app   # http://localhost:3017

Sidebar walks through:

  • Capture & Drain — insert/update/delete a client, watch the outbox grow.
  • Worker — manual drainOnce(), see field events appear, retry counts.
  • History UI — <FieldAuditIcon /> and <FieldAuditTrail /> in action.
  • Sensitive + Reveal — SSN masking and the reveal-is-itself-audited flow.
  • Skipped Writes — bulk inserts that get skipped with a warning.
  • Permissions — toggle cookies to exercise 401/403/masking.

Backed by SQLite (sql.js); the demo DB lives at test-app/data/demo.db.

What's new in v2.0.0

auditIntent(adapter, name, callback) — group multi-write transactions

import { auditIntent, runWithAuditContext } from 'hazo_audit/server';

await runWithAuditContext({ actor_kind: 'user', actor_user_id: 'u1' }, () =>
  auditIntent(adapter, 'person.merge', async () => {
    await crud.update(personA.id, { ...mergedFields });
    await crud.delete(personB.id);
  }, { metadata: { reason: 'manual merge by admin' } }),
);

All captured writes inside the callback share a correlation_id. An opening hazo_audit_intent row is written when the callback starts; a closing row (person.merge.completed or person.merge.failed) lands on completion or throw. Nested calls reuse the outer correlation_id.

snapshot(adapter, { table, recordId, asOf })

import { snapshot } from 'hazo_audit/server';

const snap = await snapshot(adapter, {
  table: 'app_persons',
  recordId: 'p1',
  asOf: new Date('2026-05-01T10:00:00Z'),
});
// → { state: { name: 'Alice', email: '[email protected]' }, deleted: false, exists: true }

Replays hazo_audit_field events for the subject up to asOf and returns the computed field state.

revert(app_adapter, { table, recordId, toTimestamp, reason })

import { revert, RecordNotFoundError } from 'hazo_audit/server';

try {
  const result = await revert(wrapped, {
    audit_adapter: base,  // optional; falls back to app_adapter when omitted
    table: 'app_persons',
    recordId: 'p1',
    toTimestamp: new Date('2026-05-01T10:00:00Z'),
    actorUserId: 'admin1',
    reason: 'user-requested rollback',
  });
  // result.applied = 'insert' | 'update' | 'delete' | 'noop'
} catch (e) {
  if (e instanceof RecordNotFoundError) { /* no audit history exists */ }
}

Computes the target snapshot, diffs against the current row, and applies the necessary PATCH/INSERT/DELETE — all wrapped in an audit.revert intent.

<AuditViewer /> (R3) — cross-record admin viewer

import { AuditViewer } from 'hazo_audit/client';

<AuditViewer
  api_base="/api/audit"
  initialFilters={{ subject_kind: 'person' }}
/>

Filterable, grouped-by-correlation_id list of all audit events with CSV export.

<RevertConfirmDialog />

Modal helper for confirming a revert with a typed reason.

Capture default exclusions widened (R2)

The sample INI now ships with hazo_logs, hazo_notifications in exclude_tables by default. The capture matrix is documented inline in config/hazo_audit_config.ini.sample.

Related

  • hazo_connect — provides the adapter interface and claimRows() primitive.
  • hazo_auth — typical source of session + permissions for getAuth.
  • [email protected]+ — auto-wires <FieldAuditIcon /> via field_audit prop.

See design/PRD.md for the full design rationale and decision log.