- Updated all component headers and documentation
- Changed navbar and footer branding
- Updated homepage hero badge
- Modified page title in index.html
- Simplified footer text to 'Built with ❤️'
- Consistent V2 capitalization across all references
293 lines
10 KiB
JavaScript
293 lines
10 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.rateLimitService = exports.BurstLimitError = exports.RateLimitError = exports.RateLimitService = exports.ANONYMOUS_TIER = exports.RATE_LIMIT_TIERS = void 0;
|
|
const rate_limiter_flexible_1 = require("rate-limiter-flexible");
|
|
const logger_1 = require("../lib/logger");
|
|
exports.RATE_LIMIT_TIERS = {
|
|
free: {
|
|
name: 'Free',
|
|
requestsPerHour: 100,
|
|
requestsPerMinute: 10,
|
|
bulkJobsPerDay: 2,
|
|
maxUrls: 50,
|
|
exportLimit: 5,
|
|
},
|
|
pro: {
|
|
name: 'Pro',
|
|
requestsPerHour: 1000,
|
|
requestsPerMinute: 50,
|
|
bulkJobsPerDay: 20,
|
|
maxUrls: 1000,
|
|
exportLimit: 100,
|
|
},
|
|
enterprise: {
|
|
name: 'Enterprise',
|
|
requestsPerHour: 10000,
|
|
requestsPerMinute: 200,
|
|
bulkJobsPerDay: 100,
|
|
maxUrls: 10000,
|
|
exportLimit: 1000,
|
|
},
|
|
};
|
|
exports.ANONYMOUS_TIER = {
|
|
name: 'Anonymous',
|
|
requestsPerHour: 50,
|
|
requestsPerMinute: 5,
|
|
bulkJobsPerDay: 0,
|
|
maxUrls: 10,
|
|
exportLimit: 0,
|
|
};
|
|
class RateLimitService {
|
|
rateLimiters;
|
|
constructor() {
|
|
this.rateLimiters = new Map();
|
|
this.initializeRateLimiters();
|
|
}
|
|
initializeRateLimiters() {
|
|
this.rateLimiters.set('legacy', new rate_limiter_flexible_1.RateLimiterMemory({
|
|
keyPrefix: 'rl_legacy',
|
|
points: 100,
|
|
duration: 3600,
|
|
blockDuration: 3600,
|
|
execEvenly: true,
|
|
}));
|
|
this.rateLimiters.set('anonymous', new rate_limiter_flexible_1.RateLimiterMemory({
|
|
keyPrefix: 'rl_anon',
|
|
points: exports.ANONYMOUS_TIER.requestsPerHour,
|
|
duration: 3600,
|
|
blockDuration: 3600,
|
|
execEvenly: true,
|
|
}));
|
|
Object.keys(exports.RATE_LIMIT_TIERS).forEach(tier => {
|
|
const config = exports.RATE_LIMIT_TIERS[tier];
|
|
this.rateLimiters.set(`user_${tier}_hour`, new rate_limiter_flexible_1.RateLimiterMemory({
|
|
keyPrefix: `rl_user_${tier}_h`,
|
|
points: config.requestsPerHour,
|
|
duration: 3600,
|
|
blockDuration: 900,
|
|
execEvenly: true,
|
|
}));
|
|
this.rateLimiters.set(`user_${tier}_minute`, new rate_limiter_flexible_1.RateLimiterMemory({
|
|
keyPrefix: `rl_user_${tier}_m`,
|
|
points: config.requestsPerMinute,
|
|
duration: 60,
|
|
blockDuration: 60,
|
|
execEvenly: true,
|
|
}));
|
|
this.rateLimiters.set(`bulk_${tier}_day`, new rate_limiter_flexible_1.RateLimiterMemory({
|
|
keyPrefix: `rl_bulk_${tier}_d`,
|
|
points: config.bulkJobsPerDay,
|
|
duration: 86400,
|
|
blockDuration: 86400,
|
|
execEvenly: false,
|
|
}));
|
|
this.rateLimiters.set(`export_${tier}_day`, new rate_limiter_flexible_1.RateLimiterMemory({
|
|
keyPrefix: `rl_export_${tier}_d`,
|
|
points: config.exportLimit,
|
|
duration: 86400,
|
|
blockDuration: 86400,
|
|
execEvenly: false,
|
|
}));
|
|
});
|
|
}
|
|
async getUserTier(userId) {
|
|
if (!userId || userId === 'anonymous-user') {
|
|
return exports.ANONYMOUS_TIER;
|
|
}
|
|
return exports.RATE_LIMIT_TIERS.free;
|
|
}
|
|
async checkRateLimit(type, key, userId) {
|
|
try {
|
|
const tier = await this.getUserTier(userId);
|
|
let limiterKey;
|
|
let limit;
|
|
if (type === 'legacy') {
|
|
limiterKey = 'legacy';
|
|
limit = 100;
|
|
}
|
|
else if (!userId || userId === 'anonymous-user') {
|
|
limiterKey = 'anonymous';
|
|
limit = exports.ANONYMOUS_TIER.requestsPerHour;
|
|
}
|
|
else {
|
|
const tierName = tier.name.toLowerCase();
|
|
switch (type) {
|
|
case 'tracking':
|
|
limiterKey = `user_${tierName}_hour`;
|
|
limit = tier.requestsPerHour;
|
|
break;
|
|
case 'bulk':
|
|
limiterKey = `bulk_${tierName}_day`;
|
|
limit = tier.bulkJobsPerDay;
|
|
break;
|
|
case 'export':
|
|
limiterKey = `export_${tierName}_day`;
|
|
limit = tier.exportLimit;
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown rate limit type: ${type}`);
|
|
}
|
|
}
|
|
const rateLimiter = this.rateLimiters.get(limiterKey);
|
|
if (!rateLimiter) {
|
|
throw new Error(`Rate limiter not found: ${limiterKey}`);
|
|
}
|
|
const result = await rateLimiter.consume(key, 1);
|
|
return {
|
|
limit,
|
|
remaining: result.remainingPoints || 0,
|
|
reset: new Date(Date.now() + (result.msBeforeNext || 0)),
|
|
tier: tier.name,
|
|
};
|
|
}
|
|
catch (error) {
|
|
if (error instanceof Error && error.message.includes('Rate limit')) {
|
|
const tier = await this.getUserTier(userId);
|
|
throw new RateLimitError(tier.name, 0, new Date(Date.now() + 3600000));
|
|
}
|
|
logger_1.logger.error('Rate limit check failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
async checkBurstLimit(userId) {
|
|
const tier = await this.getUserTier(userId);
|
|
if (tier === exports.ANONYMOUS_TIER || userId === 'anonymous-user')
|
|
return;
|
|
const tierName = tier.name.toLowerCase();
|
|
const limiterKey = `user_${tierName}_minute`;
|
|
const rateLimiter = this.rateLimiters.get(limiterKey);
|
|
if (!rateLimiter) {
|
|
logger_1.logger.warn(`Burst rate limiter not found: ${limiterKey}`);
|
|
return;
|
|
}
|
|
try {
|
|
await rateLimiter.consume(userId, 1);
|
|
}
|
|
catch (error) {
|
|
throw new BurstLimitError(tier.name, tier.requestsPerMinute);
|
|
}
|
|
}
|
|
async getRateLimitStatus(type, key, userId) {
|
|
const tier = await this.getUserTier(userId);
|
|
let limiterKey;
|
|
let limit;
|
|
if (type === 'legacy') {
|
|
limiterKey = 'legacy';
|
|
limit = 100;
|
|
}
|
|
else if (!userId || userId === 'anonymous-user') {
|
|
limiterKey = 'anonymous';
|
|
limit = exports.ANONYMOUS_TIER.requestsPerHour;
|
|
}
|
|
else {
|
|
const tierName = tier.name.toLowerCase();
|
|
switch (type) {
|
|
case 'tracking':
|
|
limiterKey = `user_${tierName}_hour`;
|
|
limit = tier.requestsPerHour;
|
|
break;
|
|
case 'bulk':
|
|
limiterKey = `bulk_${tierName}_day`;
|
|
limit = tier.bulkJobsPerDay;
|
|
break;
|
|
case 'export':
|
|
limiterKey = `export_${tierName}_day`;
|
|
limit = tier.exportLimit;
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown rate limit type: ${type}`);
|
|
}
|
|
}
|
|
const rateLimiter = this.rateLimiters.get(limiterKey);
|
|
if (!rateLimiter) {
|
|
throw new Error(`Rate limiter not found: ${limiterKey}`);
|
|
}
|
|
try {
|
|
const result = await rateLimiter.get(key);
|
|
return {
|
|
limit,
|
|
remaining: result ? result.remainingPoints || 0 : limit,
|
|
reset: result ? new Date(Date.now() + (result.msBeforeNext || 0)) : new Date(),
|
|
tier: tier.name,
|
|
};
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Failed to get rate limit status:', error);
|
|
return {
|
|
limit,
|
|
remaining: limit,
|
|
reset: new Date(),
|
|
tier: tier.name,
|
|
};
|
|
}
|
|
}
|
|
async resetRateLimit(key, type) {
|
|
try {
|
|
if (type) {
|
|
const rateLimiter = this.rateLimiters.get(type);
|
|
if (rateLimiter) {
|
|
await rateLimiter.delete(key);
|
|
}
|
|
}
|
|
else {
|
|
for (const rateLimiter of this.rateLimiters.values()) {
|
|
await rateLimiter.delete(key).catch(() => { });
|
|
}
|
|
}
|
|
logger_1.logger.info(`Rate limit reset for key: ${key}`, { type });
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Failed to reset rate limit:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
async getStatistics() {
|
|
try {
|
|
return {
|
|
totalRequests: 0,
|
|
activeKeys: this.rateLimiters.size,
|
|
tierDistribution: {
|
|
anonymous: 1,
|
|
free: Object.keys(exports.RATE_LIMIT_TIERS).length,
|
|
pro: Object.keys(exports.RATE_LIMIT_TIERS).length,
|
|
enterprise: Object.keys(exports.RATE_LIMIT_TIERS).length,
|
|
},
|
|
};
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Failed to get rate limit statistics:', error);
|
|
return {
|
|
totalRequests: 0,
|
|
activeKeys: 0,
|
|
tierDistribution: { anonymous: 0, free: 0, pro: 0, enterprise: 0 },
|
|
};
|
|
}
|
|
}
|
|
}
|
|
exports.RateLimitService = RateLimitService;
|
|
class RateLimitError extends Error {
|
|
tier;
|
|
remaining;
|
|
reset;
|
|
constructor(tier, remaining, reset) {
|
|
super(`Rate limit exceeded for ${tier} tier`);
|
|
this.tier = tier;
|
|
this.remaining = remaining;
|
|
this.reset = reset;
|
|
this.name = 'RateLimitError';
|
|
}
|
|
}
|
|
exports.RateLimitError = RateLimitError;
|
|
class BurstLimitError extends Error {
|
|
tier;
|
|
limit;
|
|
constructor(tier, limit) {
|
|
super(`Burst limit exceeded for ${tier} tier (${limit} requests per minute)`);
|
|
this.tier = tier;
|
|
this.limit = limit;
|
|
this.name = 'BurstLimitError';
|
|
}
|
|
}
|
|
exports.BurstLimitError = BurstLimitError;
|
|
exports.rateLimitService = new RateLimitService();
|
|
//# sourceMappingURL=rate-limit.service.js.map
|