Search docs

Jump between documentation pages.

Composition & network

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.

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 — 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 (cf-connecting-ip, 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.

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 });