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

@kajaril/audit-event-mcp

v0.2.0

Published

Agent steward infrastructure — hash-chained audit log, Merkle notary, and GDPR/AI-Act compliance skill.

Readme

@kajaril/audit-event-mcp

Agent steward infrastructure — hash-chained audit log with Merkle notarisation and a GDPR/AI-Act compliance skill.

CI npm npm downloads License: MIT Smithery


What it does

  • Records agent events (tool calls, decisions, human turns) to a per-client SQLite log with a SHA-256 hash chain — every record is cryptographically linked to the previous one
  • Optionally notarises batches with a Merkle root + Ed25519 signature (paid tier) — external auditors can verify without contacting kajaril
  • Exports all events for a data subject as NDJSON on demand (GDPR Art. 20 portability)
  • Ships a Claude Code skill that runs 8 GDPR/AI-Act axes against any event store

MCP endpoint

https://audit-event.kajaril.com/mcp

JSON-RPC 2.0 over HTTPS, authenticated via Cloudflare Access (service token) or an OAuth client-credentials access token — see Authentication. Contact [email protected] for onboarding.

Tools

| Tool | Description | |---|---| | record_event | Write one audit event. Returns { id, chain_hash }. | | verify_chain | Recompute chain_hash for a range of events. Returns verified count and broken entries. | | query_events | Filter by session, agent, event type, or date range. input_hash is never returned. | | export_dossier | Export all events for a subjectId as NDJSON (GDPR Art. 20). Returns a 1-hour download URL. | | request_approval | Ask a human to approve an action. Returns approvalId, an approvalUrl approve page, and your webhookSecret. Witnessed as approval.requested in the chain. | | check_approval | Poll an approval: pending \| approved \| denied \| timeout, with reason and responder once decided. |

Quick start

Record an event:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "record_event",
    "arguments": {
      "agentId": "travel-assistant",
      "eventType": "tool.call",
      "purpose": "User requested flight search via travel assistant",
      "sessionId": "550e8400-e29b-41d4-a716-446655440000",
      "input": { "tool": "search_flights", "origin": "LHR", "dest": "JFK" },
      "lawfulBasis": "contract",
      "subjectId": "user-8821"
    }
  }
}

Verify the chain:

{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
  "params": { "name": "verify_chain", "arguments": { "limit": 100 } } }

Export a data subject's records:

{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
  "params": { "name": "export_dossier", "arguments": { "subjectId": "user-8821" } } }

Authentication

Three ways in; all end at a signature-verified client_id that selects your tenant — nothing user-supplied ever does.

Cloudflare Access service token (manual onboarding): send the token headers, CF Access injects a verified JWT. Full surface.

OAuth browser flow (MCP clients, zero copy-paste): point your client at the server and let it discover the rest —

claude mcp add --transport http audit-event https://audit-event.kajaril.com/mcp

RFC 8414 metadata advertises dynamic client registration (/oauth/register), authorization with PKCE S256 (/oauth/authorize), and the shared token endpoint. Consent is approved in the browser by a signed-in tenant operator; the granted scope (agent or admin) is bound into the issued token.

OAuth client-credentials (M2M): exchange a client secret for a 1-hour Bearer token at POST /oauth/token (application/x-www-form-urlencoded, HTTP Basic or client_id / client_secret body params):

curl -s https://audit-event.kajaril.com/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials -d scope=agent

Two scopes, issued and rotated independently:

| Scope | May call | |---|---| | agent | record_event, request_approval, check_approval | | admin | everything, plus POST /credentials/rotate |

Secrets are shown once at issue/rotation (POST /credentials/rotate, body {"scope":"agent"}) and stored only as hashes in your tenant's Durable Object. Rotating a scope invalidates its old secret immediately; outstanding access tokens expire within the hour.

The approval tools are also plain REST for production backends: POST /approvals (same body as request_approval) and GET /approvals/:id, with Authorization: Bearer <access token>.

How it works

