Files
curriculum-project-hub/hub/src/deployment/siloRateLimit.ts
T
hongjr03 ae5f78f036 chore: release v0.0.28
Exempt SPA static assets and admin HTML shell from silo HTTP rate limit so
page loads no longer exhaust HUB_HTTP_REQUESTS_PER_MINUTE.
2026-07-15 22:33:07 +08:00

50 lines
1.8 KiB
TypeScript

/**
* Silo-wide HTTP request rate limit (ADR-0022 `requestRate`).
*
* Counts dynamic traffic only: APIs, auth, and other application handlers.
* Static SPA assets and the org-admin HTML shell are exempt so a single page
* load (dozens of `/_app/*` chunks + favicon) does not exhaust the minute budget.
*/
/** Paths that must not consume the silo HTTP request-rate budget. */
export function isSiloHttpRateLimitExempt(url: string): boolean {
const path = (url.split("?", 1)[0] ?? url) || "/";
if (path === "/api/healthz") return true;
// SvelteKit build output and top-level static files (see admin/static.ts).
if (path === "/_app" || path.startsWith("/_app/")) return true;
if (path === "/favicon.ico" || path === "/favicon.svg" || path === "/robots.txt") return true;
// SPA index shell for client-side routes (not an API).
if (path === "/admin" || path.startsWith("/admin/")) return true;
return false;
}
export class SiloFixedWindowRateLimiter {
private windowStartedAt: number;
private used = 0;
constructor(readonly limit: number, readonly windowMs: number, now = Date.now()) {
if (!Number.isSafeInteger(limit) || limit <= 0) throw new Error("rate limit must be a positive integer");
if (!Number.isSafeInteger(windowMs) || windowMs <= 0) throw new Error("rate window must be a positive integer");
this.windowStartedAt = now;
}
consume(now = Date.now()): { readonly allowed: true } | { readonly allowed: false; readonly retryAfterSeconds: number } {
if (now >= this.windowStartedAt + this.windowMs) {
this.windowStartedAt = now;
this.used = 0;
}
if (this.used >= this.limit) {
return {
allowed: false,
retryAfterSeconds: Math.max(1, Math.ceil((this.windowStartedAt + this.windowMs - now) / 1000)),
};
}
this.used += 1;
return { allowed: true };
}
}