@codaijs/keel 0.2.2 → 0.2.4

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.
Files changed (80) hide show
  1. package/dist/__tests__/sail-installer.test.js +25 -25
  2. package/dist/sail-installer.js +174 -174
  3. package/dist/scaffold.js +68 -68
  4. package/package.json +58 -58
  5. package/sails/_template/addon.json +20 -20
  6. package/sails/_template/install.ts +402 -402
  7. package/sails/admin-dashboard/README.md +117 -117
  8. package/sails/admin-dashboard/addon.json +28 -28
  9. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
  10. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
  11. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
  12. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
  13. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
  14. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
  15. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
  16. package/sails/admin-dashboard/install.ts +305 -305
  17. package/sails/analytics/README.md +178 -178
  18. package/sails/analytics/addon.json +27 -27
  19. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
  20. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
  21. package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
  22. package/sails/analytics/install.ts +297 -297
  23. package/sails/file-uploads/addon.json +30 -30
  24. package/sails/file-uploads/files/backend/routes/files.ts +198 -198
  25. package/sails/file-uploads/files/backend/schema/files.ts +36 -36
  26. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
  27. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
  28. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
  29. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
  30. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
  31. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
  32. package/sails/file-uploads/install.ts +466 -466
  33. package/sails/gdpr/README.md +174 -174
  34. package/sails/gdpr/addon.json +27 -27
  35. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
  36. package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
  37. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
  38. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
  39. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
  40. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
  41. package/sails/gdpr/install.ts +756 -756
  42. package/sails/google-oauth/README.md +121 -121
  43. package/sails/google-oauth/addon.json +22 -22
  44. package/sails/google-oauth/files/GoogleButton.tsx +50 -50
  45. package/sails/google-oauth/install.ts +252 -252
  46. package/sails/i18n/README.md +193 -193
  47. package/sails/i18n/addon.json +30 -30
  48. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
  49. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
  50. package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
  51. package/sails/i18n/files/frontend/locales/de/common.json +44 -44
  52. package/sails/i18n/files/frontend/locales/en/common.json +44 -44
  53. package/sails/i18n/install.ts +407 -407
  54. package/sails/push-notifications/README.md +163 -163
  55. package/sails/push-notifications/addon.json +31 -31
  56. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
  57. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
  58. package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
  59. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
  60. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
  61. package/sails/push-notifications/install.ts +384 -384
  62. package/sails/r2-storage/addon.json +29 -29
  63. package/sails/r2-storage/files/backend/services/storage.ts +71 -71
  64. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
  65. package/sails/r2-storage/install.ts +412 -412
  66. package/sails/rate-limiting/addon.json +20 -20
  67. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
  68. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
  69. package/sails/rate-limiting/install.ts +300 -300
  70. package/sails/registry.json +107 -107
  71. package/sails/stripe/README.md +214 -214
  72. package/sails/stripe/addon.json +24 -24
  73. package/sails/stripe/files/backend/routes/stripe.ts +154 -154
  74. package/sails/stripe/files/backend/schema/stripe.ts +74 -74
  75. package/sails/stripe/files/backend/services/stripe.ts +224 -224
  76. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
  77. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
  78. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
  79. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
  80. package/sails/stripe/install.ts +378 -378
