npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rules-cascade/server

v1.0.0-alpha.11

Published

Stateless HTTP rule server for Rule Cascade: serves manifests and evaluates operations. Run as many replicas as you need.

Readme

@rules-cascade/server

A stateless HTTP service that implements the evaluation API, described in OpenAPI 3.1. Use it for callers that cannot embed a runtime. It keeps nothing between requests, so you run as many replicas as you need behind a load balancer.

npm ci && npm run build                                    # at the repository root
RULE_SERVER_TOKEN=$(openssl rand -hex 32) RULES_DIR=examples/contracts node packages/server/dist/main.js

| Variable | Meaning | Default | |---|---|---| | RULES_DIR | Directory with the rulesets to serve; see below | ./rules | | PORT | Listen port, 0 to 65535 | 8080 | | RULE_SERVER_TOKEN | Bearer token for evaluations, the server manifest and bundles | required | | RULE_SERVER_ALLOW_OPEN | 1 or true: start without a token and serve every endpoint openly | unset | | DRAIN_SECONDS | Time between "not ready" and shutdown on SIGTERM, in whole seconds | 5 | | RULE_SERVER_MAX_BODY_BYTES | Largest accepted request body; a larger one is answered 413 | 1048576 (1 MiB) | | RULE_SERVER_KEEP_ALIVE_TIMEOUT_MS | Close an idle keep-alive connection after this long; keep it above the idle timeout of the load balancer in front; at least 1 | 65000 | | RULE_SERVER_HEADERS_TIMEOUT_MS | Longest time to receive the request headers; longer than the keep-alive timeout, no longer than the request timeout | 66000 | | RULE_SERVER_REQUEST_TIMEOUT_MS | Longest time to receive a whole request, body included; 0 for no limit | 120000 | | RULES_REFRESH | manual (SIGHUP only), interval or cron: also reload on a schedule when the files changed | manual | | RULES_REFRESH_INTERVAL | With interval: 30s, 5m, 1h or seconds; at least 1s | none | | RULES_REFRESH_CRON | With cron: minute hour day-of-month month day-of-week | none | | RULES_REFRESH_TZ | Time zone of the cron schedule | UTC | | RULE_SERVER_WORKERS | Evaluate on this many worker threads; 0 evaluates on the event loop | 0 | | RULE_EVALUATION_TIMEOUT_MS | With workers: answer 503 and replace the worker after this long | 5000 | | RULE_SERVER_CORS_ORIGINS | Comma-separated browser origins (https://app.example.com) or * that may read the ruleset list, client manifests and rule details from another site; see Caching | none | | RULE_SERVER_CORS_MAX_AGE | Seconds a browser may keep a CORS preflight answer | 600 | | RULE_SERVER_REQUEST_LOG | on or off: one JSON log line per request (health probes excepted). Turn it off when the platform in front already logs requests: the load test then answers about 13 % more requests per second | on | | RULE_SERVER_MAX_QUEUE | With workers: evaluations waiting for a worker beyond this many are answered 503 with Retry-After: 1 | 256 per worker |

Every number is checked at start-up. A value that is not a whole number in range, an empty one included, stops the process with exit code 1 and a fatal line that names the variable, as do timeouts that contradict each other.

Scheduled refresh, the cache policy and the checksum URL are described in Caching; the worker pool and measurements in Performance.

The rules directory

The server loads two kinds of files from RULES_DIR:

| File | What happens at start-up and on reload | |---|---| | *.ruleset.yaml, *.ruleset.yml, *.ruleset.json | Compiled here: schema validation, inheritance and every load check. Entity schemas are read from the same directory | | *.bundle.json | Read as a precompiled bundle. Only the bundle format is checked; the compiler that produced it already ran the load checks |

An optional cache-policy.yaml (or .yml, .json) sets the Cache-Control of client manifests per ruleset: revalidate (the default), ttl or permanent. It is checked with the rulesets; a bad one stops the start or refuses the reload. See Caching.

A ruleset id may be defined once, by one file. A source ruleset can only extend a parent that is also present as a source document, because a bundle does not hold what inheritance needs.

A bundle carries no title, scope or status. GET /rulesets therefore lists a ruleset that came from a bundle with its id, version and checksum only, and a ?scope= filter does not match it.

Endpoints

| Method and path | Purpose | Needs the token | |---|---|---| | GET /healthz | Liveness | no | | GET /readyz | Readiness; 503 while draining | no | | GET /rulesets?scope= | List rulesets, optionally under a hierarchy path | no | | GET /rulesets/{id}/manifest?channel=client | Client-safe manifest, with ETag | no | | GET /rulesets/{id}/manifest?channel=server | Full manifest for backend runtimes | yes | | GET /rulesets/{id}/bundle | The compiled bundle, for runtimes that evaluate in process; with ETag | yes | | GET /rulesets/{id}/rules/{ruleId} | One resolved rule | server-only rules: yes | | POST /evaluations | Evaluate an operation | yes |

The endpoints marked "yes" answer 401 without Authorization: Bearer <token>. The bundle contains the server manifest, so it is protected exactly like it: the token is checked before the ruleset is looked up and before a conditional request is answered. The endpoints marked "no" are public on purpose: browsers fetch the client manifest directly, and load balancers probe the health endpoints.

The server fails closed. Without RULE_SERVER_TOKEN the process logs a fatal line and exits 1 before it loads any rules. To serve every endpoint without a token, which is only acceptable on a private network, set RULE_SERVER_ALLOW_OPEN=1; the server then logs a warn line at start-up and marks its listen line "auth":"open". An empty token counts as unset. Embedded, createRuleServer throws unless it is given token or allowOpen: true.

curl -s localhost:8080/evaluations -H "Authorization: Bearer $RULE_SERVER_TOKEN" -H 'Content-Type: application/json' -d '{
  "ruleset": "acme.payments.transfer", "entity": "Transfer", "operation": "create",
  "data": {"type": "international", "amount": 500, "currency": "USD", "memo": "gift",
           "beneficiary": {"name": "X", "country": "KP", "swiftCode": "ABCDKPPY"}}}'
