Documentation

EdgeShield is an edge-native security toolkit for modern TypeScript runtimes. Compose rate limiting, bot detection, and CSRF guards with pluggable storage — no Node.js APIs required.

Edge-first

Web Standard Request / Response. Runs on Cloudflare Workers, Vercel Edge, Deno, Bun, and Node 20+.

Composable guards

Chain rateLimit, botGuard, and csrfGuard via middleware or presets.

Storage-agnostic

Memory, Upstash, Cloudflare KV, Vercel KV, and Deno KV adapters with a shared conformance suite in CI.

Installation

npm
npm install edgeshield

Import only the subpaths you need — the package is tree-shakeable and ships zero runtime dependencies.

Quick start

Rate-limit incoming requests with an in-memory store (swap in KV/Redis for production):

TypeScript / ESM
import { rateLimit, slidingWindow } from "edgeshield/ratelimit";
import { memory } from "edgeshield/storage/memory";

const limiter = rateLimit({
  storage: memory(),
  algorithm: slidingWindow(100, "15m")
});

const result = await limiter.check(request);
if (!result.success) {
  return new Response("Too Many Requests", {
    status: 429,
    headers: result.headers
  });
}
Production storage Replace memory() with cloudflareKV, upstash, vercelKV, or denoKV so counters survive cold starts and scale across regions.

Supported runtimes

Runtime CI Notes
Node.js 20 / 22 test Lint, typecheck, coverage, build, bundle size check
Bun bun bun run test against source
Deno 2.x deno Smoke tests against built dist/
Cloudflare Workers cloudflareKV adapter
Vercel Edge vercelKV adapter

Rate limiting

EdgeShield supports sliding and fixed window algorithms with configurable limits, windows, and key prefixes.

import { rateLimit, slidingWindow, fixedWindow } from "edgeshield/ratelimit";
import { upstash } from "edgeshield/storage/upstash";

const limiter = rateLimit({
  storage: upstash({ url: env.UPSTASH_URL, token: env.UPSTASH_TOKEN }),
  prefix: "edgeshield:api",
  algorithm: slidingWindow(100, "15m"),
  failOpen: true // default — allow traffic if storage throws
});

Algorithms

Function Description
slidingWindow(limit, window) Rolling window counter — smoother burst handling
fixedWindow(limit, window) Discrete window buckets — simpler, lower storage cost

Window strings accept s, m, h, or d suffixes (e.g. "15m", "1h").

Bot detection

Score requests using fingerprint heuristics and optional allow/block rules. Three modes:

Mode Behaviour
detect Score only — never blocks; use for logging or metrics
block Return 403 when score exceeds threshold; optional VDF header challenge
challenge Serve HTML Sloth VDF page to suspicious browser clients
import { botGuard } from "edgeshield/bot";

const guard = botGuard({
  mode: "block",
  threshold: 60,
  rules: {
    allow: [/googlebot/i, /bingbot/i],
    block: [/curl/i, /python-requests/i, /scrapy/i]
  }
});

const bot = await guard.check(request);
if (!bot.success) {
  return new Response("Forbidden", { status: 403, headers: bot.headers });
}

Sloth VDF challenge

In block mode, suspicious clients can be asked to solve a Sloth VDF proof-of-work before retrying. The core implementation is adapted from dignity.js (Apache-2.0).

import { botGuard, VDF } from "edgeshield/bot";

const guard = botGuard({
  mode: "block",
  threshold: 40,
  vdf: { enabled: true, steps: 20, maxAgeMs: 300_000 }
});

const first = await guard.check(request);
if (first.reason === "vdf_challenge_required") {
  const challenge = first.headers.get("x-edgeshield-vdf-challenge");
  const steps = Number(first.headers.get("x-edgeshield-vdf-steps"));
  const challengeHex = challenge?.split(".")[0] ?? "";
  const proof = await VDF.compute(challengeHex, steps);
  // Retry with:
  // x-edgeshield-vdf-challenge: <challenge>
  // x-edgeshield-vdf-solution: <proof>
}

Challenge mode

Use mode: "challenge" for browser traffic. Suspicious clients receive a self-contained HTML page that runs the VDF client-side and retries automatically.

import { botGuard } from "edgeshield/bot";
import { memory } from "edgeshield/storage/memory";

const guard = botGuard({
  mode: "challenge",
  threshold: 60,
  storage: memory(), // optional: one-time solution anti-replay
  vdf: { steps: 20, maxAgeMs: 300_000 },
  challenge: {
    renderer: (context) => `<html>...</html>` // optional custom HTML
  }
});

const result = await guard.check(request);
if (!result.success && result.body) {
  return new Response(result.body, {
    status: 403,
    headers: result.headers
  });
}
Middleware integration Next.js and Hono middleware return the HTML challenge page automatically when body is present on the guard result.

CSRF protection

Protect state-changing routes with double-submit cookies or origin header verification.

import { csrfGuard } from "edgeshield/csrf";

const csrf = csrfGuard({
  mode: "double-submit",
  secret: process.env.CSRF_SECRET!,
  ttl: "1h",
  ignorePaths: ["/api/webhooks/**"]
});

const token = await csrf.generate(request);
const cookie = csrf.buildCookie(token);

const verify = await csrf.verify(request);
if (!verify.valid) {
  return new Response("Forbidden", { status: 403 });
}
Mode Mechanism
double-submit HMAC-signed token in cookie + header/body field
origin-check Validate Origin / Referer against allowed hosts

