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

@invoance/audit-viewer

v0.3.0

Published

Embeddable React audit-log viewer for Invoance: signed event table, filters, export, and in-browser Ed25519 tamper verification.

Readme

@invoance/audit-viewer

Embed the Invoance audit-log viewer in your own product: the signed event table, filters, CSV/JSON export, and the in-browser Ed25519 tamper test, as a native React component. No iframe. The data comes from the Invoance API; the cryptographic verification runs in your user's browser via Web Crypto, so the proof does not depend on trusting the page that renders it.

Install

npm install @invoance/audit-viewer

React 18+ is a peer dependency. The package ships compiled, inv--prefixed CSS with no reset, so it cannot fight your app's styles; you do not need Tailwind.

Quick start

import { AuditLogViewer } from "@invoance/audit-viewer";
import "@invoance/audit-viewer/styles.css";

export function AuditPage() {
    return (
        <AuditLogViewer
            getPortalToken={async () => {
                const r = await fetch("/api/audit-portal-token", { method: "POST" });
                return (await r.json()).token;
            }}
        />
    );
}

Your backend mints the token server-side with your API key (never expose the API key to the browser):

// e.g. an authenticated route in your app's backend
const session = await client.audit.portalSessions.create({
    organization_id: currentCustomerOrgId,
    intent: "audit_logs",
    session_duration_seconds: 3600,
});
return { token: session.token };

Auth

  • getPortalToken is an async callback, not a static prop. The component invokes it on mount and again whenever the session JWT expires, so short session_duration_seconds values are free; sessions are cheap to re-mint.
  • The exchanged JWT is org-scoped: it can only ever read the one organization the session was minted for.
  • tokenFromUrl enables the hosted-page style instead: the component reads a one-time ?token= from the URL, shows an "Open audit log" button, and exchanges the token only when the visitor clicks it, then strips it from the URL. Links are single-use, and mail scanners (Microsoft Defender Safe Links and similar) render pages on delivery; exchanging on load would burn the link before the person arrives. Loading the page posts nothing. Combine with persistSession to survive page refreshes via sessionStorage: a refresh after opening reuses the stored session without the button.
  • confirmBeforeOpen={false} restores the exchange on page load for tokenFromUrl, for links that are never sent by email. The getPortalToken path is always automatic; the button never shows there.
  • A session minted with intent: "log_streams" renders the stream-destination config screen instead of the event viewer.

Props

| Prop | Default | What it does | |---|---|---| | getPortalToken | - | Async callback returning a fresh one-time portal token | | tokenFromUrl | false | Read and strip ?token= from the page URL | | confirmBeforeOpen | true | With tokenFromUrl, exchange the token only after an "Open audit log" click | | persistSession | false | Cache the session JWT in sessionStorage | | baseUrl | https://api.invoance.com | API origin (point at your dev backend) | | pageSize | 25 | Initial rows per page (25, 50, or 100) | | autoRefresh | true | Quietly re-pull page 1 every 15s while idle | | theme | - | CSS-variable overrides, see Theming | | className | - | Extra class(es) on the root element | | onSessionExpired | - | Called when the session cannot be renewed | | syncUrl | true | Keep the open event, filters and page in the page URL | | urlParamPrefix | "" | Prefix for the viewer's URL params, e.g. "audit_" |

Filters

One filter bar replaces separate inputs. Empty, it reads "Filter by actors, actions, targets and dates" and the whole bar opens a menu of fields: actor name, actor id, action, target (type), target id and date. Picking a field lists the values that exist, most used first, with a search box that narrows by prefix; picking a value adds a chip such as action: document.deleted.

  • Chips on different fields combine with AND. Several action chips mean any of those actions. Every other field holds one value, so a second pick replaces the first.
  • Date offers Today, Last 7, 14 and 30 days (rolling, so Live keeps finding new events) and a custom range of two calendar days.
  • Typing a value that is not in the list offers "Filter by ..." for it.
  • Suggestions come from GET /v1/audit/portal/facets. Against an API that predates that endpoint the viewer suggests from the rows already loaded and leaves out actor name and target, which such an API does not filter on.

