238 lines
7.8 KiB
JavaScript
Executable File
238 lines
7.8 KiB
JavaScript
Executable File
// 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 = {};
|
|
this.interval = setInterval(async () => {
|
|
await this.resetAll();
|
|
}, this.windowMs);
|
|
if (this.interval.unref)
|
|
this.interval.unref();
|
|
}
|
|
async increment(key) {
|
|
var _a;
|
|
const totalHits = ((_a = this.hits[key]) != null ? _a : 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);
|
|
}
|
|
shutdown() {
|
|
clearInterval(this.interval);
|
|
}
|
|
};
|
|
|
|
// 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 legacyStore.decrement(key);
|
|
}
|
|
async resetKey(key) {
|
|
return legacyStore.resetKey(key);
|
|
}
|
|
async resetAll() {
|
|
if (typeof legacyStore.resetAll === "function")
|
|
return legacyStore.resetAll();
|
|
}
|
|
}
|
|
return new PromisifiedStore();
|
|
};
|
|
var parseOptions = (passedOptions) => {
|
|
var _a, _b, _c;
|
|
const notUndefinedOptions = omitUndefinedOptions(passedOptions);
|
|
const config = {
|
|
windowMs: 60 * 1e3,
|
|
max: 5,
|
|
message: "Too many requests, please try again later.",
|
|
statusCode: 429,
|
|
legacyHeaders: (_a = passedOptions.headers) != null ? _a : true,
|
|
standardHeaders: (_b = passedOptions.draft_polli_ratelimit_headers) != null ? _b : 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;
|
|
},
|
|
async handler(request, response, _next, _optionsUsed) {
|
|
response.status(config.statusCode);
|
|
const message = typeof config.message === "function" ? await config.message(
|
|
request,
|
|
response
|
|
) : config.message;
|
|
if (!response.writableEnded) {
|
|
response.send(message != null ? message : "Too many requests, please try again later.");
|
|
}
|
|
},
|
|
onLimitReached(_request, _response, _optionsUsed) {
|
|
},
|
|
...notUndefinedOptions,
|
|
store: promisifyStore((_c = notUndefinedOptions.store) != null ? _c : new MemoryStore())
|
|
};
|
|
if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || typeof config.store.resetAll !== "undefined" && typeof config.store.resetAll !== "function" || typeof config.store.init !== "undefined" && typeof config.store.init !== "function") {
|
|
throw new TypeError(
|
|
"An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface."
|
|
);
|
|
}
|
|
return config;
|
|
};
|
|
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 != null ? 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 omitUndefinedOptions = (passedOptions) => {
|
|
const omittedOptions = {};
|
|
for (const k of Object.keys(passedOptions)) {
|
|
const key = k;
|
|
if (passedOptions[key] !== void 0) {
|
|
omittedOptions[key] = passedOptions[key];
|
|
}
|
|
}
|
|
return omittedOptions;
|
|
};
|
|
var lib_default = rateLimit;
|
|
export {
|
|
MemoryStore,
|
|
lib_default as default,
|
|
lib_default as rateLimit
|
|
};
|