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

@grovetech/defender

v0.25.0

Published

Active runtime protection for vibe-coded apps — drops in as Express middleware (web + AI layer), blocks prompt injection, PII leaks, and sensitive paths in real time. By Grovetech AI.

Readme

@grovetech/defender

Active runtime protection for vibe-coded apps. Drop-in Express middleware that blocks prompt injection, sensitive-path probes, and PII / API-key leaks in your LLM responses — in real time, without touching anyone else's systems.

Defender is the active counterpart to the Grovetech Vibe Code Health Scanner. The scanner is a one-shot pen-test; Defender is permanent runtime defence.

Install

npm install @grovetech/defender

Get an API key at https://grovetechai.com/dashboard?tab=defender. Free plan includes 1 000 requests/month with the input guard enabled.

Try it without installing anythingopen the live demo on Replit or read the source in server.ts.

Quick start (Express)

import express from "express";
import { defender } from "@grovetech/defender";

const app = express();
app.use(express.json());

const d = defender({ apiKey: process.env.GROVETECH_DEFENDER_KEY });

// Web layer — security headers + sensitive-path block (.env, .git/*, …)
app.use(d.web());

// AI layer — screens req.body.prompt / .messages for prompt injection
app.use("/api/chat", d.ai());

// Output guard — wrap your own LLM call
app.post("/api/chat", async (req, res) => {
  const reply = await callOpenAi(req.body.prompt);
  const out = d.guardOutput(reply);
  if (!out.allowed) return res.status(502).json({ error: "Output blocked" });
  res.json({ reply });
});

app.listen(3000);

Build-time secret check

Add to package.json:

{ "scripts": { "postbuild": "defender check ./dist" } }

The build fails if any leaked OpenAI / Anthropic / AWS / Stripe key, GitHub PAT or PEM private key is found in the emitted bundle.

What gets blocked

| Layer | Examples | | -------- | -------------------------------------------------------------- | | Web | /.env, /.git/config, /wp-config.php, missing CSP/HSTS | | AI input | "ignore previous instructions", DAN/jailbreak, token bombs | | AI output| Leaked sk-…, AKIA…, ghp_…, PEM keys, system-prompt leak |

Detection patterns are recycled from the same engine that powers our hosted scanner (server/security-scan.ts, server/vibe-coding-scan.ts, server/ai-agent/attacks.json) so what we test for in audits is what we block at runtime.

AI conversation defense (new in v2)

Three opt-in helpers for chatbot and RAG apps. None of them auto-mount — call them where you want the protection.

// 1. Multi-turn jailbreak + per-session rate limit
app.use("/api/chat", d.conversationGuard({
  sessionIdFrom: (req) => req.body.sessionId,
  // defaults: 20 msg/min, 50k tokens/h, window 10, threshold 6.0
}));

// 2. RAG poisoning guard — invisible Unicode, embedded directives,
//    markdown image exfil, base64 smuggling. Two hooks:
const rag = d.rag();

app.post("/api/docs", async (req, res) => {
  const r = await rag.inspectIngest({ id, source, content });
  if (r.blocked) return res.status(400).json({ findings: r.findings });
  await vectorDb.upsert({ id, source, content: r.sanitizedContent });
});

app.post("/api/chat", async (req, res) => {
  const chunks = await vectorDb.query({ ... });
  const { allowed } = await rag.inspectRetrieval(chunks);
  // pass only `allowed` to your LLM
});

// 3. Behavioral anomaly — z-score on message length + gap between messages
const anomaly = d.anomalyDetector();
const obs = await anomaly.observe(sessionId, { text: req.body.message });
if (obs.suspicious) { /* log, captcha, ban — your choice */ }

Full reference, defaults table, rollout playbook and privacy notes: docs/defender/conversation-rag-anomaly.md.

