The capability gateway

A pattern, about 20 lines you write over Cruft's built-in primitives, that runs each HTTP route in its own Compartment endowed only the capabilities it needs, under a timeout the route cannot escape, so a compromised or runaway route stays contained instead of taking down the whole edge.

nginx, Envoy, or an Express app in front of your services carries middleware: auth, rate limiting, request rewriting, a vendored plugin. Every one of those filters runs with the gateway's full authority, and nothing separates one filter from the next. A compromised auth filter, or a rewrite rule with a bug, can read any file the gateway can and reach any upstream. The most exposed tier in a stack runs the most third-party code with the most ambient power.

Cruft's isolation primitives compose into a different edge, one where each route runs in its own Compartment endowed only the capabilities it needs, under a deadline the route cannot escape but the edge can contain. A compromised or buggy route becomes a contained failure rather than an edge-wide one, and a route that is only supposed to talk to one upstream cannot reach any other authority. That constraint is structural, not a matter of convention. This page documents the pattern, the primitives it rests on, and the guarantees it gives.

A pattern you compose, not a subcommand. There is no cruft gateway command or cruft:gateway module. The gateway is a small dispatch function you write, about 20 lines, over Cruft's built-in primitives: cruft:serve, Compartment, capability endowment, and the containable timeout. What the runtime provides is the enforcement that makes those 20 lines secure; the dispatch below is the composition.

What the pattern buys

Three properties, applied to the edge at once:

  • Per-route least authority. A route handler holds only the capabilities you endow it, nothing ambient. See compartments and capabilities.
  • Per-route ingress boundary. An inbound request is the untrusted-data crossing, validated before the handler sees it. See CruftScript.
  • Per-route containment. A runaway or hostile handler is bounded by a deadline and cannot take the edge down.

The dispatch model

Per request, four steps:

  1. Route match: routes[req.path] (or a "*" fallback); a miss is a 404 with no compartment created.
  2. Ingress boundary: the route's validate(req) runs first, in the host, against the untrusted request. A drift returns 400, and the handler never sees a contract-violating request.
  3. Least-authority dispatch: a fresh Compartment endowed only { request, ...caps } and a timeout_ms. The handler runs via evaluate. It cannot reach ambient authority, cannot reach a sibling route's caps, and is bounded by the deadline.
  4. Contained execution: a throwing or timed-out handler is caught at the dispatch boundary and mapped to 503. One bad route cannot take down the edge.

The whole core:

function dispatch(routes, req) {
  const r = routes[req.path] ?? routes["*"];
  if (!r) return { status: 404 };
  if (r.validate) {
    const v = r.validate(req);
    if (v !== true) return { status: 400, body: "boundary: " + v };
  }
  const c = new Compartment({
    globals: { request: req, ...(r.caps ?? {}) },   // the ONLY authority
    timeout_ms: r.timeout_ms ?? 100,                // the deadline
  });
  try {
    return { status: 200, body: String(c.evaluate(r.handler)) };
  } catch (e) {
    return { status: 503, body: "contained: " + e.message };  // host-catchable
  }
}

Front it with cruft:serve for real HTTP(S) ingress: serve({ port, tls, handler: (req) => dispatch(routes, req) }).

The four guarantees

The model gives four properties, each a behavior of the dispatch above:

GuaranteeBehavior
Route uses its endowed capabilityWith caps: { up: (p) => "upstream("+p+")" }, a handler calling up(request.path) returns upstream(/up).
Route reaches nothing ambientInside the handler, typeof fetch and typeof process are both undefined.
Slow route contained, edge survivesA /slow handler running while(true){} under timeout_ms: 50 returns 503 contained: Compartment evaluate exceeded its 50 ms timeout, and the next request still returns 200.
Ingress crosses by valueA handler that mutates request.name does not change the caller's copy.

The third guarantee rests on the containable timeout described below.

What the runtime enforces underneath

The pattern is secure because of what the runtime enforces, each piece documented in depth elsewhere:

  • cruft:serve: the ingress primitive, Request in and Response out, with TLS/ALPN if configured. See cruft:serve.
  • Compartment least-authority endowment: the handler's realm starts empty; globals is the entire authority it has, so typeof fetch inside is undefined. See compartments and capabilities.
  • Ingress by value: the request crosses the compartment boundary cloned, so a handler cannot tamper with a request another route or middleware later reads. This falls out of the structured-clone boundary.
  • The containable timeout: a compartment's timeout_ms is tenant-uncatchable (the handler's own try/catch cannot swallow its deadline) and host-catchable (the gateway's try/catch around evaluate does catch it, as an ordinary Error). That asymmetry is what lets one runaway route be contained while the edge keeps serving.

Capability endowment: the one rule

Capabilities are by-reference closures endowed through globals: caps.upstream = (path) => fetch(origin + path) is a capability at the JS tier, the handler can call it but holds no other egress.

The rule, and a real footgun: a capability must return narrow data, never raw host authority. Endow (path) => Response, never () => fetch or () => globalThis. An endowed function's return value crosses back unmediated, so a cap that returns an authority-bearing object hands the handler an escape from its own sandbox. Keep each cap a narrow, purpose-built egress.

Limitations

This is a pattern demonstrated by a working example, not a turnkey product.

  • Boundaries are JS predicates today. The dispatch model uses a validate(req) function. Full CruftScript .fts route contracts, with HALT/SANITIZE/PROPAGATE on the request as a typed boundary, are the intended end state, gated on .fts lowering maturity. The ORM already runs the boundary machinery for database rows; the edge is the same machinery for requests.
  • Fresh-per-request costs about 10 ms. A same-thread new Compartment(...) clones a full set of realm intrinsics, about 10 ms. Fresh-per-request buys maximum inter-request isolation but pays that latency. A pooled-per-route (warm compartment) variant is the performance option, and the deeper fix is shared-frozen intrinsics (an SES-style model) so a fresh realm does not deep-clone. Until then, budget for the creation cost or pool.
  • The security caveats compose. A route endowed an upstream capability built on fetch inherits fetch's posture: the TLS client validates the chain, hostname, and expiry, but interop is young and rejectUnauthorized is thread-global. Review the security model before pointing a route at an adversarial network.