JavaScript scripting agent for Zentinel reverse proxy. Write custom request/response processing logic in JavaScript.
- Execute JavaScript scripts on request/response lifecycle events
- Fast, lightweight QuickJS engine (via rquickjs)
- Console API for logging (console.log, console.warn, console.error)
- Return-based decision model (allow, block, redirect)
- Header manipulation (add/remove request and response headers)
- Fail-open mode for graceful error handling
# Install just this agent
zentinel bundle install js
# Or install all bundled agents
zentinel bundle installThe bundle command downloads the correct binary for your platform and places it in the standard location. See the bundle documentation for details.
cargo install zentinel-agent-jsgit clone https://github.com/zentinelproxy/zentinel-agent-js
cd zentinel-agent-js
cargo build --releasezentinel-js-agent --socket /var/run/zentinel/js.sock \
--script /etc/zentinel/scripts/handler.js| Option | Environment Variable | Description | Default |
|---|---|---|---|
--socket |
AGENT_SOCKET |
Unix socket path | /tmp/zentinel-js.sock |
--script |
JS_SCRIPT |
JavaScript script file | (required) |
--verbose |
JS_VERBOSE |
Enable debug logging | false |
--fail-open |
FAIL_OPEN |
Allow requests on script errors | false |
function on_request_headers(request) {
// Block admin access
if (request.uri.includes("/admin")) {
return { decision: "block", status: 403, body: "Forbidden" };
}
// Allow all other requests
return { decision: "allow" };
}| Hook | Description |
|---|---|
on_request_headers(request) |
Called when request headers are received |
on_response_headers(response) |
Called when response headers are received |
{
method: "GET", // HTTP method
uri: "/api/users", // Request URI with query string
client_ip: "192.168.1.1", // Client IP address
correlation_id: "abc123", // Request correlation ID
headers: { // Request headers (name -> value)
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0..."
}
}{
status: 200, // HTTP status code
correlation_id: "abc123", // Request correlation ID
headers: { // Response headers (name -> value)
"Content-Type": "application/json",
"X-Custom": "value"
}
}Scripts should return a decision object:
// Allow the request
return { decision: "allow" };
// Block with custom status and body
return { decision: "block", status: 403, body: "Access Denied" };
// Redirect to another URL
return { decision: "redirect", status: 302, body: "https://example.com/login" };function on_request_headers(request) {
return {
decision: "allow",
add_request_headers: {
"X-Processed-By": "js-agent",
"X-Client-IP": request.client_ip
},
remove_request_headers: ["X-Debug"],
add_response_headers: {
"X-Frame-Options": "DENY"
}
};
}Add tags for logging and analytics:
function on_request_headers(request) {
if (request.headers["User-Agent"]?.includes("bot")) {
return {
decision: "allow",
tags: ["bot-detected", "monitoring"]
};
}
return { decision: "allow" };
}function on_request_headers(request) {
console.log("Processing request:", request.uri);
console.warn("Warning message");
console.error("Error message");
return { decision: "allow" };
}function on_request_headers(request) {
const ua = request.headers["User-Agent"] || "";
const badBots = ["sqlmap", "nikto", "nessus", "masscan"];
for (const bot of badBots) {
if (ua.toLowerCase().includes(bot)) {
return {
decision: "block",
status: 403,
tags: ["bot-blocked", bot]
};
}
}
return { decision: "allow" };
}function on_request_headers(request) {
// Add rate limit tier based on path
let tier = "standard";
if (request.uri.startsWith("/api/v1/")) {
tier = "api";
} else if (request.uri.startsWith("/admin/")) {
tier = "admin";
}
return {
decision: "allow",
add_request_headers: {
"X-Rate-Limit-Tier": tier
}
};
}function on_request_headers(request) {
// Skip for public paths
if (request.uri.startsWith("/public/") || request.uri === "/health") {
return { decision: "allow" };
}
// Check for auth header
if (!request.headers["Authorization"]) {
return {
decision: "block",
status: 401,
body: "Authentication required"
};
}
return { decision: "allow" };
}function on_response_headers(response) {
return {
decision: "allow",
add_response_headers: {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000"
}
};
}agents {
agent "js" {
type "custom"
transport "unix_socket" {
path "/var/run/zentinel/js.sock"
}
events "request_headers" "response_headers"
timeout-ms 100
failure-mode "open"
}
}When --fail-open is enabled, script errors will:
- Log the error
- Allow the request to proceed
- Add
js-errorandfail-opentags to audit metadata
When --fail-open is disabled (default), script errors will:
- Log the error
- Block the request with 500 status
- Add
js-errortag to audit metadata
| Feature | zentinel-agent-js | zentinel-agent-lua |
|---|---|---|
| Engine | QuickJS | mlua (Lua 5.4) |
| Scripting | Single script file | Multiple scripts with metadata |
| Hot Reload | No | Yes |
| VM Pooling | No | Yes |
| Resource Limits | Minimal | Comprehensive (memory, CPU, time) |
| Standard Library | Basic (console) | Rich (JSON, crypto, regex, etc.) |
| Use Case | Simple scripts | Production workloads |
Use zentinel-agent-js for:
- Simple request filtering logic
- Quick prototyping
- Lightweight deployments
Use zentinel-agent-lua for:
- Complex processing logic
- Production environments with strict resource limits
- Multiple scripts with hot reload
# Run with debug logging
RUST_LOG=debug cargo run -- --socket /tmp/test.sock --script ./test.js --verbose
# Run tests
cargo testApache-2.0