@bonhomie/api-shield
Version:
A modern Node.js API utility toolkit: rate limiter, fingerprinting, validators, caching, logger, error handler, and cron helpers.
1,177 lines (1,146 loc) • 30.3 kB
JavaScript
// src/rateLimiter/memoryLimiter.js
function createMemoryRateLimiter2(options = {}) {
const {
windowMs = 6e4,
max = 60,
keyGenerator = (req) => req.ip || "global",
onLimitReached
} = options;
const hits = /* @__PURE__ */ new Map();
return function memoryRateLimiter(req, res, next) {
const key = keyGenerator(req);
const now = Date.now();
const windowStart = now - windowMs;
const timestamps = hits.get(key) || [];
const recent = timestamps.filter((ts) => ts > windowStart);
recent.push(now);
hits.set(key, recent);
if (recent.length > max) {
if (onLimitReached) {
onLimitReached(req, res);
}
if (!res.headersSent) {
res.status(429).json({
success: false,
message: "Too many requests, please try again later."
});
}
return;
}
next();
};
}
// src/rateLimiter/redisLimiter.js
function createRedisRateLimiter2(options) {
const {
redis,
windowMs = 6e4,
max = 60,
keyGenerator = (req) => req.ip || "global",
prefix = "api-shield:rl:",
onLimitReached
} = options;
if (!redis) {
throw new Error(
"[api-shield] createRedisRateLimiter: 'redis' instance is required."
);
}
const ttlSeconds = Math.ceil(windowMs / 1e3);
return async function redisRateLimiter(req, res, next) {
const key = prefix + keyGenerator(req);
try {
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, ttlSeconds);
}
if (current > max) {
if (onLimitReached) {
onLimitReached(req, res);
}
if (!res.headersSent) {
res.status(429).json({
success: false,
message: "Too many requests, please try again later."
});
}
return;
}
next();
} catch (err) {
next(err);
}
};
}
// src/rateLimiter/index.js
function createRateLimiter(options = {}) {
if ("redis" in options && options.redis) {
return createRedisRateLimiter(
/** @type any */
options
);
}
return createMemoryRateLimiter(
/** @type any */
options
);
}
// src/validators/emailValidator.js
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function isEmail(email) {
return EMAIL_REGEX.test(email);
}
// src/validators/requestValidator.js
function validateSegment(data, rules, location) {
const errors = [];
for (const [field, rule] of Object.entries(rules)) {
const value = data?.[field];
const hasValue = value !== void 0 && value !== null && value !== "";
if (rule.required && !hasValue) {
errors.push({
field,
location,
message: "Field is required"
});
continue;
}
if (!hasValue) continue;
const actualType = typeof value;
if (rule.type === "number") {
const numVal = Number(value);
if (Number.isNaN(numVal)) {
errors.push({
field,
location,
message: "Field must be a number"
});
continue;
}
if (rule.min !== void 0 && numVal < rule.min) {
errors.push({
field,
location,
message: `Number must be >= ${rule.min}`
});
}
if (rule.max !== void 0 && numVal > rule.max) {
errors.push({
field,
location,
message: `Number must be <= ${rule.max}`
});
}
continue;
}
if (rule.type === "boolean") {
const strVal = String(value).toLowerCase();
if (!["true", "false", "1", "0"].includes(strVal) && actualType !== "boolean") {
errors.push({
field,
location,
message: "Field must be a boolean"
});
}
continue;
}
if (rule.type === "string") {
const str = String(value);
if (rule.minLength !== void 0 && str.length < rule.minLength) {
errors.push({
field,
location,
message: `String length must be >= ${rule.minLength}`
});
}
if (rule.maxLength !== void 0 && str.length > rule.maxLength) {
errors.push({
field,
location,
message: `String length must be <= ${rule.maxLength}`
});
}
if (rule.pattern && !rule.pattern.test(str)) {
errors.push({
field,
location,
message: "Invalid format"
});
}
if (rule.enum && !rule.enum.includes(str)) {
errors.push({
field,
location,
message: `Value must be one of: ${rule.enum.join(", ")}`
});
}
}
}
return errors;
}
function validateRequest(schema) {
return function requestValidator(req, res, next) {
let errors = [];
if (schema.body) {
errors = errors.concat(validateSegment(req.body, schema.body, "body"));
}
if (schema.query) {
errors = errors.concat(validateSegment(req.query, schema.query, "query"));
}
if (schema.params) {
errors = errors.concat(
validateSegment(req.params, schema.params, "params")
);
}
if (errors.length > 0) {
return res.status(400).json({
success: false,
message: "Validation failed",
errors
});
}
next();
};
}
// src/fingerprint/getIp.js
function getClientIp(req, options = {}) {
const { trustProxy = true } = options;
if (trustProxy) {
const xfwd = req.headers?.["x-forwarded-for"];
if (typeof xfwd === "string" && xfwd.length > 0) {
const parts = xfwd.split(",").map((p) => p.trim());
if (parts[0]) return parts[0];
}
}
const ip = req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || req.connection?.socket?.remoteAddress;
if (!ip) return null;
if (ip.startsWith("::ffff:")) {
return ip.substring(7);
}
return ip;
}
// src/fingerprint/getDevice.js
import { UAParser } from "ua-parser-js";
import crypto from "crypto";
function getCookies(req) {
if (req.cookies && typeof req.cookies === "object") {
return req.cookies;
}
const header = req.headers?.cookie;
if (!header) return {};
const out = {};
const parts = header.split(";");
for (const part of parts) {
const [rawKey, ...rest] = part.trim().split("=");
const key = decodeURIComponent(rawKey);
const value = decodeURIComponent(rest.join("=") || "");
out[key] = value;
}
return out;
}
function getDeviceInfo(req) {
const uaString = req.headers?.["user-agent"] || "";
const parser = new UAParser(uaString);
const result = parser.getResult();
const browser = `${result.browser.name || "Unknown"} ${result.browser.version || ""}`.trim();
const os = `${result.os.name || "Unknown"} ${result.os.version || ""}`.trim();
let deviceType = "desktop";
if (result.device.type === "mobile") deviceType = "mobile";
if (result.device.type === "tablet") deviceType = "tablet";
return {
userAgent: uaString,
browser,
os,
deviceType
};
}
function getRequestFingerprint(req) {
const ip = getClientIp(req) || "unknown-ip";
const ua = req.headers?.["user-agent"] || "unknown-ua";
const lang = req.headers?.["accept-language"] || "unknown-lang";
const raw = `${ip}|${ua}|${lang}`;
return crypto.createHash("sha256").update(raw).digest("hex");
}
function getFingerprintComponents(req) {
const ip = getClientIp(req) || "unknown-ip";
const ua = req.headers?.["user-agent"] || "unknown-ua";
const lang = req.headers?.["accept-language"] || "unknown-lang";
const uaHash = crypto.createHash("sha256").update(ua).digest("hex");
return {
ip,
ua,
uaHash,
lang
};
}
function getRequestFingerprintV2(req, options = {}) {
const { cookieName = "api_shield_fp" } = options;
const cookies = getCookies(req);
const existing = cookies[cookieName];
const components = getFingerprintComponents(req);
if (existing && typeof existing === "string" && existing.length > 0) {
return {
fingerprint: existing,
fromCookie: true,
components
};
}
const raw = `${components.ip}|${components.uaHash}|${components.lang}`;
const fingerprint = crypto.createHash("sha256").update(raw).digest("hex");
return {
fingerprint,
fromCookie: false,
components
};
}
function computeFingerprintStability(prev, current) {
let score = 0;
if (!prev || !current) return 0;
if (prev.ip === current.ip) {
score += 40;
}
if (prev.uaHash === current.uaHash) {
score += 40;
}
if (prev.lang === current.lang) {
score += 20;
}
return score;
}
function getFingerprintCookie(req, cookieName = "api_shield_fp") {
const cookies = getCookies(req);
return cookies[cookieName] || null;
}
// src/cache/memoryCache.js
function createMemoryCache2() {
const store = /* @__PURE__ */ new Map();
function now() {
return Date.now();
}
function isExpired(entry) {
return entry.expires !== 0 && entry.expires < now();
}
return {
/**
* Store a value
*/
set(key, value, ttlMs = 0) {
store.set(key, {
value,
expires: ttlMs > 0 ? now() + ttlMs : 0
});
},
/**
* Retrieve a value
*/
get(key) {
const entry = store.get(key);
if (!entry) return null;
if (isExpired(entry)) {
store.delete(key);
return null;
}
return entry.value;
},
/**
* Delete a key
*/
del(key) {
store.delete(key);
},
/**
* Compute → Cache → Return
*/
async wrap(key, fn, ttlMs = 0) {
const cached = this.get(key);
if (cached !== null) return cached;
const value = await fn();
this.set(key, value, ttlMs);
return value;
}
};
}
// src/cache/redisCache.js
function createRedisCache2(redis) {
if (!redis) {
throw new Error("[api-shield] createRedisCache: Redis instance required.");
}
return {
async set(key, value, ttlMs = 0) {
const serialized = JSON.stringify(value);
if (ttlMs > 0) {
await redis.set(key, serialized, "PX", ttlMs);
} else {
await redis.set(key, serialized);
}
},
async get(key) {
const data = await redis.get(key);
if (!data) return null;
try {
return JSON.parse(data);
} catch {
return data;
}
},
async del(key) {
await redis.del(key);
},
async wrap(key, fn, ttlMs = 0) {
const cached = await this.get(key);
if (cached !== null) return cached;
const value = await fn();
await this.set(key, value, ttlMs);
return value;
}
};
}
// src/cache/index.js
function createCache(options = {}) {
if (options.redis) {
return createRedisCache(options.redis);
}
return createMemoryCache();
}
// src/logger/logger.js
var logger = {
info: (...args) => console.log("[INFO]", (/* @__PURE__ */ new Date()).toISOString(), ...args),
warn: (...args) => console.warn("[WARN]", (/* @__PURE__ */ new Date()).toISOString(), ...args),
error: (...args) => console.error("[ERROR]", (/* @__PURE__ */ new Date()).toISOString(), ...args),
debug: (...args) => {
if (process.env.NODE_ENV !== "production") {
console.log("[DEBUG]", (/* @__PURE__ */ new Date()).toISOString(), ...args);
}
}
};
// src/logger/requestLogger.js
function requestLogger(options = {}) {
const {
logFn = console.log,
includeBody = false,
// optional
includeHeaders = false
// optional
} = options;
return (req, res, next) => {
const start = Date.now();
const ip = getClientIp(req);
const fingerprint = getRequestFingerprintV2(req).fingerprint;
const ua = req.headers["user-agent"] || "";
const originalEnd = res.end;
res.end = function(chunk, encoding, cb) {
const duration = Date.now() - start;
const status = res.statusCode;
const log = {
method: req.method,
path: req.originalUrl,
status,
duration,
ip,
ua,
fingerprint,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
};
if (includeBody) {
log.body = req.body;
}
if (includeHeaders) {
log.headers = req.headers;
}
logFn(log);
originalEnd.call(res, chunk, encoding, cb);
};
next();
};
}
// src/errors/ApiError.js
var ApiError = class extends Error {
/**
* @param {number} statusCode - HTTP status code
* @param {string} message - Error message
* @param {any} [details] - Extra metadata (optional)
*/
constructor(statusCode, message, details = null) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
this.details = details;
if (process.env.NODE_ENV === "production") {
this.stack = void 0;
}
}
/**
* Convenience helper to convert to JSON
*/
toJSON() {
return {
success: false,
error: {
message: this.message,
statusCode: this.statusCode,
details: this.details || void 0
}
};
}
};
// src/errors/errorHandler.js
function errorHandler(err, req, res, next) {
if (res.headersSent) {
return next(err);
}
if (err instanceof ApiError) {
return res.status(err.statusCode).json({
success: false,
error: {
message: err.message,
statusCode: err.statusCode,
details: err.details || void 0
}
});
}
console.error("[api-shield] Unhandled Error:", err);
const status = err.statusCode || 500;
const message = status === 500 ? "Internal server error" : err.message || "Unexpected error";
return res.status(status).json({
success: false,
error: {
message,
statusCode: status
}
});
}
// src/utils/safe.js
async function safe(fn) {
try {
const data = await fn();
return { data, error: null };
} catch (error) {
return { data: null, error };
}
}
// src/cron/cronHelpers.js
import { CronJob } from "cron";
function safeCron(fn) {
return async () => {
try {
await fn();
} catch (err) {
console.error("[api-shield] Cron job error:", err);
}
};
}
function createCron(schedule, fn, options = {}) {
const job = new CronJob(
schedule,
safeCron(fn),
null,
options.start || true,
options.timezone || "UTC"
);
return job;
}
function everyMinute(fn) {
return createCron("*/1 * * * *", fn);
}
function everyXMinutes(minutes, fn) {
return createCron(`*/${minutes} * * * *`, fn);
}
function everyHour(fn) {
return createCron("0 * * * *", fn);
}
function dailyAt(time, fn) {
const [hour, minute] = time.split(":");
return createCron(`${minute} ${hour} * * *`, fn);
}
async function cronRetry(fn, retries = 3) {
let attempt = 0;
while (attempt < retries) {
try {
return await fn();
} catch (e) {
attempt++;
const delay = Math.pow(2, attempt) * 500;
await new Promise((res) => setTimeout(res, delay));
if (attempt === retries) throw e;
}
}
}
// src/security/botDetector.js
var BOT_UA_PATTERNS = [
/bot/i,
/crawl/i,
/spider/i,
/slurp/i,
/headless/i,
/phantomjs/i,
/selenium/i,
/puppeteer/i,
/scrapy/i,
/curl/i,
/wget/i,
/python-requests/i,
/httpclient/i,
/http-client/i,
/postman/i,
/insomnia/i,
/java\/\d/i,
/okhttp/i
];
function isSuspiciousIp(ip) {
if (!ip) return true;
return ip.startsWith("10.") || ip.startsWith("192.168.") || ip.startsWith("172.16.") || ip.startsWith("172.17.") || ip.startsWith("172.18.") || ip.startsWith("172.19.") || ip.startsWith("172.20.") || ip.startsWith("172.21.") || ip.startsWith("172.22.") || ip.startsWith("172.23.") || ip.startsWith("172.24.") || ip.startsWith("172.25.") || ip.startsWith("172.26.") || ip.startsWith("172.27.") || ip.startsWith("172.28.") || ip.startsWith("172.29.") || ip.startsWith("172.30.") || ip.startsWith("172.31.") || ip === "127.0.0.1" || ip === "::1";
}
function detectBot(req, options = {}) {
const { threshold = 50 } = options;
const headers = req.headers || {};
const userAgent = (headers["user-agent"] || "").toString();
const acceptLang = headers["accept-language"];
const xfwd = headers["x-forwarded-for"];
const ip = getClientIp(req) || null;
let score = 0;
const reasons = [];
if (!userAgent || userAgent.trim() === "") {
score += 40;
reasons.push("Missing User-Agent header");
} else {
if (BOT_UA_PATTERNS.some((re) => re.test(userAgent))) {
score += 50;
reasons.push("User-Agent matches known bot/crawler pattern");
}
if (userAgent.length < 20) {
score += 10;
reasons.push("Very short User-Agent string");
}
}
if (!acceptLang) {
score += 10;
reasons.push("Missing Accept-Language header");
}
if (typeof xfwd === "string") {
const parts = xfwd.split(",").map((p) => p.trim());
if (parts.length > 3) {
score += 10;
reasons.push("Long X-Forwarded-For chain (> 3 hops)");
}
}
if (isSuspiciousIp(ip || "")) {
score += 10;
reasons.push(
"IP appears to be private/loopback (possible proxy or script)"
);
}
const isBot = score >= threshold;
return {
isBot,
score,
reasons,
ip,
userAgent
};
}
function botGuard(options = {}) {
const { threshold = 50, block = false } = options;
return function botGuardMiddleware(req, res, next) {
const result = detectBot(req, { threshold });
req.botDetection = result;
if (block && result.isBot) {
return res.status(403).json({
success: false,
error: {
message: "Access denied (suspected automated traffic)",
statusCode: 403
}
});
}
next();
};
}
// src/security/nonce.js
import crypto2 from "crypto";
function generateNonce(size = 32) {
return crypto2.randomBytes(size).toString("hex");
}
function verifyNonce(nonce, expected) {
if (!nonce || !expected) return false;
try {
return crypto2.timingSafeEqual(Buffer.from(nonce), Buffer.from(expected));
} catch {
return false;
}
}
// src/security/hmac.js
import crypto3 from "crypto";
function createHmac(secret, data) {
return crypto3.createHmac("sha256", secret).update(data).digest("hex");
}
function verifyHmac(secret, data, expected) {
const hash = createHmac(secret, data);
return crypto3.timingSafeEqual(
Buffer.from(hash, "hex"),
Buffer.from(expected, "hex")
);
}
// src/security/replay.js
import crypto4 from "crypto";
function createReplayToken(ttlMs = 3e4) {
return {
token: crypto4.randomBytes(16).toString("hex") + "-" + Date.now(),
ttl: ttlMs
};
}
function createReplayStoreMemory() {
const used = /* @__PURE__ */ new Map();
return {
/**
* Returns true if token is fresh
*/
verify(token) {
const now = Date.now();
const prev = used.get(token);
if (prev && prev > now) {
return false;
}
const expiry = now + 3e4;
used.set(token, expiry);
return true;
}
};
}
function createReplayStoreRedis(redis) {
return {
async verify(token) {
const key = "api_shield_replay:" + token;
const exists = await redis.exists(key);
if (exists) return false;
await redis.set(key, "1", "PX", 3e4);
return true;
}
};
}
// src/security/password.js
import argon2 from "argon2";
import crypto5 from "crypto";
async function hashPassword(password) {
return argon2.hash(password, { type: argon2.argon2id });
}
async function verifyPassword(password, hash) {
try {
return await argon2.verify(hash, password);
} catch {
return false;
}
}
function timingSafeEquals(a, b) {
if (!a || !b) return false;
try {
return crypto5.timingSafeEqual(Buffer.from(a), Buffer.from(b));
} catch {
return false;
}
}
// src/response/formatter.js
function success(data, meta = null) {
return {
success: true,
data,
meta: meta || void 0
};
}
function fail(message, statusCode = 400) {
return {
success: false,
error: {
message,
statusCode
}
};
}
function paginate(items, meta) {
return {
success: true,
data: items,
pagination: {
page: meta.page,
perPage: meta.perPage,
total: meta.total,
totalPages: Math.ceil(meta.total / meta.perPage)
}
};
}
function responseFormatter() {
return (req, res, next) => {
res.success = (data, meta) => res.json(success(data, meta));
res.fail = (message, statusCode = 400) => res.status(statusCode).json(fail(message, statusCode));
res.paginate = (items, meta) => res.json(paginate(items, meta));
next();
};
}
// src/jwt/jwtUtils.js
import jwt from "jsonwebtoken";
function signJwt(payload, options) {
const { secret, expiresIn = "1d" } = options;
if (!secret) {
throw new Error("[api-shield] signJwt: secret is required.");
}
return jwt.sign(payload, secret, { expiresIn });
}
function verifyJwt(token, options) {
const { secret } = options;
if (!secret) {
throw new Error("[api-shield] verifyJwt: secret is required.");
}
try {
return jwt.verify(token, secret);
} catch (err) {
throw new ApiError(401, "Invalid or expired token");
}
}
// src/jwt/extractToken.js
function extractToken(req, cookieName = "token") {
const auth = req.headers["authorization"];
if (auth && auth.startsWith("Bearer ")) {
return auth.substring(7);
}
if (req.cookies && req.cookies[cookieName]) {
return req.cookies[cookieName];
}
if (req.headers["x-access-token"]) {
return req.headers["x-access-token"];
}
return null;
}
// src/jwt/authMiddleware.js
function requireAuth(options) {
const { secret, cookieName = "token", roles = [] } = options;
if (!secret) {
throw new Error("[api-shield] requireAuth: secret is required.");
}
return (req, res, next) => {
const token = extractToken(req, cookieName);
if (!token) {
throw new ApiError(401, "Missing authentication token");
}
const decoded = verifyJwt(token, { secret });
req.user = decoded;
if (roles.length > 0) {
const userRole = decoded.role;
if (!roles.includes(userRole)) {
throw new ApiError(403, "Access denied (insufficient permissions)");
}
}
next();
};
}
// src/jwt/refreshToken.js
function refreshAccessToken(refreshToken, options) {
const { refreshSecret, accessSecret, accessExpiresIn = "15m" } = options;
if (!refreshSecret || !accessSecret) {
throw new Error("[api-shield] refreshAccessToken: missing secrets.");
}
const decoded = verifyJwt(refreshToken, { secret: refreshSecret });
const newAccessToken = signJwt(
{ id: decoded.id, role: decoded.role },
{ secret: accessSecret, expiresIn: accessExpiresIn }
);
return { accessToken: newAccessToken, user: decoded };
}
// src/security/attacks/patterns.js
var SQLI_PATTERNS = [
/(\b)(select|update|union|insert|delete|drop|alter|create|exec|sleep)(\b)/i,
/(\%27)|(\')|(\-\-)|(\%23)|(#)/i,
/((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-))/i,
/\bOR\s+1=1\b/i,
/UNION\s+SELECT/i,
/';--/i
];
var XSS_PATTERNS = [
/<script[\s\S]*?>[\s\S]*?<\/script>/i,
/on\w+\s*=/i,
/(javascript:)/i,
/alert\s*\(/i,
/document\.cookie/i,
/<img[\s\S]*?onerror=/i
];
var PATH_TRAVERSAL_PATTERNS = [
/\.\.\//,
/\.\.\\/,
/%2e%2e%2f/i,
/%2e%2e\\\//i
];
var ENCODING_ATTACKS = [
/%00/,
// Null byte
/%25/i,
// Double encoded %
/%2f/i
// Encoded /
];
// src/security/attacks/attackDetector.js
function matchAny(str, patterns) {
if (!str) return false;
return patterns.some((re) => re.test(str));
}
function extractStrings(req) {
const list = [];
for (const v of Object.values(req.query || {})) {
if (typeof v === "string") list.push(v);
}
for (const v of Object.values(req.params || {})) {
if (typeof v === "string") list.push(v);
}
if (req.body && typeof req.body === "object") {
for (const v of Object.values(req.body)) {
if (typeof v === "string") list.push(v);
}
}
return list;
}
function detectAttack(req) {
const ip = getClientIp(req);
const ua = req.headers["user-agent"] || "";
const reasons = [];
let score = 0;
const strings = extractStrings(req);
for (const s of strings) {
if (matchAny(s, SQLI_PATTERNS)) {
score += 40;
reasons.push("Possible SQL injection");
}
if (matchAny(s, XSS_PATTERNS)) {
score += 40;
reasons.push("Possible XSS attack");
}
if (matchAny(s, PATH_TRAVERSAL_PATTERNS)) {
score += 30;
reasons.push("Possible path traversal");
}
if (matchAny(s, ENCODING_ATTACKS)) {
score += 10;
reasons.push("Suspicious encoding detected");
}
}
if (!ua || ua.length < 10) {
score += 10;
reasons.push("Suspicious User-Agent");
}
return {
ip,
userAgent: ua,
score,
isAttack: score >= 40,
reasons
};
}
function attackGuard(options = {}) {
const { block = false } = options;
return (req, res, next) => {
const result = detectAttack(req);
req.attackDetection = result;
if (block && result.isAttack) {
return res.status(403).json({
success: false,
error: {
message: "Request blocked (suspicious input detected)",
statusCode: 403,
reasons: result.reasons
}
});
}
next();
};
}
// src/csrf/csrf.js
import crypto6 from "crypto";
function generateCsrfToken(size = 32) {
return crypto6.randomBytes(size).toString("hex");
}
function safeEqual(a, b) {
if (!a || !b) return false;
try {
return crypto6.timingSafeEqual(Buffer.from(a), Buffer.from(b));
} catch {
return false;
}
}
function csrfCookie(options = {}) {
const { cookieName = "csrf_token", cookieOptions = {} } = options;
return (req, res, next) => {
const cookies = req.cookies || {};
let token = cookies[cookieName];
if (!token) {
token = generateCsrfToken();
if (typeof res.cookie === "function") {
res.cookie(cookieName, token, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
...cookieOptions
});
}
}
req.csrfToken = token;
next();
};
}
function csrfProtect(options = {}) {
const {
cookieName = "csrf_token",
headerName = "x-csrf-token",
bodyField = "csrfToken",
queryField = "csrfToken"
} = options;
const UNSAFE_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
return (req, res, next) => {
if (!UNSAFE_METHODS.includes(req.method.toUpperCase())) {
return next();
}
const cookies = req.cookies || {};
const cookieToken = cookies[cookieName];
const headerToken = req.headers[headerName] || req.headers[headerName.toLowerCase()];
const bodyToken = req.body && req.body[bodyField];
const queryToken = req.query && req.query[queryField];
const provided = headerToken || bodyToken || queryToken;
if (!cookieToken || !provided || !safeEqual(String(cookieToken), String(provided))) {
return res.status(403).json({
success: false,
error: {
message: "Invalid CSRF token",
statusCode: 403
}
});
}
next();
};
}
// src/rbac/rbac.js
function hasRole(user, allowedRoles = []) {
if (!user) return false;
const userRoles = new Set(
[user.role, ...Array.isArray(user.roles) ? user.roles : []].filter(
Boolean
)
);
return allowedRoles.some((r) => userRoles.has(r));
}
function hasPermission(user, perms) {
if (!user) return false;
const userPerms = new Set(user.permissions || []);
const list = Array.isArray(perms) ? perms : [perms];
return list.every((p) => userPerms.has(p));
}
function requireRole(roles = []) {
return (req, res, next) => {
if (!req.user) {
throw new ApiError(401, "Authentication required");
}
if (!hasRole(req.user, roles)) {
throw new ApiError(403, "Access denied (role required)");
}
next();
};
}
function requirePermission(perms) {
return (req, res, next) => {
if (!req.user) {
throw new ApiError(401, "Authentication required");
}
if (!hasPermission(req.user, perms)) {
throw new ApiError(403, "Access denied (permission required)");
}
next();
};
}
// src/sanitizer/sanitizer.js
import xss from "xss";
function sanitizeString(input, options) {
if (!input) return "";
return xss(input, options);
}
function escapeHtml(input) {
if (!input) return "";
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
function sanitizeDeep(value) {
if (typeof value === "string") {
return sanitizeString(value);
}
if (Array.isArray(value)) {
return value.map((v) => sanitizeDeep(v));
}
if (value && typeof value === "object") {
const out = {};
for (const [k, v] of Object.entries(value)) {
out[k] = sanitizeDeep(v);
}
return out;
}
return value;
}
function sanitizeRequest() {
return (req, res, next) => {
if (req.body) req.body = sanitizeDeep(req.body);
if (req.query) req.query = sanitizeDeep(req.query);
if (req.params) req.params = sanitizeDeep(req.params);
next();
};
}
export {
ApiError,
attackGuard,
botGuard,
computeFingerprintStability,
createCache,
createCron,
createHmac,
createMemoryCache2 as createMemoryCache,
createMemoryRateLimiter2 as createMemoryRateLimiter,
createRateLimiter,
createRedisCache2 as createRedisCache,
createRedisRateLimiter2 as createRedisRateLimiter,
createReplayStoreMemory,
createReplayStoreRedis,
createReplayToken,
cronRetry,
csrfCookie,
csrfProtect,
dailyAt,
detectAttack,
detectBot,
errorHandler,
escapeHtml,
everyHour,
everyMinute,
everyXMinutes,
extractToken,
fail,
generateCsrfToken,
generateNonce,
getClientIp,
getDeviceInfo,
getFingerprintCookie,
getRequestFingerprint,
getRequestFingerprintV2,
hasPermission,
hasRole,
hashPassword,
isEmail,
logger,
paginate,
refreshAccessToken,
requestLogger,
requireAuth,
requirePermission,
requireRole,
responseFormatter,
safe,
safeCron,
sanitizeDeep,
sanitizeRequest,
sanitizeString,
signJwt,
success,
timingSafeEquals,
validateRequest,
verifyHmac,
verifyJwt,
verifyNonce,
verifyPassword
};