# {"decision":"deny","findings":[{"code":"ORG-TRF-001","message":"Transfers to KP are not permitted.", ...}], ...}

The request body is the evaluation request of the specification plus ruleset, and optionally version and channel. The body is checked before the ruleset is looked up or anything is evaluated, with the shape check every runtime applies (specification section 8). A malformed body is answered with 400 and a problem document whose title says what is wrong, for example 'view' must be an object; an unknown ruleset or version is 404. Optional members may be null; a null channel is the default channel, server. A body with a number too large for a double (1e999) is refused with 400 as well. A body larger than 1 MiB (RULE_SERVER_MAX_BODY_BYTES, or maxBodyBytes when embedded) is 413, and a path with malformed percent-encoding is 400.

trigger, view, resolutions and locale are passed to the runtime as given. A view narrows the evaluation to one place in the user interface, and a finding reports the place its rule names as location:

curl -s localhost:8080/evaluations -H "Authorization: Bearer $RULE_SERVER_TOKEN" -H 'Content-Type: application/json' -d '{
  "ruleset": "acme.payments.transfer", "entity": "Transfer", "operation": "create",
  "view": {"component": "beneficiary-panel"},
  "data": {"type": "international", "amount": 500, "currency": "USD", "memo": "gift",
           "beneficiary": {"name": "X", "country": "ES"}}}'
# {"decision":"deny","findings":[{"code":"PAY-TRF-002", ..., "location":{"component":"beneficiary-panel"}}],"effects":[],"commands":[]}

Without the view the same request also returns the fee that a rule of the amount-panel computes.

A runtime without a compiler downloads the bundle and evaluates it in process:

curl -s -H "Authorization: Bearer $RULE_SERVER_TOKEN" localhost:8080/rulesets/acme.payments.transfer/bundle -o transfer.bundle.json
# ETag: "sha256:c192...2c7e-bundle"; send it back as If-None-Match and the answer is 304 until the ruleset changes

Custom operators

A ruleset may use custom operators (x-*), which are functions of the host application. The rule-cascade-server command registers none: it serves such a ruleset, logs a warning that names the missing operators at start-up and after every reload, and the rules that need them fail closed (the evaluation denies with a RULE-EVALUATION-ERROR finding).

To supply operators, embed the server in a program of your own:

import { createRuleServer, RuleStore } from '@rules-cascade/server';