Hash chain

Every event commits to all events before it. The chain is recomputable from first principles:

input_hash_slot = input_hash ?? input_hash_omitted_reason
chain_hash      = SHA-256(id + "|" + event_type + "|" + input_hash_slot + "|" + prev_hash)

When a caller supplies inputHashOmittedReason instead of input, the reason string enters the chain — the omission itself is tamper-evident.

Merkle notary (paid tier)

Every 15 minutes (or at 1,000 pending events), the notary Worker:

  1. Collects { id, chain_hash } pairs from pending records
  2. Sorts them by id and builds a SHA-256 binary Merkle tree
  3. Signs the root with Ed25519
  4. Writes merkle_root + notary_sig back to each record

The notary public key is published at /.well-known/notary-pubkey (also proxied same-origin on go.kajaril.com for the verify page). Any auditor can verify signatures offline without contacting kajaril. The notary never receives input_hash, payload_ref, or any payload content.

Verifying a dossier

export_dossier returns a link to a human-readable dossier on go.kajaril.com with a raw JSONL evidence file attached. Anyone can drop that file onto https://go.kajaril.com/verify — the browser recomputes every record's chain fingerprint from its exported preimage (id | event_type | input_hash ?? omitted_reason | prev_hash) and checks each notary Ed25519 signature against the published key. Nothing is uploaded; the verdict is computed client-side.

Privacy design

  • input is hashed locally; the raw value is not stored unless an R2 bucket is explicitly bound
  • input_hash is excluded from query_events responses; dossiers export it (with prev_hash) as the chain preimage that makes independent verification possible — they are digests, never content
  • payload_ref is never returned to callers — enforced at every handler boundary
  • An empty export_dossier result (eventCount: 0) is valid GDPR evidence that no data exists for a subject

Event types

tool.call | tool.result | decision.made |
human.turn | memory.read | memory.write | error.raised |
approval.requested | approval.decided

approval.* types are reserved: the witness records them itself when a human approval is requested and decided. record_event refuses them — an agent must never be able to fabricate human-decision evidence. (approval.deferred and approval.escalated are reserved for later.)

Decision webhooks

When an approval carries a callback_url, the decision (approve/deny) is POSTed to it the moment a human decides — this is what lets an interrupted agent resume instead of poll. Timeouts never fire a webhook; polling resolves those.

Every delivery is signed. The body is JSON:

{
  "type": "approval.decided",
  "approval": { "id": "…", "status": "approved", "reason": null, "responderId": "…",
                "agentId": "…", "sessionId": "…", "actionSummary": "…",
                "actionPayloadHash": "…", "createdAt": "…", "decidedAt": "…", "expiresAt": "…" },
  "chainEvent": { "id": "…", "chainHash": "…" }
}

chainEvent points at the approval.decided entry in the hash chain — cite it as evidence.

Verifying the signature. Your webhook secret (whsec_…) is returned in every request_approval response as webhookSecret — no dashboard needed. Each delivery carries:

X-Kajaril-Signature: t=<unix seconds>,v1=<hex>

To verify:

  1. Read t and v1 from the header. Reject if |now − t| exceeds 300 seconds.
  2. Compute HMAC-SHA256(key = the literal secret string (UTF-8, whsec_ prefix included), message = "{t}." + raw request body).
  3. Hex-encode and compare to v1 with a constant-time comparison.
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));

The reference implementation is verifyWebhookSignature in src/lib/webhook.ts — the same code path our tests run.

Self-hosting note: webhook signing derives per-tenant secrets from the WEBHOOK_SIGNING_SECRET Workers Secret. If it is unset, webhooks are not sent at all (never unsigned) and request_approval returns webhookSecret: null; polling still works.

Notification channels

request_approval accepts channels: ["telegram", "email"] and notifies the approver over a ladder: instant first, email as the fallback.

