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

audkit

v0.3.0

Published

Tamper-evident audit logging for TypeScript apps and AI agents - RFC 6962 Merkle trees, Ed25519-signed events, offline verification.

Readme

audkit

Tamper-evident audit logging for TypeScript apps and AI agents.

Every event is sequenced and committed as a leaf of your project's RFC 6962 Merkle tree, whose root is Ed25519-signed and published hourly to a public transparency log. There is no API to edit or delete a record — and if anyone rewrites history out-of-band (a rogue DBA, a doctored backup), verify() rebuilds the tree and names the exact sequence where it breaks.

npm install audkit

Five minutes to a verifiable trail

  1. Create a project at audkit.dev — the tree starts at sequence 1.
  2. Mint a scoped API key (Project → Settings → Keys) and set it as AUDKIT_API_KEY.
  3. Wrap the code you already have — pick the wrapper that matches where your mutations live:

| Where the action happens | Import | Wrapper | | --- | --- | --- | | Next.js route handler | audkit/nextjs | withAuditLogging() | | Next.js Server Action | audkit/nextjs | withAuditAction() | | AI agent tool call | audkit/ai | auditedTool() | | Anywhere else | audkit | audit.log() |

Then prove the whole history is intact whenever you like:

const receipt = await audit.verify();
// { valid: true, checkedCount: 18442, treeSize: 18442, rootHash: "a8d772…", ... } — signed

Route handlers — withAuditLogging()

Wraps a Next.js route handler (web-standard Request/Response only — no next import). Every config field is a literal value or a (possibly async) function of the request, response, result, and error:

import { withAuditLogging } from "audkit/nextjs";

export const POST = withAuditLogging(
  {
    action: "api_key.created",
    risk: "medium",
    actor: async ({ request }) => {
      const session = await auth.api.getSession({ headers: request.headers });
      return { type: "user", id: session!.user.id };
    },
    target: ({ result }) => ({ type: "api_key", id: result.apiKey.id }),
    metadata: ({ result }) => ({ scopes: result.apiKey.scopes }),
  },
  async (request) => {
    return createApiKey(await request.json());
  },
);

The event status defaults to failed when the handler throws or responds ≥ 400. If your handler returns a Response, read it in resolvers with responseJson() / responseText() — bodies are cloned so your original streams are untouched. On Vercel, the requester IP (spoof-proof x-vercel-forwarded-for) and geolocation/deployment trace are captured automatically; pass forwardVercelHeaders: false to opt out.

Server Actions — withAuditAction()

Same resolver pattern for the place most App Router mutations actually live. Resolvers see { args, result, error, status }:

"use server";
import { withAuditAction } from "audkit/nextjs";

export const changeRole = withAuditAction(
  {
    action: "role.changed",
    risk: "high",
    actor: async () => {
      const session = await getSession();
      return { type: "user", id: session.user.id };
    },
    target: ({ args }) => ({ type: "member", id: args[0].memberId }),
    metadata: ({ args, result }) => ({
      to: args[0].role,
      previous: result?.previousRole,
    }),
  },
  async (input: { memberId: string; role: string }) => {
    return updateRole(input);
  },
);

Next.js control flow is respected: a redirect() thrown from the action logs as success (the action completed, then navigated); notFound() and real errors log as failed. Whatever was thrown is rethrown so Next handles it normally.

AI agent tools — auditedTool()

Wraps an AI SDK tool so every call an agent makes enters the record — exactly one event per call, carrying success, denied, or failed. Refusals are logged before the error is thrown. Risk labels, required reasons, and an authorization gate are built in:

import { auditedTool } from "audkit/ai";

const refund = auditedTool({
  name: "refund_payment",
  inputSchema: refundSchema,
  risk: "high",
  requireReason: true,
  authorize: ({ input }) => input.amountCents <= 50_00,
  handler: ({ paymentId }) => payments.refund(paymentId),
});

await generateText({
  model,
  tools: { refund_payment: refund },
  experimental_context: { actor: { type: "user", id: user.id } },
  prompt,
});

If the tool input includes a string reason field it is recorded automatically; make it required in the schema plus requireReason: true for reliable reasons. The returned object is structurally compatible with AI SDK tools (inputSchema and parameters both supported).

