Embeddable viewer
@invoance/audit-viewer puts the audit log inside your own product (say, admin.yourapp.com/audit): the same signed event table, filters, export, and in-browser Ed25519 tamper test as the hosted portal, rendered as a native React component in your DOM. No iframe, no styling conflicts, and the cryptographic verification runs in your customer's browser, so the proof never depends on trusting the page that renders it.
Actor, targets, action, and gap-free sequence numbers, with filters, keyset paging, CSV/JSON export, and 15-second live refresh.
A detail drawer with the raw signed event: your customer edits any field and watches the Ed25519 verification flip from Authentic to Tampered.
Mint the session with intent log_streams and the same component renders the SIEM webhook destination screen instead.
Before you begin
- An API key with the audit scopes (create one) and at least one audit org receiving events.
- React 18 or newer. The package has no other runtime dependencies.
- No Tailwind required: the stylesheet ships compiled, prefixed, and themed by CSS variables.
npm install @invoance/audit-viewerThe component authenticates with a short-lived, org-scoped portal session that your backend mints for whichever of your customers is signed in. This is the one server-side piece.
// app/api/audit-portal-token/route.ts (Next.js example; any backend works)
import { InvoanceClient } from "invoance";
const client = new InvoanceClient({ apiKey: process.env.INVOANCE_API_KEY });
export async function POST() {
const organizationId = await currentCustomerOrgId(); // however your app resolves it
const session = await client.audit.portalSessions.create({
organization_id: organizationId,
intent: "audit_logs",
session_duration_seconds: 3600,
});
return Response.json({ token: session.token });
}Every SDK exposes the same call; in Python it is client.audit.portal_sessions.create(...). Keep session_duration_seconds short: the component re-mints through your endpoint automatically when it expires.
"use client";
import { AuditLogViewer } from "@invoance/audit-viewer";
import "@invoance/audit-viewer/styles.css";
export default function AuditPage() {
return (
<AuditLogViewer
getPortalToken={async () => {
const r = await fetch("/api/audit-portal-token", { method: "POST" });
return (await r.json()).token;
}}
/>
);
}getPortalToken is a callback, not a static prop, so expiry never strands the visitor. In the Next.js App Router the package already carries the "use client" banner, so you can import it from a server component tree. In Vite or CRA, mount it anywhere; only the fetch to your own token endpoint matters.
Open the page as a signed-in customer. You should see their organization's events with your issuer branding in the header. Click any row and press Verify signature: the verdict should read Authentic. Now add a character to any field in the editor and verify again: it flips to Tampered, computed entirely in the browser. That is the demo worth showing your customers.
Props
| Prop | Default | What it does |
|---|---|---|
| getPortalToken | - | Async callback returning a fresh one-time portal token from your backend. Re-invoked automatically when the session expires. |
| tokenFromUrl | false | Read and strip a one-time ?token= from the page URL (hosted-page style). |
| persistSession | false | Cache the session JWT in sessionStorage so a refresh survives without a new token. |
| baseUrl | https://api.invoance.com | API origin; point it at a dev backend while developing. |
| pageSize | 25 | Initial rows per page (25, 50, or 100); the visitor can change it. |
| autoRefresh | true | Quietly re-pull page 1 every 15 seconds while idle. |
| theme | - | CSS-variable overrides, e.g. { variables: { background: "240 6% 12%" } }. |
| className | - | Extra class(es) on the root element. |
| onSessionExpired | - | Called when the session cannot be renewed. |
Theming
Nine CSS variables (HSL triples, shadcn-style) drive every color. Override them on :root, on any ancestor of the viewer, or through the theme prop; bare names get the --inv- prefix automatically. A dark embed is three lines:
<AuditLogViewer
getPortalToken={getPortalToken}
theme={{ variables: { background: "240 6% 12%", foreground: "0 0% 98%", border: "240 6% 20%", muted: "240 5% 16%", popover: "240 6% 12%" } }}
/>| Variable | Default (light) |
|---|---|
| --inv-background | 0 0% 100% |
| --inv-foreground | 240 10% 8% |
| --inv-border | 240 6% 90% |
| --inv-muted | 240 4% 95% |
| --inv-popover | 0 0% 100% |
| --inv-popover-foreground | 240 10% 8% |
| --inv-success | 161 94% 30% |
| --inv-danger | 0 72% 51% |
| --inv-warning | 32 95% 44% |
Sessions and failure modes
- The exchanged JWT is org-scoped: it can only ever read the one organization the session was minted for. Portal traffic is rate-limited per visitor IP and never counts against your API budget.
- On a 401 mid-session the component calls
getPortalTokenonce and retries; if that fails it shows a clear expiry message and callsonSessionExpired. - A rate-limited exchange (many visitors behind one office NAT clicking at once) retries automatically honoring Retry-After; the one-time link is not consumed by a rate-limit rejection.
- In browsers whose Web Crypto lacks Ed25519, the tamper test still checks the hash and reports the signature as not verifiable in this browser, distinct from a failed verification.
- Events return
contextandmetadataverbatim; whoever can render the embed can read them. Your tenant id is never exposed.
The mint endpoint is documented under Audit Logs endpoints, and the quick start covers sending the events the viewer displays. Source and issues: github.com/Invoance/invoance-audit-viewer.