@@ -1,20 +1,20 @@
1
- {
2
- "name": "rate-limiting",
3
- "displayName": "API Rate Limiting",
4
- "description": "In-memory sliding window rate limiting middleware for API routes. No external dependencies required.",
5
- "version": "1.0.0",
6
- "compatibility": ">=1.0.0",
7
- "requiredEnvVars": [],
8
- "dependencies": {
9
- "backend": {},
10
- "frontend": {}
11
- },
12
- "modifies": {
13
- "backend": ["src/index.ts", "src/env.ts"],
14
- "frontend": []
15
- },
16
- "adds": {
17
- "backend": ["src/middleware/rate-limit.ts", "src/middleware/rate-limit-store.ts"],
18
- "frontend": []
19
- }
20
- }
1
+ {
2
+ "name": "rate-limiting",
3
+ "displayName": "API Rate Limiting",
4
+ "description": "In-memory sliding window rate limiting middleware for API routes. No external dependencies required.",
5
+ "version": "1.0.0",
6
+ "compatibility": ">=1.0.0",
7
+ "requiredEnvVars": [],
8
+ "dependencies": {
9
+ "backend": {},
10
+ "frontend": {}
11
+ },
12
+ "modifies": {
13
+ "backend": ["src/index.ts", "src/env.ts"],
14
+ "frontend": []
15
+ },
16
+ "adds": {
17
+ "backend": ["src/middleware/rate-limit.ts", "src/middleware/rate-limit-store.ts"],
18
+ "frontend": []
19
+ }
20
+ }
@@ -1,104 +1,104 @@
1
- /**
2
- * Rate Limit Store Abstraction
3
- *
4
- * Provides an interface and in-memory implementation for rate limit tracking.
5
- * This abstraction allows swapping to a Redis-backed store in production
6
- * without changing the middleware logic.
7
- */
8
-
9
- // ---------------------------------------------------------------------------
10
- // Interface
11
- // ---------------------------------------------------------------------------
12
-
13
- export interface RateLimitEntry {
14
- /** Number of requests made in the current window. */
15
- count: number;
16
- /** Timestamp (ms) when the current window resets. */
17
- resetAt: number;
18
- }
19
-
20
- export interface RateLimitStore {
21
- /**
22
- * Increment the request count for `key` within a window of `windowMs`.
23
- * If the key does not exist or the window has expired a new window is
24
- * started automatically.
25
- */
26
- increment(key: string, windowMs: number): Promise<RateLimitEntry>;
27
-
28
- /** Decrement the count for `key` (useful for undoing a counted request). */
29
- decrement(key: string): Promise<void>;
30
-
31
- /** Reset (delete) the entry for `key`. */
32
- reset(key: string): Promise<void>;
33
- }
34
-
35
- // ---------------------------------------------------------------------------
36
- // In-memory implementation
37
- // ---------------------------------------------------------------------------
38
-
39
- /**
40
- * Simple in-memory store backed by a `Map`.
41
- *
42
- * A periodic cleanup timer prunes expired entries every `cleanupIntervalMs`
43
- * (default 5 minutes) so the map does not grow unboundedly.
44
- */
45
- export class MemoryStore implements RateLimitStore {
46
- private store = new Map<string, RateLimitEntry>();
47
- private cleanupTimer: ReturnType<typeof setInterval> | null = null;
48
-
49
- constructor(cleanupIntervalMs = 5 * 60 * 1000) {
50
- this.cleanupTimer = setInterval(() => {
51
- this.prune();
52
- }, cleanupIntervalMs);
53
-
54
- // Allow the Node process to exit even if the timer is still active.
55
- if (this.cleanupTimer && typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
56
- this.cleanupTimer.unref();
57
- }
58
- }
59
-
60
- async increment(key: string, windowMs: number): Promise<RateLimitEntry> {
61
- const now = Date.now();
62
- const existing = this.store.get(key);
63
-
64
- if (existing && existing.resetAt > now) {
65
- // Window still active — increment.
66
- existing.count += 1;
67
- return { count: existing.count, resetAt: existing.resetAt };
68
- }
69
-
70
- // No entry or window expired — start a fresh window.
71
- const entry: RateLimitEntry = { count: 1, resetAt: now + windowMs };
72
- this.store.set(key, entry);
73
- return { count: 1, resetAt: entry.resetAt };
74
- }
75
-
76
- async decrement(key: string): Promise<void> {
77
- const entry = this.store.get(key);
78
- if (entry && entry.count > 0) {
79
- entry.count -= 1;
80
- }
81
- }
82
-
83
- async reset(key: string): Promise<void> {
84
- this.store.delete(key);
85
- }
86
-
87
- /** Remove all expired entries from the map. */
88
- private prune(): void {
89
- const now = Date.now();
90
- for (const [key, entry] of this.store) {
91
- if (entry.resetAt <= now) {
92
- this.store.delete(key);
93
- }
94
- }
95
- }
96
-
97
- /** Stop the cleanup timer (useful for tests / graceful shutdown). */
98
- destroy(): void {
99
- if (this.cleanupTimer) {
100
- clearInterval(this.cleanupTimer);
101
- this.cleanupTimer = null;
102
- }
103
- }
104
- }
1
+ /**
2
+ * Rate Limit Store Abstraction
3
+ *
4
+ * Provides an interface and in-memory implementation for rate limit tracking.
5
+ * This abstraction allows swapping to a Redis-backed store in production
6
+ * without changing the middleware logic.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Interface
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface RateLimitEntry {
14
+ /** Number of requests made in the current window. */
15
+ count: number;
16
+ /** Timestamp (ms) when the current window resets. */
17
+ resetAt: number;
18
+ }
19
+
20
+ export interface RateLimitStore {
21
+ /**
22
+ * Increment the request count for `key` within a window of `windowMs`.
23
+ * If the key does not exist or the window has expired a new window is
24
+ * started automatically.
25
+ */
26
+ increment(key: string, windowMs: number): Promise<RateLimitEntry>;
27
+
28
+ /** Decrement the count for `key` (useful for undoing a counted request). */
29
+ decrement(key: string): Promise<void>;
30
+
31
+ /** Reset (delete) the entry for `key`. */
32
+ reset(key: string): Promise<void>;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // In-memory implementation
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Simple in-memory store backed by a `Map`.
41
+ *
42
+ * A periodic cleanup timer prunes expired entries every `cleanupIntervalMs`
43
+ * (default 5 minutes) so the map does not grow unboundedly.
44
+ */
45
+ export class MemoryStore implements RateLimitStore {
46
+ private store = new Map<string, RateLimitEntry>();
47
+ private cleanupTimer: ReturnType<typeof setInterval> | null = null;
48
+
49
+ constructor(cleanupIntervalMs = 5 * 60 * 1000) {
50
+ this.cleanupTimer = setInterval(() => {
51
+ this.prune();
52
+ }, cleanupIntervalMs);
53
+
54
+ // Allow the Node process to exit even if the timer is still active.
55
+ if (this.cleanupTimer && typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
56
+ this.cleanupTimer.unref();
57
+ }
58
+ }
59
+
60
+ async increment(key: string, windowMs: number): Promise<RateLimitEntry> {
61
+ const now = Date.now();
62
+ const existing = this.store.get(key);
63
+
64
+ if (existing && existing.resetAt > now) {
65
+ // Window still active — increment.
66
+ existing.count += 1;
67
+ return { count: existing.count, resetAt: existing.resetAt };
68
+ }
69
+
70
+ // No entry or window expired — start a fresh window.
71
+ const entry: RateLimitEntry = { count: 1, resetAt: now + windowMs };
72
+ this.store.set(key, entry);
73
+ return { count: 1, resetAt: entry.resetAt };
74
+ }
75
+
76
+ async decrement(key: string): Promise<void> {
77
+ const entry = this.store.get(key);
78
+ if (entry && entry.count > 0) {
79
+ entry.count -= 1;
80
+ }
81
+ }
82
+
83
+ async reset(key: string): Promise<void> {
84
+ this.store.delete(key);
85
+ }
86
+
87
+ /** Remove all expired entries from the map. */
88
+ private prune(): void {
89
+ const now = Date.now();
90
+ for (const [key, entry] of this.store) {
91
+ if (entry.resetAt <= now) {
92
+ this.store.delete(key);
93
+ }
94
+ }
95
+ }
96
+
97
+ /** Stop the cleanup timer (useful for tests / graceful shutdown). */
98
+ destroy(): void {
99
+ if (this.cleanupTimer) {
100
+ clearInterval(this.cleanupTimer);
101
+ this.cleanupTimer = null;
102
+ }
103
+ }
104
+ }
@@ -1,137 +1,137 @@
1
- /**
2
- * Sliding-window rate limiting middleware for Express.
3
- *
4
- * Uses an in-memory store by default (no Redis required). You can swap in any
5
- * implementation of `RateLimitStore` for distributed deployments.
6
- *
7
- * Usage:
8
- * import { apiLimiter, authLimiter, createRateLimiter } from "./middleware/rate-limit.js";
9
- *
10
- * app.use("/api", apiLimiter); // 100 req / 15 min
11
- * app.use("/api/auth", authLimiter); // 10 req / 15 min
12
- *
13
- * // Custom:
14
- * app.use("/api/special", createRateLimiter({ windowMs: 60_000, maxRequests: 5 }));
15
- */
16
-
17
- import type { Request, Response, NextFunction } from "express";
18
- import { MemoryStore } from "./rate-limit-store.js";
19
- import type { RateLimitStore } from "./rate-limit-store.js";
20
-
21
- // ---------------------------------------------------------------------------
22
- // Types
23
- // ---------------------------------------------------------------------------
24
-
25
- export interface RateLimitOptions {
26
- /** Time window in milliseconds. Default: 15 minutes. */
27
- windowMs?: number;
28
- /** Maximum number of requests allowed in the window. Default: 100. */
29
- maxRequests?: number;
30
- /** Extract the key used to identify the client. Defaults to IP, or userId when authenticated. */
31
- keyGenerator?: (req: Request) => string;
32
- /** Store implementation. Defaults to `MemoryStore`. */
33
- store?: RateLimitStore;
34
- /** Custom message returned when the limit is exceeded. */
35
- message?: string;
36
- }
37
-
38
- // ---------------------------------------------------------------------------
39
- // Defaults from environment (optional overrides)
40
- // ---------------------------------------------------------------------------
41
-
42
- const DEFAULT_WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000;
43
- const DEFAULT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_MAX_REQUESTS) || 100;
44
-
45
- // ---------------------------------------------------------------------------
46
- // Shared store — one MemoryStore instance for the entire process
47
- // ---------------------------------------------------------------------------
48
-
49
- const sharedStore = new MemoryStore();
50
-
51
- // ---------------------------------------------------------------------------
52
- // Key generator
53
- // ---------------------------------------------------------------------------
54
-
55
- function defaultKeyGenerator(req: Request): string {
56
- // Prefer the authenticated user id when available so that rate limits are
57
- // per-user rather than per-IP for logged-in users.
58
- const userId = (req as Record<string, unknown>).user
59
- ? ((req as Record<string, unknown>).user as { id?: string })?.id
60
- : undefined;
61
-
62
- if (userId) return `user:${userId}`;
63
-
64
- // Fall back to IP address.
65
- const forwarded = req.headers["x-forwarded-for"];
66
- const ip =
67
- typeof forwarded === "string"
68
- ? forwarded.split(",")[0].trim()
69
- : req.socket.remoteAddress ?? "unknown";
70
-
71
- return `ip:${ip}`;
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // Factory
76
- // ---------------------------------------------------------------------------
77
-
78
- /**
79
- * Create a rate-limiting middleware with the given options.
80
- */
81
- export function createRateLimiter(options: RateLimitOptions = {}) {
82
- const {
83
- windowMs = DEFAULT_WINDOW_MS,
84
- maxRequests = DEFAULT_MAX_REQUESTS,
85
- keyGenerator = defaultKeyGenerator,
86
- store = sharedStore,
87
- message = "Too many requests, please try again later.",
88
- } = options;
89
-
90
- return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
91
- const key = keyGenerator(req);
92
-
93
- try {
94
- const { count, resetAt } = await store.increment(key, windowMs);
95
-
96
- // Always set informational headers.
97
- res.setHeader("X-RateLimit-Limit", String(maxRequests));
98
- res.setHeader("X-RateLimit-Remaining", String(Math.max(0, maxRequests - count)));
99
- res.setHeader("X-RateLimit-Reset", String(Math.ceil(resetAt / 1000)));
100
-
101
- if (count > maxRequests) {
102
- const retryAfterSeconds = Math.ceil((resetAt - Date.now()) / 1000);
103
- res.setHeader("Retry-After", String(retryAfterSeconds));
104
- res.status(429).json({ error: message });
105
- return;
106
- }
107
-
108
- next();
109
- } catch (err) {
110
- // If the store fails we let the request through rather than blocking.
111
- console.error("[rate-limit] Store error:", err);
112
- next();
113
- }
114
- };
115
- }
116
-
117
- // ---------------------------------------------------------------------------
118
- // Preset limiters
119
- // ---------------------------------------------------------------------------
120
-
121
- /** General API limiter — 100 requests per 15 minutes. */
122
- export const apiLimiter = createRateLimiter({
123
- windowMs: DEFAULT_WINDOW_MS,
124
- maxRequests: DEFAULT_MAX_REQUESTS,
125
- });
126
-
127
- /** Auth limiter — 10 requests per 15 minutes (login, signup). */
128
- export const authLimiter = createRateLimiter({
129
- windowMs: 15 * 60 * 1000,
130
- maxRequests: 10,
131
- });
132
-
133
- /** Strict limiter — 5 requests per 15 minutes (password reset, sensitive ops). */
134
- export const strictLimiter = createRateLimiter({
135
- windowMs: 15 * 60 * 1000,
136
- maxRequests: 5,
137
- });
1
+ /**
2
+ * Sliding-window rate limiting middleware for Express.
3
+ *
4
+ * Uses an in-memory store by default (no Redis required). You can swap in any
5
+ * implementation of `RateLimitStore` for distributed deployments.
6
+ *
7
+ * Usage:
8
+ * import { apiLimiter, authLimiter, createRateLimiter } from "./middleware/rate-limit.js";
9
+ *
10
+ * app.use("/api", apiLimiter); // 100 req / 15 min
11
+ * app.use("/api/auth", authLimiter); // 10 req / 15 min
12
+ *
13
+ * // Custom:
14
+ * app.use("/api/special", createRateLimiter({ windowMs: 60_000, maxRequests: 5 }));
15
+ */
16
+
17
+ import type { Request, Response, NextFunction } from "express";
18
+ import { MemoryStore } from "./rate-limit-store.js";
19
+ import type { RateLimitStore } from "./rate-limit-store.js";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface RateLimitOptions {
26
+ /** Time window in milliseconds. Default: 15 minutes. */
27
+ windowMs?: number;
28
+ /** Maximum number of requests allowed in the window. Default: 100. */
29
+ maxRequests?: number;
30
+ /** Extract the key used to identify the client. Defaults to IP, or userId when authenticated. */
31
+ keyGenerator?: (req: Request) => string;
32
+ /** Store implementation. Defaults to `MemoryStore`. */
33
+ store?: RateLimitStore;
34
+ /** Custom message returned when the limit is exceeded. */
35
+ message?: string;
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Defaults from environment (optional overrides)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const DEFAULT_WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000;
43
+ const DEFAULT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_MAX_REQUESTS) || 100;
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Shared store — one MemoryStore instance for the entire process
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const sharedStore = new MemoryStore();
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Key generator
53
+ // ---------------------------------------------------------------------------
54
+
55
+ function defaultKeyGenerator(req: Request): string {
56
+ // Prefer the authenticated user id when available so that rate limits are
57
+ // per-user rather than per-IP for logged-in users.
58
+ const userId = (req as Record<string, unknown>).user
59
+ ? ((req as Record<string, unknown>).user as { id?: string })?.id
60
+ : undefined;
61
+
62
+ if (userId) return `user:${userId}`;
63
+
64
+ // Fall back to IP address.
65
+ const forwarded = req.headers["x-forwarded-for"];
66
+ const ip =
67
+ typeof forwarded === "string"
68
+ ? forwarded.split(",")[0].trim()
69
+ : req.socket.remoteAddress ?? "unknown";
70
+
71
+ return `ip:${ip}`;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Factory
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /**
79
+ * Create a rate-limiting middleware with the given options.
80
+ */
81
+ export function createRateLimiter(options: RateLimitOptions = {}) {
82
+ const {
83
+ windowMs = DEFAULT_WINDOW_MS,
84
+ maxRequests = DEFAULT_MAX_REQUESTS,
85
+ keyGenerator = defaultKeyGenerator,
86
+ store = sharedStore,
87
+ message = "Too many requests, please try again later.",
88
+ } = options;
89
+
90
+ return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
91
+ const key = keyGenerator(req);
92
+
93
+ try {
94
+ const { count, resetAt } = await store.increment(key, windowMs);
95
+
96
+ // Always set informational headers.
97
+ res.setHeader("X-RateLimit-Limit", String(maxRequests));
98
+ res.setHeader("X-RateLimit-Remaining", String(Math.max(0, maxRequests - count)));
99
+ res.setHeader("X-RateLimit-Reset", String(Math.ceil(resetAt / 1000)));
100
+
101
+ if (count > maxRequests) {
102
+ const retryAfterSeconds = Math.ceil((resetAt - Date.now()) / 1000);
103
+ res.setHeader("Retry-After", String(retryAfterSeconds));
104
+ res.status(429).json({ error: message });
105
+ return;
106
+ }
107
+
108
+ next();
109
+ } catch (err) {
110
+ // If the store fails we let the request through rather than blocking.
111
+ console.error("[rate-limit] Store error:", err);
112
+ next();
113
+ }
114
+ };
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Preset limiters
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /** General API limiter — 100 requests per 15 minutes. */
122
+ export const apiLimiter = createRateLimiter({
123
+ windowMs: DEFAULT_WINDOW_MS,
124
+ maxRequests: DEFAULT_MAX_REQUESTS,
125
+ });
126
+
127
+ /** Auth limiter — 10 requests per 15 minutes (login, signup). */
128
+ export const authLimiter = createRateLimiter({
129
+ windowMs: 15 * 60 * 1000,
130
+ maxRequests: 10,
131
+ });
132
+
133
+ /** Strict limiter — 5 requests per 15 minutes (password reset, sensitive ops). */
134
+ export const strictLimiter = createRateLimiter({
135
+ windowMs: 15 * 60 * 1000,
136
+ maxRequests: 5,
137
+ });