The raw client

import { Audkit } from "audkit";

const audit = new Audkit({ apiKey: process.env.AUDKIT_API_KEY! });

await audit.log({
  action: "invoice.approved",
  actor: { type: "user", id: user.id, display: user.email },
  target: { type: "invoice", id: invoice.id },
  metadata: { amountCents: invoice.amountCents },
});

const { events } = await audit.query({ action: "invoice.approved", limit: 50 });

// Entity drill-down: what it did (outgoing) vs what happened to it (incoming)
const outgoing = await audit.query({
  entityType: "user",
  entityId: user.id,
  direction: "outgoing",
});

All wrappers accept apiKey/baseUrl directly, an existing client, or fall back to the AUDKIT_API_KEY / AUDKIT_BASE_URL environment variables.

Customer-held signing (Ed25519)

Bring your own key and the history becomes unforgeable even by the platform. Generate a keypair in the dashboard (Project → Settings → Signing keys) — the private key is downloaded once and never stored server-side. The SDK then signs every event's payload hash locally before it leaves your infrastructure:

const audit = new Audkit({
  apiKey: process.env.AUDKIT_API_KEY!,
  signingKeyId: process.env.AUDKIT_SIGNING_KEY_ID!,
  signingPrivateKey: process.env.AUDKIT_SIGNING_PRIVATE_KEY!,
});

The wrappers honor AUDKIT_SIGNING_KEY_ID / AUDKIT_SIGNING_PRIVATE_KEY env vars automatically. Once a signing key is registered for a project, ingest rejects unsigned events; verify() re-checks every signature against the registered public key — and keeps checking it even after retention has shredded the payload, because the signature covers the payload commitment, which the leaf retains.

Verification

const receipt = await audit.verify();
receipt.valid;        // the whole tree rebuilt from sequence 1, leaf by leaf
receipt.checkedCount; // leaves rebuilt
receipt.treeSize;     // size of the tree they reproduce
receipt.rootHash;     // the root they reproduce
receipt.firstBreak;   // { sequence, reason } — names the exact broken record
receipt.signature;    // Ed25519 by the platform's service key — check it yourself

// The platform's own control-plane actions form a second verifiable stream:
const controlPlane = await audit.verify({ stream: "project-audit" });

The client also checks the platform's work without asking it to grade itself. It pins the last signed tree head it saw and, after log()/query() (throttled) or whenever you call checkConsistency(), proves with local hash arithmetic that the served tree still contains the pinned one — throwing AudkitTamperError if it doesn't:

await audit.checkConsistency();                       // proof-checked locally
const { sth } = await audit.getRoot();                // signed head: treeSize + rootHash
const { proof } = await audit.getInclusionProof({ eventId });

Store receipts and heads over time and pin (treeSize, rootHash): a later tree that isn't consistent with an earlier one proves history was truncated or rewritten, even if it self-verifies. Roots are also published hourly to Sigstore Rekor, where the platform cannot edit them.

Offline, with nothing but an export and a root you trust:

npx audkit verify full --export events.ndjson --root a8d772…
# exit 0 = verified · 1 = tamper detected, and where · 2 = bad input

API

  • new Audkit({ apiKey, baseUrl?, signingKeyId?, signingPrivateKey? })
  • audit.log(input) → { id, status: "sealed" }
  • audit.query(input?) → { events, nextCursor? }
  • audit.exportEvents(input?) → streamed CSV / NDJSON
  • audit.verify(input?) → signed tree receipt
  • audit.getRoot(stream?) → current signed tree head + service public key
  • audit.getInclusionProof({ eventId | leafIndex, treeSize? }) → RFC 6962 audit path
  • audit.getConsistencyProof({ oldSize, newSize? }) → RFC 6962 consistency proof
  • audit.checkConsistency(stream?) → locally proves the served tree extends the pinned one

Project API keys need matching scopes: log:write for ingestion, log:read for queries, log:export for exports, and log:verify for verification receipts. Errors throw an AudkitError with status and details.

Canonicalization (RFC 8785 JCS), RFC 6962 Merkle math, and Ed25519 primitives — shared byte-for-byte with the platform's verifier — live in audkit/protocol.

License

MIT