kaelum 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/rateLimit.js +153 -0
- package/core/setConfig.js +58 -0
- package/core/shutdown.js +167 -0
- package/core/start.js +13 -0
- package/createApp.js +14 -0
- package/index.d.ts +39 -0
- package/package.json +2 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// core/rateLimit.js
|
|
2
|
+
// Kaelum built-in rate limiting middleware.
|
|
3
|
+
// Zero-dependency, in-memory sliding-window rate limiter.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-memory store for tracking request counts per key.
|
|
7
|
+
* Automatically cleans up expired entries on an interval.
|
|
8
|
+
*/
|
|
9
|
+
class MemoryStore {
|
|
10
|
+
/**
|
|
11
|
+
* @param {number} windowMs - window duration in milliseconds
|
|
12
|
+
*/
|
|
13
|
+
constructor(windowMs) {
|
|
14
|
+
this.windowMs = windowMs;
|
|
15
|
+
/** @type {Map<string, { hits: number, resetTime: number }>} */
|
|
16
|
+
this.hits = new Map();
|
|
17
|
+
// clean up expired entries every 60s (or every window if shorter)
|
|
18
|
+
this._cleanupInterval = setInterval(
|
|
19
|
+
() => this._cleanup(),
|
|
20
|
+
Math.min(windowMs, 60000)
|
|
21
|
+
);
|
|
22
|
+
// don't block process exit
|
|
23
|
+
if (this._cleanupInterval.unref) {
|
|
24
|
+
this._cleanupInterval.unref();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Increment the hit counter for a key.
|
|
30
|
+
* @param {string} key
|
|
31
|
+
* @returns {{ totalHits: number, resetTime: number }}
|
|
32
|
+
*/
|
|
33
|
+
increment(key) {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const entry = this.hits.get(key);
|
|
36
|
+
|
|
37
|
+
if (entry && now < entry.resetTime) {
|
|
38
|
+
entry.hits += 1;
|
|
39
|
+
return { totalHits: entry.hits, resetTime: entry.resetTime };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// new window
|
|
43
|
+
const resetTime = now + this.windowMs;
|
|
44
|
+
const record = { hits: 1, resetTime };
|
|
45
|
+
this.hits.set(key, record);
|
|
46
|
+
return { totalHits: 1, resetTime };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reset the counter for a specific key.
|
|
51
|
+
* @param {string} key
|
|
52
|
+
*/
|
|
53
|
+
resetKey(key) {
|
|
54
|
+
this.hits.delete(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Remove expired entries from the store.
|
|
59
|
+
*/
|
|
60
|
+
_cleanup() {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
for (const [key, entry] of this.hits) {
|
|
63
|
+
if (now >= entry.resetTime) {
|
|
64
|
+
this.hits.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Shut down the store and clear the cleanup interval.
|
|
71
|
+
*/
|
|
72
|
+
shutdown() {
|
|
73
|
+
clearInterval(this._cleanupInterval);
|
|
74
|
+
this.hits.clear();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a rate-limiting middleware function.
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} [options]
|
|
82
|
+
* @param {number} [options.windowMs=900000] - window duration in ms (default 15 min)
|
|
83
|
+
* @param {number} [options.max=100] - max requests per window per key
|
|
84
|
+
* @param {string|Object} [options.message] - response body when limited
|
|
85
|
+
* @param {number} [options.statusCode=429] - HTTP status when limited
|
|
86
|
+
* @param {Function} [options.keyGenerator] - (req) => string (default: req.ip)
|
|
87
|
+
* @param {Function} [options.skip] - (req) => boolean (default: false)
|
|
88
|
+
* @param {boolean} [options.headers=true] - send standard rate-limit headers
|
|
89
|
+
* @param {Object} [options.store] - custom store (must implement increment/resetKey/shutdown)
|
|
90
|
+
* @returns {Function} Express middleware
|
|
91
|
+
*/
|
|
92
|
+
function createRateLimiter(options = {}) {
|
|
93
|
+
const {
|
|
94
|
+
windowMs = 15 * 60 * 1000,
|
|
95
|
+
max = 100,
|
|
96
|
+
message = { error: "Too many requests, please try again later." },
|
|
97
|
+
statusCode = 429,
|
|
98
|
+
keyGenerator = (req) => req.ip || req.connection.remoteAddress || "unknown",
|
|
99
|
+
skip = () => false,
|
|
100
|
+
headers = true,
|
|
101
|
+
store: customStore,
|
|
102
|
+
} = options;
|
|
103
|
+
|
|
104
|
+
const store = customStore || new MemoryStore(windowMs);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Express middleware function.
|
|
108
|
+
*/
|
|
109
|
+
function rateLimitMiddleware(req, res, next) {
|
|
110
|
+
// allow skipping for certain requests
|
|
111
|
+
if (skip(req)) {
|
|
112
|
+
return next();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const key = keyGenerator(req);
|
|
116
|
+
const { totalHits, resetTime } = store.increment(key);
|
|
117
|
+
const remaining = Math.max(0, max - totalHits);
|
|
118
|
+
|
|
119
|
+
// attach rate-limit info to request for downstream use
|
|
120
|
+
req.rateLimit = { limit: max, remaining, resetTime };
|
|
121
|
+
|
|
122
|
+
// set standard headers
|
|
123
|
+
if (headers) {
|
|
124
|
+
res.setHeader("RateLimit-Limit", String(max));
|
|
125
|
+
res.setHeader("RateLimit-Remaining", String(remaining));
|
|
126
|
+
res.setHeader(
|
|
127
|
+
"RateLimit-Reset",
|
|
128
|
+
String(Math.ceil(resetTime / 1000))
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// exceeded limit
|
|
133
|
+
if (totalHits > max) {
|
|
134
|
+
if (headers) {
|
|
135
|
+
const retryAfter = Math.ceil((resetTime - Date.now()) / 1000);
|
|
136
|
+
res.setHeader("Retry-After", String(Math.max(retryAfter, 0)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const body =
|
|
140
|
+
typeof message === "string" ? { error: message } : message;
|
|
141
|
+
return res.status(statusCode).json(body);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
next();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// attach store reference so setConfig can call shutdown on removal
|
|
148
|
+
rateLimitMiddleware._store = store;
|
|
149
|
+
|
|
150
|
+
return rateLimitMiddleware;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { createRateLimiter, MemoryStore };
|
package/core/setConfig.js
CHANGED
|
@@ -90,6 +90,23 @@ function removeKaelumHelmet(app) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Remove Kaelum-installed rate-limiting middleware (if any)
|
|
95
|
+
* Also shuts down the MemoryStore cleanup interval.
|
|
96
|
+
* @param {Object} app
|
|
97
|
+
*/
|
|
98
|
+
function removeKaelumRateLimit(app) {
|
|
99
|
+
const prev = app.locals && app.locals._kaelum_ratelimit;
|
|
100
|
+
if (prev) {
|
|
101
|
+
// shut down the store's cleanup interval if present
|
|
102
|
+
if (prev._store && typeof prev._store.shutdown === "function") {
|
|
103
|
+
prev._store.shutdown();
|
|
104
|
+
}
|
|
105
|
+
removeMiddlewareByFn(app, prev);
|
|
106
|
+
app.locals._kaelum_ratelimit = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
/**
|
|
94
111
|
* Apply configuration options to the app
|
|
95
112
|
* @param {Object} app - express app instance
|
|
@@ -257,6 +274,47 @@ function setConfig(app, options = {}) {
|
|
|
257
274
|
}
|
|
258
275
|
}
|
|
259
276
|
|
|
277
|
+
// --- Rate Limiting ---
|
|
278
|
+
if (options.hasOwnProperty("rateLimit")) {
|
|
279
|
+
if (options.rateLimit) {
|
|
280
|
+
const { createRateLimiter } = require("./rateLimit");
|
|
281
|
+
const rateLimitOpts =
|
|
282
|
+
options.rateLimit === true ? {} : options.rateLimit;
|
|
283
|
+
|
|
284
|
+
// remove previous Kaelum-installed rate limiter if exists
|
|
285
|
+
removeKaelumRateLimit(app);
|
|
286
|
+
|
|
287
|
+
const rateLimitFn = createRateLimiter(rateLimitOpts);
|
|
288
|
+
app.locals._kaelum_ratelimit = rateLimitFn;
|
|
289
|
+
app.use(rateLimitFn);
|
|
290
|
+
console.log("⏱️ Rate limiting activated.");
|
|
291
|
+
} else {
|
|
292
|
+
// disable Kaelum-installed rate limiter if present
|
|
293
|
+
removeKaelumRateLimit(app);
|
|
294
|
+
console.log("⏱️ Rate limiting disabled (Kaelum-managed).");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --- Graceful Shutdown ---
|
|
299
|
+
if (options.hasOwnProperty("gracefulShutdown")) {
|
|
300
|
+
const server = app.locals && app.locals._kaelum_server;
|
|
301
|
+
if (server && server.listening) {
|
|
302
|
+
const {
|
|
303
|
+
enableGracefulShutdown,
|
|
304
|
+
removeSignalHandlers,
|
|
305
|
+
} = require("./shutdown");
|
|
306
|
+
if (options.gracefulShutdown === false) {
|
|
307
|
+
removeSignalHandlers(app);
|
|
308
|
+
} else {
|
|
309
|
+
const shutdownOpts =
|
|
310
|
+
typeof options.gracefulShutdown === "object"
|
|
311
|
+
? options.gracefulShutdown
|
|
312
|
+
: {};
|
|
313
|
+
enableGracefulShutdown(app, shutdownOpts);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
260
318
|
// Return the full merged config for convenience
|
|
261
319
|
return app.locals.kaelumConfig;
|
|
262
320
|
}
|
package/core/shutdown.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// core/shutdown.js
|
|
2
|
+
// Kaelum graceful shutdown module.
|
|
3
|
+
// Handles process signal interception, connection draining, and cleanup hook execution.
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
6
|
+
const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register a cleanup function to run during shutdown.
|
|
10
|
+
*/
|
|
11
|
+
function onShutdown(app, fn) {
|
|
12
|
+
if (!app) throw new Error("onShutdown requires an app instance");
|
|
13
|
+
if (typeof fn !== "function") {
|
|
14
|
+
throw new Error("onShutdown: expected a function, got " + typeof fn);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!Array.isArray(app.locals._kaelum_shutdown_hooks)) {
|
|
18
|
+
app.locals._kaelum_shutdown_hooks = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
app.locals._kaelum_shutdown_hooks.push(fn);
|
|
22
|
+
return app;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run all registered cleanup hooks in order.
|
|
27
|
+
* Errors in individual hooks are caught and logged but do not abort the sequence.
|
|
28
|
+
*/
|
|
29
|
+
async function runCleanupHooks(app) {
|
|
30
|
+
const hooks = (app.locals && app.locals._kaelum_shutdown_hooks) || [];
|
|
31
|
+
for (const fn of hooks) {
|
|
32
|
+
try {
|
|
33
|
+
await Promise.resolve(fn());
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(
|
|
36
|
+
"Kaelum shutdown: cleanup hook error:",
|
|
37
|
+
err && err.message ? err.message : err
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gracefully close the server and run cleanup hooks.
|
|
45
|
+
*
|
|
46
|
+
* If callback is provided: calls cb(err) and returns app.
|
|
47
|
+
* If no callback: returns a Promise<void>.
|
|
48
|
+
*/
|
|
49
|
+
function close(app, cb) {
|
|
50
|
+
if (!app) throw new Error("close requires an app instance");
|
|
51
|
+
|
|
52
|
+
// Prevent double-shutdown
|
|
53
|
+
if (app.locals._kaelum_shutdown_in_progress) {
|
|
54
|
+
if (typeof cb === "function") {
|
|
55
|
+
process.nextTick(() => cb());
|
|
56
|
+
return app;
|
|
57
|
+
}
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
app.locals._kaelum_shutdown_in_progress = true;
|
|
62
|
+
|
|
63
|
+
const timeout = app.locals._kaelum_shutdown_timeout || DEFAULT_TIMEOUT;
|
|
64
|
+
const server = app.locals._kaelum_server;
|
|
65
|
+
|
|
66
|
+
// Remove signal handlers to prevent re-entry
|
|
67
|
+
removeSignalHandlers(app);
|
|
68
|
+
|
|
69
|
+
const doShutdown = async () => {
|
|
70
|
+
try {
|
|
71
|
+
// Phase 1: close the HTTP server (drain existing connections)
|
|
72
|
+
if (server && typeof server.close === "function") {
|
|
73
|
+
await new Promise((resolve) => {
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
console.error(
|
|
76
|
+
"Kaelum shutdown: server close timed out after " + timeout + "ms"
|
|
77
|
+
);
|
|
78
|
+
resolve();
|
|
79
|
+
}, timeout);
|
|
80
|
+
if (timer.unref) timer.unref();
|
|
81
|
+
|
|
82
|
+
server.close((err) => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
if (err) {
|
|
85
|
+
console.error(
|
|
86
|
+
"Kaelum shutdown: server close error:",
|
|
87
|
+
err.message || err
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Phase 2: run cleanup hooks (always runs, even after timeout)
|
|
96
|
+
await runCleanupHooks(app);
|
|
97
|
+
} finally {
|
|
98
|
+
app.locals._kaelum_shutdown_in_progress = false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (typeof cb === "function") {
|
|
103
|
+
doShutdown()
|
|
104
|
+
.then(() => cb(null))
|
|
105
|
+
.catch((err) => cb(err));
|
|
106
|
+
return app;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return doShutdown();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Register process signal handlers for automatic graceful shutdown.
|
|
114
|
+
* Called internally from start.js after server creation.
|
|
115
|
+
*/
|
|
116
|
+
function enableGracefulShutdown(app, options) {
|
|
117
|
+
if (!app) return;
|
|
118
|
+
|
|
119
|
+
const cfg = typeof options === "object" && options !== null ? options : {};
|
|
120
|
+
const timeout = cfg.timeout || DEFAULT_TIMEOUT;
|
|
121
|
+
const signals = Array.isArray(cfg.signals) ? cfg.signals : DEFAULT_SIGNALS;
|
|
122
|
+
|
|
123
|
+
app.locals._kaelum_shutdown_timeout = timeout;
|
|
124
|
+
|
|
125
|
+
// Remove existing handlers before registering new ones
|
|
126
|
+
removeSignalHandlers(app);
|
|
127
|
+
|
|
128
|
+
const handlers = {};
|
|
129
|
+
|
|
130
|
+
for (const signal of signals) {
|
|
131
|
+
const handler = () => {
|
|
132
|
+
console.log(
|
|
133
|
+
"\nKaelum: received " + signal + ", starting graceful shutdown..."
|
|
134
|
+
);
|
|
135
|
+
close(app)
|
|
136
|
+
.then(() => process.exit(0))
|
|
137
|
+
.catch((err) => {
|
|
138
|
+
console.error("Kaelum shutdown error:", err.message || err);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
handlers[signal] = handler;
|
|
143
|
+
process.on(signal, handler);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
app.locals._kaelum_shutdown_handlers = handlers;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Remove signal handlers previously installed by enableGracefulShutdown.
|
|
151
|
+
*/
|
|
152
|
+
function removeSignalHandlers(app) {
|
|
153
|
+
const handlers = app.locals && app.locals._kaelum_shutdown_handlers;
|
|
154
|
+
if (handlers && typeof handlers === "object") {
|
|
155
|
+
for (const signal of Object.keys(handlers)) {
|
|
156
|
+
process.removeListener(signal, handlers[signal]);
|
|
157
|
+
}
|
|
158
|
+
app.locals._kaelum_shutdown_handlers = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
onShutdown,
|
|
164
|
+
close,
|
|
165
|
+
enableGracefulShutdown,
|
|
166
|
+
removeSignalHandlers,
|
|
167
|
+
};
|
package/core/start.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
// - If a server is already running, returns the existing server (no double-listen)
|
|
6
6
|
// - Stores the server instance in app.locals._kaelum_server
|
|
7
7
|
// - Attaches basic error logging for startup errors
|
|
8
|
+
// - Enables graceful shutdown by default (unless config disables it)
|
|
9
|
+
|
|
10
|
+
const { enableGracefulShutdown } = require("./shutdown");
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Normalize and validate a port-like value.
|
|
@@ -109,6 +112,16 @@ function start(app, port, cb) {
|
|
|
109
112
|
// persist reference so we can check or close later
|
|
110
113
|
app.locals._kaelum_server = server;
|
|
111
114
|
|
|
115
|
+
// enable graceful shutdown by default unless explicitly disabled
|
|
116
|
+
const shutdownCfg = cfg.gracefulShutdown;
|
|
117
|
+
if (shutdownCfg !== false) {
|
|
118
|
+
const shutdownOpts =
|
|
119
|
+
typeof shutdownCfg === "object" && shutdownCfg !== null
|
|
120
|
+
? shutdownCfg
|
|
121
|
+
: {};
|
|
122
|
+
enableGracefulShutdown(app, shutdownOpts);
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
return server;
|
|
113
126
|
}
|
|
114
127
|
|
package/createApp.js
CHANGED
|
@@ -20,6 +20,7 @@ const registerHealth = require("./core/healthCheck");
|
|
|
20
20
|
const redirect = require("./core/redirect");
|
|
21
21
|
const { removeMiddlewareByFn } = require("./core/utils");
|
|
22
22
|
const { registerPlugin, getPlugins } = require("./core/plugin");
|
|
23
|
+
const { onShutdown, close } = require("./core/shutdown");
|
|
23
24
|
|
|
24
25
|
function createApp() {
|
|
25
26
|
const app = express();
|
|
@@ -31,6 +32,8 @@ function createApp() {
|
|
|
31
32
|
app.locals = app.locals || {};
|
|
32
33
|
app.locals.kaelumConfig = app.locals.kaelumConfig || {};
|
|
33
34
|
app.locals._kaelum_plugins = [];
|
|
35
|
+
app.locals._kaelum_shutdown_hooks = [];
|
|
36
|
+
app.locals._kaelum_shutdown_in_progress = false;
|
|
34
37
|
// persist baseline config so app.get("kaelum:config") is always available
|
|
35
38
|
app.set("kaelum:config", app.locals.kaelumConfig);
|
|
36
39
|
|
|
@@ -182,6 +185,17 @@ function createApp() {
|
|
|
182
185
|
return getPlugins(app);
|
|
183
186
|
};
|
|
184
187
|
|
|
188
|
+
// ---------------------------
|
|
189
|
+
// Graceful shutdown
|
|
190
|
+
// ---------------------------
|
|
191
|
+
app.onShutdown = function (fn) {
|
|
192
|
+
return onShutdown(app, fn);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
app.close = function (cb) {
|
|
196
|
+
return close(app, cb);
|
|
197
|
+
};
|
|
198
|
+
|
|
185
199
|
return app;
|
|
186
200
|
}
|
|
187
201
|
|
package/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ interface KaelumConfig {
|
|
|
10
10
|
port?: number;
|
|
11
11
|
views?: { engine?: string; path?: string };
|
|
12
12
|
logger?: boolean | false;
|
|
13
|
+
gracefulShutdown?: boolean | GracefulShutdownConfig;
|
|
14
|
+
rateLimit?: boolean | RateLimitConfig;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
interface HealthOptions {
|
|
@@ -32,6 +34,36 @@ interface ErrorHandlerOptions {
|
|
|
32
34
|
onError?: (err: Error, req: any, res: any) => void;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
interface GracefulShutdownConfig {
|
|
38
|
+
/** Timeout in milliseconds before forcing shutdown (default: 10000) */
|
|
39
|
+
timeout?: number;
|
|
40
|
+
/** Process signals to handle (default: ["SIGTERM", "SIGINT"]) */
|
|
41
|
+
signals?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface RateLimitConfig {
|
|
45
|
+
/** Window duration in ms (default: 900000 = 15 min) */
|
|
46
|
+
windowMs?: number;
|
|
47
|
+
/** Max requests per window per key (default: 100) */
|
|
48
|
+
max?: number;
|
|
49
|
+
/** Response body when rate limited */
|
|
50
|
+
message?: string | object;
|
|
51
|
+
/** HTTP status when rate limited (default: 429) */
|
|
52
|
+
statusCode?: number;
|
|
53
|
+
/** Custom key generator (default: req.ip) */
|
|
54
|
+
keyGenerator?: (req: any) => string;
|
|
55
|
+
/** Skip rate limiting for certain requests */
|
|
56
|
+
skip?: (req: any) => boolean;
|
|
57
|
+
/** Send standard rate-limit headers (default: true) */
|
|
58
|
+
headers?: boolean;
|
|
59
|
+
/** Custom store (must implement increment, resetKey, shutdown) */
|
|
60
|
+
store?: {
|
|
61
|
+
increment(key: string): { totalHits: number; resetTime: number };
|
|
62
|
+
resetKey(key: string): void;
|
|
63
|
+
shutdown(): void;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
interface RedirectEntry {
|
|
36
68
|
path: string;
|
|
37
69
|
to: string;
|
|
@@ -102,6 +134,13 @@ interface KaelumApp extends Express {
|
|
|
102
134
|
|
|
103
135
|
/** List registered plugin names */
|
|
104
136
|
getPlugins(): string[];
|
|
137
|
+
|
|
138
|
+
/** Gracefully close the server and run cleanup hooks */
|
|
139
|
+
close(): Promise<void>;
|
|
140
|
+
close(cb: (err?: Error | null) => void): KaelumApp;
|
|
141
|
+
|
|
142
|
+
/** Register a cleanup function to run during graceful shutdown */
|
|
143
|
+
onShutdown(fn: () => void | Promise<void>): KaelumApp;
|
|
105
144
|
}
|
|
106
145
|
|
|
107
146
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kaelum",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A minimalist Node.js framework for building web pages and APIs with simplicity and speed.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|
|
41
|
-
"url": "https://github.com/kaelumjs/kaelum.git"
|
|
41
|
+
"url": "git+https://github.com/kaelumjs/kaelum.git"
|
|
42
42
|
},
|
|
43
43
|
"bugs": {
|
|
44
44
|
"url": "https://github.com/kaelumjs/kaelum/issues"
|