Node.js Architecture and Middleware Decisions
The Node package has an explicit Rust/WASM request engine and a retained Express-compatible legacy pipeline. Framework adapters translate their request and response APIs into that pipeline, while storage and model modules provide the state read by each check.
Source and package versions
One AIWAF repository.
Python: py/aiwaf/ · Node.js: js/ · Rust/WASM: rust/ · Java: java/.
Source and canonical README.
Versions shown describe the source checkout; package releases are independent.
Current Python/Node source targets are aiwaf 1.1.8 (Python) and
aiwaf 1.1.5 (npm), with Rust/Python 0.2.7 and
WASM 0.2.5 and Java 1.3.5. Sandbox guides retain their separately pinned builds.
The source candidate is aiwaf 1.1.5 with the declared aiwaf-wasm ^0.2.5 dependency. Publish the matching WASM companion first. The native request engine still requires explicit registration and awaits initialization; a missing native API fails startup. Legacy adapters keep their separate configuration and execution path.
Offline training now uses WASM receipts to prevent handled historical requests from extending or recreating blocks after expiry or manual removal. Models still fit on history; enforcement uses fresh requests. Preserve node_training_evidence.json in the blocklist data directory, or set AIWAF_TRAINING_STATE_DIR to a durable shared directory. Invalid timestamps are excluded. Missing WASM APIs, corrupt receipts, and storage failures make training fail visibly. A crashed trainer can leave node_training_evidence.lock; confirm it has stopped before removing that lock directory. Block and receipt writes are separate, so a crash between them can require recovery.
The current reference tests use Node 22.18.0, Express 4, Fastify 5, and NestJS 11. These are tested configurations, not a claim that every framework version is supported. Use the framework and adapter matching your application. Next.js examples require Node API request/response objects; Edge middleware is not supported by this adapter.
Legacy blocklist request checks and listing are read-only. Explicit database cleanup checks expiry in the DELETE statement; CSV cleanup reads current rows under the same cross-process lock used by blocklist writes. A crash can leave <blocklist CSV path>.lock; confirm its writer has stopped before removing that directory. Lock acquisition fails after 60 seconds. Restart all old writers when upgrading. These fixes do not make the explicit native engine's per-worker state distributed.
For source development, use the committed lockfile with npm ci, run the coverage and real native checks, and audit the complete tree with npm audit. Read dependency versions and verification and native migration.
Rust-owned Node request engine
Native mode: Node source candidate aiwaf 1.1.5 declares aiwaf-wasm ^0.2.5. Explicit native registration validates NativeRequestPipeline at startup; missing or incompatible support is an error. There is no JavaScript policy fallback for this engine. Installing the dependency does not switch an existing legacy middleware chain.
The explicit native adapter validates configuration once, then Rust/WASM owns geo/blocklist/keyword/structured SQL decisions, rate and flood counters, honeypot timing, UUID scores, anomaly history/model inference, keyword learning and CSV formatting. Node retains framework facts, bounded body parsing, response handling and filesystem writes with cached file descriptors. Existing integrations keep their legacy middleware path.
- StartupValidate the native API and immutable profile, replay dedicated persisted state, optionally snapshot a trained model, and open CSV writers.
- Begin in WASMNode passes one normalized request envelope; Rust applies enabled protections and returns an engine-owned context with its decision.
- ApplicationNode dispatches allowed requests or constructs a denial. Parsed bodies are bounded before inspection.
- Finish in WASMAt response finish/close, Rust updates response-dependent state and emits escaped CSV rows; Node appends them immediately.
Execution order and transport
geo_block → ip_keyword_block → rate_limit → ai_anomaly → honeypot → uuid_tamper → header_validation → logging. SQL traversal is bounded by bytes, nodes and depth; nested serialized JSON and double URL escapes are inspected. Unknown routes still receive applicable checks. Profile-level exemptions are global; required route metadata overrides route decorator exemptions.
Express/Sails use await aiwaf.native(options). Other Node adapters accept the sole outer option AIWAF_NATIVE_PIPELINE: options. Fastify runs after parsing at preValidation, including its not-found handler. Hapi begins at onRequest so unknown routes are covered, then makes an additional native body/authentication timing check at onPreHandler. Koa, Nest, Adonis and Next use their existing transport wrappers. This is Node API-handler integration, not an Edge/Fetch middleware.
State and persistence
Rate buckets, form timestamps, UUID scores, anomaly history and policy snapshots are per engine/worker. Blocks and learned keywords append to dedicated synced CSV files and reload at startup. They are not live synchronized across workers and do not use the legacy ORM/reputation/CLI schemas. Use separate data/log paths per worker; use the legacy integration when live shared Redis/custom stores are required.
Immediate logging has no in-memory batch queue. Log fsync is optional; policy event writes sync immediately. Rust formats CSV; WASM does not directly open or mutate host files. Elapsed time is monotonic; aborted responses log 499, engine contexts finalize once, and unused payloads are not retained during streaming responses.
Model contract
Pass a fitted six-feature Rust/WASM forest as model. The engine snapshots it; setModel(model) replaces the snapshot and setModel(null) disables inference. Node retains [pathLen, kwHits, statusIdx, responseTime, burst, total404] with milliseconds, distinct from Python. Response-time inference/behavior gates can block subsequent requests; they cannot retract a completed response. Without a model, history and learning remain active.
Source map
rust/crates/aiwaf_wasm/src/request_pipeline.rs: immutable profile, state, inspection and CSV formatting.rust/crates/aiwaf_wasm/src/lib.rs: NativeRequestPipeline bindings.js/lib/nativePipeline.js: transport facts, startup and immediate host writes.js/lib/wasmAdapter.js: one-time API/model binding.
Read the native lifecycle, configuration, deployment and measurements, and migration.
How AIWAF works: system architecture
Adapter boundary
js/index.js exports the main middleware and Fastify, Hapi, Koa, NestJS, Next.js API-route, AdonisJS, and Sails adapters. Each adapter maps its framework's request and response methods into the core contract and translates an early rejection back into the framework.
Body parsing must happen before protections that inspect parsed fields. Next.js integration uses the Node-style API-handler contract; a Fetch API or Edge request is a different boundary. An adapter is responsible for continuation and response completion, not for maintaining a separate policy implementation.
Middleware construction and request planning
wafMiddleware.js normalizes configuration, loads path rules, initializes check modules, and creates the middleware function. Each request resolves its path, method, client address, and route plan. shouldApply combines global middleware selection with the route plan.
The logger is attached before the ordinary checks when enabled. A shared denial helper records the reason and sends a status with a JSON or text error according to the request and configuration. A rejected request returns early instead of calling the downstream handler.
Legacy enforcement sequence
The current core executes the following sequence for a request. Individual layers run only when enabled and applicable:
- Attach logging and request timing; resolve the route plan.
- Enforce the global method policy when enabled. This happens before the request-exemption shortcut.
- Check IP/path exemptions, then the active blocklist.
- Validate headers and any enabled WASM header, URL, content, or recent-behavior checks.
- Apply geo policy, honeypot timing, and rate/flood limits.
- Inspect static and learned malicious keywords, then UUID probing.
- Evaluate the eligible unknown-route anomaly path.
- Attach completed-response keyword learning and call
next()for an allowed request.
Method rejection can return 405; ordinary rate excess can return 429; blocklist and other blocking paths can return 403. The configured checks decide whether a denial also changes IP reputation.
What unknown-route anomaly detection means
The core compares the requested path with discoverable Express-style routes. If the route table is unavailable or empty, it treats the request as known rather than applying aggressive unknown-route anomaly detection to every proxied request.
For an eligible unknown route, the anomaly module scores features. A positive model result alone is insufficient: the model must meet training-volume requirements and recent-behavior analysis must confirm scanning-related activity before the core issues its anomaly block. This gate explains why a suspicious score and an actual denial can differ.
Route analysis becomes a per-request plan
sourceAst.js uses bounded handler-source analysis to infer API, form, and authentication intent. pathManifest.js combines source signals with live framework route extraction. The compiler turns discovered protections into path overrides; it does not take over the framework router.
pathRules.js evaluates those rules with route metadata for each request. Explicit policy can disable one check or override rate limits for a prefix. Rebuilding a manifest is a discovery operation; enforcing its compiled policy is a runtime operation.
State and process boundaries
| Module group | State or computation | Scope to consider |
|---|---|---|
rateLimiter.js | Recent timestamps for request-rate windows. | An external cache can share the window; the fallback cache is local. |
| Block, exemption, keyword, and geo stores | Operational policy and learned data. | SQLite and CSV persistence paths differ from distributed request state. |
anomalyDetector.js and modelStore.js | Loaded model and recent behavior. | A shared model artifact does not automatically share every behavior buffer. |
middlewareLogger.js | Completed requests and denial reasons. | Log persistence supplies evidence and input for offline training. |
The rate-limiter module initializes its cache from configuration and otherwise uses a process-local fallback. Configure shared state deliberately when multiple Node workers must enforce one limit.
Response hooks and offline learning
For allowed requests, a response finish hook supplies the actual status to keyword learning. That hook runs after the handler response finishes; it cannot undo an already delivered response. The middleware logger also observes completion.
- Request logsCompleted responses supply paths, client identity, status, and timing.
- Feature extractionRecords are normalized and combined with per-client activity windows.
- Train and reviewVolume thresholds gate model training; route exemptions filter learned keywords.
- Publish artifactsThe runtime loads persisted model state and updated learned keywords.
The training job is separate from ordinary application request handling. Reading logs and rebuilding a forest does not need to happen for every request. Runtime keyword learning, where enabled, is a separate mechanism that can use completed-response signals.
Offline training reads configured access logs or middleware records and persists a language-native forest artifact. Review feature and model compatibility before moving artifacts between Node, Rust/WASM, Python, and Java.
| Feature | What it describes | Why it helps |
|---|---|---|
path_len | Length of the request path. | Unusually long probing paths. |
kw_hits | Matches against malicious keywords. | Known probing patterns within a path. |
resp_time | Response duration in seconds. | Behavior that differs from the normal timing distribution. |
status_idx | An encoded response-status category. | Response outcomes without treating status codes as continuous measurements. |
burst_count | Requests associated with the client in the burst window. | Rapid repeated activity. |
total_404 | Not-found responses in the relevant per-client history. | Repeated discovery of nonexistent routes. |
The feature order is part of the model contract. A trainer and scorer must agree on the order, status mapping, time units, and window semantics. Summary fields ending in _ms use milliseconds; the feature vector uses seconds. Sharing feature names alone does not make every model artifact interchangeable.
Optional WASM execution
wasmAdapter.js loads available Rust-backed primitives and converts JavaScript inputs into the WASM representation. The middleware still decides which checks apply, what to log, and when to reject a request. If initialization or conversion is unsuitable, JavaScript implementations remain available.
Crossing the binding boundary has a fixed conversion cost. Large feature batches and forest workloads can benefit; small object-heavy calls may be faster in JavaScript. WASM is a computation backend, not a separate HTTP firewall service.
Worked example: one denied request
A nonexempt request with valid headers reaches the rate limiter. If its shared or local window exceeds the configured maximum, the middleware marks the denial, sends 429 unless a flood or blocklist path determines another status, and returns. The application handler does not run. Logging can still observe the completed denial because its hook was attached earlier.
System overview explains the common layers; operations covers CLI, training, and runtime configuration.
Trace the implementation
Follow these files in the monorepo to connect the architecture to the code:
