Skip to main content

Security Best Practices

Protect your AI APIs with comprehensive security measures

This guide covers authentication, authorization, rate limiting, and other security best practices for deploying NeuroLink server adapters in production.


Authentication​

NeuroLink server adapters support multiple authentication strategies out of the box.

Bearer Token Authentication​

Bearer tokens (JWT, OAuth tokens) are the most common authentication method for APIs:

import { createServer, createAuthMiddleware } from "@juspay/neurolink";
import jwt from "jsonwebtoken";

const server = await createServer(neurolink, {
framework: "hono",
config: { port: 3000 },
});

server.registerMiddleware(
createAuthMiddleware({
type: "bearer",
validate: async (token, ctx) => {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return {
id: decoded.sub,
email: decoded.email,
roles: decoded.roles || [],
};
} catch (error) {
return null; // Invalid token
}
},
skipPaths: ["/api/health", "/api/ready"],
errorMessage: "Invalid or expired token",
}),
);

await server.initialize();
await server.start();

API Key Authentication​

For service-to-service communication or simple API access:

server.registerMiddleware(
createAuthMiddleware({
type: "api-key",
headerName: "x-api-key", // Default header for api-key type
validate: async (apiKey, ctx) => {
// Validate against your API key store
const keyRecord = await db.apiKeys.findOne({ key: apiKey });

if (!keyRecord || keyRecord.revoked) {
return null;
}

// Check if key is expired
if (keyRecord.expiresAt && new Date() > keyRecord.expiresAt) {
return null;
}

return {
id: keyRecord.userId,
roles: keyRecord.scopes || [],
metadata: {
keyId: keyRecord.id,
rateLimit: keyRecord.rateLimit,
},
};
},
skipPaths: ["/api/health", "/api/ready", "/api/version"],
}),
);

Basic Authentication​

For simple username/password authentication:

server.registerMiddleware(
createAuthMiddleware({
type: "basic",
validate: async (credentials, ctx) => {
// credentials is base64 encoded "username:password"
const decoded = Buffer.from(credentials, "base64").toString();
const [username, password] = decoded.split(":");

const user = await db.users.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return null;
}

return {
id: user.id,
email: user.email,
roles: user.roles,
};
},
skipPaths: ["/api/health"],
}),
);

Custom Authentication​

For OAuth 2.0, OIDC, or custom schemes:

server.registerMiddleware(
createAuthMiddleware({
type: "custom",
extractToken: (ctx) => {
// Extract from custom header or query param
return ctx.headers["x-custom-auth"] || ctx.query.token || null;
},
validate: async (token, ctx) => {
// Validate with external auth provider
const response = await fetch("https://auth.example.com/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});

if (!response.ok) {
return null;
}

const user = await response.json();
return {
id: user.sub,
email: user.email,
roles: user.roles,
};
},
skipPaths: ["/api/health", "/api/ready"],
}),
);

Carrying middleware state to guards and handlers (carryMiddlewareState)​

Authentication middleware sets ctx.user. On the built-in adapters, whether the middleware that runs after it, and the route handler, see it depends on the adapter and on carryMiddlewareState, which is off by default: each adapter then propagates context as it always did. Express, Fastify and Koa build a fresh context for every middleware step and for the handler and carry only the response headers; Hono shares one context between the steps and the handler only when the client sent its own X-Request-ID.

The option does not apply to a context you build yourself: a custom adapter, or a route handler you call directly, can supply a context that already has ctx.user set.

So by default a role guard (createRoleMiddleware) registered after createAuthMiddleware sees the user on one request shape only. Measured: it answers 401 to every request on Express, Fastify and Koa, and on Hono without a client X-Request-ID, even for a user holding the required role, and a rate-limit keyGenerator reads ctx.user as undefined there. Set the option on the server to carry ctx.user, ctx.session, ctx.metadata and the abort signal to later middleware and the handler on every built-in adapter:

const server = await createServer(neurolink, {
framework: "express",
config: { carryMiddlewareState: true },
});

With it on, the role guard admits a user holding the role (200) and refuses one without it (403), and a keyGenerator sees ctx.user, on every built-in adapter. A key generator that keys on the user id then does what it says, so make it ignore a shared identity (ctx.user.shared), or every holder of one API key falls into a single bucket; the rate-limit examples below do. Turning the option on does not change whose identity the agent routes trust, and registerAllRoutes turns it on by itself when given perUserAuth, as the next section says.

Who the agent routes treat as the user (perUserAuth)​

Whether the agent routes (/agent/execute, /agent/stream, and the skill mutation routes' requestedBy) and the memory routes (/memory/*) trust ctx.user as a person is a separate choice from whether the server carries it: a single shared API key sets the same ctx.user for every caller, and taking it as the user would give every client one userId and no conversation memory (agent routes), or make every client a non-admin of its own deployment (memory routes).

So without an opt-in the agent routes resolve identity as they always have:

  • On Express, Fastify and Koa, and on Hono when the client sent no X-Request-ID, userId and sessionId come from the request body. ctx.user and ctx.session are ignored (they are not in the handler's context at all unless the server carries middleware state), and the skill routes use the body's requestedBy.
  • On Hono, when the client sent its own X-Request-ID header, an authenticated ctx.user wins over the body (and ctx.session over the body's sessionId), as it did before. Hono kept the middleware's context under that header, so the handler found it. That is the only built-in combination where the default ever guarded userId.
  • A context the built-in adapters did not hand the carry to keeps the same rule, an authenticated ctx.user wins: an adapter you wrote that sets ctx.user itself, or a route handler you call directly with a context you built (a test harness, for one).

The memory routes have none of these exceptions: without the opt-in they ignore ctx.user on every adapter and for every request.

Set perUserAuth: true when your middleware identifies individual people (one credential per person). Only this option, with carryMiddlewareState on, makes the agent routes take the user from ctx.user, and ignore the body's userId, on every built-in adapter and for every request:

registerAllRoutes(server, "/api", { perUserAuth: true });

registerAllRoutes also turns on carryMiddlewareState for the server, because the routes can only take identity from ctx.user if the adapter hands it to the handler. With carryMiddlewareState on, a perUserAuth request that your middleware authenticated reaches the handler with that user on every built-in adapter. A request that no middleware authenticated (optional authentication) runs as anonymous, with no userId and no session.

If you register the agent routes yourself with createAgentRoutes("/api", { perUserAuth: true }), create the server with carryMiddlewareState: true. Without it, whether a perUserAuth request has a user depends on the adapter and the request:

  • Hono, when the client sent its own X-Request-ID header: an authenticated request has its user and session. Hono hands the handler the middleware's context for such a request whether or not carryMiddlewareState is on, so a client that chooses to send the header is identified.
  • Every other request (Hono without that header, Express, Fastify and Koa): the handler has no ctx.user, so the request runs as anonymous, with no userId and no session. The body's userId and sessionId are still ignored.

Both are the strict behaviour for the requests that have no user, but the inconsistency between them is rarely what you want, so set the option.

A server with per-person authentication must set perUserAuth. Without it, on every adapter any authenticated caller can read or erase another person's sessions through the memory routes, and on every built-in adapter except Hono with X-Request-ID they can name another person's userId and sessionId in the request body and the agent routes will run as them. A custom adapter that sets ctx.user itself, or a handler called directly with a context you built, already takes the user from ctx.user over the body on the agent routes; the memory routes still ignore it. Any truthy value turns it on.

The routes can tell you when that is happening, but only when the server carries middleware state. Without perUserAuth and with carryMiddlewareState on, the first time a request that is served from the body names a different userId than its non-shared ctx.user, the agent routes write one warning to stderr (once per server, so a busy server is not flooded; the warning shows even with debug logging off) saying that per-person authentication without perUserAuth lets a caller name another user. The request is not refused: the body's userId is still used. The message carries no user id or other request value.

With carryMiddlewareState off, which is the default, there is no warning: the handler has no ctx.user on the requests served from the body, so there is nothing to compare the body's userId against. A per-person server that sets neither carryMiddlewareState nor perUserAuth therefore gets no notice, and any authenticated caller can still name another person; registerAllRoutes leaves carryMiddlewareState off unless it is given perUserAuth.

When it is raised, the warning stays quiet for a request whose ctx.user is marked shared: true, for a body that names the same user or none, for a request with no authenticated user, for a Hono request with X-Request-ID (where ctx.user already wins), and whenever perUserAuth is on. shared: true marks an identity that stands for every holder of one credential, such as a shared API key, rather than one person. Middleware that authenticates a shared key should set it on the user it returns, from createAuthMiddleware's validate or on ctx.user directly:

createAuthMiddleware({
type: "api-key",
validate: async (key) =>
key === process.env.TEAM_KEY ? { id: "team-key", shared: true } : null,
});

neurolink serve with NEUROLINK_SERVER_API_KEY sets the mark for you, and so does the dev playground user.

ModeAdapter and requestuserIdsession
default, no authenticationanythe body's userIdthe body's (on Hono with X-Request-ID, a ctx.session set by middleware wins)
default, one shared key (shared: true)Express, Fastify, Koa; Hono without X-Request-IDthe body's userIdthe body's
default, one shared keyHono with X-Request-IDthe key's identity (api-key), body ignorednone (ctx.session only), body ignored
default, per-person authenticationExpress, Fastify, Koa; Hono without X-Request-IDthe body's userId: any caller can name anyone (one warning, only with carryMiddlewareState on)the body's; ctx.session is ignored
default, per-person authenticationHono with X-Request-IDctx.user.id, body ignoredctx.session only, body ignored
default, per-person authenticationyour own adapter, or a direct handler callctx.user.id, body ignoredctx.session only, body ignored
perUserAuth: true, user identifiedany, when the handler receives ctx.user (always with registerAllRoutes)ctx.user.id, body ignoredctx.session only, body ignored
perUserAuth: true, no authenticated userany; also createAgentRoutes without carryMiddlewareState on Express, Fastify, Koa and Hono without X-Request-ID, where the handler never receives ctx.usernone, body ignorednone: body and ctx.session ignored, so anonymous callers never share a conversation

On Hono with X-Request-ID and no authenticated user (optional authentication), the default uses the body's userId and ctx.session, else the body's sessionId. The skill routes' requestedBy follows the same split: the body's by default, with the authenticated user winning on Hono with X-Request-ID, and always the authenticated user under perUserAuth.

The memory routes follow the same switch, without the per-adapter exceptions in the table above. By default they behave as they always have: ?userId= chooses the user on Redis, in-memory sessions are keyed by id alone, and GET /memory/stats and DELETE /memory/sessions are open to every caller that passes your authentication, whatever carryMiddlewareState says, because they never read ctx.user. With perUserAuth: true every memory route except GET /memory/health answers 401 to a request with no authenticated user (or an empty user id), a caller reaches only its own sessions, and statistics and clear-all need a role in memory.adminRoles (default ["admin"]). On Redis an admin reaches another user's session by naming that user in ?userId=; the "Memory routes and per-user authentication" section of the server adapters API reference lists exactly what an admin can and cannot reach.

registerAllRoutes with perUserAuth is what hands ctx.user to the memory routes, because it turns on carryMiddlewareState for the server. If you register the memory routes yourself with createMemoryRoutes("/api", { perUserAuth: true }), create the server with carryMiddlewareState: true. Without it, the user your authentication middleware sets does not reach the routes, so every request whose context has no ctx.user is answered 401, except GET /memory/health. The routes read ctx.user directly, so a custom adapter or a direct handler call that sets it itself is served as that user either way.

The scoped memory routes refuse a : in a user or session id. The Redis manager keys a session as <keyPrefix><userId>:<sessionId> without escaping either part, so user a asking for session b:c would reach the session c of user a:b. With perUserAuth, the memory routes therefore answer 400 when the authenticated user's id, an admin's ?userId= or the session id contains :, on every manager. A deployment whose user ids contain : cannot use the scoped memory routes. They also answer 400 when the authenticated user's id or an admin's ?userId= is session-only: that is the user id a session created without a userId is stored under on Redis, and the Redis manager reserves it, so do not issue it to a user.

neurolink serve with NEUROLINK_SERVER_API_KEY is a shared key and uses the default. Do not set perUserAuth on a server with no authentication: nothing sets ctx.user, so every agent request would run without a user or a session and every memory route would answer 401.

Hono behind one shared key has always been the odd case: clients that send X-Request-ID (NeuroLink's own HTTP client always does) run as the key's identity, api-key with no session, so all of them share one user and have no conversation memory, while clients that do not send it choose their own userId and sessionId. The default keeps that behaviour as it was. Do not set perUserAuth to fix it with a shared key: that makes every caller api-key on every adapter.

Skip Paths Configuration​

Certain endpoints should bypass authentication:

const authMiddleware = createAuthMiddleware({
type: "bearer",
validate: validateToken,
skipPaths: [
"/api/health", // Health checks
"/api/ready", // Readiness probes
"/api/version", // Version info
"/api/metrics", // Prometheus metrics (consider securing separately)
"/api/docs", // API documentation
"/api/auth/login", // Login endpoint
"/api/auth/refresh", // Token refresh
],
});

Rate Limiting​

Protect your API from abuse with configurable rate limiting.

Basic Configuration​

const server = await createServer(neurolink, {
framework: "hono",
config: {
port: 3000,
rateLimit: {
enabled: true,
maxRequests: 100, // 100 requests
windowMs: 60000, // per minute
message: "Too many requests, please try again later",
skipPaths: ["/api/health", "/api/ready"],
},
},
});

Per-IP Rate Limiting​

The default behavior limits requests by client IP:

import { createRateLimitMiddleware } from "@juspay/neurolink";

server.registerMiddleware(
createRateLimitMiddleware({
maxRequests: 100,
windowMs: 60000, // 1 minute
keyGenerator: (ctx) => {
// Use X-Forwarded-For when behind a proxy
return (
ctx.headers["x-forwarded-for"]?.split(",")[0].trim() ||
ctx.headers["x-real-ip"] ||
"unknown"
);
},
skipPaths: ["/api/health"],
}),
);

Per-User Rate Limiting​

Limit based on authenticated user:

server.registerMiddleware(
createRateLimitMiddleware({
maxRequests: 1000,
windowMs: 3600000, // 1 hour
keyGenerator: (ctx) => {
// A person's bucket is their user ID. A shared identity (one API key for
// many callers) would put every caller in one bucket, so it, and an
// anonymous caller, fall back to the forwarded address. Read
// x-forwarded-for only behind a trusted proxy that sets or overwrites it:
// otherwise the client chooses its own bucket by sending any value.
return ctx.user && !ctx.user.shared
? ctx.user.id
: ctx.headers["x-forwarded-for"] || "anonymous";
},
}),
);

Per-API-Key Rate Limiting​

Different limits for different API keys:

server.registerMiddleware(
createRateLimitMiddleware({
maxRequests: 100, // Default limit
windowMs: 60000,
keyGenerator: (ctx) => {
const apiKey = ctx.headers["x-api-key"];
// x-forwarded-for only behind a trusted proxy that sets or overwrites it
// (see above), and a string key when the header is absent
return apiKey
? `key:${apiKey}`
: `ip:${ctx.headers["x-forwarded-for"] || "unknown"}`;
},
onRateLimitExceeded: (ctx, retryAfter) => {
// Custom response with tier info
return {
error: {
code: "RATE_LIMIT_EXCEEDED",
message: "Rate limit exceeded. Upgrade your plan for higher limits.",
retryAfter,
upgradeUrl: "https://example.com/pricing",
},
};
},
}),
);

Sliding Window Rate Limiting​

For smoother rate limiting that prevents burst-and-wait patterns:

import { createSlidingWindowRateLimitMiddleware } from "@juspay/neurolink";

server.registerMiddleware(
createSlidingWindowRateLimitMiddleware({
maxRequests: 100,
windowMs: 60000,
subWindows: 10, // 10 sub-windows for smoother limiting
// x-forwarded-for only behind a trusted proxy (see above), and always a
// string key: a request without the header would otherwise key on undefined
keyGenerator: (ctx) =>
ctx.user && !ctx.user.shared
? ctx.user.id
: ctx.headers["x-forwarded-for"] || "anonymous",
}),
);

Rate Limit Headers​

Rate limit middleware automatically adds headers to responses:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706745660

Rate Limit Response Headers​

When a request exceeds the rate limit, the server returns HTTP 429 (Too Many Requests) with these headers:

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests per window100
X-RateLimit-RemainingRequests remaining in window0
X-RateLimit-ResetUnix timestamp when limit resets1706745660
Retry-AfterSeconds to wait before retrying60

Clients should respect the Retry-After header to avoid unnecessary requests.


Stream Redaction​

Protect sensitive data in streaming responses. Redaction is disabled by default and must be explicitly enabled.

Why Disabled by Default?​

Stream redaction is disabled by default because:

  1. It adds processing overhead to every stream chunk
  2. Developers should consciously decide what to redact
  3. Overly aggressive redaction can break functionality

Enabling Stream Redaction​

const server = await createServer(neurolink, {
framework: "hono",
config: {
port: 3000,
redaction: {
enabled: true, // Must explicitly enable
// Default redacted fields: apiKey, token, authorization,
// credentials, password, secret, request, args, result
},
},
});

Custom Redaction Configuration​

const server = await createServer(neurolink, {
framework: "hono",
config: {
port: 3000,
redaction: {
enabled: true,

// Add custom fields to redact
additionalFields: ["ssn", "creditCard", "bankAccount", "privateKey"],

// Preserve fields that would normally be redacted
preserveFields: ["result"], // Don't redact tool results

// Control tool-specific redaction
redactToolArgs: true, // Redact tool arguments
redactToolResults: false, // Don't redact results

// Custom placeholder
placeholder: "[SENSITIVE DATA REMOVED]",
},
},
});

Programmatic Redaction​

For custom streaming routes:

import { createStreamRedactor } from "@juspay/neurolink";

const redactor = createStreamRedactor({
enabled: true,
additionalFields: ["customSecret"],
});

// Use in custom stream handling
for await (const chunk of stream) {
const redactedChunk = redactor(chunk);
response.write(redactedChunk);
}

CORS Configuration​

Properly configure Cross-Origin Resource Sharing:

const server = await createServer(neurolink, {
framework: "hono",
config: {
port: 3000,
cors: {
enabled: true,

// Specific origins only (never use "*" in production)
origins: [
"https://myapp.com",
"https://staging.myapp.com",
"https://admin.myapp.com",
],

// Allowed HTTP methods
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],

// Allowed headers
headers: ["Content-Type", "Authorization", "X-API-Key", "X-Request-ID"],

// Allow credentials (cookies, authorization headers)
credentials: true,

// Preflight cache (reduce OPTIONS requests)
maxAge: 86400, // 24 hours
},
},
});

Dynamic CORS Origins​

For multi-tenant applications:

// Using Hono's native CORS middleware
import { cors } from "hono/cors";

const app = server.getFrameworkInstance();

app.use(
"/api/*",
cors({
origin: (origin, c) => {
// Validate origin against allowed list
const allowedPattern = /^https:\/\/.*\.myapp\.com$/;

if (allowedPattern.test(origin)) {
return origin;
}

// Check database for custom domains
// (be careful with async operations here)
return null;
},
credentials: true,
}),
);

Security Headers​

Add essential security headers to all responses. NeuroLink provides a built-in createSecurityHeadersMiddleware that works with all server adapters (Hono, Express, Fastify, Koa).

The recommended approach is to use NeuroLink's built-in security headers middleware, which works consistently across all frameworks:

import {
createServer,
createSecurityHeadersMiddleware,
} from "@juspay/neurolink";

const server = await createServer(neurolink, {
framework: "hono", // Works with: hono, express, fastify, koa
config: { port: 3000 },
});

server.registerMiddleware(
createSecurityHeadersMiddleware({
contentSecurityPolicy: "default-src 'self'; script-src 'self'",
frameOptions: "DENY",
contentTypeOptions: "nosniff",
hstsMaxAge: 31536000,
referrerPolicy: "strict-origin-when-cross-origin",
customHeaders: {
"X-Custom-Header": "custom-value",
},
}),
);

await server.initialize();
await server.start();

Configuration Options​

OptionTypeDefaultDescription
contentSecurityPolicystringundefinedContent-Security-Policy header
frameOptions"DENY" | "SAMEORIGIN" | false"DENY"X-Frame-Options header
contentTypeOptions"nosniff" | false"nosniff"X-Content-Type-Options header
hstsMaxAgenumber | false31536000 (1 year)HSTS max-age in seconds
referrerPolicystring | false"strict-origin-when-cross-origin"Referrer-Policy header
customHeadersRecord<string, string>{}Additional custom headers

Headers Set by the Middleware​

The middleware automatically sets these security headers:

HeaderDefault ValuePurpose
X-Frame-OptionsDENYPrevents clickjacking attacks
X-Content-Type-OptionsnosniffPrevents MIME type sniffing
Strict-Transport-Securitymax-age=31536000; includeSubDomainsEnforces HTTPS connections
Referrer-Policystrict-origin-when-cross-originControls referrer information
X-XSS-Protection1; mode=blockXSS filter for older browsers
Content-Security-Policy(only if configured)Controls resource loading

Express Example​

import { NeuroLink } from "@juspay/neurolink";
import {
createServer,
createSecurityHeadersMiddleware,
} from "@juspay/neurolink";

const neurolink = new NeuroLink({ defaultProvider: "openai" });

const server = await createServer(neurolink, {
framework: "express",
config: { port: 3000 },
});

server.registerMiddleware(
createSecurityHeadersMiddleware({
contentSecurityPolicy: "default-src 'self'; img-src 'self' data:",
frameOptions: "SAMEORIGIN",
hstsMaxAge: 63072000, // 2 years
}),
);

await server.initialize();
await server.start();

Fastify Example​

import { NeuroLink } from "@juspay/neurolink";
import {
createServer,
createSecurityHeadersMiddleware,
} from "@juspay/neurolink";

const neurolink = new NeuroLink({ defaultProvider: "anthropic" });

const server = await createServer(neurolink, {
framework: "fastify",
config: { port: 3000 },
});

server.registerMiddleware(
createSecurityHeadersMiddleware({
frameOptions: "DENY",
referrerPolicy: "no-referrer",
customHeaders: {
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
},
}),
);

await server.initialize();
await server.start();

Koa Example​

import { NeuroLink } from "@juspay/neurolink";
import {
createServer,
createSecurityHeadersMiddleware,
} from "@juspay/neurolink";

const neurolink = new NeuroLink({ defaultProvider: "openai" });

const server = await createServer(neurolink, {
framework: "koa",
config: { port: 3000 },
});

server.registerMiddleware(
createSecurityHeadersMiddleware({
contentSecurityPolicy: "default-src 'self'",
hstsMaxAge: 31536000,
}),
);

await server.initialize();
await server.start();

Hono Example​

import { NeuroLink } from "@juspay/neurolink";
import {
createServer,
createSecurityHeadersMiddleware,
} from "@juspay/neurolink";

const neurolink = new NeuroLink({ defaultProvider: "openai" });

const server = await createServer(neurolink, {
framework: "hono",
config: { port: 3000 },
});

server.registerMiddleware(
createSecurityHeadersMiddleware({
contentSecurityPolicy:
"default-src 'self'; script-src 'self' 'unsafe-inline'",
frameOptions: "DENY",
}),
);

await server.initialize();
await server.start();

Disabling Specific Headers​

Set any option to false to disable that header:

server.registerMiddleware(
createSecurityHeadersMiddleware({
frameOptions: false, // Disable X-Frame-Options
hstsMaxAge: false, // Disable HSTS
referrerPolicy: false, // Disable Referrer-Policy
}),
);

Framework-Specific Alternatives​

If you prefer to use framework-native security middleware, you can access the underlying framework instance:

Using Hono's secureHeaders​

import { secureHeaders } from "hono/secure-headers";

const app = server.getFrameworkInstance();

app.use(
"*",
secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
xFrameOptions: "DENY",
xContentTypeOptions: "nosniff",
referrerPolicy: "strict-origin-when-cross-origin",
permissionsPolicy: {
camera: [],
microphone: [],
geolocation: [],
},
}),
);

Using Express with Helmet​

import helmet from "helmet";

const app = server.getFrameworkInstance();

app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
}),
);

Using Koa with koa-helmet​

import helmet from "koa-helmet";

const app = server.getFrameworkInstance();

app.use(helmet());

Production Security Checklist​

Authentication​

  • Implement authentication middleware
  • Use secure token validation (verify signatures, check expiration)
  • Configure skip paths carefully
  • Implement token refresh mechanism
  • Log authentication failures
  • Implement account lockout after failed attempts

Authorization​

  • Implement role-based access control (RBAC)
  • Validate permissions for each endpoint
  • Use principle of least privilege
  • Audit authorization decisions

Rate Limiting​

  • Enable rate limiting globally
  • Configure appropriate limits per endpoint type
  • Use sliding window for critical endpoints
  • Implement different tiers for different users
  • Monitor rate limit hits

Data Protection​

  • Enable stream redaction for sensitive operations
  • Configure custom fields to redact
  • Validate and sanitize all inputs
  • Encrypt sensitive data at rest
  • Use TLS for all connections

CORS​

  • Configure specific allowed origins (no wildcards)
  • Restrict allowed methods and headers
  • Enable credentials only if needed
  • Set appropriate preflight cache

Headers​

  • Add Content-Security-Policy
  • Set X-Frame-Options to DENY
  • Enable X-Content-Type-Options
  • Configure Referrer-Policy
  • Add Strict-Transport-Security (HSTS)

Infrastructure​

  • Use HTTPS everywhere (terminate at load balancer)
  • Configure firewall rules
  • Use private networking for internal services
  • Implement request timeout
  • Set maximum body size limits
  • Enable access logging
  • Set up intrusion detection

Monitoring​

  • Monitor authentication failures
  • Alert on rate limit breaches
  • Track unusual API patterns
  • Log all security events
  • Set up anomaly detection

Security Validation via CLI​

Use CLI commands to validate security configuration:

Verify Security Settings​

# Check authentication configuration
neurolink server config --get auth

# Check rate limiting settings
neurolink server config --get rateLimit
neurolink server config --get rateLimit.maxRequests

# Check CORS configuration
neurolink server config --get cors
neurolink server config --get cors.enabled

Route Security Audit​

# List all routes to verify middleware is applied
neurolink server routes --format json

# Check specific route groups
neurolink server routes --group agent # Verify auth on agent routes
neurolink server routes --group health # Health routes (typically public)

Security Configuration Checklist​

SettingCheck CommandRecommended
Rate Limitingserver config --get rateLimit.enabledtrue
Max Requestsserver config --get rateLimit.maxRequests100 per minute
CORSserver config --get cors.enabledtrue in production
CORS Originsserver config --get cors.originsSpecific domains

Hardening Configuration​

# Set stricter rate limits for production
neurolink server config --set rateLimit.maxRequests=50
neurolink server config --set rateLimit.windowMs=60000

# Verify changes
neurolink server config --format json

Example: Complete Secure Server​

import { NeuroLink } from "@juspay/neurolink";
import {
createServer,
createAuthMiddleware,
createRateLimitMiddleware,
createRoleMiddleware,
} from "@juspay/neurolink";

const neurolink = new NeuroLink({
defaultProvider: "openai",
});

const server = await createServer(neurolink, {
framework: "hono",
config: {
port: 3000,
host: "0.0.0.0",
basePath: "/api",
timeout: 30000,

cors: {
enabled: true,
origins: [process.env.ALLOWED_ORIGIN],
methods: ["GET", "POST"],
headers: ["Content-Type", "Authorization"],
credentials: true,
},

rateLimit: {
enabled: true,
maxRequests: 100,
windowMs: 60000,
skipPaths: ["/api/health", "/api/ready"],
},

bodyParser: {
enabled: true,
maxSize: "1mb",
jsonLimit: "1mb",
},

redaction: {
enabled: true,
additionalFields: ["ssn", "creditCard"],
},
},
});

// Authentication
server.registerMiddleware(
createAuthMiddleware({
type: "bearer",
validate: async (token) => {
// Your JWT validation logic
return validateJWT(token);
},
skipPaths: ["/api/health", "/api/ready", "/api/auth/login"],
}),
);

// Additional rate limit for AI endpoints
server.registerMiddleware({
name: "ai-rate-limit",
order: 6,
paths: ["/api/agent/*"],
handler: async (ctx, next) => {
// Stricter rate limit for AI operations
// Implementation here
return next();
},
});

// Admin-only endpoints
server.registerMiddleware(
createRoleMiddleware({
requiredRoles: ["admin"],
errorMessage: "Admin access required",
}),
);

await server.initialize();
await server.start();

console.log("Secure server running on port 3000");


Need Help? Join our GitHub Discussions or open an issue.