nextjs-secure 0.5.0 → 0.7.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 CHANGED
@@ -4,14 +4,18 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
6
6
  [![Next.js](https://img.shields.io/badge/Next.js-13+-black.svg)](https://nextjs.org/)
7
+ [![Tests](https://img.shields.io/badge/tests-568%20passing-brightgreen.svg)]()
7
8
 
8
- Production-ready security middleware for Next.js App Router. Zero config, maximum protection.
9
+ Production-ready security middleware for Next.js 13+ App Router. Zero config, maximum protection.
9
10
 
10
11
  ```typescript
11
- import { withRateLimit } from 'nextjs-secure'
12
+ import { withRateLimit, withJWT, withValidation } from 'nextjs-secure'
12
13
 
13
- export const GET = withRateLimit(
14
- async (req) => Response.json({ message: 'Hello!' }),
14
+ export const POST = withRateLimit(
15
+ withJWT(
16
+ withValidation(handler, { body: schema }),
17
+ { secret: process.env.JWT_SECRET }
18
+ ),
15
19
  { limit: 100, window: '15m' }
16
20
  )
17
21
  ```
@@ -27,6 +31,7 @@ Building secure APIs in Next.js shouldn't require hours of boilerplate. Most pro
27
31
  - **Edge Ready** - Works on Vercel Edge, Cloudflare Workers, Node.js
28
32
  - **Flexible** - Memory, Redis, or Upstash storage backends
29
33
  - **Lightweight** - No bloated dependencies, tree-shakeable
34
+ - **Complete** - Rate limiting, auth, CSRF, headers, validation, audit logging
30
35
 
31
36
  ## Installation
32
37
 
@@ -42,32 +47,20 @@ pnpm add nextjs-secure
42
47
 
43
48
  - [Quick Start](#quick-start)
44
49
  - [Rate Limiting](#rate-limiting)
45
- - [Basic Usage](#basic-usage)
46
- - [Algorithms](#algorithms)
47
- - [Storage Backends](#storage-backends)
48
- - [Custom Identifiers](#custom-identifiers)
49
- - [Response Customization](#response-customization)
50
50
  - [CSRF Protection](#csrf-protection)
51
- - [Basic Setup](#basic-setup)
52
- - [Client-Side Usage](#client-side-usage)
53
- - [Configuration](#configuration-1)
54
- - [Manual Validation](#manual-validation)
55
51
  - [Security Headers](#security-headers)
56
- - [Quick Start](#quick-start-1)
57
- - [Presets](#presets)
58
- - [Custom Configuration](#custom-configuration)
59
52
  - [Authentication](#authentication)
60
- - [JWT Authentication](#jwt-authentication)
61
- - [API Key Authentication](#api-key-authentication)
62
- - [Session Authentication](#session-authentication)
63
- - [Role-Based Access Control](#role-based-access-control)
64
- - [Combined Authentication](#combined-authentication)
53
+ - [Input Validation](#input-validation)
54
+ - [Audit Logging](#audit-logging)
55
+ - [Bot Detection](#bot-detection)
65
56
  - [Utilities](#utilities)
66
57
  - [API Reference](#api-reference)
67
58
  - [Examples](#examples)
68
59
  - [Roadmap](#roadmap)
69
60
  - [Contributing](#contributing)
70
61
 
62
+ ---
63
+
71
64
  ## Quick Start
72
65
 
73
66
  ### Protect an API Route
@@ -81,48 +74,53 @@ export const GET = withRateLimit(
81
74
  const posts = await db.posts.findMany()
82
75
  return Response.json(posts)
83
76
  },
84
- {
85
- limit: 100, // 100 requests
86
- window: '15m' // per 15 minutes
87
- }
77
+ { limit: 100, window: '15m' }
88
78
  )
89
79
  ```
90
80
 
91
- ### Create a Reusable Limiter
81
+ ### Full Security Stack
92
82
 
93
83
  ```typescript
94
- // lib/rate-limit.ts
95
- import { createRateLimiter } from 'nextjs-secure'
96
-
97
- export const apiLimiter = createRateLimiter({
98
- limit: 100,
99
- window: '15m',
100
- })
101
-
102
- export const strictLimiter = createRateLimiter({
103
- limit: 10,
104
- window: '1m',
105
- })
106
- ```
84
+ // app/api/admin/users/route.ts
85
+ import { withRateLimit, withJWT, withRoles, withValidation, withAuditLog } from 'nextjs-secure'
86
+ import { MemoryStore } from 'nextjs-secure/audit'
107
87
 
108
- ```typescript
109
- // app/api/users/route.ts
110
- import { apiLimiter, strictLimiter } from '@/lib/rate-limit'
88
+ const auditStore = new MemoryStore({ maxEntries: 1000 })
111
89
 
112
- export const GET = apiLimiter(async (req) => {
113
- // ...
114
- })
90
+ const schema = {
91
+ email: { type: 'email', required: true },
92
+ role: { type: 'string', enum: ['user', 'admin'] }
93
+ }
115
94
 
116
- export const POST = strictLimiter(async (req) => {
117
- // ...
118
- })
95
+ export const POST = withAuditLog(
96
+ withRateLimit(
97
+ withJWT(
98
+ withRoles(
99
+ withValidation(
100
+ async (req, ctx) => {
101
+ // ctx.user = authenticated user
102
+ // ctx.validated = validated body
103
+ return Response.json({ success: true })
104
+ },
105
+ { body: schema }
106
+ ),
107
+ { roles: ['admin'] }
108
+ ),
109
+ { secret: process.env.JWT_SECRET }
110
+ ),
111
+ { limit: 10, window: '1m' }
112
+ ),
113
+ { store: auditStore }
114
+ )
119
115
  ```
120
116
 
117
+ ---
118
+
121
119
  ## Rate Limiting
122
120
 
123
- ### Basic Usage
121
+ Protect your APIs from abuse with configurable rate limiting.
124
122
 
125
- The `withRateLimit` higher-order function wraps your route handler:
123
+ ### Basic Usage
126
124
 
127
125
  ```typescript
128
126
  import { withRateLimit } from 'nextjs-secure'
@@ -144,29 +142,18 @@ export const GET = withRateLimit(handler, {
144
142
  ### Algorithms
145
143
 
146
144
  #### Sliding Window (Default)
147
-
148
- Prevents request bursts at window boundaries. Uses weighted counting between current and previous windows.
145
+ Prevents request bursts at window boundaries.
149
146
 
150
147
  ```typescript
151
148
  export const GET = withRateLimit(handler, {
152
149
  limit: 100,
153
150
  window: '15m',
154
- algorithm: 'sliding-window', // default
151
+ algorithm: 'sliding-window',
155
152
  })
156
153
  ```
157
154
 
158
- **How it works:**
159
- ```
160
- Window 1: |----[80 requests]-----|
161
- Window 2: |--[30 requests]-------|
162
- ^ 50% through window 2
163
-
164
- Weighted count = 30 + (80 × 0.5) = 70 requests
165
- ```
166
-
167
155
  #### Fixed Window
168
-
169
- Simple counter that resets at fixed intervals. Lower memory usage but allows bursts at boundaries.
156
+ Simple counter that resets at fixed intervals.
170
157
 
171
158
  ```typescript
172
159
  export const GET = withRateLimit(handler, {
@@ -176,83 +163,45 @@ export const GET = withRateLimit(handler, {
176
163
  })
177
164
  ```
178
165
 
179
- **Burst scenario:**
180
- ```
181
- Window 1: |------------------[100]| <- 100 requests at :59
182
- Window 2: |[100]------------------| <- 100 requests at :00
183
- 200 requests in 2 seconds!
184
- ```
185
-
186
166
  #### Token Bucket
187
-
188
- Allows controlled bursts while maintaining average rate. Tokens refill continuously.
167
+ Allows controlled bursts while maintaining average rate.
189
168
 
190
169
  ```typescript
191
170
  export const GET = withRateLimit(handler, {
192
- limit: 100, // Bucket capacity
193
- window: '1m', // Full refill time
171
+ limit: 100,
172
+ window: '1m',
194
173
  algorithm: 'token-bucket',
195
174
  })
196
175
  ```
197
176
 
198
- **Use case:** APIs where occasional bursts are acceptable but average rate must be controlled.
199
-
200
177
  ### Storage Backends
201
178
 
202
179
  #### Memory Store (Default)
203
180
 
204
- Built-in, zero-config. Perfect for development and single-instance deployments.
205
-
206
181
  ```typescript
207
182
  import { withRateLimit, MemoryStore } from 'nextjs-secure'
208
183
 
209
184
  const store = new MemoryStore({
210
- cleanupInterval: 60000, // Cleanup every minute
211
- maxKeys: 10000, // LRU eviction after 10k keys
185
+ cleanupInterval: 60000,
186
+ maxKeys: 10000,
212
187
  })
213
188
 
214
- export const GET = withRateLimit(handler, {
215
- limit: 100,
216
- window: '15m',
217
- store,
218
- })
189
+ export const GET = withRateLimit(handler, { limit: 100, window: '15m', store })
219
190
  ```
220
191
 
221
- **Limitations:**
222
- - Data lost on restart
223
- - Not shared between instances
224
- - Not suitable for serverless (cold starts)
225
-
226
192
  #### Redis Store
227
193
 
228
- For distributed deployments. Works with ioredis, node-redis, or any compatible client.
229
-
230
194
  ```typescript
231
195
  import Redis from 'ioredis'
232
196
  import { withRateLimit, createRedisStore } from 'nextjs-secure/rate-limit'
233
197
 
234
198
  const redis = new Redis(process.env.REDIS_URL)
199
+ const store = createRedisStore({ client: redis, prefix: 'myapp:rl' })
235
200
 
236
- const store = createRedisStore({
237
- client: redis,
238
- prefix: 'myapp:rl', // Key prefix
239
- })
240
-
241
- export const GET = withRateLimit(handler, {
242
- limit: 100,
243
- window: '15m',
244
- store,
245
- })
201
+ export const GET = withRateLimit(handler, { limit: 100, window: '15m', store })
246
202
  ```
247
203
 
248
- **Features:**
249
- - Atomic operations via Lua scripts
250
- - Automatic key expiration
251
- - Cluster-ready
252
-
253
- #### Upstash Store
254
-
255
- Optimized for serverless and edge. Uses HTTP-based Redis.
204
+ #### Upstash Store (Edge/Serverless)
256
205
 
257
206
  ```typescript
258
207
  import { withRateLimit, createUpstashStore } from 'nextjs-secure/rate-limit'
@@ -262,41 +211,20 @@ const store = createUpstashStore({
262
211
  token: process.env.UPSTASH_REDIS_REST_TOKEN,
263
212
  })
264
213
 
265
- export const GET = withRateLimit(handler, {
266
- limit: 100,
267
- window: '15m',
268
- store,
269
- })
270
-
271
- // Or from environment variables
272
- import { createUpstashStoreFromEnv } from 'nextjs-secure/rate-limit'
273
- const store = createUpstashStoreFromEnv()
214
+ export const GET = withRateLimit(handler, { limit: 100, window: '15m', store })
274
215
  ```
275
216
 
276
- **Benefits:**
277
- - No TCP connections
278
- - Works on Edge Runtime
279
- - Global distribution support
280
-
281
217
  ### Custom Identifiers
282
218
 
283
- By default, rate limiting is per-IP. Customize with the `identifier` option:
284
-
285
- #### By API Key
286
-
287
219
  ```typescript
220
+ // By API Key
288
221
  export const GET = withRateLimit(handler, {
289
222
  limit: 1000,
290
223
  window: '1h',
291
- identifier: (req) => {
292
- return req.headers.get('x-api-key') ?? 'anonymous'
293
- },
224
+ identifier: (req) => req.headers.get('x-api-key') ?? 'anonymous',
294
225
  })
295
- ```
296
226
 
297
- #### By User ID
298
-
299
- ```typescript
227
+ // By User ID
300
228
  export const GET = withRateLimit(handler, {
301
229
  limit: 100,
302
230
  window: '15m',
@@ -307,104 +235,20 @@ export const GET = withRateLimit(handler, {
307
235
  })
308
236
  ```
309
237
 
310
- #### By Route + IP
311
-
312
- ```typescript
313
- export const GET = withRateLimit(handler, {
314
- limit: 100,
315
- window: '15m',
316
- identifier: (req) => {
317
- const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1'
318
- return `${req.nextUrl.pathname}:${ip}`
319
- },
320
- })
321
- ```
322
-
323
- ### Response Customization
324
-
325
- #### Custom Error Response
326
-
327
- ```typescript
328
- export const GET = withRateLimit(handler, {
329
- limit: 100,
330
- window: '15m',
331
- onLimit: (req, info) => {
332
- return Response.json(
333
- {
334
- error: 'rate_limit_exceeded',
335
- message: `Too many requests. Try again in ${info.retryAfter} seconds.`,
336
- limit: info.limit,
337
- reset: new Date(info.reset * 1000).toISOString(),
338
- },
339
- { status: 429 }
340
- )
341
- },
342
- })
343
- ```
344
-
345
- #### Skip Certain Requests
346
-
347
- ```typescript
348
- export const GET = withRateLimit(handler, {
349
- limit: 100,
350
- window: '15m',
351
- skip: (req) => {
352
- // Skip for internal services
353
- const key = req.headers.get('x-internal-key')
354
- return key === process.env.INTERNAL_API_KEY
355
- },
356
- })
357
- ```
358
-
359
- #### Disable Headers
360
-
361
- ```typescript
362
- export const GET = withRateLimit(handler, {
363
- limit: 100,
364
- window: '15m',
365
- headers: false, // Don't add X-RateLimit-* headers
366
- })
367
- ```
368
-
369
238
  ### Response Headers
370
239
 
371
- When `headers: true` (default), responses include:
372
-
373
- | Header | Description | Example |
374
- |--------|-------------|---------|
375
- | `X-RateLimit-Limit` | Maximum requests allowed | `100` |
376
- | `X-RateLimit-Remaining` | Requests remaining | `95` |
377
- | `X-RateLimit-Reset` | Unix timestamp when limit resets | `1699999999` |
378
- | `Retry-After` | Seconds until retry (only on 429) | `60` |
379
-
380
- ### Manual Rate Limit Check
381
-
382
- For existing handlers or complex logic:
383
-
384
- ```typescript
385
- import { checkRateLimit } from 'nextjs-secure'
386
-
387
- export async function GET(request: NextRequest) {
388
- const { success, info, headers } = await checkRateLimit(request, {
389
- limit: 100,
390
- window: '15m',
391
- })
392
-
393
- if (!success) {
394
- return Response.json(
395
- { error: 'Rate limited' },
396
- { status: 429, headers }
397
- )
398
- }
240
+ | Header | Description |
241
+ |--------|-------------|
242
+ | `X-RateLimit-Limit` | Maximum requests allowed |
243
+ | `X-RateLimit-Remaining` | Requests remaining |
244
+ | `X-RateLimit-Reset` | Unix timestamp when limit resets |
245
+ | `Retry-After` | Seconds until retry (only on 429) |
399
246
 
400
- // Your logic here
401
- return Response.json({ data: '...' }, { headers })
402
- }
403
- ```
247
+ ---
404
248
 
405
249
  ## CSRF Protection
406
250
 
407
- Protect your forms against Cross-Site Request Forgery attacks using the double submit cookie pattern.
251
+ Protect forms against Cross-Site Request Forgery attacks.
408
252
 
409
253
  ### Basic Setup
410
254
 
@@ -414,7 +258,6 @@ import { generateCSRF } from 'nextjs-secure/csrf'
414
258
 
415
259
  export async function GET() {
416
260
  const { token, cookieHeader } = await generateCSRF()
417
-
418
261
  return Response.json(
419
262
  { csrfToken: token },
420
263
  { headers: { 'Set-Cookie': cookieHeader } }
@@ -428,7 +271,6 @@ import { withCSRF } from 'nextjs-secure/csrf'
428
271
 
429
272
  export const POST = withCSRF(async (req) => {
430
273
  const data = await req.json()
431
- // Safe to process - CSRF validated
432
274
  return Response.json({ success: true })
433
275
  })
434
276
  ```
@@ -444,111 +286,51 @@ fetch('/api/submit', {
444
286
  method: 'POST',
445
287
  headers: {
446
288
  'Content-Type': 'application/json',
447
- 'x-csrf-token': csrfToken // Token in header
289
+ 'x-csrf-token': csrfToken
448
290
  },
449
291
  body: JSON.stringify({ data: '...' })
450
292
  })
451
293
  ```
452
294
 
453
- Or include in form body:
454
-
455
- ```typescript
456
- fetch('/api/submit', {
457
- method: 'POST',
458
- headers: { 'Content-Type': 'application/json' },
459
- body: JSON.stringify({
460
- _csrf: csrfToken, // Token in body
461
- data: '...'
462
- })
463
- })
464
- ```
465
-
466
295
  ### Configuration
467
296
 
468
297
  ```typescript
469
- import { withCSRF } from 'nextjs-secure/csrf'
470
-
471
298
  export const POST = withCSRF(handler, {
472
- // Cookie settings
473
299
  cookie: {
474
- name: '__csrf', // Cookie name
475
- httpOnly: true, // Not accessible via JS
476
- secure: true, // HTTPS only
477
- sameSite: 'strict', // Strict same-site policy
478
- maxAge: 86400 // 24 hours
300
+ name: '__csrf',
301
+ httpOnly: true,
302
+ secure: true,
303
+ sameSite: 'strict',
304
+ maxAge: 86400
479
305
  },
480
-
481
- // Where to look for token
482
- headerName: 'x-csrf-token', // Header name
483
- fieldName: '_csrf', // Body field name
484
-
485
- // Token settings
486
- secret: process.env.CSRF_SECRET, // Signing secret
487
- tokenLength: 32, // Token size in bytes
488
-
489
- // Protected methods (default: POST, PUT, PATCH, DELETE)
490
- protectedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
491
-
492
- // Skip protection conditionally
306
+ headerName: 'x-csrf-token',
307
+ fieldName: '_csrf',
308
+ secret: process.env.CSRF_SECRET,
493
309
  skip: (req) => req.headers.get('x-api-key') === 'trusted',
494
-
495
- // Custom error response
496
- onError: (req, reason) => {
497
- return new Response(`CSRF failed: ${reason}`, { status: 403 })
498
- }
499
310
  })
500
311
  ```
501
312
 
502
- ### Manual Validation
503
-
504
- ```typescript
505
- import { validateCSRF } from 'nextjs-secure/csrf'
506
-
507
- export async function POST(req) {
508
- const result = await validateCSRF(req)
509
-
510
- if (!result.valid) {
511
- console.log('CSRF failed:', result.reason)
512
- // reason: 'missing_cookie' | 'invalid_cookie' | 'missing_token' | 'token_mismatch'
513
- return Response.json({ error: result.reason }, { status: 403 })
514
- }
515
-
516
- // Continue processing
517
- }
518
- ```
519
-
520
- ### Environment Variable
521
-
522
- Set `CSRF_SECRET` in your environment:
523
-
524
- ```env
525
- CSRF_SECRET=your-secret-key-min-32-chars-recommended
526
- ```
313
+ ---
527
314
 
528
315
  ## Security Headers
529
316
 
530
- Add security headers to your responses with pre-configured presets or custom configuration.
317
+ Add security headers to protect against common attacks.
531
318
 
532
319
  ### Quick Start
533
320
 
534
321
  ```typescript
535
322
  import { withSecurityHeaders } from 'nextjs-secure/headers'
536
323
 
537
- // Use strict preset (default)
538
- export const GET = withSecurityHeaders(async (req) => {
539
- return Response.json({ data: 'protected' })
540
- })
324
+ export const GET = withSecurityHeaders(handler)
541
325
  ```
542
326
 
543
327
  ### Presets
544
328
 
545
- Three presets available: `strict`, `relaxed`, `api`
546
-
547
329
  ```typescript
548
330
  // Strict: Maximum security (default)
549
331
  export const GET = withSecurityHeaders(handler, { preset: 'strict' })
550
332
 
551
- // Relaxed: Development-friendly, allows inline scripts
333
+ // Relaxed: Development-friendly
552
334
  export const GET = withSecurityHeaders(handler, { preset: 'relaxed' })
553
335
 
554
336
  // API: Optimized for JSON APIs
@@ -558,36 +340,22 @@ export const GET = withSecurityHeaders(handler, { preset: 'api' })
558
340
  ### Custom Configuration
559
341
 
560
342
  ```typescript
561
- import { withSecurityHeaders } from 'nextjs-secure/headers'
562
-
563
343
  export const GET = withSecurityHeaders(handler, {
564
344
  config: {
565
- // Content-Security-Policy
566
345
  contentSecurityPolicy: {
567
346
  defaultSrc: ["'self'"],
568
347
  scriptSrc: ["'self'", "'unsafe-inline'"],
569
348
  styleSrc: ["'self'", "'unsafe-inline'"],
570
349
  imgSrc: ["'self'", 'data:', 'https:'],
571
350
  },
572
-
573
- // Strict-Transport-Security
574
351
  strictTransportSecurity: {
575
352
  maxAge: 31536000,
576
353
  includeSubDomains: true,
577
354
  preload: true,
578
355
  },
579
-
580
- // Other headers
581
- xFrameOptions: 'DENY', // or 'SAMEORIGIN'
582
- xContentTypeOptions: true, // X-Content-Type-Options: nosniff
356
+ xFrameOptions: 'DENY',
357
+ xContentTypeOptions: true,
583
358
  referrerPolicy: 'strict-origin-when-cross-origin',
584
-
585
- // Cross-Origin headers
586
- crossOriginOpenerPolicy: 'same-origin',
587
- crossOriginEmbedderPolicy: 'require-corp',
588
- crossOriginResourcePolicy: 'same-origin',
589
-
590
- // Permissions-Policy (disable features)
591
359
  permissionsPolicy: {
592
360
  camera: [],
593
361
  microphone: [],
@@ -597,31 +365,6 @@ export const GET = withSecurityHeaders(handler, {
597
365
  })
598
366
  ```
599
367
 
600
- ### Disable Specific Headers
601
-
602
- ```typescript
603
- export const GET = withSecurityHeaders(handler, {
604
- config: {
605
- contentSecurityPolicy: false, // Disable CSP
606
- xFrameOptions: false, // Disable X-Frame-Options
607
- }
608
- })
609
- ```
610
-
611
- ### Manual Header Creation
612
-
613
- ```typescript
614
- import { createSecurityHeaders } from 'nextjs-secure/headers'
615
-
616
- export async function GET() {
617
- const headers = createSecurityHeaders({ preset: 'api' })
618
-
619
- return new Response(JSON.stringify({ ok: true }), {
620
- headers,
621
- })
622
- }
623
- ```
624
-
625
368
  ### Available Headers
626
369
 
627
370
  | Header | Description |
@@ -636,9 +379,11 @@ export async function GET() {
636
379
  | Cross-Origin-Embedder-Policy | Controls embedding |
637
380
  | Cross-Origin-Resource-Policy | Controls resource sharing |
638
381
 
382
+ ---
383
+
639
384
  ## Authentication
640
385
 
641
- Flexible authentication middleware supporting JWT, API keys, session cookies, and role-based access control.
386
+ Flexible authentication supporting JWT, API keys, sessions, and RBAC.
642
387
 
643
388
  ### JWT Authentication
644
389
 
@@ -647,52 +392,17 @@ import { withJWT } from 'nextjs-secure/auth'
647
392
 
648
393
  export const GET = withJWT(
649
394
  async (req, ctx) => {
650
- // ctx.user contains the authenticated user
651
395
  return Response.json({ user: ctx.user })
652
396
  },
653
397
  {
654
398
  secret: process.env.JWT_SECRET,
655
- // or use publicKey for RS256/ES256
399
+ algorithms: ['HS256', 'RS256'],
400
+ issuer: 'https://myapp.com',
401
+ audience: 'my-api',
656
402
  }
657
403
  )
658
404
  ```
659
405
 
660
- #### Configuration
661
-
662
- ```typescript
663
- export const GET = withJWT(handler, {
664
- // Secret for HMAC algorithms (HS256, HS384, HS512)
665
- secret: process.env.JWT_SECRET,
666
-
667
- // Public key for RSA/ECDSA (RS256, ES256, etc.)
668
- publicKey: process.env.JWT_PUBLIC_KEY,
669
-
670
- // Allowed algorithms (default: ['HS256'])
671
- algorithms: ['HS256', 'RS256'],
672
-
673
- // Validate issuer
674
- issuer: 'https://myapp.com',
675
- // or multiple issuers
676
- issuer: ['https://auth.myapp.com', 'https://api.myapp.com'],
677
-
678
- // Validate audience
679
- audience: 'my-api',
680
-
681
- // Clock tolerance in seconds (for exp/nbf claims)
682
- clockTolerance: 30,
683
-
684
- // Custom token extraction
685
- getToken: (req) => req.headers.get('x-auth-token'),
686
-
687
- // Custom user mapping from JWT payload
688
- mapUser: (payload) => ({
689
- id: payload.sub,
690
- email: payload.email,
691
- roles: payload.roles || [],
692
- }),
693
- })
694
- ```
695
-
696
406
  ### API Key Authentication
697
407
 
698
408
  ```typescript
@@ -703,42 +413,16 @@ export const GET = withAPIKey(
703
413
  return Response.json({ user: ctx.user })
704
414
  },
705
415
  {
706
- validate: async (apiKey, req) => {
707
- // Return user object if valid, null if invalid
416
+ validate: async (apiKey) => {
708
417
  const user = await db.users.findByApiKey(apiKey)
709
418
  return user || null
710
419
  },
420
+ headerName: 'x-api-key',
421
+ queryParam: 'api_key',
711
422
  }
712
423
  )
713
424
  ```
714
425
 
715
- #### Configuration
716
-
717
- ```typescript
718
- export const GET = withAPIKey(handler, {
719
- // Required: validation function
720
- validate: async (apiKey, req) => {
721
- // Lookup API key and return user or null
722
- return db.apiKeys.findUser(apiKey)
723
- },
724
-
725
- // Header name (default: 'x-api-key')
726
- headerName: 'x-api-key',
727
-
728
- // Query parameter name (default: 'api_key')
729
- queryParam: 'api_key',
730
- })
731
- ```
732
-
733
- API keys can be sent via header or query parameter:
734
- ```bash
735
- # Via header
736
- curl -H "x-api-key: YOUR_API_KEY" https://api.example.com/data
737
-
738
- # Via query parameter
739
- curl https://api.example.com/data?api_key=YOUR_API_KEY
740
- ```
741
-
742
426
  ### Session Authentication
743
427
 
744
428
  ```typescript
@@ -749,415 +433,769 @@ export const GET = withSession(
749
433
  return Response.json({ user: ctx.user })
750
434
  },
751
435
  {
752
- validate: async (sessionId, req) => {
753
- // Return user object if session valid, null if invalid
436
+ validate: async (sessionId) => {
754
437
  const session = await db.sessions.find(sessionId)
755
438
  return session?.user || null
756
439
  },
440
+ cookieName: 'session',
757
441
  }
758
442
  )
759
443
  ```
760
444
 
761
- #### Configuration
762
-
763
- ```typescript
764
- export const GET = withSession(handler, {
765
- // Required: session validation function
766
- validate: async (sessionId, req) => {
767
- const session = await redis.get(`session:${sessionId}`)
768
- if (!session) return null
769
- return JSON.parse(session)
770
- },
771
-
772
- // Cookie name (default: 'session')
773
- cookieName: 'session',
774
- })
775
- ```
776
-
777
445
  ### Role-Based Access Control
778
446
 
779
- Use `withRoles` after an authentication middleware to enforce role/permission requirements.
780
-
781
447
  ```typescript
782
448
  import { withJWT, withRoles } from 'nextjs-secure/auth'
783
449
 
784
- // Chain with JWT auth
785
- const authenticatedHandler = withJWT(
450
+ export const GET = withJWT(
786
451
  withRoles(
787
452
  async (req, ctx) => {
788
453
  return Response.json({ admin: true })
789
454
  },
790
- { roles: ['admin'] }
455
+ {
456
+ roles: ['admin', 'moderator'],
457
+ permissions: ['users:read', 'users:write'],
458
+ }
791
459
  ),
792
460
  { secret: process.env.JWT_SECRET }
793
461
  )
794
-
795
- export const GET = authenticatedHandler
796
- ```
797
-
798
- #### Configuration
799
-
800
- ```typescript
801
- withRoles(handler, {
802
- // Required roles (any match = authorized)
803
- roles: ['admin', 'moderator'],
804
-
805
- // Required permissions (all must match)
806
- permissions: ['read', 'write'],
807
-
808
- // Custom role extraction
809
- getUserRoles: (user) => user.roles || [],
810
-
811
- // Custom permission extraction
812
- getUserPermissions: (user) => user.permissions || [],
813
-
814
- // Custom authorization logic
815
- authorize: async (user, req) => {
816
- // Return true if authorized, false otherwise
817
- return user.subscriptionTier === 'pro'
818
- },
819
- })
820
462
  ```
821
463
 
822
464
  ### Combined Authentication
823
465
 
824
- Use `withAuth` for flexible multi-strategy authentication:
825
-
826
466
  ```typescript
827
467
  import { withAuth } from 'nextjs-secure/auth'
828
468
 
829
469
  export const GET = withAuth(
830
470
  async (req, ctx) => {
831
- // Authenticated via any method
832
471
  return Response.json({ user: ctx.user })
833
472
  },
834
473
  {
835
- // Try JWT first
836
- jwt: {
837
- secret: process.env.JWT_SECRET,
838
- },
839
-
840
- // Fall back to API key
841
- apiKey: {
842
- validate: (key) => db.apiKeys.findUser(key),
843
- },
844
-
845
- // Fall back to session
846
- session: {
847
- validate: (id) => db.sessions.findUser(id),
848
- },
849
-
850
- // Optional RBAC
851
- rbac: {
852
- roles: ['user', 'admin'],
853
- },
854
-
855
- // Callbacks
856
- onSuccess: async (req, user) => {
857
- // Log successful auth
858
- console.log(`Authenticated: ${user.id}`)
859
- },
860
-
861
- onError: (req, error) => {
862
- // Custom error response
863
- return Response.json({ error: error.message }, { status: error.status })
864
- },
474
+ jwt: { secret: process.env.JWT_SECRET },
475
+ apiKey: { validate: (key) => db.apiKeys.findUser(key) },
476
+ session: { validate: (id) => db.sessions.findUser(id) },
477
+ rbac: { roles: ['user', 'admin'] },
865
478
  }
866
479
  )
867
480
  ```
868
481
 
869
482
  ### Optional Authentication
870
483
 
871
- For routes that work with or without authentication:
872
-
873
484
  ```typescript
874
485
  import { withOptionalAuth } from 'nextjs-secure/auth'
875
486
 
876
487
  export const GET = withOptionalAuth(
877
488
  async (req, ctx) => {
878
489
  if (ctx.user) {
879
- // Authenticated user
880
490
  return Response.json({ user: ctx.user })
881
491
  }
882
- // Anonymous access
883
492
  return Response.json({ guest: true })
884
493
  },
885
- {
886
- jwt: { secret: process.env.JWT_SECRET },
887
- }
494
+ { jwt: { secret: process.env.JWT_SECRET } }
888
495
  )
889
496
  ```
890
497
 
891
- ### JWT Utilities
498
+ ---
499
+
500
+ ## Input Validation
501
+
502
+ Validate and sanitize user input to prevent attacks.
503
+
504
+ ### Schema Validation
892
505
 
893
506
  ```typescript
894
- import { verifyJWT, decodeJWT, extractBearerToken } from 'nextjs-secure/auth'
507
+ import { withValidation } from 'nextjs-secure/validation'
508
+
509
+ // Built-in schema
510
+ const schema = {
511
+ email: { type: 'email', required: true },
512
+ password: { type: 'string', minLength: 8, maxLength: 100 },
513
+ age: { type: 'number', min: 18, max: 120 },
514
+ role: { type: 'string', enum: ['user', 'admin'] },
515
+ }
516
+
517
+ export const POST = withValidation(handler, { body: schema })
895
518
 
896
- // Verify and decode JWT
897
- const { payload, error } = await verifyJWT(token, {
898
- secret: process.env.JWT_SECRET,
899
- issuer: 'myapp',
519
+ // Or use Zod
520
+ import { z } from 'zod'
521
+
522
+ const zodSchema = z.object({
523
+ email: z.string().email(),
524
+ password: z.string().min(8),
900
525
  })
901
526
 
902
- if (error) {
903
- console.log(error.code) // 'expired_token', 'invalid_signature', etc.
904
- }
527
+ export const POST = withValidation(handler, { body: zodSchema })
528
+ ```
529
+
530
+ ### XSS Protection
531
+
532
+ ```typescript
533
+ import { withXSSProtection, withSanitization, sanitize, detectXSS } from 'nextjs-secure/validation'
905
534
 
906
- // Decode without verification (for inspection only)
907
- const decoded = decodeJWT(token)
908
- // { header, payload, signature }
535
+ // Block XSS attempts
536
+ export const POST = withXSSProtection(handler)
909
537
 
910
- // Extract token from Authorization header
911
- const token = extractBearerToken(req.headers.get('authorization'))
912
- // 'Bearer xxx' -> 'xxx'
538
+ // Sanitize specific fields
539
+ export const POST = withSanitization(handler, {
540
+ fields: ['content', 'bio'],
541
+ mode: 'escape', // 'escape' | 'strip' | 'allow-safe'
542
+ })
543
+
544
+ // Manual sanitization
545
+ const clean = sanitize(userInput, {
546
+ mode: 'allow-safe',
547
+ allowedTags: ['b', 'i', 'em', 'strong'],
548
+ })
549
+
550
+ // Detection only
551
+ const { hasXSS, matches } = detectXSS(input)
913
552
  ```
914
553
 
915
- ## Utilities
554
+ ### SQL Injection Protection
916
555
 
917
- ### Duration Parsing
556
+ ```typescript
557
+ import { withSQLProtection, detectSQLInjection, hasSQLInjection } from 'nextjs-secure/validation'
558
+
559
+ // Block SQL injection
560
+ export const POST = withSQLProtection(handler, {
561
+ mode: 'block', // 'block' | 'detect'
562
+ minSeverity: 'medium', // 'low' | 'medium' | 'high'
563
+ })
564
+
565
+ // Manual detection
566
+ const result = detectSQLInjection(input)
567
+ // { hasSQLi: true, severity: 'high', patterns: ['UNION SELECT'] }
568
+
569
+ // Simple check
570
+ if (hasSQLInjection(input)) {
571
+ // Block request
572
+ }
573
+ ```
574
+
575
+ ### Path Traversal Prevention
918
576
 
919
577
  ```typescript
920
- import { parseDuration, formatDuration } from 'nextjs-secure'
578
+ import { validatePath, sanitizePath, sanitizeFilename } from 'nextjs-secure/validation'
921
579
 
922
- parseDuration('15m') // 900000
923
- parseDuration('1h 30m') // 5400000
924
- parseDuration('2d') // 172800000
580
+ // Validate path
581
+ const result = validatePath(userPath, {
582
+ basePath: '/uploads',
583
+ allowedExtensions: ['.jpg', '.png'],
584
+ maxDepth: 3,
585
+ })
925
586
 
926
- formatDuration(900000) // '15m'
927
- formatDuration(5400000) // '1h 30m'
928
- formatDuration(90061000) // '1d 1h 1m 1s'
587
+ if (!result.valid) {
588
+ console.log(result.reason) // 'traversal_detected', 'invalid_extension', etc.
589
+ }
590
+
591
+ // Sanitize
592
+ const safePath = sanitizePath('../../../etc/passwd') // 'etc/passwd'
593
+ const safeFilename = sanitizeFilename('../../evil.exe') // 'evil.exe'
929
594
  ```
930
595
 
931
- ### IP Utilities
596
+ ### File Validation
932
597
 
933
598
  ```typescript
934
- import { getClientIp, anonymizeIp, isPrivateIp } from 'nextjs-secure'
599
+ import { withFileValidation, validateFile } from 'nextjs-secure/validation'
935
600
 
936
- // Extract client IP from request
937
- const ip = getClientIp(request)
601
+ export const POST = withFileValidation(handler, {
602
+ maxSize: 5 * 1024 * 1024, // 5MB
603
+ allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'],
604
+ validateMagicNumbers: true, // Check actual file content
605
+ maxFiles: 10,
606
+ })
938
607
 
939
- // Handles: cf-connecting-ip, x-real-ip, x-forwarded-for, etc.
940
- const ip = getClientIp(request, {
941
- trustProxy: true,
942
- customHeaders: ['x-custom-ip'],
943
- fallback: '0.0.0.0',
608
+ // Manual validation
609
+ const result = await validateFile(file, {
610
+ maxSize: 5 * 1024 * 1024,
611
+ allowedTypes: ['image/jpeg'],
944
612
  })
613
+ ```
945
614
 
946
- // Anonymize for logging (GDPR compliant)
947
- anonymizeIp('192.168.1.100') // '192.168.1.xxx'
615
+ ### Combined Security Validation
948
616
 
949
- // Check if private
950
- isPrivateIp('192.168.1.1') // true
951
- isPrivateIp('8.8.8.8') // false
617
+ ```typescript
618
+ import { withSecureValidation } from 'nextjs-secure/validation'
619
+
620
+ export const POST = withSecureValidation(handler, {
621
+ xss: true,
622
+ sql: { minSeverity: 'medium' },
623
+ contentType: ['application/json'],
624
+ })
952
625
  ```
953
626
 
954
- ## API Reference
627
+ ---
955
628
 
956
- ### `withRateLimit(handler, config)`
629
+ ## Audit Logging
957
630
 
958
- Wraps a route handler with rate limiting.
631
+ Track requests and security events for monitoring and compliance.
632
+
633
+ ### Request Logging
959
634
 
960
635
  ```typescript
961
- interface RateLimitConfig {
962
- limit: number
963
- window: string | number
964
- algorithm?: 'sliding-window' | 'fixed-window' | 'token-bucket'
965
- identifier?: 'ip' | 'user' | ((req: NextRequest) => string | Promise<string>)
966
- store?: RateLimitStore
967
- headers?: boolean
968
- skip?: (req: NextRequest) => boolean | Promise<boolean>
969
- onLimit?: (req: NextRequest, info: RateLimitInfo) => Response | Promise<Response>
970
- prefix?: string
971
- message?: string
972
- statusCode?: number
973
- }
636
+ import { withAuditLog, MemoryStore, ConsoleStore } from 'nextjs-secure/audit'
637
+
638
+ const store = new MemoryStore({ maxEntries: 1000 })
639
+
640
+ export const POST = withAuditLog(handler, {
641
+ store,
642
+ include: {
643
+ ip: true,
644
+ userAgent: true,
645
+ headers: false,
646
+ query: true,
647
+ response: true,
648
+ duration: true,
649
+ },
650
+ exclude: {
651
+ paths: ['/health', '/metrics'],
652
+ methods: ['OPTIONS'],
653
+ statusCodes: [304],
654
+ },
655
+ pii: {
656
+ fields: ['password', 'token', 'ssn', 'creditCard'],
657
+ mode: 'mask', // 'mask' | 'hash' | 'remove'
658
+ },
659
+ })
974
660
  ```
975
661
 
976
- ### `createRateLimiter(config)`
662
+ ### Storage Backends
663
+
664
+ ```typescript
665
+ import { MemoryStore, ConsoleStore, createDatadogStore, MultiStore } from 'nextjs-secure/audit'
666
+
667
+ // Memory (development)
668
+ const memoryStore = new MemoryStore({ maxEntries: 1000, ttl: 3600000 })
669
+
670
+ // Console (development)
671
+ const consoleStore = new ConsoleStore({ colorize: true, level: 'info' })
977
672
 
978
- Creates a reusable rate limiter function.
673
+ // Datadog (production)
674
+ const datadogStore = createDatadogStore({
675
+ apiKey: process.env.DATADOG_API_KEY,
676
+ service: 'my-api',
677
+ environment: 'production',
678
+ })
979
679
 
980
- ### `checkRateLimit(request, config)`
680
+ // Multiple stores
681
+ const multiStore = new MultiStore([consoleStore, datadogStore])
682
+ ```
981
683
 
982
- Manually check rate limit without wrapping.
684
+ ### Security Event Tracking
983
685
 
984
- Returns:
985
686
  ```typescript
986
- {
987
- success: boolean
988
- info: RateLimitInfo
989
- headers: Headers
990
- response?: Response // Only if rate limited
991
- }
687
+ import { createSecurityTracker, trackSecurityEvent } from 'nextjs-secure/audit'
688
+
689
+ const tracker = createSecurityTracker({ store })
690
+
691
+ // Authentication failures
692
+ await tracker.authFailed({
693
+ ip: '192.168.1.1',
694
+ email: 'user@example.com',
695
+ reason: 'Invalid password',
696
+ })
697
+
698
+ // Rate limit exceeded
699
+ await tracker.rateLimitExceeded({
700
+ ip: '192.168.1.1',
701
+ endpoint: '/api/login',
702
+ limit: 10,
703
+ window: '15m',
704
+ })
705
+
706
+ // XSS detected
707
+ await tracker.xssDetected({
708
+ ip: '192.168.1.1',
709
+ field: 'comment',
710
+ endpoint: '/api/comments',
711
+ })
712
+
713
+ // SQL injection detected
714
+ await tracker.sqliDetected({
715
+ ip: '192.168.1.1',
716
+ field: 'username',
717
+ pattern: 'UNION SELECT',
718
+ severity: 'high',
719
+ endpoint: '/api/users',
720
+ })
721
+
722
+ // CSRF validation failure
723
+ await tracker.csrfInvalid({
724
+ ip: '192.168.1.1',
725
+ endpoint: '/api/transfer',
726
+ reason: 'Token mismatch',
727
+ })
728
+
729
+ // IP blocked
730
+ await tracker.ipBlocked({
731
+ ip: '192.168.1.1',
732
+ reason: 'Too many failed attempts',
733
+ duration: 3600,
734
+ })
735
+
736
+ // Custom events
737
+ await tracker.custom({
738
+ message: 'Suspicious activity detected',
739
+ severity: 'high',
740
+ details: { pattern: 'automated_scanning' },
741
+ })
992
742
  ```
993
743
 
994
- ### `RateLimitInfo`
744
+ ### PII Redaction
995
745
 
996
746
  ```typescript
997
- interface RateLimitInfo {
998
- limit: number // Max requests
999
- remaining: number // Requests left
1000
- reset: number // Unix timestamp
1001
- limited: boolean // Whether rate limited
1002
- retryAfter?: number // Seconds until retry
1003
- }
747
+ import { redactObject, redactEmail, redactCreditCard, redactIP, DEFAULT_PII_FIELDS } from 'nextjs-secure/audit'
748
+
749
+ // Redact object
750
+ const safeData = redactObject(userData, {
751
+ fields: DEFAULT_PII_FIELDS,
752
+ mode: 'mask',
753
+ })
754
+
755
+ // Specific redactors
756
+ redactEmail('john@example.com') // '****@example.com'
757
+ redactCreditCard('4111111111111111') // '**** **** **** 1111'
758
+ redactIP('192.168.1.100') // '192.168.*.*'
1004
759
  ```
1005
760
 
1006
- ## Examples
761
+ ### Request ID & Timing
762
+
763
+ ```typescript
764
+ import { withRequestId, withTiming } from 'nextjs-secure/audit'
765
+
766
+ // Add request ID to responses
767
+ export const GET = withRequestId(handler, {
768
+ headerName: 'x-request-id',
769
+ generateId: () => `req_${Date.now()}`,
770
+ })
771
+
772
+ // Add response timing
773
+ export const GET = withTiming(handler, {
774
+ headerName: 'x-response-time',
775
+ log: true,
776
+ })
777
+ ```
1007
778
 
1008
- ### Different Limits per HTTP Method
779
+ ### Log Formatters
1009
780
 
1010
781
  ```typescript
1011
- // app/api/posts/route.ts
1012
- import { withRateLimit } from 'nextjs-secure'
782
+ import { JSONFormatter, TextFormatter, CLFFormatter, StructuredFormatter } from 'nextjs-secure/audit'
1013
783
 
1014
- // Generous limit for reads
1015
- export const GET = withRateLimit(getHandler, {
1016
- limit: 1000,
1017
- window: '15m',
784
+ // JSON (default)
785
+ const jsonFormatter = new JSONFormatter({ pretty: true })
786
+
787
+ // Human-readable text
788
+ const textFormatter = new TextFormatter({
789
+ template: '{timestamp} [{level}] {message}',
1018
790
  })
1019
791
 
1020
- // Strict limit for writes
1021
- export const POST = withRateLimit(postHandler, {
1022
- limit: 10,
1023
- window: '1m',
792
+ // Apache/Nginx Common Log Format
793
+ const clfFormatter = new CLFFormatter()
794
+
795
+ // Key=value (ELK/Splunk)
796
+ const structuredFormatter = new StructuredFormatter({
797
+ delimiter: ' ',
798
+ kvSeparator: '=',
1024
799
  })
800
+ ```
1025
801
 
1026
- // Very strict for deletes
1027
- export const DELETE = withRateLimit(deleteHandler, {
1028
- limit: 5,
1029
- window: '1h',
802
+ ---
803
+
804
+ ## Bot Detection
805
+
806
+ Protect your endpoints from automated bots, scrapers, and spam.
807
+
808
+ ### Basic Usage
809
+
810
+ ```typescript
811
+ import { withBotProtection } from 'nextjs-secure/bot'
812
+
813
+ export const POST = withBotProtection(handler, {
814
+ userAgent: {
815
+ blockAllBots: false,
816
+ allowList: ['Googlebot', 'Bingbot'],
817
+ },
818
+ honeypot: true,
819
+ behavior: {
820
+ maxRequestsPerSecond: 10,
821
+ },
1030
822
  })
1031
823
  ```
1032
824
 
1033
- ### Tiered Rate Limiting
825
+ ### Presets
1034
826
 
1035
827
  ```typescript
1036
- // lib/rate-limit.ts
1037
- import { createRateLimiter, createUpstashStore } from 'nextjs-secure/rate-limit'
828
+ import { withBotProtectionPreset } from 'nextjs-secure/bot'
1038
829
 
1039
- const store = createUpstashStore({
1040
- url: process.env.UPSTASH_REDIS_REST_URL!,
1041
- token: process.env.UPSTASH_REDIS_REST_TOKEN!,
830
+ // Relaxed: Only blocks obvious bots
831
+ export const GET = withBotProtectionPreset(handler, 'relaxed')
832
+
833
+ // Standard: Good balance (default)
834
+ export const GET = withBotProtectionPreset(handler, 'standard')
835
+
836
+ // Strict: Maximum protection
837
+ export const GET = withBotProtectionPreset(handler, 'strict')
838
+
839
+ // API: Optimized for API endpoints
840
+ export const GET = withBotProtectionPreset(handler, 'api')
841
+ ```
842
+
843
+ ### User-Agent Detection
844
+
845
+ ```typescript
846
+ import { withUserAgentProtection, analyzeUserAgent, KNOWN_BOT_PATTERNS } from 'nextjs-secure/bot'
847
+
848
+ // Middleware
849
+ export const GET = withUserAgentProtection(handler, {
850
+ blockAllBots: true,
851
+ allowCategories: ['search_engine', 'social_media'],
852
+ allowList: ['Googlebot', 'Twitterbot'],
853
+ blockList: ['BadBot'],
1042
854
  })
1043
855
 
1044
- // Free tier: 100 req/day
1045
- export const freeLimiter = createRateLimiter({
1046
- limit: 100,
1047
- window: '1d',
1048
- store,
1049
- identifier: async (req) => {
1050
- const apiKey = req.headers.get('x-api-key')
1051
- return `free:${apiKey}`
1052
- },
856
+ // Manual detection
857
+ const result = analyzeUserAgent('Googlebot/2.1')
858
+ // { isBot: true, category: 'search_engine', name: 'Googlebot', confidence: 0.95 }
859
+ ```
860
+
861
+ ### Honeypot Protection
862
+
863
+ ```typescript
864
+ import { withHoneypotProtection, generateHoneypotHTML, generateHoneypotCSS } from 'nextjs-secure/bot'
865
+
866
+ // Middleware
867
+ export const POST = withHoneypotProtection(handler, {
868
+ fieldName: '_hp_email',
869
+ additionalFields: ['_hp_name', '_hp_phone'],
1053
870
  })
1054
871
 
1055
- // Pro tier: 10000 req/day
1056
- export const proLimiter = createRateLimiter({
1057
- limit: 10000,
1058
- window: '1d',
872
+ // Generate HTML for forms
873
+ const honeypotHTML = generateHoneypotHTML({ fieldName: '_hp_email' })
874
+ // Returns hidden input fields
875
+
876
+ // Generate CSS
877
+ const honeypotCSS = generateHoneypotCSS({ fieldName: '_hp_email' })
878
+ // Returns CSS to hide fields
879
+ ```
880
+
881
+ ### Behavior Analysis
882
+
883
+ ```typescript
884
+ import { withBehaviorProtection, MemoryBehaviorStore } from 'nextjs-secure/bot'
885
+
886
+ const store = new MemoryBehaviorStore()
887
+
888
+ export const GET = withBehaviorProtection(handler, {
1059
889
  store,
1060
- identifier: async (req) => {
1061
- const apiKey = req.headers.get('x-api-key')
1062
- return `pro:${apiKey}`
890
+ minRequestInterval: 100, // Min ms between requests
891
+ maxRequestsPerSecond: 10, // Max requests per second
892
+ patterns: {
893
+ sequentialAccess: true, // Detect sequential URL patterns
894
+ regularTiming: true, // Detect bot-like timing
895
+ missingHeaders: true, // Detect missing browser headers
1063
896
  },
1064
897
  })
1065
898
  ```
1066
899
 
1067
- ### With Authentication
900
+ ### CAPTCHA Integration
1068
901
 
1069
902
  ```typescript
1070
- import { withRateLimit } from 'nextjs-secure'
1071
- import { getServerSession } from 'next-auth'
903
+ import { withCaptchaProtection, verifyCaptcha } from 'nextjs-secure/bot'
904
+
905
+ // reCAPTCHA v3
906
+ export const POST = withCaptchaProtection(handler, {
907
+ provider: 'recaptcha-v3',
908
+ siteKey: process.env.RECAPTCHA_SITE_KEY,
909
+ secretKey: process.env.RECAPTCHA_SECRET_KEY,
910
+ threshold: 0.5,
911
+ })
1072
912
 
1073
- export const GET = withRateLimit(
1074
- async (req, ctx) => {
1075
- const session = await getServerSession()
913
+ // hCaptcha
914
+ export const POST = withCaptchaProtection(handler, {
915
+ provider: 'hcaptcha',
916
+ siteKey: process.env.HCAPTCHA_SITE_KEY,
917
+ secretKey: process.env.HCAPTCHA_SECRET_KEY,
918
+ })
1076
919
 
1077
- if (!session) {
1078
- return Response.json({ error: 'Unauthorized' }, { status: 401 })
1079
- }
920
+ // Cloudflare Turnstile
921
+ export const POST = withCaptchaProtection(handler, {
922
+ provider: 'turnstile',
923
+ siteKey: process.env.TURNSTILE_SITE_KEY,
924
+ secretKey: process.env.TURNSTILE_SECRET_KEY,
925
+ })
1080
926
 
1081
- return Response.json({ user: session.user })
1082
- },
1083
- {
1084
- limit: 100,
1085
- window: '15m',
1086
- identifier: async (req) => {
1087
- const session = await getServerSession()
1088
- return session?.user?.id ?? 'anonymous'
1089
- },
1090
- }
1091
- )
927
+ // Manual verification
928
+ const result = await verifyCaptcha(token, {
929
+ provider: 'recaptcha-v3',
930
+ secretKey: process.env.RECAPTCHA_SECRET_KEY,
931
+ })
1092
932
  ```
1093
933
 
1094
- ### Webhook Endpoint
934
+ ### Manual Bot Detection
1095
935
 
1096
936
  ```typescript
1097
- import { withRateLimit } from 'nextjs-secure'
1098
- import { headers } from 'next/headers'
1099
- import crypto from 'crypto'
937
+ import { detectBot } from 'nextjs-secure/bot'
1100
938
 
1101
- export const POST = withRateLimit(
1102
- async (req) => {
1103
- const body = await req.text()
1104
- const signature = headers().get('x-webhook-signature')
1105
-
1106
- // Verify signature
1107
- const expected = crypto
1108
- .createHmac('sha256', process.env.WEBHOOK_SECRET!)
1109
- .update(body)
1110
- .digest('hex')
1111
-
1112
- if (signature !== expected) {
1113
- return Response.json({ error: 'Invalid signature' }, { status: 401 })
1114
- }
939
+ const result = await detectBot(request, {
940
+ userAgent: { blockAllBots: true },
941
+ honeypot: true,
942
+ behavior: { maxRequestsPerSecond: 10 },
943
+ })
944
+
945
+ if (result.isBot) {
946
+ console.log(`Bot detected: ${result.reason}`)
947
+ console.log(`Category: ${result.category}`)
948
+ console.log(`Confidence: ${result.confidence}`)
949
+ }
950
+ ```
1115
951
 
1116
- // Process webhook
1117
- const data = JSON.parse(body)
1118
- await processWebhook(data)
952
+ ### Bot Categories
1119
953
 
1120
- return Response.json({ received: true })
1121
- },
954
+ | Category | Examples |
955
+ |----------|----------|
956
+ | `search_engine` | Googlebot, Bingbot, Yandex |
957
+ | `social_media` | Twitterbot, FacebookBot, LinkedInBot |
958
+ | `ai_crawler` | GPTBot, Claude-Web, Anthropic |
959
+ | `monitoring` | UptimeRobot, Pingdom |
960
+ | `feed_reader` | Feedly, Feedbin |
961
+ | `preview` | Slackbot, Discord |
962
+ | `scraper` | Scrapy, DataMiner |
963
+ | `spam` | Spam bots, malicious crawlers |
964
+ | `unknown` | Unidentified automated traffic |
965
+
966
+ ---
967
+
968
+ ## Utilities
969
+
970
+ ### Duration Parsing
971
+
972
+ ```typescript
973
+ import { parseDuration, formatDuration } from 'nextjs-secure'
974
+
975
+ parseDuration('15m') // 900000
976
+ parseDuration('1h 30m') // 5400000
977
+ parseDuration('2d') // 172800000
978
+
979
+ formatDuration(900000) // '15m'
980
+ formatDuration(5400000) // '1h 30m'
981
+ ```
982
+
983
+ ### IP Utilities
984
+
985
+ ```typescript
986
+ import { getClientIp, anonymizeIp, isPrivateIp, isLocalhost } from 'nextjs-secure'
987
+
988
+ // Extract client IP
989
+ const ip = getClientIp(request, {
990
+ trustProxy: true,
991
+ customHeaders: ['x-custom-ip'],
992
+ })
993
+
994
+ // Anonymize for GDPR
995
+ anonymizeIp('192.168.1.100') // '192.168.1.xxx'
996
+
997
+ // Check IP type
998
+ isPrivateIp('192.168.1.1') // true
999
+ isPrivateIp('8.8.8.8') // false
1000
+ isLocalhost('127.0.0.1') // true
1001
+ ```
1002
+
1003
+ ---
1004
+
1005
+ ## API Reference
1006
+
1007
+ ### Rate Limiting
1008
+
1009
+ | Function | Description |
1010
+ |----------|-------------|
1011
+ | `withRateLimit(handler, config)` | Wrap handler with rate limiting |
1012
+ | `createRateLimiter(config)` | Create reusable rate limiter |
1013
+ | `checkRateLimit(request, config)` | Manual rate limit check |
1014
+ | `getRateLimitStatus(key, config)` | Get current status without incrementing |
1015
+ | `resetRateLimit(key, config)` | Reset rate limit for key |
1016
+
1017
+ ### CSRF
1018
+
1019
+ | Function | Description |
1020
+ |----------|-------------|
1021
+ | `withCSRF(handler, config)` | Wrap handler with CSRF protection |
1022
+ | `generateCSRF(config)` | Generate CSRF token and cookie |
1023
+ | `validateCSRF(request, config)` | Manual CSRF validation |
1024
+
1025
+ ### Security Headers
1026
+
1027
+ | Function | Description |
1028
+ |----------|-------------|
1029
+ | `withSecurityHeaders(handler, config)` | Add security headers |
1030
+ | `createSecurityHeaders(config)` | Create headers object |
1031
+ | `buildCSP(config)` | Build CSP header string |
1032
+ | `getPreset(name)` | Get preset configuration |
1033
+
1034
+ ### Authentication
1035
+
1036
+ | Function | Description |
1037
+ |----------|-------------|
1038
+ | `withJWT(handler, config)` | JWT authentication |
1039
+ | `withAPIKey(handler, config)` | API key authentication |
1040
+ | `withSession(handler, config)` | Session authentication |
1041
+ | `withAuth(handler, config)` | Combined authentication |
1042
+ | `withRoles(handler, config)` | Role-based access control |
1043
+ | `withOptionalAuth(handler, config)` | Optional authentication |
1044
+ | `verifyJWT(token, config)` | Verify JWT token |
1045
+ | `decodeJWT(token)` | Decode JWT without verification |
1046
+
1047
+ ### Validation
1048
+
1049
+ | Function | Description |
1050
+ |----------|-------------|
1051
+ | `withValidation(handler, config)` | Schema validation |
1052
+ | `withXSSProtection(handler)` | Block XSS attempts |
1053
+ | `withSanitization(handler, config)` | Sanitize input |
1054
+ | `withSQLProtection(handler, config)` | Block SQL injection |
1055
+ | `withFileValidation(handler, config)` | File upload validation |
1056
+ | `sanitize(input, config)` | Manual sanitization |
1057
+ | `detectXSS(input)` | Detect XSS patterns |
1058
+ | `detectSQLInjection(input)` | Detect SQL injection |
1059
+ | `validatePath(path, config)` | Validate file path |
1060
+
1061
+ ### Audit Logging
1062
+
1063
+ | Function | Description |
1064
+ |----------|-------------|
1065
+ | `withAuditLog(handler, config)` | Request logging |
1066
+ | `withRequestId(handler, config)` | Add request ID |
1067
+ | `withTiming(handler, config)` | Add response timing |
1068
+ | `createSecurityTracker(config)` | Create event tracker |
1069
+ | `trackSecurityEvent(store, event)` | Track single event |
1070
+ | `redactObject(obj, config)` | Redact PII from object |
1071
+
1072
+ ### Bot Detection
1073
+
1074
+ | Function | Description |
1075
+ |----------|-------------|
1076
+ | `withBotProtection(handler, config)` | Combined bot protection |
1077
+ | `withUserAgentProtection(handler, config)` | User-agent only protection |
1078
+ | `withHoneypotProtection(handler, config)` | Honeypot only protection |
1079
+ | `withBehaviorProtection(handler, config)` | Behavior analysis only |
1080
+ | `withCaptchaProtection(handler, config)` | CAPTCHA verification |
1081
+ | `withBotProtectionPreset(handler, preset)` | Use preset configuration |
1082
+ | `detectBot(request, config)` | Manual bot detection |
1083
+ | `analyzeUserAgent(userAgent, config)` | Analyze user-agent string |
1084
+ | `checkHoneypot(request, config)` | Check honeypot fields |
1085
+ | `checkBehavior(request, config)` | Check request behavior |
1086
+ | `verifyCaptcha(token, config)` | Verify CAPTCHA token |
1087
+ | `generateHoneypotHTML(config)` | Generate honeypot HTML |
1088
+ | `generateHoneypotCSS(config)` | Generate honeypot CSS |
1089
+
1090
+ ---
1091
+
1092
+ ## Examples
1093
+
1094
+ ### Complete API with All Security Features
1095
+
1096
+ ```typescript
1097
+ // lib/security.ts
1098
+ import { createRateLimiter, MemoryStore } from 'nextjs-secure/rate-limit'
1099
+ import { createSecurityTracker, MemoryStore as AuditStore } from 'nextjs-secure/audit'
1100
+
1101
+ export const apiLimiter = createRateLimiter({
1102
+ limit: 100,
1103
+ window: '15m',
1104
+ store: new MemoryStore(),
1105
+ })
1106
+
1107
+ export const strictLimiter = createRateLimiter({
1108
+ limit: 5,
1109
+ window: '1m',
1110
+ })
1111
+
1112
+ export const auditStore = new AuditStore({ maxEntries: 10000 })
1113
+ export const securityTracker = createSecurityTracker({ store: auditStore })
1114
+ ```
1115
+
1116
+ ```typescript
1117
+ // app/api/users/route.ts
1118
+ import { withJWT, withRoles } from 'nextjs-secure/auth'
1119
+ import { withValidation } from 'nextjs-secure/validation'
1120
+ import { withAuditLog } from 'nextjs-secure/audit'
1121
+ import { apiLimiter, auditStore, securityTracker } from '@/lib/security'
1122
+
1123
+ const createUserSchema = {
1124
+ email: { type: 'email', required: true },
1125
+ name: { type: 'string', minLength: 2, maxLength: 100 },
1126
+ role: { type: 'string', enum: ['user', 'admin'] },
1127
+ }
1128
+
1129
+ async function createUser(req, ctx) {
1130
+ const { email, name, role } = ctx.validated
1131
+ const user = await db.users.create({ email, name, role })
1132
+ return Response.json(user, { status: 201 })
1133
+ }
1134
+
1135
+ export const POST = withAuditLog(
1136
+ apiLimiter(
1137
+ withJWT(
1138
+ withRoles(
1139
+ withValidation(createUser, { body: createUserSchema }),
1140
+ { roles: ['admin'] }
1141
+ ),
1142
+ { secret: process.env.JWT_SECRET }
1143
+ )
1144
+ ),
1122
1145
  {
1123
- limit: 1000,
1124
- window: '1m',
1125
- identifier: (req) => {
1126
- // Rate limit by webhook source
1127
- return req.headers.get('x-webhook-source') ?? 'unknown'
1128
- },
1146
+ store: auditStore,
1147
+ include: { ip: true, userAgent: true },
1148
+ pii: { fields: ['password'], mode: 'remove' },
1129
1149
  }
1130
1150
  )
1131
1151
  ```
1132
1152
 
1153
+ ### Tiered Rate Limiting
1154
+
1155
+ ```typescript
1156
+ import { createRateLimiter, createUpstashStore } from 'nextjs-secure/rate-limit'
1157
+
1158
+ const store = createUpstashStore({
1159
+ url: process.env.UPSTASH_REDIS_REST_URL,
1160
+ token: process.env.UPSTASH_REDIS_REST_TOKEN,
1161
+ })
1162
+
1163
+ const freeLimiter = createRateLimiter({
1164
+ limit: 100,
1165
+ window: '1d',
1166
+ store,
1167
+ identifier: (req) => `free:${req.headers.get('x-api-key')}`,
1168
+ })
1169
+
1170
+ const proLimiter = createRateLimiter({
1171
+ limit: 10000,
1172
+ window: '1d',
1173
+ store,
1174
+ identifier: (req) => `pro:${req.headers.get('x-api-key')}`,
1175
+ })
1176
+
1177
+ export async function GET(req) {
1178
+ const tier = await getUserTier(req)
1179
+ const limiter = tier === 'pro' ? proLimiter : freeLimiter
1180
+ return limiter(handler)(req)
1181
+ }
1182
+ ```
1183
+
1184
+ ---
1185
+
1133
1186
  ## Roadmap
1134
1187
 
1135
- - [x] Rate Limiting (v0.1.0)
1136
- - [x] Sliding window algorithm
1137
- - [x] Fixed window algorithm
1138
- - [x] Token bucket algorithm
1139
- - [x] Memory store
1140
- - [x] Redis store
1141
- - [x] Upstash store
1142
- - [x] CSRF Protection (v0.2.0)
1143
- - [x] Double submit cookie pattern
1144
- - [x] Token generation/validation
1145
- - [x] Configurable cookie settings
1146
- - [x] Security Headers (v0.3.0)
1147
- - [x] Content-Security-Policy
1148
- - [x] Strict-Transport-Security
1149
- - [x] X-Frame-Options, X-Content-Type-Options
1150
- - [x] Permissions-Policy
1151
- - [x] COOP, COEP, CORP
1152
- - [x] Presets (strict, relaxed, api)
1153
- - [x] Authentication (v0.4.0)
1154
- - [x] JWT validation (HS256, RS256, ES256)
1155
- - [x] API Key authentication
1156
- - [x] Session/Cookie authentication
1157
- - [x] Role-Based Access Control (RBAC)
1158
- - [x] Combined multi-strategy auth
1159
- - [ ] Input Validation (v0.5.0)
1160
- - [ ] Audit Logging (v0.6.0)
1188
+ - [x] **v0.1.x** - Rate Limiting
1189
+ - [x] **v0.2.0** - CSRF Protection
1190
+ - [x] **v0.3.0** - Security Headers
1191
+ - [x] **v0.4.0** - Authentication
1192
+ - [x] **v0.5.0** - Input Validation
1193
+ - [x] **v0.6.0** - Audit Logging
1194
+ - [x] **v0.7.0** - Bot Detection
1195
+
1196
+ See [ROADMAP.md](ROADMAP.md) for detailed progress and future plans.
1197
+
1198
+ ---
1161
1199
 
1162
1200
  ## Contributing
1163
1201
 
@@ -1178,10 +1216,20 @@ npm test
1178
1216
  npm run build
1179
1217
  ```
1180
1218
 
1219
+ ### Running Tests
1220
+
1221
+ ```bash
1222
+ npm run test # Watch mode
1223
+ npm run test:run # Single run
1224
+ npm run test:coverage # With coverage
1225
+ ```
1226
+
1227
+ ---
1228
+
1181
1229
  ## License
1182
1230
 
1183
1231
  MIT License - see [LICENSE](LICENSE) for details.
1184
1232
 
1185
1233
  ---
1186
1234
 
1187
- **Star this repo** if you find it useful!
1235
+ **Made with security in mind for the Next.js community.**