const store = new RuleStore('./rules');
const { server, missingOperators } = createRuleServer(store, {
  token: process.env.RULE_SERVER_TOKEN,   // required; createRuleServer throws without it unless allowOpen: true
  operators: { 'x-luhn': (text) => typeof text === 'string' && luhn(text) },
});
missingOperators();   // {} when everything is registered, otherwise { '<ruleset id>': ['x-...'] }
server.listen(8080);

An operator receives plain JSON values and returns a JSON value. It must be synchronous and pure. Every runtime that evaluates the ruleset needs the same operators: a browser that evaluates the client manifest registers them too.

Behaviour that matters in production

  • Fails fast. If any ruleset fails a load check, or any bundle is unusable, the process exits before it listens.
  • Fails closed. Without RULE_SERVER_TOKEN (or the explicit RULE_SERVER_ALLOW_OPEN=1) the process exits 1.
  • Constant-time token check. The token and the one a caller sends are compared as SHA-256 digests in constant time, so a refusal takes as long whatever was sent and reveals neither the token nor its length. A refusal is 401 with WWW-Authenticate: Bearer.
  • Timeouts for load balancers and slow clients. An idle keep-alive connection is closed after 65 s, after the 60 s idle timeout of common load balancers, so the server never closes a connection the load balancer is about to reuse (which shows up as an occasional 502). If yours waits longer, raise RULE_SERVER_KEEP_ALIVE_TIMEOUT_MS above it and RULE_SERVER_HEADERS_TIMEOUT_MS above that. A client gets 66 s to send its headers and 120 s for the whole request. Embedded, pass keepAliveTimeoutMs, headersTimeoutMs and requestTimeoutMs.
  • CORS, opt-in and for public endpoints only. With RULE_SERVER_CORS_ORIGINS (embedded: corsOrigins, corsMaxAgeSeconds) a browser on a listed origin can read the ruleset list, client manifests and rule details, revalidate with If-None-Match and see the ETag. Protected endpoints never answer another origin.
  • No content sniffing. Every answer carries X-Content-Type-Options: nosniff.
  • Scheduled refresh. With RULES_REFRESH=interval or cron the server also checks the directory on a schedule and reloads, with the same safety, only when a file changed. Reloads and refusals are logged with their trigger; unchanged checks are not.
  • Safe reload. SIGHUP reloads the directory. If the new rules fail to load, the server logs reload refused, still serving the previous rules with the reason and keeps serving the previous ones; /readyz keeps reporting the previous loadedAt.
  • Graceful shutdown. On SIGTERM (or SIGINT) readiness turns 503 at once; after DRAIN_SECONDS the server stops accepting connections, in-flight requests finish, and the process exits 0.
  • Cacheable manifests and bundles. The ETag is the ruleset checksum plus the channel, or plus bundle; revalidation returns 304. If-None-Match may list several entity tags, use weak validators (W/"..."), or be *. Client manifests follow the cache policy; server manifests and bundles are always private, no-cache. By default the client manifest is sent with Cache-Control: public, max-age=0, must-revalidate, so every use is revalidated. A browser that uses createManifestClient of the runtime keeps evaluating its last manifest while this endpoint is unreachable; the evaluation on the server remains the decision.
  • Checksum URLs. GET /rulesets/{id}/manifest?channel=client&checksum=sha256:... answers the manifest only when that is the checksum being served (in permanent mode with Cache-Control: public, max-age=31536000, immutable), and 409 with Cache-Control: no-store and the served checksum in the problem body otherwise. This parameter is an extension of this server; it is not in the OpenAPI description.
  • Worker threads. With RULE_SERVER_WORKERS=N evaluations run on N worker threads with the same results; one that outlives RULE_EVALUATION_TIMEOUT_MS is answered 503 and its worker replaced. Requests waiting for a worker beyond RULE_SERVER_MAX_QUEUE are answered 503 with Retry-After: 1, so a burst fails fast instead of piling up memory. Embedded, pass workers, evaluationTimeoutMs, maxQueue and, for custom operators, operatorsModule (an ES module exporting them) to createRuleServer, and call terminateWorkers() on shutdown.
  • Structured logs. One JSON line per request; health probes are not logged. Request lines are written in batches, once per turn of the event loop; warnings and errors are written at once. RULE_SERVER_REQUEST_LOG=off (embedded: requestLog: false) leaves request lines out.

Container and Kubernetes files are in the Dockerfile and the Kubernetes deployment.