UNPKG

z-secure-service

Version:

A rate-limiting and API protection middleware that helps developers add security features to their APIs effortlessly.

135 lines (134 loc) 5.73 kB
// index.ts import axios from "axios"; import requestIp from 'request-ip'; import { generateApiKey } from "./findIp.js"; import { validateApiKey, validateIdentificationKey, validateRateLimiting, validateShield, } from "./validation.js"; class ZSecure { constructor(options) { if (!options.API_KEY) { throw new Error("API key is required"); } if (!options.rateLimitingRule && !options.shieldRule) { throw new Error("At least one of rateLimitingRule or shieldRule must be defined"); } this.API_KEY = options.API_KEY; this.rateLimitingRule = options.rateLimitingRule; this.shieldRule = options.shieldRule; this.serverUrl = options.ZSECURE_URL ?? process.env.ZSECURE_URL ?? "http://localhost:3000"; this.identificationKey = generateApiKey(16); // Adjust length if necessary this.logging = options.logs ?? false; // Default to false if not provided this.bannedUsers = new Map(); this.resetTime = this.calculateResetTime(); if (this.logging) { console.log("[ZSecure] Initialized with options:", options); console.log("[ZSecure] Calculated reset time:", this.resetTime); } } calculateResetTime() { const rateLimitReset = (() => { if (this.rateLimitingRule) { if ("refillIntervalMs" in this.rateLimitingRule) { return this.rateLimitingRule.refillIntervalMs; // TokenBucketRule } if ("windowMs" in this.rateLimitingRule) { return this.rateLimitingRule.windowMs; // FixedWindowRule and SlidingWindowRule } if ("timeout" in this.rateLimitingRule) { return this.rateLimitingRule.timeout; // LeakyBucketRule } } return Infinity; // No rate limit defined })(); const shieldReset = this.shieldRule?.blocktime ?? Infinity; // Return the minimum of shield blocktime and rate limit reset time return Math.min(rateLimitReset, shieldReset); } checkBannedUser(userId) { const resetTime = this.bannedUsers.get(userId); if (resetTime && resetTime > Date.now()) { if (this.logging) { console.log(`[ZSecure] User ${userId} is banned until ${new Date(resetTime).toISOString()}`); } return true; } return false; } async protect(req, userId, requestedTokens = 1) { if (userId === null) { userId = requestIp.getClientIp(req)?.toString(); } if (typeof userId !== "string" && typeof userId !== "number") { return { isdenied: true, message: "Correct userId is required", }; } // Check if the user is already banned if (this.checkBannedUser(userId)) { return { isdenied: true, status: 403, message: "User is banned" }; } const shieldRequest = this.shieldRule ? { params: JSON.stringify(req.params) ?? "", url: JSON.stringify(req.url), query: JSON.stringify(req.query) ?? "", body: JSON.stringify(req.body) ?? "", } : undefined; if (this.logging) { console.log("[ZSecure] Shield request data:", shieldRequest); } const payload = { key: this.API_KEY, identificationKey: this.identificationKey, userId: userId, rateLimiting: this.rateLimitingRule ? { ...this.rateLimitingRule, requested: requestedTokens } : undefined, shield: this.shieldRule ? { ...this.shieldRule, request: shieldRequest } : undefined, }; try { validateApiKey(payload.key); validateIdentificationKey(payload.identificationKey); validateRateLimiting(payload.rateLimiting); validateShield(payload.shield); } catch (validationError) { if (this.logging) { console.error("[ZSecure] Validation Error:", validationError); } return { isdenied: true, status: 400, message: validationError instanceof Error ? validationError.message : "Unknown validation error", }; } try { const response = await axios.post(`${this.serverUrl}/protection`, payload); if (this.logging) { console.log("[ZSecure] Server response:", response.data); } // If the server denies the user, add them to the banned users map if (response.data.isDenied || response.data.isDenied === 'true') { const banResetTime = Date.now() + this.resetTime; this.bannedUsers.set(userId, banResetTime); if (this.logging) { console.log(`[ZSecure] User ${userId} banned until ${new Date(banResetTime).toISOString()}`); } } return response.data; } catch (error) { if (this.logging) { console.error("[ZSecure] Error in protect function:", error); } if (axios.isAxiosError(error) && error.response && error.response.status === 429) { return { isdenied: true, status: 429, message: "Rate limit exceeded" }; } return { isdenied: true, status: 500, message: "Internal server error" }; } } } export default (options) => new ZSecure(options);