Status: beta — covered by 599 unit tests (packages/defender/src/*.test.ts), dogfooded on the Viki chat widget at grovetechai.com in audit-only mode. Start with audit, flip to block after a week of clean logs.

Tool-call guard (new in 0.4.0)

Deterministic allowlist/denylist for AI agents with tools — decide which tools the agent may call before execution. Glob patterns (* = one segment, trailing .* = one or more), deny wins over allow, empty allow = audit mode (nothing blocked, fail-open).

const guard = d.tool({
  allow: ["search", "db.read.*"],
  deny:  ["db.write.*", "payments.*"],
});

// Before executing a tool call:
const v = guard.check({ tool: "db.write.payments" });
// { allowed: false, rule: "tool_denylist", matched: "db.write.*" }

// Or wrap an OpenAI/Vercel AI SDK-style tools array — disallowed tools are
// removed from the list, allowed ones get a runtime check around execute():
const safeTools = guard.wrap(tools);

Violations report to telemetry as layer tool (dashboard on Pro+; local enforcement always works regardless of plan).

Agent layer (0.6.0 – 0.9.0)

Once the agent can act, screening the prompt is no longer enough. These four additions decide on a single agent step, right before it executes.

Argument inspection (0.6.0). An attack rarely looks like payments.send; it looks like an allowed db.query carrying DROP TABLE. The guard now inspects what gets passed in, not just the tool name.

guard.check({ tool: "db.query", args: { sql: "DROP TABLE users" } });
// { allowed: false, rule: "tool_args", … }

Egress allowlist (0.7.0). With agents, exfiltration usually leaves through an HTTP request. d.egress() decides on the destination. Internal ranges and cloud metadata (169.254.169.254, metadata.google.internal) are blocked by default.

const egress = d.egress({ allow: ["api.openai.com", "*.stripe.com"] });
await egress.check("http://169.254.169.254/latest/meta-data/");
// { allowed: false, rule: "egress_internal" }

It is a decision function, not a global patch — call it where the outbound request is actually made, otherwise it protects nothing.

MCP policy + rug pull (0.8.0). The verdict of an MCP configuration scan gets enforced at runtime. d.mcpPolicy() pulls the policy on its own (ETag, 15-minute interval, on-disk cache for cold start); unknown servers stay fail-open, dangerous is blocked. Pass definition and a server that rewrote its tool description after approval is blocked too — even if the scan marked it safe, which is exactly what a rug pull looks like.

const policy = d.mcpPolicy();
const guard  = d.tool({ mcp: () => policy.current() });   // function, not object:
                                                          // a fresh scan applies
                                                          // without a restart

Destructive action budget (0.9.0). A cap on the number of irreversible steps per session — cost guards watch dollars, but five silent deletes cost a fraction of a cent.

d.tool({ budget: { maxDestructivePerSession: 3, destructive: ["db.write.*"] } });

Destructive means you declared it, or the arguments gave it away. It is never guessed from the tool namesearch_deleted_items is a read. A blocked call does not count against the budget, otherwise a series of rejected attempts would exhaust the cap and turn the protection into a denial-of-service lever.

Fail-closed mode (0.20.0, OWASP ASI02)

By default the tool guard is fail-open in ambiguous situations: an MCP server the scan has never seen is allowed, and if the policy source throws, the call proceeds with a warning. That is the safe choice for availability, but OWASP ASI02 (Tool Misuse) wants the opposite — when in doubt, stop.

d.tool({ strict: true, /* … */ });

strict flips only the ambiguous defaults to block:

  • an MCP server not present in the scan,
  • the MCP policy source throwing an exception.

It does not override anything you set explicitly (onUnknown / onMcpPolicyUnavailable always win) and it does not close audit mode — an empty allow list still means "let everything through and just watch", because that is a deliberate choice, not ambiguity. Default stays false for backward compatibility; turn it on once you know your MCP servers.

Model & provider allowlist (0.12.0)

d.tool() guards tools, d.egress() guards generic outbound calls. Neither catches a compromised dependency rewriting your LLM base URL — at which point your prompts, system prompt and key go to someone else's server while the app looks fine.

const m = d.model({ allowModels: ["gpt-4o-mini"], allowHosts: ["api.openai.com"] });
m.guard({ model: "gpt-4o-mini", baseUrl: openai.baseURL }, () => openai.chat…);

It is a decision function, not a global patch — call it where the model is called, or it protects nothing. Note: if you set allowHosts and then call without baseUrl, the call is blocked. Passing silently would promise protection we are not performing.

Human approval for dangerous actions (0.12.0)

A third state between allow and deny (OWASP LLM06). The agent may delete a customer or send a payment — but not without a human knowing.

const g = d.tool({
  allow: ["db.*", "payments.*"],
  approval: { require: ["payments.*", "db.delete.*"], onRequest: sendToSlack },
});

const v = g.check({ tool: "payments.send", session, args });
// { allowed: false, rule: "approval_required", approvalId: "apr_…" }
g.approve(v.approvalId, "ondrej");   // once a human clicks

