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.2.0

Published

Tamper-evident audit logging for TypeScript apps and AI agents — hash-chained, Ed25519-signed events with one-call verification.

Readme

audkit

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

Every event is sequenced, hash-linked to the one before it, and signed. 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() re-walks the chain 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 chain 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, headSequence: 18442, ... } — 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 — as pending, then success, denied, or failed. 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 a payload is deleted by retention, because the signature covers the retained payload hash.

Verification

const receipt = await audit.verify();
receipt.valid;        // the whole chain re-walked from sequence 1
receipt.firstBreak;   // { sequence, reason } — names the exact broken record
receipt.signature;    // the receipt itself is signed by the platform

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

Store receipts over time and pin headSequence/headHash: a later receipt reporting a lower head — or a different hash at the same sequence — means history was truncated, even if the remaining chain still self-verifies.

API

  • new Audkit({ apiKey, baseUrl?, signingKeyId?, signingPrivateKey? })
  • audit.log(input){ id, status: "queued" }
  • audit.query(input?){ events, nextCursor? }
  • audit.exportEvents(input?) → streamed CSV / NDJSON
  • audit.verify(input?) → signed chain receipt

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.

Low-level canonicalization and Ed25519 primitives (shared byte-for-byte with the platform's verifier) live in audkit/protocol.

License

MIT