Traffic Control
Traffic policies control admission, rate, payload safety, upstream shaping, and response filtering.
Policy Summary
Section titled “Policy Summary”| Policy | Priority | Purpose |
|---|---|---|
ipFilter | 1 | IP allow/deny with CIDR support. |
geoIpFilter | 1 | Country allow/deny via header (default cf-ipcountry). |
sslEnforce | 5 | Enforce HTTPS and apply HSTS. |
requestLimit | 5 | Reject large requests via content-length. |
jsonThreatProtection | 5 | Enforce JSON structural/body limits. |
regexThreatProtection | 5 | Block suspicious patterns in path/query/headers/body. |
rateLimit | 20 | Sliding-window rate limiting with pluggable store. |
cache | 40 | Response caching with pluggable store. |
dynamicRouting | 50 | Evaluate rules and set dynamic routing context values. |
httpCallout | 50 | Call external services during pipeline execution. |
interrupt | 100 | Conditionally short-circuit with static response. |
trafficShadow | 92 | Mirror a sampled subset of traffic to shadow target. |
resourceFilter | 92 | Remove/allow JSON response fields. |
rateLimit
Section titled “rateLimit”Sliding-window limiting. Uses InMemoryRateLimitStore by default.
import { rateLimit } from "@homegrower-club/stoma";Configuration
Section titled “Configuration”interface RateLimitConfig { max: number; windowSeconds?: number; // default: 60 keyBy?: (c: Context) => string | Promise<string>; store?: RateLimitStore; statusCode?: number; // default: 429 message?: string; // default: "Rate limit exceeded" ipHeaders?: string[]; // default via extractClientIp skip?: (c: Context) => boolean | Promise<boolean>;}Headers
Section titled “Headers”Every response includes:
x-ratelimit-limitx-ratelimit-remainingx-ratelimit-reset
Rejected responses also include:
retry-after
Store Interface
Section titled “Store Interface”interface RateLimitStore { increment(key: string, windowSeconds: number): Promise<{ count: number; resetAt: number }>; destroy?(): void;}ipFilter
Section titled “ipFilter”Allow/deny by IP or CIDR.
import { ipFilter } from "@homegrower-club/stoma";interface IpFilterConfig { allow?: string[]; deny?: string[]; mode?: "allow" | "deny"; // default: "deny" ipHeaders?: string[]; skip?: (c: Context) => boolean | Promise<boolean>;}allowmode denies unless the IP matches.denymode allows unless the IP matches.
Error: 403 ip_denied.
geoIpFilter
Section titled “geoIpFilter”Allow/deny by country code from a request header.
import { geoIpFilter } from "@homegrower-club/stoma";interface GeoIpFilterConfig { allow?: string[]; deny?: string[]; mode?: "allow" | "deny"; // default: "deny" countryHeader?: string; // default: "cf-ipcountry" skip?: (c: Context) => boolean | Promise<boolean>;}Error: 403 geo_denied.
Response cache with configurable TTL, method filter, key strategy, and store backend.
import { cache } from "@homegrower-club/stoma";Configuration
Section titled “Configuration”interface CacheConfig { ttlSeconds?: number; // default: 300 methods?: string[]; // default: ["GET"] cacheKeyFn?: (c: Context) => string; varyHeaders?: string[]; store?: CacheStore; // default: InMemoryCacheStore respectCacheControl?: boolean; // default: true cacheStatusHeader?: string; // default: "x-cache" bypassDirectives?: string[]; // default: ["no-store", "no-cache"] skip?: (c: Context) => boolean | Promise<boolean>;}Store Interface
Section titled “Store Interface”interface CacheStore { get(key: string): Promise<Response | null>; put(key: string, response: Response, ttlSeconds: number): Promise<void>; delete(key: string): Promise<boolean>;}Responses get x-cache: HIT|MISS|BYPASS (or your configured header name).
sslEnforce
Section titled “sslEnforce”Enforce HTTPS and set HSTS on secure responses.
import { sslEnforce } from "@homegrower-club/stoma";interface SslEnforceConfig { redirect?: boolean; // default: true hstsMaxAge?: number; // default: 31536000 includeSubDomains?: boolean; // default: false preload?: boolean; // default: false skip?: (c: Context) => boolean | Promise<boolean>;}Behavior:
- Non-HTTPS with
redirect: true: returns301withLocationonhttps://. - Non-HTTPS with
redirect: false: throws403ssl_required. - HTTPS responses: adds
Strict-Transport-Security.
requestLimit
Section titled “requestLimit”Reject requests whose declared content-length exceeds maxBytes.
import { requestLimit } from "@homegrower-club/stoma";interface RequestLimitConfig { maxBytes: number; message?: string; // default: "Request body too large" skip?: (c: Context) => boolean | Promise<boolean>;}Error: 413 request_too_large.
jsonThreatProtection
Section titled “jsonThreatProtection”Validate JSON body structure and size limits before business logic.
import { jsonThreatProtection } from "@homegrower-club/stoma";interface JsonThreatProtectionConfig { maxDepth?: number; // default: 20 maxKeys?: number; // default: 100 maxStringLength?: number; // default: 10000 maxArraySize?: number; // default: 100 maxBodySize?: number; // default: 1048576 contentTypes?: string[]; // default: ["application/json"] skip?: (c: Context) => boolean | Promise<boolean>;}Errors:
413body_too_large400invalid_json400json_threat
regexThreatProtection
Section titled “regexThreatProtection”Run regex threat rules against path/query/headers/body.
import { regexThreatProtection } from "@homegrower-club/stoma";interface RegexPatternRule { regex: string; targets: Array<"path" | "headers" | "body" | "query">; message?: string;}
interface RegexThreatProtectionConfig { patterns: RegexPatternRule[]; flags?: string; // default: "i" contentTypes?: string[]; // default: ["application/json", "text/plain"] maxBodyScanLength?: number; // default: 65536 skip?: (c: Context) => boolean | Promise<boolean>;}Error: 400 threat_detected.
interrupt
Section titled “interrupt”Conditionally short-circuit the pipeline and return a static response.
import { interrupt } from "@homegrower-club/stoma";interface InterruptConfig { condition: (c: Context) => boolean | Promise<boolean>; statusCode?: number; // default: 200 body?: unknown; headers?: Record<string, string>; // default: {} skip?: (c: Context) => boolean | Promise<boolean>;}When condition is true, next() is not called.
dynamicRouting
Section titled “dynamicRouting”Evaluate routing rules and expose match results via context keys.
import { dynamicRouting } from "@homegrower-club/stoma";interface RoutingRule { name?: string; condition: (c: Context) => boolean | Promise<boolean>; target: string; rewritePath?: (path: string) => string; headers?: Record<string, string>;}
interface DynamicRoutingConfig { rules: RoutingRule[]; fallthrough?: boolean; // default: true skip?: (c: Context) => boolean | Promise<boolean>;}When a rule matches, it sets:
c.set("_dynamicTarget", string)c.set("_dynamicRewrite", (path) => string)(optional)c.set("_dynamicHeaders", Record<string,string>)(optional)
Error when fallthrough: false and no rule matches: 404 no_route.
httpCallout
Section titled “httpCallout”Make an external HTTP call during request processing.
import { httpCallout } from "@homegrower-club/stoma";interface HttpCalloutConfig { url: string | ((c: Context) => string | Promise<string>); method?: string; // default: "GET" headers?: Record<string, string | ((c: Context) => string | Promise<string>)>; body?: unknown | ((c: Context) => unknown | Promise<unknown>); timeout?: number; // default: 5000
onResponse: (response: Response, c: Context) => void | Promise<void>; onError?: (error: unknown, c: Context) => void | Promise<void>; abortOnFailure?: boolean; // default: true
skip?: (c: Context) => boolean | Promise<boolean>;}Default error path throws 502 callout_failed.
trafficShadow
Section titled “trafficShadow”Mirror sampled requests to a secondary target after primary response flow.
import { trafficShadow } from "@homegrower-club/stoma";interface TrafficShadowConfig { target: string; percentage?: number; // default: 100 methods?: string[]; // default: ["GET","POST","PUT","PATCH","DELETE"] mirrorBody?: boolean; // default: true timeout?: number; // default: 5000 onError?: (error: unknown) => void; skip?: (c: Context) => boolean | Promise<boolean>;}Notes:
- Shadow failures never affect the primary response.
- If adapter
waitUntilis available, shadow work is scheduled there.
resourceFilter
Section titled “resourceFilter”Filter JSON response fields in allow or deny mode.
import { resourceFilter } from "@homegrower-club/stoma";interface ResourceFilterConfig { mode: "allow" | "deny"; fields: string[]; // dot paths, e.g. "user.password" contentTypes?: string[]; // default: ["application/json"] applyToArrayItems?: boolean; // default: true skip?: (c: Context) => boolean | Promise<boolean>;}deny: remove listed fields.allow: keep only listed fields.
Example Composition
Section titled “Example Composition”import { ipFilter, sslEnforce, requestLimit, jsonThreatProtection, regexThreatProtection, rateLimit,} from "@homegrower-club/stoma";
[ ipFilter({ deny: ["203.0.113.0/24"] }), sslEnforce({ redirect: true }), requestLimit({ maxBytes: 256_000 }), jsonThreatProtection({ maxDepth: 10, maxBodySize: 256_000 }), regexThreatProtection({ patterns: [{ regex: "<script", targets: ["body", "query"] }] }), rateLimit({ max: 100, windowSeconds: 60 }),]import { cache, trafficShadow } from "@homegrower-club/stoma";
[ cache({ ttlSeconds: 120, varyHeaders: ["accept-language"] }), trafficShadow({ target: "https://shadow.internal", percentage: 10 }),]