# @leumas/security

How every Leumas backend defends itself, declared once — the programmable security policy (session · cors · rate limits · body limits · http) plus the middleware that enforces it, mounted in the one...


How every Leumas backend defends itself, declared once: **one policy file, one `applySecurity()` call.**

Before this package the same stack was hand-rolled per service and the copies had drifted —
`http://localhost:5173` was the CORS default in five separate files, `express.json({limit:'2mb'})`
appeared in two servers, and the two backends that terminate HTTP alongside the API (the Imperium
host and the compute worker) shipped **no security headers at all**. Meanwhile the API constructed a
fresh body parser on every request and never swept a single rate-limit bucket.

## Use it

```js
import express from 'express';
import { applySecurity, applyServerTimeouts, staticCacheOptions } from '@leumas/security';

const app = express();
const security = applySecurity(app, { json: express.json, servesSpa: Boolean(dist) });

mountYourAuth(app);                       // between the stack and the backstop — see below
app.get('/health', …);                    // before the backstop, so a probe is never throttled
security.mountBackstop();

app.use('/leviathan', security.lanes.llm, leviathanRouter);   // a lane is an array; express takes it

applyServerTimeouts(server);                                  // on the http.Server, not the app
app.use(express.static(dist, { index: false, ...staticCacheOptions() }));
```

`json` is **injected** rather than imported, so this package never depends on express — and injecting
it is also what lets the body parsers be built once, at boot, instead of per request.

## Order is the point

Every service that hand-rolled this got a different order, and the order is where the bugs are, not
the individual options. `applySecurity` mounts:

1. `x-powered-by` off
2. **helmet** — headers must be set before anything can answer, including a 429
3. **compression** — must wrap `res.write` before a route writes, and must skip SSE
4. **CORS**, public lane then credentialed lane
5. **body parsers** — after CORS, so a rejected preflight never reads a body
6. *(the caller mounts auth here)*
7. **the per-IP backstop** — last, after auth, and after `/health`

The caller mounts auth between 5 and 7 because only the caller knows what auth means for it: the API
has sessions, the compute worker has a shared key, the Imperium host has neither.

## The three traps this encodes

Each of these has already cost this repo a live defect. They are handled here so no service has to
rediscover them.

**CORS cannot be one layer.** `cors()` emits `Access-Control-Allow-Credentials: true`
*unconditionally* — even for an origin it does not allow. So the public-catalog paths cannot simply
join the allowlist; they need their own un-credentialed wildcard layer mounted **first**, and the
credentialed layer has to **skip those same paths**. When both ran, responses carried `Allow-Origin: *`
*and* `Allow-Credentials: true` — a pair browsers reject — and the catalog silently stopped being
readable cross-origin, which is the one thing the wildcard existed for. `'*'` inside `origins` is
handled as the literal wildcard, because `cors()` otherwise compares it for exact equality and matches
nothing.

**Compression buffers SSE.** `compression`'s default filter asks the `compressible` package, which
answers *true* for `text/event-stream` because it matches `text/*`. Compressing a live stream buffers
it: every feed in the product — automation, surveillance detections, release build logs, the serial
monitor — appears to hang and then arrive all at once. It looks like a network fault, not a setting.
Excluded unconditionally, along with bodies that already set their own `Content-Encoding`.

**Some routes must not be parsed at all.** A `null` in `body.byPrefix` means *skip* — Stripe verifies
its signature over the **unmodified** bytes, and a parser anywhere above the webhook breaks every one
of them with an error that reads like a key mismatch.

## The policy

Defaults → repo `ops/config/security.json` → `<dataRoot>/security.json` → `LEUMAS_SECURITY_CONFIG`
→ env. Cached ~5s, so an edit applies with **no restart**; a parse error keeps the last good value
rather than failing open. Five sections: `session`, `cors`, `limits`, `body`, `http`.

**Nothing in it is a credential.** Cookie name, cookie domain and `JWT_SECRET` stay in env, because
they are deployment *identity* rather than *policy* — and this file is checked into the repo and
editable from Studio. `smoke-security-stack.mjs` asserts that boundary rather than trusting it.

## Verify

```
pnpm smoke:security          # access + integrity + stack
pnpm smoke:security-stack    # headers, the CORS pair, SSE-safe compression, per-prefix bodies
pnpm smoke:session           # the session half of the same policy file
pnpm check:routes            # the mounted layer list, per role
```


---
Source: shared/packages/security/README.md
Canonical: https://docs.leumas.tech/p/packages/security
