199 lines
6.8 KiB
JavaScript
199 lines
6.8 KiB
JavaScript
|
// source/memory-store.ts
|
||
|
var calculateNextResetTime = (windowMs) => {
|
||
|
const resetTime = new Date();
|
||
|
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
|
||
|
return resetTime;
|
||
|
};
|
||
|
var MemoryStore = class {
|
||
|
init(options) {
|
||
|
this.windowMs = options.windowMs;
|
||
|
this.resetTime = calculateNextResetTime(this.windowMs);
|
||
|
this.hits = {};
|
||
|
const interval = setInterval(async () => {
|
||
|
await this.resetAll();
|
||
|
}, this.windowMs);
|
||
|
if (interval.unref) {
|
||
|
interval.unref();
|
||
|
}
|
||
|
}
|
||
|
async increment(key) {
|
||
|
const totalHits = (this.hits[key] ?? 0) + 1;
|
||
|
this.hits[key] = totalHits;
|
||
|
return {
|
||
|
totalHits,
|
||
|
resetTime: this.resetTime
|
||
|
};
|
||
|
}
|
||
|
async decrement(key) {
|
||
|
const current = this.hits[key];
|
||
|
if (current) {
|
||
|
this.hits[key] = current - 1;
|
||
|
}
|
||
|
}
|
||
|
async resetKey(key) {
|
||
|
delete this.hits[key];
|
||
|
}
|
||
|
async resetAll() {
|
||
|
this.hits = {};
|
||
|
this.resetTime = calculateNextResetTime(this.windowMs);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// source/lib.ts
|
||
|
var isLegacyStore = (store) => typeof store.incr === "function" && typeof store.increment !== "function";
|
||
|
var promisifyStore = (passedStore) => {
|
||
|
if (!isLegacyStore(passedStore)) {
|
||
|
return passedStore;
|
||
|
}
|
||
|
const legacyStore = passedStore;
|
||
|
class PromisifiedStore {
|
||
|
async increment(key) {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
legacyStore.incr(key, (error, totalHits, resetTime) => {
|
||
|
if (error)
|
||
|
reject(error);
|
||
|
resolve({ totalHits, resetTime });
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
async decrement(key) {
|
||
|
return Promise.resolve(legacyStore.decrement(key));
|
||
|
}
|
||
|
async resetKey(key) {
|
||
|
return Promise.resolve(legacyStore.resetKey(key));
|
||
|
}
|
||
|
async resetAll() {
|
||
|
if (typeof legacyStore.resetAll === "function")
|
||
|
return Promise.resolve(legacyStore.resetAll());
|
||
|
}
|
||
|
}
|
||
|
return new PromisifiedStore();
|
||
|
};
|
||
|
var parseOptions = (passedOptions) => {
|
||
|
const options = {
|
||
|
windowMs: 60 * 1e3,
|
||
|
store: new MemoryStore(),
|
||
|
max: 5,
|
||
|
message: "Too many requests, please try again later.",
|
||
|
statusCode: 429,
|
||
|
legacyHeaders: passedOptions.headers ?? true,
|
||
|
standardHeaders: passedOptions.draft_polli_ratelimit_headers ?? false,
|
||
|
requestPropertyName: "rateLimit",
|
||
|
skipFailedRequests: false,
|
||
|
skipSuccessfulRequests: false,
|
||
|
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
||
|
skip: (_request, _response) => false,
|
||
|
keyGenerator: (request, _response) => {
|
||
|
if (!request.ip) {
|
||
|
console.error("WARN | `express-rate-limit` | `request.ip` is undefined. You can avoid this by providing a custom `keyGenerator` function, but it may be indicative of a larger issue.");
|
||
|
}
|
||
|
return request.ip;
|
||
|
},
|
||
|
handler: (_request, response, _next, _optionsUsed) => {
|
||
|
response.status(options.statusCode).send(options.message);
|
||
|
},
|
||
|
onLimitReached: (_request, _response, _optionsUsed) => {
|
||
|
},
|
||
|
...passedOptions
|
||
|
};
|
||
|
if (typeof options.store.incr !== "function" && typeof options.store.increment !== "function" || typeof options.store.decrement !== "function" || typeof options.store.resetKey !== "function" || typeof options.store.resetAll !== "undefined" && typeof options.store.resetAll !== "function" || typeof options.store.init !== "undefined" && typeof options.store.init !== "function") {
|
||
|
throw new TypeError("An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface.");
|
||
|
}
|
||
|
options.store = promisifyStore(options.store);
|
||
|
return options;
|
||
|
};
|
||
|
var handleAsyncErrors = (fn) => async (request, response, next) => {
|
||
|
try {
|
||
|
await Promise.resolve(fn(request, response, next)).catch(next);
|
||
|
} catch (error) {
|
||
|
next(error);
|
||
|
}
|
||
|
};
|
||
|
var rateLimit = (passedOptions) => {
|
||
|
const options = parseOptions(passedOptions ?? {});
|
||
|
if (typeof options.store.init === "function")
|
||
|
options.store.init(options);
|
||
|
const middleware = handleAsyncErrors(async (request, response, next) => {
|
||
|
const skip = await options.skip(request, response);
|
||
|
if (skip) {
|
||
|
next();
|
||
|
return;
|
||
|
}
|
||
|
const augmentedRequest = request;
|
||
|
const key = await options.keyGenerator(request, response);
|
||
|
const { totalHits, resetTime } = await options.store.increment(key);
|
||
|
const retrieveQuota = typeof options.max === "function" ? options.max(request, response) : options.max;
|
||
|
const maxHits = await retrieveQuota;
|
||
|
augmentedRequest[options.requestPropertyName] = {
|
||
|
limit: maxHits,
|
||
|
current: totalHits,
|
||
|
remaining: Math.max(maxHits - totalHits, 0),
|
||
|
resetTime
|
||
|
};
|
||
|
if (options.legacyHeaders && !response.headersSent) {
|
||
|
response.setHeader("X-RateLimit-Limit", maxHits);
|
||
|
response.setHeader("X-RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||
|
if (resetTime instanceof Date) {
|
||
|
response.setHeader("Date", new Date().toUTCString());
|
||
|
response.setHeader("X-RateLimit-Reset", Math.ceil(resetTime.getTime() / 1e3));
|
||
|
}
|
||
|
}
|
||
|
if (options.standardHeaders && !response.headersSent) {
|
||
|
response.setHeader("RateLimit-Limit", maxHits);
|
||
|
response.setHeader("RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||
|
if (resetTime) {
|
||
|
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
||
|
response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
||
|
}
|
||
|
}
|
||
|
if (options.skipFailedRequests || options.skipSuccessfulRequests) {
|
||
|
let decremented = false;
|
||
|
const decrementKey = async () => {
|
||
|
if (!decremented) {
|
||
|
await options.store.decrement(key);
|
||
|
decremented = true;
|
||
|
}
|
||
|
};
|
||
|
if (options.skipFailedRequests) {
|
||
|
response.on("finish", async () => {
|
||
|
if (!options.requestWasSuccessful(request, response))
|
||
|
await decrementKey();
|
||
|
});
|
||
|
response.on("close", async () => {
|
||
|
if (!response.writableEnded)
|
||
|
await decrementKey();
|
||
|
});
|
||
|
response.on("error", async () => {
|
||
|
await decrementKey();
|
||
|
});
|
||
|
}
|
||
|
if (options.skipSuccessfulRequests) {
|
||
|
response.on("finish", async () => {
|
||
|
if (options.requestWasSuccessful(request, response))
|
||
|
await decrementKey();
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
if (maxHits && totalHits === maxHits + 1) {
|
||
|
options.onLimitReached(request, response, options);
|
||
|
}
|
||
|
if (maxHits && totalHits > maxHits) {
|
||
|
if ((options.legacyHeaders || options.standardHeaders) && !response.headersSent) {
|
||
|
response.setHeader("Retry-After", Math.ceil(options.windowMs / 1e3));
|
||
|
}
|
||
|
options.handler(request, response, next, options);
|
||
|
return;
|
||
|
}
|
||
|
next();
|
||
|
});
|
||
|
middleware.resetKey = options.store.resetKey.bind(options.store);
|
||
|
return middleware;
|
||
|
};
|
||
|
var lib_default = rateLimit;
|
||
|
|
||
|
// source/index.ts
|
||
|
var source_default = lib_default;
|
||
|
export {
|
||
|
source_default as default
|
||
|
};
|