Allow message retries for "pending" status, refactor message context handling, remove workspace import/export routes, and add SQLite-backed rate limiting.
CI / check (22) (push) Failing after 16m26s
CI / check (24) (push) Failing after 15m49s
CI / check (22) (pull_request) Failing after 16m26s
CI / check (24) (pull_request) Failing after 15m50s

This commit is contained in:
2026-10-09 19:28:43 +02:00
parent 8666e09049
commit f67d918d09
13 changed files with 313 additions and 193 deletions
+27 -2
View File
@@ -8,7 +8,7 @@ function pruneBucket(bucket, windowMs, now) {
}
}
export function createRateLimiter({windowMs, max, keyGenerator, message, cleanupIntervalMs = 60 * 1000}) {
export function createRateLimiter({windowMs, max, keyGenerator, message, cleanupIntervalMs = 60 * 1000, store = null}) {
const hits = new Map();
let cleanupTimer = null;
@@ -22,7 +22,7 @@ export function createRateLimiter({windowMs, max, keyGenerator, message, cleanup
}
};
if (cleanupIntervalMs > 0) {
if (!store && cleanupIntervalMs > 0) {
cleanupTimer = setInterval(cleanup, cleanupIntervalMs);
// Der Aufräumtimer allein soll den Node-Prozess beim Herunterfahren nicht offen halten.
cleanupTimer.unref();
@@ -35,6 +35,10 @@ export function createRateLimiter({windowMs, max, keyGenerator, message, cleanup
}
const now = Date.now();
if (store) {
if (!store.consume(key, now, windowMs, max)) return res.status(429).json({error: message});
return next();
}
const bucket = hits.get(key) || [];
pruneBucket(bucket, windowMs, now);
if (!bucket.length) {
@@ -59,3 +63,24 @@ export function createRateLimiter({windowMs, max, keyGenerator, message, cleanup
return limiter;
}
// SQLite is already shared by all application instances and serializes this
// short transaction, so the check-and-increment remains atomic across workers.
export function createSqliteRateLimitStore(db) {
db.exec(`CREATE TABLE IF NOT EXISTS rate_limit_hits (
bucket_key TEXT NOT NULL,
hit_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS rate_limit_hits_expiry ON rate_limit_hits(hit_at);`);
db.pragma("busy_timeout = 5000");
const prune = db.prepare("DELETE FROM rate_limit_hits WHERE bucket_key = ? AND hit_at <= ?");
const count = db.prepare("SELECT COUNT(*) AS count FROM rate_limit_hits WHERE bucket_key = ?");
const insert = db.prepare("INSERT INTO rate_limit_hits (bucket_key, hit_at) VALUES (?, ?)");
const consume = db.transaction((key, now, windowMs, max) => {
prune.run(key, now - windowMs);
if (count.get(key).count >= max) return false;
insert.run(key, now);
return true;
});
return {consume};
}
+6 -2
View File
@@ -3,12 +3,13 @@
*/
import {createRateLimiter} from "./rateLimit.js";
export function createAuthRateLimiters(config) {
export function createAuthRateLimiters(config, store = null) {
return [
createRateLimiter({
windowMs: config.authRateLimitWindowMs,
max: config.authRateLimitMaxPerIp,
cleanupIntervalMs: config.rateLimitCleanupIntervalMs,
store,
keyGenerator: (req) => `auth:ip:${req.ip}`,
message: "Too many authentication attempts from this IP. Please try again later."
}),
@@ -16,6 +17,7 @@ export function createAuthRateLimiters(config) {
windowMs: config.authRateLimitWindowMs,
max: config.authRateLimitMaxPerAccount,
cleanupIntervalMs: config.rateLimitCleanupIntervalMs,
store,
keyGenerator: (req) => {
const username = String(req.body?.username || "").trim().toLowerCase();
return username ? `auth:user:${username}` : null;
@@ -25,12 +27,13 @@ export function createAuthRateLimiters(config) {
];
}
export function createChatRateLimiters(config) {
export function createChatRateLimiters(config, store = null) {
return [
createRateLimiter({
windowMs: config.chatRateLimitWindowMs,
max: config.chatRateLimitMaxPerIp,
cleanupIntervalMs: config.rateLimitCleanupIntervalMs,
store,
keyGenerator: (req) => `chat:ip:${req.ip}`,
message: "Too many chat requests from this IP. Please slow down."
}),
@@ -38,6 +41,7 @@ export function createChatRateLimiters(config) {
windowMs: config.chatRateLimitWindowMs,
max: config.chatRateLimitMaxPerAccount,
cleanupIntervalMs: config.rateLimitCleanupIntervalMs,
store,
keyGenerator: (req) => req.session.user ? `chat:user:${req.session.user}` : null,
message: "Too many chat requests for this account. Please slow down."
})