ship-safe 1.0.1 → 3.0.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/README.md +281 -23
- package/ai-defense/cost-protection.md +292 -0
- package/ai-defense/llm-security-checklist.md +324 -0
- package/ai-defense/prompt-injection-patterns.js +283 -0
- package/cli/bin/ship-safe.js +44 -2
- package/cli/commands/fix.js +216 -0
- package/cli/commands/guard.js +297 -0
- package/cli/commands/mcp.js +303 -0
- package/cli/commands/scan.js +231 -39
- package/cli/utils/entropy.js +126 -0
- package/cli/utils/output.js +10 -1
- package/cli/utils/patterns.js +376 -24
- package/configs/firebase/firestore-rules.txt +215 -0
- package/configs/firebase/security-checklist.md +236 -0
- package/configs/firebase/storage-rules.txt +206 -0
- package/configs/ship-safeignore-template +50 -0
- package/configs/supabase/rls-templates.sql +242 -0
- package/configs/supabase/secure-client.ts +225 -0
- package/configs/supabase/security-checklist.md +278 -0
- package/package.json +11 -2
- package/snippets/README.md +89 -25
- package/snippets/api-security/api-security-checklist.md +412 -0
- package/snippets/api-security/cors-config.ts +322 -0
- package/snippets/api-security/input-validation.ts +430 -0
- package/snippets/auth/jwt-checklist.md +322 -0
- package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
- package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# JWT Security Checklist
|
|
2
|
+
|
|
3
|
+
**Secure your JWT implementation before launch.**
|
|
4
|
+
|
|
5
|
+
Based on [JWT Best Practices 2025](https://jwt.app/blog/jwt-best-practices/) and OWASP guidelines.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Critical: Algorithm & Signing
|
|
10
|
+
|
|
11
|
+
### 1. [ ] Using secure algorithm (not HS256 in production)
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// BAD: HS256 with weak secret
|
|
15
|
+
jwt.sign(payload, 'my-secret', { algorithm: 'HS256' });
|
|
16
|
+
|
|
17
|
+
// GOOD: RS256 (asymmetric) for production
|
|
18
|
+
jwt.sign(payload, privateKey, { algorithm: 'RS256' });
|
|
19
|
+
|
|
20
|
+
// GOOD: ES256 (elliptic curve) - smaller keys, same security
|
|
21
|
+
jwt.sign(payload, privateKey, { algorithm: 'ES256' });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Why:** HS256 secrets can be brute-forced. RS256/ES256 use public/private key pairs.
|
|
25
|
+
|
|
26
|
+
### 2. [ ] Algorithm specified in verification (not "auto")
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// BAD: Accepts any algorithm (algorithm confusion attack)
|
|
30
|
+
jwt.verify(token, key);
|
|
31
|
+
|
|
32
|
+
// GOOD: Explicitly specify allowed algorithms
|
|
33
|
+
jwt.verify(token, key, { algorithms: ['RS256'] });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. [ ] Strong secret/key used
|
|
37
|
+
|
|
38
|
+
For HS256 (if you must use it):
|
|
39
|
+
- [ ] At least 256 bits (32 characters)
|
|
40
|
+
- [ ] Random, not dictionary words
|
|
41
|
+
- [ ] Stored in environment variable
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Generate a strong secret
|
|
45
|
+
openssl rand -base64 32
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Critical: Token Lifetime
|
|
51
|
+
|
|
52
|
+
### 4. [ ] Access tokens are short-lived
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// GOOD: 15 minutes for access tokens
|
|
56
|
+
jwt.sign(payload, key, { expiresIn: '15m' });
|
|
57
|
+
|
|
58
|
+
// BAD: Long-lived access tokens
|
|
59
|
+
jwt.sign(payload, key, { expiresIn: '30d' }); // Too long!
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Recommended lifetimes:**
|
|
63
|
+
- Access tokens: 15-60 minutes
|
|
64
|
+
- Refresh tokens: 7-30 days
|
|
65
|
+
- Remember me: 30-90 days (with re-auth for sensitive actions)
|
|
66
|
+
|
|
67
|
+
### 5. [ ] Expiration claim (exp) always set
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Always verify expiration
|
|
71
|
+
jwt.verify(token, key, {
|
|
72
|
+
algorithms: ['RS256'],
|
|
73
|
+
clockTolerance: 30, // 30 seconds tolerance for clock skew
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 6. [ ] Refresh token rotation implemented
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// On each refresh:
|
|
81
|
+
// 1. Invalidate old refresh token
|
|
82
|
+
// 2. Issue new access token
|
|
83
|
+
// 3. Issue new refresh token
|
|
84
|
+
|
|
85
|
+
async function refreshTokens(oldRefreshToken: string) {
|
|
86
|
+
// Verify and invalidate old token
|
|
87
|
+
const payload = await verifyAndInvalidateRefreshToken(oldRefreshToken);
|
|
88
|
+
|
|
89
|
+
// Generate new tokens
|
|
90
|
+
const accessToken = generateAccessToken(payload.userId);
|
|
91
|
+
const refreshToken = generateRefreshToken(payload.userId);
|
|
92
|
+
|
|
93
|
+
return { accessToken, refreshToken };
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## High: Storage & Transmission
|
|
100
|
+
|
|
101
|
+
### 7. [ ] Tokens stored securely
|
|
102
|
+
|
|
103
|
+
| Storage | Access Token | Refresh Token |
|
|
104
|
+
|---------|--------------|---------------|
|
|
105
|
+
| **httpOnly cookie** | Best | Best |
|
|
106
|
+
| **Memory (JS variable)** | OK | No |
|
|
107
|
+
| **sessionStorage** | OK (temporary) | No |
|
|
108
|
+
| **localStorage** | Avoid | Never |
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// GOOD: httpOnly cookie (not accessible via JavaScript)
|
|
112
|
+
res.cookie('accessToken', token, {
|
|
113
|
+
httpOnly: true,
|
|
114
|
+
secure: true, // HTTPS only
|
|
115
|
+
sameSite: 'strict', // CSRF protection
|
|
116
|
+
maxAge: 15 * 60 * 1000, // 15 minutes
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// BAD: localStorage (vulnerable to XSS)
|
|
120
|
+
localStorage.setItem('accessToken', token);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 8. [ ] Secure flag set (HTTPS only)
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
res.cookie('token', value, {
|
|
127
|
+
secure: process.env.NODE_ENV === 'production',
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 9. [ ] SameSite attribute configured
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
res.cookie('token', value, {
|
|
135
|
+
sameSite: 'strict', // Prevents CSRF
|
|
136
|
+
// Or 'lax' if you need cross-site GET requests
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## High: Validation
|
|
143
|
+
|
|
144
|
+
### 10. [ ] All claims validated
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
jwt.verify(token, key, {
|
|
148
|
+
algorithms: ['RS256'], // Algorithm
|
|
149
|
+
issuer: 'https://myapp.com', // Who issued it
|
|
150
|
+
audience: 'https://api.myapp.com', // Who it's for
|
|
151
|
+
clockTolerance: 30, // Clock skew tolerance
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 11. [ ] Issuer (iss) validated
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Prevent tokens from other services being accepted
|
|
159
|
+
if (payload.iss !== 'https://myapp.com') {
|
|
160
|
+
throw new Error('Invalid issuer');
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 12. [ ] Audience (aud) validated
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// Prevent tokens meant for other services
|
|
168
|
+
if (payload.aud !== 'https://api.myapp.com') {
|
|
169
|
+
throw new Error('Invalid audience');
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Medium: Token Revocation
|
|
176
|
+
|
|
177
|
+
### 13. [ ] Token revocation mechanism exists
|
|
178
|
+
|
|
179
|
+
JWTs can't be invalidated by default. Implement one of:
|
|
180
|
+
|
|
181
|
+
**Option A: Short expiry + refresh tokens**
|
|
182
|
+
- Access tokens are short-lived (15 min)
|
|
183
|
+
- Revoke refresh tokens to force re-authentication
|
|
184
|
+
|
|
185
|
+
**Option B: Token blacklist/denylist**
|
|
186
|
+
```typescript
|
|
187
|
+
const revokedTokens = new Set();
|
|
188
|
+
|
|
189
|
+
function verifyToken(token) {
|
|
190
|
+
const payload = jwt.verify(token, key);
|
|
191
|
+
if (revokedTokens.has(payload.jti)) {
|
|
192
|
+
throw new Error('Token revoked');
|
|
193
|
+
}
|
|
194
|
+
return payload;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Option C: Token versioning**
|
|
199
|
+
```typescript
|
|
200
|
+
// Store token version in database
|
|
201
|
+
// Increment on password change/logout
|
|
202
|
+
if (payload.tokenVersion !== user.tokenVersion) {
|
|
203
|
+
throw new Error('Token invalidated');
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### 14. [ ] Logout invalidates tokens
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
async function logout(userId: string) {
|
|
211
|
+
// Increment token version to invalidate all tokens
|
|
212
|
+
await db.user.update({
|
|
213
|
+
where: { id: userId },
|
|
214
|
+
data: { tokenVersion: { increment: 1 } },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Clear refresh tokens
|
|
218
|
+
await db.refreshToken.deleteMany({
|
|
219
|
+
where: { userId },
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Medium: Payload Security
|
|
227
|
+
|
|
228
|
+
### 15. [ ] No sensitive data in payload
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// BAD: Sensitive data in JWT (readable by anyone with the token)
|
|
232
|
+
{
|
|
233
|
+
sub: 'user123',
|
|
234
|
+
email: 'user@example.com',
|
|
235
|
+
passwordHash: '...', // NEVER store passwords/hashes!
|
|
236
|
+
ssn: '123-45-6789', // NEVER!
|
|
237
|
+
creditCard: '4111...', // NEVER!
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// GOOD: Minimal payload
|
|
241
|
+
{
|
|
242
|
+
sub: 'user123',
|
|
243
|
+
role: 'user',
|
|
244
|
+
iat: 1234567890,
|
|
245
|
+
exp: 1234568790,
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 16. [ ] JTI (JWT ID) included for tracking
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { v4 as uuid } from 'uuid';
|
|
253
|
+
|
|
254
|
+
const token = jwt.sign({
|
|
255
|
+
sub: userId,
|
|
256
|
+
jti: uuid(), // Unique token ID
|
|
257
|
+
}, key);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Code Examples
|
|
263
|
+
|
|
264
|
+
### Complete JWT Service
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import jwt from 'jsonwebtoken';
|
|
268
|
+
import { randomUUID } from 'crypto';
|
|
269
|
+
|
|
270
|
+
const ACCESS_TOKEN_EXPIRY = '15m';
|
|
271
|
+
const REFRESH_TOKEN_EXPIRY = '7d';
|
|
272
|
+
|
|
273
|
+
interface TokenPayload {
|
|
274
|
+
sub: string;
|
|
275
|
+
role: string;
|
|
276
|
+
jti: string;
|
|
277
|
+
iat: number;
|
|
278
|
+
exp: number;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function generateAccessToken(userId: string, role: string): string {
|
|
282
|
+
return jwt.sign(
|
|
283
|
+
{
|
|
284
|
+
sub: userId,
|
|
285
|
+
role,
|
|
286
|
+
jti: randomUUID(),
|
|
287
|
+
},
|
|
288
|
+
process.env.JWT_PRIVATE_KEY!,
|
|
289
|
+
{
|
|
290
|
+
algorithm: 'RS256',
|
|
291
|
+
expiresIn: ACCESS_TOKEN_EXPIRY,
|
|
292
|
+
issuer: process.env.JWT_ISSUER,
|
|
293
|
+
audience: process.env.JWT_AUDIENCE,
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function verifyAccessToken(token: string): TokenPayload {
|
|
299
|
+
return jwt.verify(token, process.env.JWT_PUBLIC_KEY!, {
|
|
300
|
+
algorithms: ['RS256'],
|
|
301
|
+
issuer: process.env.JWT_ISSUER,
|
|
302
|
+
audience: process.env.JWT_AUDIENCE,
|
|
303
|
+
clockTolerance: 30,
|
|
304
|
+
}) as TokenPayload;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Tools
|
|
311
|
+
|
|
312
|
+
- **jwt.io** - Decode and inspect JWTs
|
|
313
|
+
- **jwt-cli** - Command line JWT tool
|
|
314
|
+
- **ship-safe** - Scan for leaked tokens in code
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
npx ship-safe scan .
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
**Remember: JWTs are not encrypted by default. Anyone with the token can read the payload. Only the signature prevents tampering.**
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js Middleware Rate Limiting
|
|
3
|
+
* =================================
|
|
4
|
+
*
|
|
5
|
+
* Rate limiting at the edge using Next.js middleware.
|
|
6
|
+
* No external dependencies - uses in-memory store (resets on deploy).
|
|
7
|
+
*
|
|
8
|
+
* WHY USE MIDDLEWARE:
|
|
9
|
+
* - Blocks requests before they hit your API
|
|
10
|
+
* - Runs at the edge (faster)
|
|
11
|
+
* - Protects all routes with one file
|
|
12
|
+
*
|
|
13
|
+
* LIMITATIONS:
|
|
14
|
+
* - In-memory store resets on deploy/restart
|
|
15
|
+
* - Doesn't sync across serverless instances
|
|
16
|
+
* - For production, use Upstash Redis instead
|
|
17
|
+
*
|
|
18
|
+
* USAGE:
|
|
19
|
+
* Copy this to middleware.ts in your Next.js project root.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { NextResponse } from 'next/server';
|
|
23
|
+
import type { NextRequest } from 'next/server';
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// CONFIGURATION
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
const RATE_LIMIT_CONFIG = {
|
|
30
|
+
// Requests per window
|
|
31
|
+
maxRequests: 60,
|
|
32
|
+
// Window size in seconds
|
|
33
|
+
windowSizeSeconds: 60,
|
|
34
|
+
// Paths to rate limit (regex patterns)
|
|
35
|
+
protectedPaths: [
|
|
36
|
+
/^\/api\//, // All API routes
|
|
37
|
+
],
|
|
38
|
+
// Stricter limits for sensitive paths
|
|
39
|
+
strictPaths: [
|
|
40
|
+
{ pattern: /^\/api\/auth\//, maxRequests: 5 },
|
|
41
|
+
{ pattern: /^\/api\/ai\//, maxRequests: 10 },
|
|
42
|
+
],
|
|
43
|
+
// Paths to exclude from rate limiting
|
|
44
|
+
excludedPaths: [
|
|
45
|
+
/^\/api\/health/, // Health checks
|
|
46
|
+
/^\/_next\//, // Next.js internals
|
|
47
|
+
/^\/favicon\.ico/,
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// IN-MEMORY RATE LIMIT STORE
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
interface RateLimitEntry {
|
|
56
|
+
count: number;
|
|
57
|
+
resetTime: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Simple in-memory store (use Redis for production)
|
|
61
|
+
const rateLimitStore = new Map<string, RateLimitEntry>();
|
|
62
|
+
|
|
63
|
+
// Clean up old entries periodically
|
|
64
|
+
setInterval(() => {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
for (const [key, entry] of rateLimitStore.entries()) {
|
|
67
|
+
if (entry.resetTime < now) {
|
|
68
|
+
rateLimitStore.delete(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, 60000); // Clean every minute
|
|
72
|
+
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// RATE LIMIT LOGIC
|
|
75
|
+
// =============================================================================
|
|
76
|
+
|
|
77
|
+
function getClientIP(request: NextRequest): string {
|
|
78
|
+
// Check various headers for the real IP
|
|
79
|
+
const forwarded = request.headers.get('x-forwarded-for');
|
|
80
|
+
if (forwarded) {
|
|
81
|
+
return forwarded.split(',')[0].trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const realIP = request.headers.get('x-real-ip');
|
|
85
|
+
if (realIP) {
|
|
86
|
+
return realIP;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fallback
|
|
90
|
+
return 'unknown';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function checkRateLimit(
|
|
94
|
+
ip: string,
|
|
95
|
+
maxRequests: number,
|
|
96
|
+
windowSeconds: number
|
|
97
|
+
): { allowed: boolean; remaining: number; resetTime: number } {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const key = `ratelimit:${ip}`;
|
|
100
|
+
const windowMs = windowSeconds * 1000;
|
|
101
|
+
|
|
102
|
+
let entry = rateLimitStore.get(key);
|
|
103
|
+
|
|
104
|
+
// Create new entry if doesn't exist or window expired
|
|
105
|
+
if (!entry || entry.resetTime < now) {
|
|
106
|
+
entry = {
|
|
107
|
+
count: 0,
|
|
108
|
+
resetTime: now + windowMs,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
entry.count++;
|
|
113
|
+
rateLimitStore.set(key, entry);
|
|
114
|
+
|
|
115
|
+
const remaining = Math.max(0, maxRequests - entry.count);
|
|
116
|
+
const allowed = entry.count <= maxRequests;
|
|
117
|
+
|
|
118
|
+
return { allowed, remaining, resetTime: entry.resetTime };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getMaxRequests(pathname: string): number {
|
|
122
|
+
// Check strict paths first
|
|
123
|
+
for (const strict of RATE_LIMIT_CONFIG.strictPaths) {
|
|
124
|
+
if (strict.pattern.test(pathname)) {
|
|
125
|
+
return strict.maxRequests;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return RATE_LIMIT_CONFIG.maxRequests;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function shouldRateLimit(pathname: string): boolean {
|
|
132
|
+
// Check exclusions first
|
|
133
|
+
for (const pattern of RATE_LIMIT_CONFIG.excludedPaths) {
|
|
134
|
+
if (pattern.test(pathname)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if path is protected
|
|
140
|
+
for (const pattern of RATE_LIMIT_CONFIG.protectedPaths) {
|
|
141
|
+
if (pattern.test(pathname)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// MIDDLEWARE
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
export function middleware(request: NextRequest) {
|
|
154
|
+
const pathname = request.nextUrl.pathname;
|
|
155
|
+
|
|
156
|
+
// Skip if path is not rate limited
|
|
157
|
+
if (!shouldRateLimit(pathname)) {
|
|
158
|
+
return NextResponse.next();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const ip = getClientIP(request);
|
|
162
|
+
const maxRequests = getMaxRequests(pathname);
|
|
163
|
+
|
|
164
|
+
const { allowed, remaining, resetTime } = checkRateLimit(
|
|
165
|
+
ip,
|
|
166
|
+
maxRequests,
|
|
167
|
+
RATE_LIMIT_CONFIG.windowSizeSeconds
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Add rate limit headers to response
|
|
171
|
+
const response = allowed
|
|
172
|
+
? NextResponse.next()
|
|
173
|
+
: NextResponse.json(
|
|
174
|
+
{
|
|
175
|
+
error: 'Too Many Requests',
|
|
176
|
+
message: 'Rate limit exceeded. Please try again later.',
|
|
177
|
+
retryAfter: Math.ceil((resetTime - Date.now()) / 1000),
|
|
178
|
+
},
|
|
179
|
+
{ status: 429 }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
response.headers.set('X-RateLimit-Limit', maxRequests.toString());
|
|
183
|
+
response.headers.set('X-RateLimit-Remaining', remaining.toString());
|
|
184
|
+
response.headers.set('X-RateLimit-Reset', resetTime.toString());
|
|
185
|
+
|
|
186
|
+
if (!allowed) {
|
|
187
|
+
response.headers.set(
|
|
188
|
+
'Retry-After',
|
|
189
|
+
Math.ceil((resetTime - Date.now()) / 1000).toString()
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return response;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// MATCHER CONFIGURATION
|
|
198
|
+
// =============================================================================
|
|
199
|
+
|
|
200
|
+
export const config = {
|
|
201
|
+
matcher: [
|
|
202
|
+
/*
|
|
203
|
+
* Match all request paths except:
|
|
204
|
+
* - _next/static (static files)
|
|
205
|
+
* - _next/image (image optimization files)
|
|
206
|
+
* - favicon.ico (favicon file)
|
|
207
|
+
* - public folder
|
|
208
|
+
*/
|
|
209
|
+
'/((?!_next/static|_next/image|favicon.ico|public/).*)',
|
|
210
|
+
],
|
|
211
|
+
};
|