The grant is single-use, time-limited and bound to a hash of the arguments — approving $100 will not authorise $100,000, and one "yes" does not open the door permanently. How you ask the human (Slack, email, a button in your admin) is up to you.

Order inside the guard: hard rules → approval → budget. A call awaiting approval has not run yet, so it must not consume the destructive-action budget; otherwise three unanswered requests would exhaust the cap.

Reverse proxy for non-Node stacks (0.11.0)

The SDK is Express middleware, so it only runs in Node. To guard a Java, PHP or Python backend, run Defender as a reverse proxy that sits between your TLS terminator and the app — not in front of it:

internet :443 → nginx (TLS, unchanged) → defender :8088 → your app :8080
defender proxy --target http://127.0.0.1:8080 --port 8088          # audit
defender proxy --target http://127.0.0.1:8080 --port 8088 --block  # enforce

It applies the same engine (sensitive paths, prompt injection on AI routes, telemetry) at the HTTP level. Measured overhead on a 1 KB response: +0 ms at the median, +2 ms at p99.

Three things worth knowing, all found by a customer and fixed in 0.11.0:

  • It binds to 127.0.0.1. Earlier versions bound to 0.0.0.0, which made the proxy reachable from outside and thus a way around your TLS terminator. Use --host if you really need another interface — you will get a warning.
  • WebSocket is tunnelled to the backend. Earlier versions dropped the connection while the docs claimed otherwise. We do not see inside the tunnel, so that traffic is not counted as protected — route it around Defender if its contents matter to you.
  • The original Host header is preserved. Use --rewrite-host for the old behaviour.

Output guard does not work in proxy mode yet; it needs a hook inside your app.

Cost attribution (new in 0.4.0)

Break down LLM spend by client and project in the dashboard (Pro+). Just tag your existing recordCost calls:

await d.recordCost(
  { model: "gpt-4o-mini", inputTokens, outputTokens,
    tags: { client: "acme", project: "web" } },
);

The SDK reports only the model, token counts, cost in micro-USD and the tags — never prompts or PII. Untagged spend shows up as "(untagged)".

Serverless (Vercel, Lambda, Cloud Run) — read this (0.19.0)

Telemetry is batched: it is sent on a timer, not on every request. On a long-running server that is what you want. On serverless it used to mean the batch was lost — the platform freezes the container the moment the response is sent, and the timer never fires.

Since 0.19.0 the SDK detects VERCEL, AWS_LAMBDA_FUNCTION_NAME, K_SERVICE, FUNCTIONS_WORKER_RUNTIME and NETLIFY, and sends the batch immediately instead of waiting. Override with defender({ serverless: true | false }).

This improves the odds; it is not a guarantee. If your response returns before the outbound fetch finishes, the batch is still lost. If the numbers matter to you, flush explicitly:

app.post("/api/contact", async (req, res) => {
  res.json({ ok: true });
  await d.flush();          // or the platform's waitUntil()
});

Why your dashboard can look empty even though it works

Two things are worth knowing before you conclude Defender is broken:

  • Static pages never reach the SDK. On Vercel, HTML is served from the CDN and the request never touches your Node function, so d.web() has nothing to run on. Defender only sees the routes your functions actually handle.
  • A CDN/WAF in front of you eats most of the traffic Defender would report. If Cloudflare already blocks /wp-admin/install.php, /.env and friends, those probes die before they reach the app. You are protected — just not by us, and not counted by us. Do not turn that off to make a graph move.

Until 0.19.0 the SDK also sent nothing at all when idle, so last seen meant "traffic last passed through", not "the SDK is alive". A low-traffic endpoint therefore showed as offline. 0.19.0 adds a heartbeat every 15 minutes (heartbeatMs, 0 disables it), so "online" now means what it says.

Plans & limits

| Plan | Req/month | Output | Tool | Conv / RAG / Anomaly | Slack/Teams | | ------ | --------- | ------ | ---- | -------------------- | ----------- | | Free | 1 000 | – | – | rate-limit only | – | | Solo | 10 000 | yes | – | yes | – | | Pro | 100 000 | yes | yes | yes | yes | | Agency | 1 000 000 | yes | yes | yes + custom | yes (custom) |

Telemetry is best-effort — if Grovetech is unreachable, Defender still blocks locally and never crashes your app.

Licence

MIT — © Grovetech AI s.r.o.