@codaijs/keel 0.1.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/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/cli.test.js +173 -0
- package/dist/__tests__/cli.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +86 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/sail-installer.test.d.ts +2 -0
- package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
- package/dist/__tests__/sail-installer.test.js +158 -0
- package/dist/__tests__/sail-installer.test.js.map +1 -0
- package/dist/create-runner.d.ts +11 -0
- package/dist/create-runner.d.ts.map +1 -0
- package/dist/create-runner.js +63 -0
- package/dist/create-runner.js.map +1 -0
- package/dist/create.d.ts +10 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +15 -0
- package/dist/create.js.map +1 -0
- package/dist/manage.d.ts +24 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +1461 -0
- package/dist/manage.js.map +1 -0
- package/dist/prompts.d.ts +36 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +208 -0
- package/dist/prompts.js.map +1 -0
- package/dist/sail-installer.d.ts +37 -0
- package/dist/sail-installer.d.ts.map +1 -0
- package/dist/sail-installer.js +935 -0
- package/dist/sail-installer.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +297 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +57 -0
- package/sails/_template/addon.json +20 -0
- package/sails/_template/install.ts +402 -0
- package/sails/admin-dashboard/README.md +117 -0
- package/sails/admin-dashboard/addon.json +28 -0
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
- package/sails/admin-dashboard/install.ts +305 -0
- package/sails/analytics/README.md +178 -0
- package/sails/analytics/addon.json +27 -0
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
- package/sails/analytics/install.ts +297 -0
- package/sails/file-uploads/README.md +191 -0
- package/sails/file-uploads/addon.json +30 -0
- package/sails/file-uploads/files/backend/routes/files.ts +198 -0
- package/sails/file-uploads/files/backend/schema/files.ts +36 -0
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
- package/sails/file-uploads/install.ts +466 -0
- package/sails/gdpr/README.md +174 -0
- package/sails/gdpr/addon.json +27 -0
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
- package/sails/gdpr/install.ts +756 -0
- package/sails/google-oauth/README.md +121 -0
- package/sails/google-oauth/addon.json +22 -0
- package/sails/google-oauth/files/GoogleButton.tsx +50 -0
- package/sails/google-oauth/install.ts +252 -0
- package/sails/i18n/README.md +193 -0
- package/sails/i18n/addon.json +30 -0
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
- package/sails/i18n/files/frontend/locales/de/common.json +44 -0
- package/sails/i18n/files/frontend/locales/en/common.json +44 -0
- package/sails/i18n/install.ts +407 -0
- package/sails/push-notifications/README.md +163 -0
- package/sails/push-notifications/addon.json +31 -0
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
- package/sails/push-notifications/install.ts +384 -0
- package/sails/r2-storage/README.md +101 -0
- package/sails/r2-storage/addon.json +29 -0
- package/sails/r2-storage/files/backend/services/storage.ts +71 -0
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
- package/sails/r2-storage/install.ts +412 -0
- package/sails/rate-limiting/README.md +145 -0
- package/sails/rate-limiting/addon.json +20 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
- package/sails/rate-limiting/install.ts +300 -0
- package/sails/registry.json +107 -0
- package/sails/stripe/README.md +214 -0
- package/sails/stripe/addon.json +24 -0
- package/sails/stripe/files/backend/routes/stripe.ts +154 -0
- package/sails/stripe/files/backend/schema/stripe.ts +74 -0
- package/sails/stripe/files/backend/services/stripe.ts +224 -0
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
- package/sails/stripe/install.ts +378 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# API Rate Limiting Sail
|
|
2
|
+
|
|
3
|
+
In-memory sliding window rate limiting middleware for Express API routes. No external dependencies or Redis required.
|
|
4
|
+
|
|
5
|
+
## What this sail adds
|
|
6
|
+
|
|
7
|
+
### Backend
|
|
8
|
+
- **`src/middleware/rate-limit.ts`** -- Rate limiting middleware factory with preset configurations:
|
|
9
|
+
- `apiLimiter` -- 100 requests per 15 minutes (general API routes)
|
|
10
|
+
- `authLimiter` -- 10 requests per 15 minutes (login, signup)
|
|
11
|
+
- `strictLimiter` -- 5 requests per 15 minutes (password reset, sensitive operations)
|
|
12
|
+
- `createRateLimiter(options)` -- factory for custom configurations
|
|
13
|
+
- **`src/middleware/rate-limit-store.ts`** -- Store abstraction with in-memory implementation:
|
|
14
|
+
- `RateLimitStore` interface for swapping storage backends
|
|
15
|
+
- `MemoryStore` with automatic cleanup of expired entries
|
|
16
|
+
|
|
17
|
+
### How it works
|
|
18
|
+
|
|
19
|
+
The middleware uses a **sliding window** algorithm:
|
|
20
|
+
|
|
21
|
+
1. Each client is identified by their authenticated user ID or IP address
|
|
22
|
+
2. Requests are counted within a configurable time window
|
|
23
|
+
3. When the limit is exceeded the server responds with `429 Too Many Requests`
|
|
24
|
+
4. Standard rate limit headers are set on every response:
|
|
25
|
+
- `X-RateLimit-Limit` -- maximum requests allowed
|
|
26
|
+
- `X-RateLimit-Remaining` -- requests remaining in the current window
|
|
27
|
+
- `X-RateLimit-Reset` -- Unix timestamp when the window resets
|
|
28
|
+
- `Retry-After` -- seconds until the client can retry (only on 429)
|
|
29
|
+
|
|
30
|
+
### Environment variables (optional)
|
|
31
|
+
|
|
32
|
+
| Variable | Description | Default |
|
|
33
|
+
|----------|-------------|---------|
|
|
34
|
+
| `RATE_LIMIT_WINDOW_MS` | Window duration in milliseconds | `900000` (15 min) |
|
|
35
|
+
| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | `100` |
|
|
36
|
+
|
|
37
|
+
These environment variables override the defaults for `apiLimiter`. The preset `authLimiter` and `strictLimiter` always use their own hardcoded values.
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
### Run the installer
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx tsx cli/sails/rate-limiting/install.ts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or use the CLI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @codaijs/keel sail add rate-limiting
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The installer will:
|
|
54
|
+
1. Copy the middleware files into your backend
|
|
55
|
+
2. Apply the rate limiter to your chosen routes
|
|
56
|
+
3. Optionally add environment variables for custom defaults
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### Apply globally
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { apiLimiter } from "./middleware/rate-limit.js";
|
|
64
|
+
|
|
65
|
+
app.use("/api", apiLimiter);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Apply to specific routes
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { authLimiter, strictLimiter } from "./middleware/rate-limit.js";
|
|
72
|
+
|
|
73
|
+
app.use("/api/auth/login", authLimiter);
|
|
74
|
+
app.use("/api/auth/signup", authLimiter);
|
|
75
|
+
app.use("/api/auth/reset-password", strictLimiter);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Create a custom limiter
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createRateLimiter } from "./middleware/rate-limit.js";
|
|
82
|
+
|
|
83
|
+
const uploadLimiter = createRateLimiter({
|
|
84
|
+
windowMs: 60 * 60 * 1000, // 1 hour
|
|
85
|
+
maxRequests: 20,
|
|
86
|
+
message: "Upload limit exceeded. Try again later.",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
app.use("/api/uploads", uploadLimiter);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Custom key generator
|
|
93
|
+
|
|
94
|
+
By default clients are identified by user ID (if authenticated) or IP address. You can provide a custom key generator:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const limiter = createRateLimiter({
|
|
98
|
+
keyGenerator: (req) => req.headers["x-api-key"] as string ?? req.ip,
|
|
99
|
+
maxRequests: 1000,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Scaling to production
|
|
104
|
+
|
|
105
|
+
The default `MemoryStore` works well for single-process deployments. For multi-process or distributed environments, implement the `RateLimitStore` interface backed by Redis:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import type { RateLimitStore, RateLimitEntry } from "./middleware/rate-limit-store.js";
|
|
109
|
+
|
|
110
|
+
class RedisStore implements RateLimitStore {
|
|
111
|
+
constructor(private redis: RedisClient) {}
|
|
112
|
+
|
|
113
|
+
async increment(key: string, windowMs: number): Promise<RateLimitEntry> {
|
|
114
|
+
const rKey = `rl:${key}`;
|
|
115
|
+
const count = await this.redis.incr(rKey);
|
|
116
|
+
if (count === 1) {
|
|
117
|
+
await this.redis.pexpire(rKey, windowMs);
|
|
118
|
+
}
|
|
119
|
+
const ttl = await this.redis.pttl(rKey);
|
|
120
|
+
return { count, resetAt: Date.now() + ttl };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async decrement(key: string): Promise<void> {
|
|
124
|
+
await this.redis.decr(`rl:${key}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async reset(key: string): Promise<void> {
|
|
128
|
+
await this.redis.del(`rl:${key}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then pass it when creating your limiter:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const limiter = createRateLimiter({
|
|
137
|
+
store: new RedisStore(redis),
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Troubleshooting
|
|
142
|
+
|
|
143
|
+
- **All requests are getting 429**: Check that the `maxRequests` value is appropriate for your traffic. Authenticated users are tracked by user ID, so shared IPs (like offices) should not cause issues for logged-in users.
|
|
144
|
+
- **Rate limits not working behind a proxy**: Make sure your Express app trusts the proxy so that `X-Forwarded-For` is parsed correctly: `app.set("trust proxy", 1)`.
|
|
145
|
+
- **Memory growing**: The `MemoryStore` prunes expired entries every 5 minutes. If you have very high traffic, consider switching to a Redis store.
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
});
|