wafio-client
v1.0.0
Published
Node.js/Bun client for Wafio WAF TCP mTLS (analyze requests, check block)
Maintainers
Readme
wafio-client
A production-ready TypeScript/JavaScript client for Wafio WAF (Web Application Firewall) over TCP mTLS. Analyze incoming HTTP requests for malicious patterns and check if clients are currently blocked.
Works with:
- ✅ Node.js 18+
- ✅ Bun
- ✅ TypeScript (full type support)
- ✅ All major frameworks: Express, Fastify, Hono, and any custom HTTP server
Features:
- Fail-open by default – if the Wafio server is unreachable, requests are allowed (circuit breaker pattern)
- Connection pooling – efficiently handle concurrent requests
- Auto-reconnect with cooldown to prevent hammering a down server
- Keepalive ping – maintains long-lived connections
- Framework-agnostic – works with any HTTP framework via
RequestSnapshot - Full feature parity with Go and PHP client libraries
Installation
npm
npm install wafio-clientMonorepo (local link)
{
"dependencies": {
"wafio-client": "file:../packages/wafio-client"
}
}Then run npm install.
Quick Start
1. Get mTLS Credentials
Call the Wafio API to generate credentials:
curl -X POST "http://localhost:9087/api/projects/{PROJECT_ID}/mtls-keys" \
-H "Authorization: Bearer {JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"my-app"}'Save the response JSON to a file (e.g., mtls-credentials.json). File format:
{
"ca_pem": "-----BEGIN CERTIFICATE-----\nMIIC8zCCAdugAwIBAgIRAIQVaAhpNJps+mtxouyf8gowDQYJKoZIhvcNAQELBQAw\n...(full CA certificate)...\n-----END CERTIFICATE-----\n",
"client_cert_pem": "-----BEGIN CERTIFICATE-----\nMIIC/DCCAeSgAwIBAgIQKJvrt86e9fY3QOe2wQJgADANBgkqhkiG9w0BAQsFADAT\n...(full client certificate)...\n-----END CERTIFICATE-----\n",
"client_key_pem": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCocinxcsZMEJT4\n...(full private key)...\n-----END PRIVATE KEY-----\n",
"tcp_url": "tcp.wafio.cloud:9443",
"created_at": "2026-02-15T04:51:12.376946+07:00",
"id": "ee528f42-c207-49fa-8e04-6d660b765588",
"name": "My MTLS Key",
"project_id": "5c83e212-1f07-4915-b086-066d361ed193",
"secret": "ebe63f6c1c053b1ddfed72d6b86e76fdfc79d848c3413336b60740542ef9fc99"
}Field Descriptions:
ca_pem(string): CA certificate in PEM format (needed to verify the Wafio server)client_cert_pem(string): Client certificate in PEM format (identifies your application to the server)client_key_pem(string): Client private key in PEM format (must keep secret)tcp_url(string, optional): TCP endpoint from server (for exampletcp.wafio.cloud:9443). If present, JS client uses this automatically whenhost/portare not set.id(string): Unique key ID (use as username for certificate renewal)secret(string): Renewal secret (use as password for certificate renewal via Basic Auth)project_id(string): The project this key belongs toname(string): Human-readable name you gave to this keycreated_at(string): ISO 8601 timestamp when the key was created
2. Analyze Requests
import { WafioClient, loadMtlsCredentialsFile } from 'wafio-client';
// Load credentials from file
const credentials = loadMtlsCredentialsFile('./mtls-credentials.json');
// Create client
const client = new WafioClient({
credentials, // tcp_url in credentials file is used automatically
});
// Connect to Wafio server
await client.connect();
// Analyze an incoming request
const result = await client.analyze({
method: 'GET',
uri: '/search?q=<script>alert(1)</script>',
remote_addr: '192.168.1.100',
host: 'example.com',
headers: {
'user-agent': ['Mozilla/5.0'],
'referer': ['https://google.com'],
},
user_agent: 'Mozilla/5.0',
request_id: 'req-12345',
});
// Check the response
if (result.action === 'block') {
console.log(`REQUEST BLOCKED: ${result.message} (categories: ${result.categories}, score: ${result.score})`);
// Return 403 Forbidden to client
} else if (result.action === 'log') {
console.log(`REQUEST LOGGED: ${result.message} (score: ${result.score})`);
// Allow request but log it
} else {
console.log(`REQUEST ALLOWED: ${result.message}`);
// Allow request
}
await client.close();Core Concepts
Analyze vs CheckBlock
analyze(req) – Full WAF analysis:
- Runs the request through the OWASP CRS (Core Rule Set)
- Scores the request based on matched rules
- Returns
action(allow/log/block),score,categories, and detailedmessage - Use this when you need to understand why a request was blocked
checkBlock(key) – Fast block status check:
- Checks if a
key(e.g., IP address, user ID) is currently in the block window - Useful for rate-limited clients (blocks for N seconds after M violations)
- Returns only
blocked(true/false) andblock_untiltimestamp - Very fast – no WAF analysis, just a cache lookup
Fail-Open Behavior
By default (failOpenOnUnreachable: true), the client will:
- On timeout/unreachable: Return
{ action: 'allow' }(not blocked) immediately - Set fail-open window: For the next 5 seconds, immediately allow all requests (no server calls)
- Resume normal operation: After cooldown, attempt to reconnect
This ensures your application never blocks traffic when Wafio is down.
To disable fail-open (strict mode):
const client = new WafioClient({
credentials: '.../mtls-credentials.json',
failOpenOnUnreachable: false, // Strict mode: throw on server unavailable
});Connection Pooling
For high-traffic applications, use a connection pool instead of a single client:
import { WafioClientPool, loadMtlsCredentialsFile } from 'wafio-client';
const pool = new WafioClientPool({
credentials: loadMtlsCredentialsFile('./mtls-credentials.json'),
poolSize: 10, // 10 concurrent connections (default 5)
});
// Initialize the pool (fetches tier limits from server)
await pool.init();
// Use the pool (clients auto-checkout/return)
const result = await pool.analyze({
method: 'POST',
uri: '/api/submit',
remote_addr: '203.0.113.42',
host: 'api.example.com',
headers: { 'content-type': ['application/json'] },
body: '{"user":"alice"}',
});
console.log('Action:', result.action);
await pool.close();Framework Integration
Express.js
import express from 'express';
import { WafioClient, fromRequest, buildAnalyzeRequest } from 'wafio-client';
const app = express();
const wafioClient = new WafioClient({
credentials: './mtls-credentials.json',
});
app.use(express.json());
app.use(express.raw({ type: '*/*', limit: '10mb' }));
// WAF middleware
app.use(async (req, res, next) => {
try {
// Convert Express request to framework-agnostic snapshot
const snapshot = fromRequest(req);
// Build analyze request
const analyzeReq = buildAnalyzeRequest(snapshot);
// Analyze with Wafio
const result = await wafioClient.analyze(analyzeReq);
if (result.action === 'block') {
return res.status(403).json({
error: 'Access Denied',
message: result.message,
categories: result.categories,
});
}
// Request is allowed; proceed to next middleware/handler
next();
} catch (err) {
console.error('WAF error:', err);
// Fail-open: allow the request if there's an error
next();
}
});
app.get('/', (req, res) => {
res.json({ message: 'Hello, World!' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});Fastify
import Fastify from 'fastify';
import { WafioClient, fromRequest, buildAnalyzeRequest } from 'wafio-client';
const fastify = Fastify();
const wafioClient = new WafioClient({
credentials: './mtls-credentials.json',
});
// WAF hook
fastify.addHook('preValidation', async (request, reply) => {
try {
const snapshot = fromRequest(request);
const analyzeReq = buildAnalyzeRequest(snapshot);
const result = await wafioClient.analyze(analyzeReq);
if (result.action === 'block') {
return reply.status(403).send({
error: 'Access Denied',
message: result.message,
});
}
} catch (err) {
console.error('WAF error:', err);
}
});
fastify.get('/', async () => {
return { message: 'Hello from Fastify!' };
});
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err;
console.log('Server running on port 3000');
});Hono
import { Hono } from 'hono';
import { WafioClient, fromRequest, buildAnalyzeRequest } from 'wafio-client';
const app = new Hono();
const wafioClient = new WafioClient({
credentials: './mtls-credentials.json',
});
// WAF middleware
app.use(async (c, next) => {
try {
const snapshot = fromRequest(c.req);
const analyzeReq = buildAnalyzeRequest(snapshot);
const result = await wafioClient.analyze(analyzeReq);
if (result.action === 'block') {
return c.json(
{
error: 'Access Denied',
message: result.message,
},
403
);
}
} catch (err) {
console.error('WAF error:', err);
}
await next();
});
app.get('/', (c) => {
return c.json({ message: 'Hello from Hono!' });
});
export default app;Configuration Options
When creating a client, you can customize timeouts and behaviors. All have sensible defaults:
const client = new WafioClient({
// === Required ===
credentials: WafioCredentials | string; // Credentials object or path to file
// === Optional endpoint override ===
host?: string; // Default: host parsed from credentials.tcp_url, fallback "localhost"
port?: number; // Default: port parsed from credentials.tcp_url, fallback 9089
// === Timeouts (all in milliseconds; shown with defaults) ===
requestTimeoutMs?: number; // Timeout for a single request
// Default: 300ms
// 0 = no timeout
// On timeout, client returns allow (if failOpenOnUnreachable=true)
connectTimeoutMs?: number; // Timeout when establishing TLS connection
// Default: 2000ms (2s)
// Only used for initial connect or reconnect
reconnectCooldownMs?: number; // Wait time after failed connection before retrying
// Default: 2000ms (2s)
// Prevents hammering a down server
failOpenCooldownMs?: number; // Duration to immediately allow requests after a timeout
// Default: 5000ms (5s)
// Works with failOpenOnUnreachable=true (circuit breaker)
keepaliveIntervalMs?: number; // Send checkBlock("__keepalive__") every N milliseconds
// Default: 25000ms (25s)
// Keeps TCP connection alive, detects dead connections early
// === Behavior ===
failOpenOnUnreachable?: boolean; // If Wafio server is unreachable, return allow (true)
// Default: true (fail-open, circuit breaker pattern)
// Set to false for strict mode (throw if server down)
maxPayloadSize?: number; // Maximum request body size in bytes (0 = no limit)
// Default: 0 (no limit)
// Rejects requests larger than this
onRequestTimeout?: (info: { timeoutMs: number }) => void; // Callback on timeout
// Useful for logging/metrics
logRequestTimeout?: boolean; // Log timeout to console
// Default: false
// Only used if onRequestTimeout is not provided
});Web Server Integration (Advanced)
Using RequestSnapshot
For framework-agnostic code, build a RequestSnapshot manually:
import { buildAnalyzeRequest, type RequestSnapshot } from 'wafio-client';
// You can build a snapshot from any HTTP context
const snapshot: RequestSnapshot = {
method: 'POST',
url: '/api/users',
headers: {
'content-type': 'application/json',
'x-forwarded-for': '203.0.113.1',
},
body: '{"name":"alice"}',
remoteAddress: '10.0.0.5', // Behind proxy; real IP in headers
host: 'api.example.com',
requestId: 'req-12345',
userAgent: 'MyApp/1.0',
};
const analyzeReq = buildAnalyzeRequest(snapshot);
const result = await client.analyze(analyzeReq);Custom Client IP Resolution
By default, buildAnalyzeRequest() resolves the client IP in this order:
- X-Forwarded-For header (first comma-separated value)
- X-Real-IP header
- Forwarded header (RFC 7239; extract
for=parameter) - remoteAddress (socket address)
- Fallback: "127.0.0.1"
To override:
import { resolveClientIp, buildAnalyzeRequest } from 'wafio-client';
const snapshot: RequestSnapshot = {
method: 'GET',
url: '/',
headers: { 'x-forwarded-for': '203.0.113.1' },
};
// Automatically resolves to 203.0.113.1
const clientIp = resolveClientIp(snapshot);
console.log(clientIp); // => "203.0.113.1"Header Normalization
The server expects Record<string, string[]> (array per header). Normalize headers:
import { normalizeHeaders } from 'wafio-client';
const raw = {
'content-type': 'application/json',
'x-forwarded-for': '203.0.113.1',
};
const normalized = normalizeHeaders(raw);
// => { 'content-type': ['application/json'], 'x-forwarded-for': ['203.0.113.1'] }Credentials from Environment
import { WafioClient } from 'wafio-client';
const client = new WafioClient({
host: process.env.WAFIO_HOST || 'localhost',
port: parseInt(process.env.WAFIO_PORT || '9089'),
credentials: {
client_cert_pem: process.env.WAFIO_CLIENT_CERT!,
client_key_pem: process.env.WAFIO_CLIENT_KEY!,
ca_pem: process.env.WAFIO_CA_CERT!,
},
});Custom Timeouts
Use withWafioTimeout() to add an application-level timeout:
import { withWafioTimeout } from 'wafio-client';
const analyzePromise = client.analyze({
method: 'POST',
uri: '/api/heavy-work',
remote_addr: '192.168.1.1',
// ...
});
// Timeout after 100ms at application level (separate from requestTimeoutMs)
const result = await withWafioTimeout(analyzePromise, 100, () => {
console.log('Application timeout – allowing request');
});API Reference
WafioClient
constructor(options: WafioClientOptions)
Creates a new Wafio client.
Parameters:
host(string): Optional endpoint override. Default: host parsed fromcredentials.tcp_url, fallbacklocalhostport(number): Optional endpoint override. Default: port parsed fromcredentials.tcp_url, fallback9089credentials(WafioCredentials | string): Credentials object or path to file. Required.requestTimeoutMs(number): Timeout per request (ms). Default: 300connectTimeoutMs(number): Timeout for TLS connection (ms). Default: 2000reconnectCooldownMs(number): Delay before reconnect (ms). Default: 2000failOpenCooldownMs(number): Circuit breaker cooldown (ms). Default: 5000keepaliveIntervalMs(number): Ping interval (ms). Default: 25000failOpenOnUnreachable(boolean): Return allow if server down. Default: truemaxPayloadSize(number): Max request body size (bytes). Default: 0 (no limit)onRequestTimeout(function): Callback on timeoutlogRequestTimeout(boolean): Log timeout to console. Default: false
async connect(): Promise<void>
Connect to the Wafio server (mTLS). Optional – analyze() and checkBlock() will auto-connect if needed.
Throws:
- Error if connection fails (credentials invalid, server unreachable, timeout, etc.)
Fail-Open Behavior:
If failOpenOnUnreachable=true and connection fails, connect() resolves (does not throw). Subsequent analyze() calls return "allow".
async analyze(req: AnalyzeRequest): Promise<AnalyzeResponse>
Analyze an HTTP request for malicious patterns.
Parameters:
req.method(string): HTTP method (GET, POST, etc.)req.uri(string): Request URI with query stringreq.remote_addr(string): Client IP addressreq.host(string): Host header valuereq.headers(Record<string, string[]>): HTTP headers (array per header name)req.body(string): Request body as stringreq.body_b64(string): Request body as base64 (if binary)req.body_size(number): Original body size in bytes (if truncated)req.user_agent(string): User-Agent headerreq.request_id(string): Unique request ID for logging
Returns:
AnalyzeResponse:action(string): "allow", "log", or "block"score(number): Overall threat score (0-1000+)categories(string[]): Attack categories detected (SQLi, XSS, etc.)message(string): Human-readable descriptionanalyze_method(string): How request was analyzed (rule-based, fastscan, bot, etc.)block_until(string): Timestamp until IP is blocked (if action == "block")error(string): Error message (if any)
Fail-Open Behavior:
If failOpenOnUnreachable=true and a timeout/connection error occurs or server-side limits are hit:
- Returns
{ action: 'allow' }immediately - Sets fail-open window – subsequent requests return "allow" for 5s without retrying
- After cooldown expires, resumes normal operation (attempts to reconnect)
async checkBlock(key: string): Promise<CheckBlockResponse>
Check if a client (IP, user ID, etc.) is currently in the block window.
Parameters:
key(string): The key to check (e.g., "192.168.1.1", "user-123")
Returns:
CheckBlockResponse:blocked(boolean): true if the key is blocked, false otherwiseblock_until(string): ISO timestamp when the block expires (if blocked)error(string): Error message (if any)
Use Case: Before running expensive operations (login, payment, file upload), quickly check if the client is blocked.
close(): void
Close the TCP connection and stop keepalive.
get connected(): boolean
Returns true if the client is currently connected to the Wafio server.
async getTierLimits(): Promise<number | undefined>
Fetch tier limits from the server (max concurrent connections allowed for your project).
Returns:
number: Maximum TCP connections allowed (0 if no limit)undefinedif request fails or tier limits not supported
Use Case:
Called automatically by WafioClientPool.init() to cap the pool size.
WafioClientPool
constructor(options: WafioClientPoolOptions)
Create a new connection pool.
Parameters:
poolSize(number): Number of TCP connections to maintain. Default: 5- All
WafioClientOptionsparameters (host, port, credentials, timeouts, etc.)
async init(): Promise<void>
Initialize the pool:
- Connects once to fetch tier limits from the server
- Caps pool size to server limit (if applicable)
- Creates and connects all pool clients
Idempotent – safe to call multiple times.
Returns:
- Promise that resolves when pool is ready
- Rejects if init fails (credentials invalid, server unreachable, etc.)
async analyze(req: AnalyzeRequest): Promise<AnalyzeResponse>
Analyze using a client from the pool. Automatically calls init().
async checkBlock(key: string): Promise<CheckBlockResponse>
Check block status using a client from the pool. Automatically calls init().
async withClient<T>(fn: (client: WafioClient) => Promise<T>): Promise<T>
Generic helper: get a client from the pool, run a function, and return the client.
Useful for custom operations:
const customResult = await pool.withClient(async (client) => {
const resp1 = await client.analyze(req1);
const resp2 = await client.analyze(req2);
return { resp1, resp2 };
});close(): void
Close all connections and clean up the pool.
Helper Functions
loadMtlsCredentialsFile(filePath: string): WafioCredentials
Load and normalize mTLS credentials from a JSON file.
Parameters:
filePath(string): Path to mtls-credentials.json
Returns:
WafioCredentialsobject withclient_cert_pem,client_key_pem,ca_pem
Throws:
- Error if file not found, JSON invalid, or required fields missing
loadMtlsCredentialsFileFull(filePath: string): MtlsCredentialsFile & WafioCredentials
Load the full credentials file (includes project_id, secret, id, etc.).
Returns:
- Full credentials object with all fields from the API response
Useful for:
- Storing/managing the
secret(needed to renew certificates later) - Logging
project_idfor audit trails
loadCredentialsFromFile(filePath: string): WafioCredentials
Alias to loadMtlsCredentialsFile().
normalizeHeaders(headers: Record<string, string | string[] | undefined>): Record<string, string[]>
Convert headers to the format expected by the server (array per header).
Parameters:
headers: Object with string or array values
Returns:
- Normalized headers with array values
Example:
const headers = normalizeHeaders({
'content-type': 'application/json',
'x-custom': ['a', 'b'], // Already an array
});
// => { 'content-type': ['application/json'], 'x-custom': ['a', 'b'] }fromRequest(req: IncomingRequestLike): RequestSnapshot
Convert a framework request object to a framework-agnostic RequestSnapshot.
Parameters:
req: An object with Express/Fastify/Hono-like properties:method(string): HTTP methodurl|originalUrl(string): Request URLheaders(Record<string, string | string[]>): HTTP headersbody(unknown): Request bodysocket?.remoteAddress(string): Socket IPip(string): Client IPhostname(string): Hostget(name: string)(function): Get header by name
Returns:
RequestSnapshotready forbuildAnalyzeRequest()
Note: This function is framework-agnostic: it doesn't import Express, Fastify, etc. Any object with the right shape can be passed.
buildAnalyzeRequest(snapshot: RequestSnapshot): AnalyzeRequest
Convert a request snapshot to an AnalyzeRequest for WAF analysis.
Parameters:
snapshot: Framework-agnostic request snapshot
Returns:
AnalyzeRequestready forclient.analyze()
What it does:
- Normalizes headers (array per header)
- Resolves client IP from proxy headers (X-Forwarded-For, X-Real-IP, Forwarded) or remoteAddress
- Defaults empty fields (method → "GET", uri → "/", etc.)
resolveClientIp(snapshot: RequestSnapshot): string
Extract the real client IP from the request.
Algorithm (in order):
- X-Forwarded-For header (first comma-separated value – the original client)
- X-Real-IP header (used by some proxies)
- Forwarded header (RFC 7239; extract
for=parameter) - remoteAddress (socket address)
- Fallback: "127.0.0.1"
When to use:
Your application is behind a reverse proxy or load balancer. Ensure the proxy populates X-Forwarded-For or X-Real-IP.
withWafioTimeout<T>(promise: Promise<T>, ms: number, onTimeout?: () => void): Promise<T>
Add an application-level timeout to a Wafio operation.
Parameters:
promise: Promise fromclient.analyze(),client.checkBlock(), etc.ms: Timeout in milliseconds. 0/negative = no timeoutonTimeout: Optional callback invoked before rejecting
Returns:
- Promise that rejects with "Wafio timeout" if time elapses
Note:
This is separate from requestTimeoutMs (which is per-request at the TCP level). Use this for application-level timeouts (e.g., "don't wait more than 100ms for WAF").
Default Configuration (Recommended)
These defaults are production-tested:
| Setting | Default | Rationale | |---------|---------|-----------| | requestTimeoutMs | 300ms | Fast response; timeout triggers fail-open | | connectTimeoutMs | 2000ms (2s) | Reasonable; prevents long hangs on connect | | reconnectCooldownMs | 2000ms (2s) | Prevents hammering a down server | | failOpenCooldownMs | 5000ms (5s) | Circuit breaker; allows recovery | | keepaliveIntervalMs | 25000ms (25s) | Keeps TCP alive; detects dead conns early | | failOpenOnUnreachable | true | Resilience; app never blocks due to WAF outage |
Override only if you have specific requirements (e.g., stricter timeout, different proxy setup).
Best Practices
1. Use Fail-Open in Production
Always set failOpenOnUnreachable: true (default). This ensures your application stays online even if Wafio is temporarily unavailable. Use monitoring/alerting to detect when Wafio is down, not blocking traffic.
2. Log & Monitor
Use onRequestTimeout callback for metrics:
const client = new WafioClient({
credentials: '...',
onRequestTimeout: (info) => {
console.warn(`Wafio timeout after ${info.timeoutMs}ms`);
// Send to observability platform
metrics.incrementCounter('wafio.timeout', {
timeout_ms: info.timeoutMs,
});
},
});3. Set Appropriate Timeouts
- Development: Higher timeout (e.g., 1s) for slower networks
- Production: Lower timeout (e.g., 300ms, default) for responsiveness
- Load testing: Use a pool with sufficient size to avoid queuing
4. Handle Server Limits
If you receive code: "RATE_LIMIT" or "CONCURRENCY_LIMIT":
- Server is busy or your app exceeded connection limits
- Response is still
action: "allow"(fail-open) - Reduce request rate or increase pool size
5. Client IP Behind Proxies
Always ensure your reverse proxy (nginx, ALB, etc.) sets:
X-Forwarded-For: <client-ip>Or:
X-Real-IP: <client-ip>Wafio will extract the real client IP automatically.
Troubleshooting
| Issue | Solution |
|-------|----------|
| "CA PEM is required" | Ensure ca_pem is in the credentials JSON |
| "Cannot connect" | Check host, port, and firewall. Verify credentials are valid. |
| All requests return "allow" | Client is likely in fail-open cooldown (Wafio unreachable). Check server logs. |
| Requests timing out | Increase requestTimeoutMs. Check network latency. |
| Getting "block" for normal requests | Check WAF rule configuration on the server side. |
| Pool is slow | Increase poolSize. Monitor pool.init() time. |
| Can't connect behind corporate proxy | TCP mTLS doesn't support HTTP proxies. Contact your network team. |
Examples
Complete examples are in package-level examples/ folders:
- packages/wafio-client/examples/client.js – basic usage (Node.js)
- packages/wafio-client/examples/web-sample/ – complete Express.js middleware
- packages/wafio-client-go/examples/go-web-sample/ – complete Go middleware sample
- packages/wafio-client-php/examples/laravel-sample/ – Laravel integration
See Also
- docs/README.md – documentation index
- docs/API_ENDPOINTS.md – WAFio management API reference
- docs/TCP_MTLS_READINESS.md – TCP mTLS runtime guide