The filter bar on its own page

The bar is exported for host pages that list audit events themselves (the Invoance dashboard does). It does not talk to the portal: you hold the chips, and you give it the request for value suggestions.

import { FilterBar, chipsToParams, type Chip } from "@invoance/audit-viewer";
import "@invoance/audit-viewer/styles.css";

const [chips, setChips] = useState<Chip[]>([]);

<FilterBar
    chips={chips}
    onChange={setChips}
    fetchFacets={(params) => fetch(`/api/audit/facets?${params}`, { credentials: "include" })}
    events={rows}
/>;

// Your list query, mapped exactly as the viewer maps it:
const qs = new URLSearchParams(chipsToParams(chips));

| Prop | What it does | |---|---| | chips, onChange | The active filters; the bar only proposes changes | | fetchFacets | (params: URLSearchParams) => Promise<Response \| null>: request your facets endpoint with field, limit and, when set, q, occurred_after, occurred_before; it should answer { values: [{ value, label, count }] } | | events | Rows on screen, the suggestion source when fetchFacets answers 404 or throws | | facetsSupport, onFacetsSupport | Optional: keep what the bar learned about facets across remounts | | className | Extra class(es) on the bar's root |

The facets endpoint should behave like GET /v1/audit/portal/facets (most used first, q a case-insensitive prefix, a 90-day window by default), and the list should accept the params chipsToParams produces.

Event detail

Clicking a row opens the event inside the component, in place of the list. The breadcrumb ("Audit log / "), Escape or the browser's Back returns to the list with the filters and page as they were.

The card shows the actor, the action and event id, the date and time with the reader's timezone, the targets, and the location and user agent. Each is a button that highlights its lines in the full signed record beside it. The record wraps long values instead of scrolling, and you can type into it to test tampering (append-only; Reset restores it). Along the bottom, the sealed payload hash and the Ed25519 signature, with Verify signature checking both in the browser.

The page URL

With syncUrl (the default) the viewer keeps its state in the URL, so a reload, a bookmark or Back lands in the same view:

?event=aevt_...&section=date              the open event and highlighted section
?action=a&action=b&actor_id=u1&date=7d    filters (date: today, 7d, 14d, 30d or 2026-10-01..2026-10-03)
?page=3&cursor=...&rows=50                the page and rows per page

Opening an event adds a history entry; filters and paging replace the current one. Only these params are touched; the host page's own params are kept. If your page already uses names such as action or page, set urlParamPrefix="audit_". Set syncUrl={false} when your app owns its URL.

Theming

The stylesheet is driven by nine CSS variables (HSL triples, shadcn-style). Override them on :root, on any ancestor, or through the theme prop:

<AuditLogViewer
    getPortalToken={...}
    theme={{ variables: { background: "240 6% 12%", foreground: "0 0% 98%" } }}
/>

Variables: --inv-background, --inv-foreground, --inv-border, --inv-muted, --inv-popover, --inv-popover-foreground, --inv-success, --inv-danger, --inv-warning. Bare names in the theme prop get the --inv- prefix automatically.

Errors

  • An invalid, expired, or already-used link renders a clear message instead of the viewer; mint a fresh session to recover.
  • On a 401 mid-session the component re-invokes getPortalToken once and retries; if that fails it shows the expiry message and calls onSessionExpired.
  • In browsers whose Web Crypto lacks Ed25519 (older Safari/Firefox), the tamper test still checks the hash and reports the signature as not verifiable in this browser, distinct from a failed verification.

Offline verification

The verification primitives are exported for your own use:

import { verifyAuditEventDetail, canonicalAuditBytes } from "@invoance/audit-viewer";

verifyAuditEventDetail(event) rebuilds the frozen invoance.audit/1 canonical bytes, recomputes the SHA-256 payload hash, and checks the Ed25519 signature against the key embedded in the event, entirely client-side.