Skip to content

Search docs

Jump between documentation pages.

Browse docs

Composition & network

Think of it like… the velvet ropes outside a club. They steer traffic into the right line (allow/deny lists viaipRestriction), share a single headcount across a group of doors (rateLimit({ groupId })), and let staff slip in through the side door without queueing (internal: trueroutes reachable only via app.inject()).

Daloy ships the composition & network slice of the secure-by-default initiative: four primitives that compose the security stack you already have. Every item is opt-in; no existing behaviour changes unless you call the new helper.

1. rateLimit({ groupId }) shared buckets

Every rateLimit() call that declares the same groupId shares one in-memory bucket. Use it to enforce a combined limit across related routes (e.g. login, OTP, password reset) without juggling a shared store yourself.

ts
import { App, rateLimit } from "@daloyjs/core";

const app = new App({ env: "production" });
const authLimit = () =>
  rateLimit({ windowMs: 60_000, max: 10, groupId: "auth" });

app.route({ method: "POST", path: "/login",          hooks: authLimit(), ... });
app.route({ method: "POST", path: "/login/otp",      hooks: authLimit(), ... });
app.route({ method: "POST", path: "/password-reset", hooks: authLimit(), ... });
// All three endpoints spend from the same bucket per IP.

When you supply a custom store, Daloy still prefixes the derived key with `${groupId}:` so two groups cannot collide in a shared Redis backend either.

2. combine primitives, every / some / except

Declarative composition for your Hooks bundles. Use them to package curated security stacks as a single value and drop the fragile if (...) await next() chains.

ts
import {
  App,
  every, some, except,
  requestId, bearerAuth, rateLimit,
} from "@daloyjs/core";

const adminStack = every(
  requestId(),
  bearerAuth({ validate: (t) => t === process.env.ADMIN_TOKEN }),
  rateLimit({ windowMs: 60_000, max: 30, groupId: "admin" }),
);

const app = new App();

// Mount one curated bundle:
app.use(adminStack);

// "Auth except the public endpoints":
app.use(except(
  ["/health", "/openapi.json", "/docs/**"],
  bearerAuth({ validate: (t) => t === process.env.API_TOKEN }),
));

// "Any one of these proofs of identity is enough":
app.use(some(
  bearerAuth({ validate: (t) => t === process.env.PUBLIC_API_TOKEN }),
  // session-cookie middleware, API-key middleware, ...
));
  • every(...layers) runs every bundle in order across every lifecycle phase. Forwards CORS / CSRF / session security markers so boot-time guards still see them on the composed bundle.
  • some(...layers) tries each layer's beforeHandle until one passes. A returned Response is treated as a denial, the next layer gets a turn. The first failure wins when every layer rejects, so place the auth scheme whose WWW-Authenticate challenge you want clients to see first.
  • except(when, hooks) skips the wrapped beforeHandle for matching paths (/health, /public/**, /v1/*/meta) or for any request where the supplied predicate returns true. Only beforeHandle is gated so shared concerns like request-id propagation keep running.
Perimeter guards run on unmatched requests too. beforeHandle guards registered with app.use(...), such as rateLimit(), ipRestriction(), csrf(), and your own WAF bundles, also run on the cold dispatch path: a request that matches no route (404), hits a registered path with the wrong method and gets 405, or arrives as an OPTIONS preflight. Without this, an attacker could flood random paths to slip past a rateLimit() that an operator reasonably expects to cover every request. Two consequences worth knowing:
  • A flood of 404s is now throttled / IP-fenced just like traffic to a real route.
  • When auth is installed globally (e.g. app.use(except(exempt, bearerAuth(...)))), an unauthenticated request to an unmatched path is rejected with 401 rather than 404. This is intentional route-enumeration resistance: the perimeter answers the same way whether or not the route exists. Per-route hooks are unaffected, and no handler is ever reached. Need public unknown paths to stay plain 404s instead? Avoid global app.use() auth for that surface; use route/group-scoped hooks for auth and reserve App({ hooks }) onRequest for checks that should truly run before routing.

3. ipRestriction(): CIDR allow / deny

Block or allow requests by source IP or CIDR range. Pairs naturally with trustProxyHeaders: true behind a trusted proxy so the matched address is the real client, not your load balancer. Supports IPv4, IPv6, and IPv4-mapped IPv6 (::ffff:a.b.c.d). deny always wins over allow.

ts
import { App, ipRestriction } from "@daloyjs/core";

const app = new App({ env: "production", trustProxy: true });

app.use(ipRestriction({
  allow: ["10.0.0.0/8", "192.168.1.0/24", "::1"],
  deny:  ["10.6.6.0/24"],
  trustProxyHeaders: true,
}));

// Rejected requests:
//   HTTP/1.1 403 Forbidden
//   content-type: application/problem+json
//   { "title": "Forbidden", "detail": "IP address not permitted" }

Invalid IP literals, invalid CIDR prefixes, and calls with neither an allow nor deny list throw at construction time, catching bugs that would otherwise hide until production traffic hits. By default the helper fails closed because Web-standard requests do not expose the peer address. Supply resolveIp if your adapter exposes connection metadata or if you sit behind a CDN that sends the real client through a custom header such as cf-connecting-ip or true-client-ip.

4. internal: true + app.inject()

Mark a route as internal: true and the public app.fetch(...) entry point returns 404: existence cannot be probed. The same route runs normally through app.inject(request), which is meant for cron jobs, admin scripts, and integration tests. Internal routes are also excluded from generated OpenAPI by default; pass includeInternal: true to generateOpenAPI()for private admin SDK generation. The framework also filtersAllow headers so a probe with a different method stays a clean 404 rather than a leaky 405.

Two entry points, one internal route
routeinternal: true route/__admin/reindex
publicapp.fetch()404, existence cannot be probed
in-processapp.inject()runs normally, 204
The public adapter returns 404 for an internal route so its existence stays hidden, while cron jobs, admin scripts, and tests reach it through app.inject(). Internal routes are also excluded from generated OpenAPI by default.
ts
import { App } from "@daloyjs/core";

const app = new App();

app.route({
  method: "POST",
  path: "/__admin/reindex",
  internal: true,
  responses: { 204: { description: "Started" } },
  handler: () => ({ status: 204 }),
});

// Public adapter - 404
await app.fetch(new Request("http://app/__admin/reindex", { method: "POST" }));

// In-process / cron / tests - 204
await app.inject(new Request("http://app/__admin/reindex", { method: "POST" }));

Opt-out

Every primitive in this slice is additive; nothing changes unless you call the helper. The earlier secure-defaults master opt-out flag still applies if you ever need to disable secure defaults in a development sandbox:

ts
const app = new App({ env: "development", secureDefaults: false });