Telegram (instant). Connect once — POST /channels/telegram/connect (admin scope) returns a one-time t.me deep link (15-minute expiry). Open it, press Start, and approval cards arrive in that chat with inline Approve / Deny / Deny with reason buttons. Deny-with-reason prompts for a short reply that is recorded with the decision — stronger Art. 14 evidence than a binary stamp. Connecting again replaces the bound chat.

Email (fallback). Set the approver address once — POST /channels/email (admin scope) with {"address": "…"}. The email fires only when it is needed:

  • immediately, if no instant channel delivered (not connected, send failed, or not requested);
  • after 10 minutes, if an instant card was delivered but the approval is still undecided;
  • never, if the approval would expire before the email could matter.

Mail is sent from [email protected] with the decide link; replies reach a human via [email protected].

Dispatch report. Every request_approval response includes what actually happened:

"notifications": { "telegram": "sent", "email": "scheduled" }

(telegram: sent | failed | not_connected | unconfigured | not_requested; email: immediate | scheduled | expires_first | no_address | unconfigured | arm_failed | not_requested.)

TTL defaults follow the channels: 30 minutes when an instant channel is in play, 4 hours when the request is email-only, explicit ttlSeconds always wins.

Self-hosting note: channels are optional. Without TELEGRAM_BOT_TOKEN / RESEND_API_KEY / CHANNELS_KV, dispatch reports unconfigured and the approval still works through approvalUrl + polling. The bindings are documented in wrangler.jsonc and wrangler.go.jsonc.

Lawful basis (GDPR Art. 6)

legitimate_interest | contract | legal_obligation |
vital_interest | public_task | consent

Provide lawfulBasis for any event that processes personal data. Omit it for events that do not.

Tiers

| Capability | Free | Paid | |---|---|---| | Hash-chained event writes | yes | yes | | verify_chain | yes | yes | | export_dossier (GDPR Art. 20) | yes | yes | | R2 payload storage | optional | optional | | Merkle root + Ed25519 notarisation | — | yes | | compliance-audit axis 6 (notary coverage) | skipped | evaluated |

compliance-audit skill

skills/compliance-audit/SKILL.md is a Claude Code skill that runs 8 GDPR/AI-Act compliance axes against any event store. Copy it into your workspace:

cp -r node_modules/@kajaril/audit-event-mcp/skills/compliance-audit .claude/skills/

Axes:

  1. Lawful basis present — GDPR Art. 5–6
  2. Purpose specificity — GDPR Art. 5(1)(b)
  3. Subject linkage on human.turn events
  4. Retention bounded (≤ 730 days)
  5. Chain integrity — 10% sample recompute
  6. Notary coverage ≥ 95% (paid tier only)
  7. High-risk event completeness — AI Act Art. 12–13
  8. Data minimisation signal — payload_ref fraction

Each axis produces pass | fail | warn with an evidence snippet. Output is JSON + a markdown summary block.

Self-hosted deployment

Prerequisites

  • Cloudflare account with Workers and Durable Objects enabled
  • wrangler CLI authenticated

Steps

# 1. Create R2 bucket
wrangler r2 bucket create audit-payloads

# 2. Set notary signing key (paid tier)
wrangler secret put NOTARY_PRIVATE_KEY --config wrangler.notary.jsonc
# Value: hex-encoded Ed25519 private key — never committed to source

# 3. Deploy Workers
wrangler deploy
wrangler deploy --config wrangler.notary.jsonc

# 4. Add audit-event.kajaril.com as a CF Access Self-hosted Application
# Issue one service token per client with custom.client_id claim

# 5. Onboard a client
npx tsx src/scripts/onboard-client.ts --client-id acme-crm --tier free --region eu

# 6. Verify
curl https://audit-event.kajaril.com/health

Never run wrangler deploy from a dirty working tree. The deployed code must match git HEAD.

Development

npm install
npm test          # 81 tests (node:sqlite adapter, no cloudflare/vitest-pool-workers)
npm run typecheck
npm run lint

License

MIT. Copyright © 2026 Kajaril Ltd.