Skip to content
Documentation

Get started

From install to a connected fleet. The full reference, with every option, lives in the package README.

Install

npm install lightmoon@alpha
# pnpm add lightmoon@alpha · yarn add lightmoon@alpha · bun add lightmoon@alpha

LightMoon has no runtime dependencies, ships ESM and CommonJS with types, and runs on Node 20+, Bun, Deno, Cloudflare Workers, Vercel and Netlify.

Frameworks

Every adapter takes the same options, or a WAF instance from createWaf() so several entry points share counters and bans.

FrameworkImportSetup
Expresslightmoon/expressapp.use(lightmoon(opts)) after express.json()
Fastifylightmoon/fastifyawait app.register(lightmoonFastify(opts))
Koalightmoon/koaapp.use(lightmoonKoa(opts))
NestJSlightmoon/nestjsconsumer.apply(lightmoonNest(opts)).forRoutes('*')
Honolightmoon/honoapp.use('*', lightmoonHono(opts))
Next.jslightmoon/nextexport default lightmoonNext(opts, { next: () => NextResponse.next() })
Nuxtlightmoon/nuxtdefineEventHandler(lightmoonNuxt(opts))
SvelteKitlightmoon/sveltekitexport const handle = lightmoonSvelteKit(opts)
Astrolightmoon/astroexport const onRequest = lightmoonAstro(opts)
React Router, Remixlightmoon/react-routerexport const middleware = [lightmoonReactRouter(opts)]
Cloudflare Workerslightmoon/cloudflarelightmoonWorker(handler, (env) => opts)
Vercellightmoon/vercelexport default lightmoonVercel(opts)
Netlifylightmoon/netlifyexport default lightmoonNetlify(opts)
AWS Lambdalightmoon/aws-lambdaexport const handler = withLightmoonLambda(fn, opts)
Bunlightmoon/bunBun.serve({ fetch: lightmoonBun(handler, opts) })
Denolightmoon/denoDeno.serve(lightmoonDeno(handler, opts))
Any Fetch handlerlightmoon/fetchwithLightmoon(handler, opts)

Adapters for hapi, Elysia, Oak, AdonisJS, h3, SolidStart, Qwik, TanStack Start, Fresh, Connect and node:http are listed in the README.

Presets

No preset ever shows a CAPTCHA. Start with balanced and move to strict once monitor mode looks clean.

balancedstrictapiparanoid
Paranoia level1213
Global rate limit300/min150/min600/min90/min
Browser checks✓✓–✓
Automated clientsscore onlyblockscore onlyblock
Response masking–✓–✓

OWASP CRS rules

The 218 rules ported from CRS 4.29 ship in a separate entry point, so you only load them if you want them:

import { crsRules } from 'lightmoon/crs';

app.use(lightmoon({ preset: 'balanced', extraRules: crsRules() }));

Rules that fire on ordinary prose are tuned down by default. crsRules({ tuning: false }) restores the original scores.

Custom rules

Rules use the Cloudflare rules language. Actions are block, allow, skip, log, score and challenge.

createWaf({
  customRules: [
    { id: 'admin-from-office', action: 'block',
      expression: 'starts_with(http.request.uri.path, "/admin") and not ip.src in $office' },
    { id: 'cms-editor', action: 'skip', skip: ['managed-rules'],
      expression: 'http.request.uri.path eq "/cms/save" and http.request.method eq "POST"' },
  ],
  lists: { office: ['203.0.113.0/24'] },
});

Sink guards

Signatures guess. Sink guards check. Just before your code runs a query, a shell command, a file read or an outbound request, the guard looks for this request's own input in the finished string and checks whether it stayed a single value.

const guard = waf.sinks(req.lightmoon);          // Hono: c.get('lightmoon')

await db.query(guard.sql(sql));                  // returns the query, or throws InjectionError (403)
exec(guard.shell(command));
const file = guard.path('/srv/uploads', name);   // resolved inside the root, or throws
await fetch(await guard.resolvedUrl(target));    // refuses internal addresses chosen by the request
await users.find(guard.nosql(filter));
Sink guards are a safety net for code you haven't fixed yet, not a replacement for parameterized queries. They are experimental in the current alpha.

Monitor mode and tuning

Deploy with mode: 'monitor' first. Requests that would have been blocked are allowed and reported through onDecision, or to the Cloud dashboard, where the Tuning page turns repeated matches from many different clients into ready-to-paste skip rules. Switch to mode: 'block' when the list is empty.

Connect to Cloud

Create a project in the dashboard and copy its key. It is shown once. Store it as a secret, then wrap your options:

import { createWaf } from 'lightmoon';
import { cloud, withCloud } from 'lightmoon/cloud';

const lm = cloud({ key: process.env.LIGHTMOON_KEY });
const waf = createWaf(withCloud(lm, { preset: 'balanced' }));

app.use(lightmoon(waf));

On Node, events are sent in the background every few seconds. On serverless and edge runtimes, flush after the response so nothing is lost:

ctx.waitUntil(lm.flush());   // Cloudflare Workers, Vercel Edge, Netlify Edge

Remote rules and blocks

Rules, IP and country blocks and fleet bans you add in the dashboard are packed into a rule pack signed with your project's Ed25519 key. The connector pulls it every minute and verifies the signature with the public key embedded in your project key, so a pack that wasn't signed for your project is never applied. If the dashboard can't be reached, instances keep the last pack they verified.

What is sent

For blocked, challenged and monitored requests only: time, request ID, action, reason, status, method, host, path without the query string, matched rule IDs, attack and bot scores, country, a short message and the client IP truncated to /24 (IPv4) or /48 (IPv6). Confirmed injections, response leaks and bans are sent as events too; bans include the banned network so other instances can apply it. Request bodies, headers, cookies, query strings and matched snippets are never sent.

The full reference, with every option, is in the package README.