Embeddable viewer
@invoance/audit-viewer renders one org's signed audit log inside your React app. Your backend mints a portal session; the browser does the rest.
What it shows
- A table of signed events with actor, action, targets and time, a filter bar and keyset paging; the row detail adds seq.
- CSV and JSON export of every event matching the filters, up to 10,000 rows, and a refresh of page 1 every 15 seconds while idle.
- A detail drawer with the raw signed event and a step-by-step check: SHA-256 of the canonical bytes, then the Ed25519 signature, in the browser with Web Crypto.
- An editor in the drawer that changes a local copy of the event, so the visitor can watch the check fail.
- The issuer name and logo from GET /v1/audit/portal/org in the header.
- For a session with intent log_streams, the stream destination screen instead of the table.
| Package | @invoance/audit-viewer 0.1.2, MIT |
|---|---|
| Needs | React 18 or newer; no other runtime dependency |
| Ships | ESM and CommonJS builds with types, plus styles.css |
| Styles | Compiled inv- prefixed CSS with no reset; Tailwind is not needed |
| Framework | React only; there is no script tag build |
| Reads | the /v1/audit/portal routes with the session JWT |
| Source | github.com/Invoance/invoance-audit-viewer |
npm install @invoance/audit-viewer
Mint on your server
POST /v1/audit/portal_sessions with an audit:write key, your organization_id and intent audit_logs. The reply carries a one-time token, valid for link_duration_seconds, 300 by default.
Return the token
Your getPortalToken callback fetches it from your backend. The API key stays on the server.
Exchange
The component POSTs the token to /v1/audit/portal/exchange and gets a JWT scoped to that org and intent. The token is spent. A 429 is retried up to twice after Retry-After.
Read
Events, checks and the org header come from the portal routes with the JWT. It lasts session_duration_seconds, 7,200 by default; on expiry the component calls getPortalToken again.
Send organization_id and intent with an audit:write key. The component then calls POST /v1/audit/portal/exchange and the portal routes listed on the Audit Logs API page.
// app/api/audit-portal-token/route.ts
// Runs on your server. The API key never reaches the browser.
import { InvoanceClient } from "invoance";
// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();
export async function POST() {
const organizationId = await currentCustomerOrgId(); // your own lookup
const session = await client.audit.portalSessions.create({
organizationId,
intent: "audit_logs",
sessionDurationSeconds: 3600,
});
return Response.json({ token: session.token });
}
All props are optional in type, but the component needs getPortalToken or tokenFromUrl to obtain a session.
getPortalTokenReturns a fresh one-time portal token from your backend; called on mount and again when the session expires. Use this or tokenFromUrl to get a session.
tokenFromUrlReads the one-time token from ?token= in the page URL, exchanges it and removes it from the URL.
persistSessionKeeps the session JWT in sessionStorage, so a page refresh does not need a new token.
baseUrlAPI origin; point it at your development backend.
pageSizeInitial rows per page; the visitor can change it. One of 25, 50, 100.
autoRefreshRe-fetches page 1 every 15 seconds while idle.
themeCSS variable overrides as { variables: { name: value } }; bare names get the --inv- prefix.
classNameExtra classes on the root element.
onSessionExpiredCalled when the session expired and could not be renewed.
Nine CSS variables, each an HSL triple, drive every color. Set them on :root, on any ancestor of the viewer, or through the theme prop.
<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%",
"popover-foreground": "0 0% 98%",
},
}}
/>
| --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% |
- The JWT is scoped to one org and one intent; it cannot read another org.
- On a 401 mid-session the component calls getPortalToken once and retries. If that fails it shows an expiry message and calls onSessionExpired.
- An invalid, expired or already used link shows a message instead of the viewer. Mint a fresh session to recover.
- Portal routes are limited per visitor IP: 10 requests per second on exchange, 60 on reads.
- In a browser whose Web Crypto has no Ed25519, the check still recomputes the hash and reports the signature as not verifiable, which is not a failed check.
- Every field the API returns for an event is shown, including context and metadata.
The package exports the check it runs in the drawer. verifyAuditEventDetail rebuilds the invoance.audit/1 canonical bytes, recomputes the SHA-256 hash and checks the Ed25519 signature against the key on the event.
The SDKs do the same outside the browser, with the option to pin your registered key; see verifying an export.
import { verifyAuditEventDetail, canonicalAuditBytes } from "@invoance/audit-viewer";
// event: one row as the portal, get, an export or a stream delivered it.
const detail = await verifyAuditEventDetail(event);
console.log(detail.canonicalOk, detail.recomputedHash === detail.storedHash);
console.log(detail.ed25519Supported, detail.signatureChecked, detail.signatureValid);
// The exact bytes that were hashed and signed, for your own tooling.
const bytes = canonicalAuditBytes(event);