Middleware

Run multiple guards in sequence. The first failure short-circuits with the appropriate response.

Next.js

import { createMiddleware } from "edgeshield/middleware/nextjs";
import { rateLimit, slidingWindow } from "edgeshield/ratelimit";
import { botGuard } from "edgeshield/bot";

export default createMiddleware(
  rateLimit({ storage, algorithm: slidingWindow(100, "15m") }),
  botGuard({ mode: "challenge", threshold: 60 })
);

Hono

import { Hono } from "hono";
import { edgeshield } from "edgeshield/middleware/hono";
import { rateLimit, slidingWindow } from "edgeshield/ratelimit";
import { botGuard } from "edgeshield/bot";

const app = new Hono();

app.use(
  "/api/*",
  edgeshield(
    rateLimit({ storage, algorithm: slidingWindow(100, "15m") }),
    botGuard({ mode: "block", threshold: 60 })
  )
);

Generic (any framework)

import { shield } from "edgeshield/middleware/generic";
import { rateLimit, slidingWindow } from "edgeshield/ratelimit";
import { botGuard } from "edgeshield/bot";

const protect = shield(
  rateLimit({ storage, algorithm: slidingWindow(100, "15m") }),
  botGuard({ mode: "block", threshold: 60 })
);

const blocked = await protect(request);
if (blocked) return blocked;

Presets

Opinionated defaults for common route classes. Import from edgeshield/presets.

import { presets } from "edgeshield/presets";
import { memory } from "edgeshield/storage/memory";

const storage = memory();

// Rate-limit-only presets
const apiLimiter = presets.api({ storage, limit: 100, window: "15m" });
const authLimiter = presets.auth({ storage });
const pageLimiter = presets.page({ storage });

// Composite shields (rate limit + bot + optional CSRF)
const apiShield = presets.apiShield({
  storage,
  csrfSecret: process.env.CSRF_SECRET
});
const authShield = presets.authShield({
  storage,
  csrfSecret: process.env.CSRF_SECRET!
});
const pageShield = presets.pageShield({ storage });
Preset Default limit Guards
api 100 / 15m Rate limit
auth 5 / 15m Rate limit
page 300 / 1m Rate limit
apiShield 100 / 15m Rate limit + bot block + optional CSRF
authShield 5 / 15m Rate limit + bot block + CSRF (required secret)
pageShield 300 / 1m Rate limit + bot challenge

Storage adapters

All adapters implement the same four-method contract and pass a shared conformance suite in CI.

Cloudflare KV

import { cloudflareKV } from "edgeshield/storage/cloudflare-kv";

const storage = cloudflareKV({ binding: env.EDGE_KV, prefix: "edgeshield" });

Vercel KV

import { vercelKV } from "edgeshield/storage/vercel-kv";

const storage = vercelKV({ client: kv, prefix: "edgeshield" });

Deno KV

import { denoKV } from "edgeshield/storage/deno-kv";

const kv = await Deno.openKv();
const storage = denoKV({ kv, prefix: "edgeshield" });

Upstash Redis

import { upstash } from "edgeshield/storage/upstash";

const storage = upstash({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!
});

Memory (dev / tests)

import { memory } from "edgeshield/storage/memory";

const storage = memory();

Custom storage adapter

Implement four methods against the shared contract:

import type { StorageAdapter } from "edgeshield";

export function createMyAdapter(client: MyKVClient): StorageAdapter {
  return {
    get: (key) => client.get(key),
    set: (key, value, ttlMs) => client.set(key, value, { px: ttlMs }),
    increment: async (key, ttlMs) => {
      const next = await client.incr(key);
      await client.pexpire(key, ttlMs);
      return next;
    },
    delete: (key) => client.del(key)
  };
}
Contract notes Keys are namespaced by guards via prefix. increment must return the new counter and honour TTL on first write. Expired keys should behave as missing on get.

Package exports

Import path Primary exports
edgeshield Core types, all guards, storage helpers, presets
edgeshield/ratelimit rateLimit, slidingWindow, fixedWindow
edgeshield/bot botGuard, VDF, createVdfChallenge
edgeshield/csrf csrfGuard, verifyOrigin
edgeshield/storage/* memory, upstash, cloudflareKV, vercelKV, denoKV
edgeshield/middleware/* createMiddleware, edgeshield, shield
edgeshield/presets presets.api, apiShield, authShield, pageShield

Machine-readable metadata: openapi-like.json

Comparison

Feature EdgeShield @upstash/ratelimit rate-limiter-flexible express-rate-limit
Edge-native Yes Yes No No
Storage-agnostic Yes Upstash only Redis/Mongo/Postgres Memory/Redis
Bot detection Yes No No No
CSRF protection Yes No No No
Tree-shakeable subpaths Yes No No No
Zero dependencies Yes Needs @upstash/redis 0 deps (core) 0 deps
Bundle size (ratelimit) < 4 KB gzip (CI enforced) ~8 KB ~15 KB ~5 KB

Development

# Run tests (181+ vitest + Deno smoke)
npm test

# Coverage
npm run test:coverage

# Build
npm run build

# Bundle size budget
npm run size:check

# Serve docs locally
npm run docs:serve

# Multi-runtime
npm run test:bun
npm run build && npm run test:deno