@tozielinski/next-upstash-nonce 1.3.1 → 1.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0](https://github.com/tozielinski/next-upstash-nonce/compare/v1.3.1...v1.4.0) (2025-11-25)
4
+
5
+
6
+ ### Features
7
+
8
+ * add rate limiter for API endpoints ([5990ac4](https://github.com/tozielinski/next-upstash-nonce/commit/5990ac49da90984769aa92ab338e6876af30a4b6))
9
+
3
10
  ## [1.3.1](https://github.com/tozielinski/next-upstash-nonce/compare/v1.3.0...v1.3.1) (2025-11-22)
4
11
 
5
12
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
3
  [![npm version](https://img.shields.io/npm/v/%40tozielinski%2Fnext-upstash-nonce)](https://www.npmjs.com/package/@tozielinski/next-upstash-nonce)
4
4
 
5
- ## Create, store, verify and delete nonces in Redis by Upstash for Next.js
5
+ ## Create, store, verify and delete nonces in Redis by Upstash for Next.js and/or secure your API endpoints with a rate limiter
6
6
 
7
7
  # Quick Start
8
8
  ### Install the package:
@@ -33,7 +33,7 @@ export async function createNonce(): Promise<string> {
33
33
  return await nonceManager.create();
34
34
  }
35
35
  ```
36
- ### Secure your API endpoint
36
+ ### Secure your API endpoint with a Nonce
37
37
  ```typescript
38
38
  'use server'
39
39
 
@@ -41,7 +41,7 @@ import {NextResponse} from "next/server";
41
41
  import {nonceManager} from "@/[wherever you store your nonceManager instance]";
42
42
 
43
43
  export async function POST(req: Request) {
44
- const nonce = req.headers.get("x-api-nonce");
44
+ const nonce: boolean = req.headers.get("x-api-nonce");
45
45
 
46
46
  if (!nonce) {
47
47
  return NextResponse.json(
@@ -61,15 +61,25 @@ export async function POST(req: Request) {
61
61
  or more simple
62
62
  ```typescript
63
63
  'use server'
64
- mport {NextResponse} from "next/server";
64
+ import {NextResponse} from "next/server";
65
65
  import {nonceManager} from "@/[wherever you store your nonceManager instance]";
66
+ import {NonceCheckResult} from "@tozielinski/next-upstash-nonce";
66
67
 
67
68
  export async function POST(req: Request) {
68
- const result = await nonceManager.verifyAndDeleteNonceFromRequest(req);
69
+ const result: NonceCheckResult = await nonceManager.verifyAndDeleteNonceFromRequest(req);
69
70
 
70
71
  // result will be {nonce: string, valid: true} or
71
72
  // {valid false, reason: string, response: NextResponse}
72
- // if nonce was not found or expired
73
+ // if nonce was not found or expired:
74
+ //
75
+ // export type NonceCheckResult = | {
76
+ // valid: true;
77
+ // nonce: string
78
+ // } | {
79
+ // valid: false;
80
+ // reason: 'missing-header' | 'invalid-or-expired';
81
+ // response: Response
82
+ // };
73
83
 
74
84
  return NextResponse.json({nonce: result.nonce, valid: result.valid});
75
85
  }
@@ -83,7 +93,7 @@ import {createNonce} from "@/[wherever you store your server action]";
83
93
 
84
94
  export default function NonceSecuredComponent() {
85
95
  const [running, setRunning] = useState(false);
86
- const [message, setMessage] = useState("");
96
+ const [message, setMessage] = useState('');
87
97
 
88
98
  const handleClick = async () => {
89
99
  if (running) return;
@@ -123,4 +133,32 @@ export default function NonceSecuredComponent() {
123
133
  )
124
134
  }
125
135
  ```
136
+ ### Secure your API endpoint with a Rate Limiter
137
+ ```typescript
138
+ 'use server'
139
+ import {NextResponse} from "next/server";
140
+ import {nonceManager} from "@/[wherever you store your nonceManager instance]";
141
+ import {RateLimitResult} from "@tozielinski/next-upstash-nonce";
126
142
 
143
+ export async function POST(req: Request) {
144
+ const rateLimiter: RateLimitResult = await nonceManager.rateLimiter(req!);
145
+
146
+ // result will be {valid: true, ip: string, requests: number} or
147
+ // {valid false, ip:string, requests: number, reason: string, response: NextResponse}
148
+ // if rate limit was reached or broken:
149
+ //
150
+ // export type RateLimitResult = | {
151
+ // valid: true;
152
+ // ip: string;
153
+ // requests: number;
154
+ // } | {
155
+ // valid: false;
156
+ // ip: string;
157
+ // requests: number;
158
+ // reason: `too-many-requests: ${number}`;
159
+ // response: Response;
160
+ // };
161
+
162
+ return NextResponse.json({valid: result.valid, ip: result.ip, requests: result.requests});
163
+ }
164
+ ```
package/dist/index.d.ts CHANGED
@@ -1,23 +1,44 @@
1
1
  import { Redis } from "@upstash/redis";
2
2
  export type NonceOptions = {
3
- length?: number;
4
- ttlSeconds?: number;
3
+ ttlNonce?: number;
5
4
  prefix?: string;
5
+ ttlRateLimit?: number;
6
+ countRateLimit?: number;
6
7
  };
7
8
  export type NonceCheckResult = {
8
9
  valid: true;
9
10
  nonce: string;
10
11
  } | {
11
12
  valid: false;
12
- reason: "missing-header" | "invalid-or-expired";
13
+ reason: 'missing-header' | 'invalid-or-expired';
14
+ response: Response;
15
+ };
16
+ export type RateLimitResult = {
17
+ valid: true;
18
+ ip: string;
19
+ requests: number;
20
+ } | {
21
+ valid: false;
22
+ ip: string;
23
+ requests: number;
24
+ reason: `too-many-requests: ${number}`;
13
25
  response: Response;
14
26
  };
15
27
  export declare class NonceManager {
16
28
  private redis;
17
- private length;
18
- private ttlSeconds;
29
+ private ttlNonce;
19
30
  private prefix;
31
+ private ttlRateLimit;
32
+ private countRateLimit;
20
33
  constructor(redis: Redis, opts?: NonceOptions);
34
+ /**
35
+ * makes option parameters available in the server in environment files
36
+ */
37
+ private getEnvValue;
38
+ /**
39
+ * extracts client IP from request headers
40
+ */
41
+ private getClientIp;
21
42
  /**
22
43
  * generates a new, secure nonce,
23
44
  * inserts it into Redis with a TTL,
@@ -48,6 +69,10 @@ export declare class NonceManager {
48
69
  * Optional: delete a nonce from Redis manually
49
70
  */
50
71
  delete(nonce: string): Promise<void>;
72
+ /**
73
+ * rate limits requests from the same IP address
74
+ */
75
+ rateLimiter(req: Request): Promise<RateLimitResult>;
51
76
  }
52
77
  export default NonceManager;
53
78
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,MAAM,MAAM,YAAY,GAAG;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAK;IAC7B,KAAK,EAAE,IAAI,CAAC;IACZ,KAAK,EAAE,MAAM,CAAA;CAChB,GAAG;IACA,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,gBAAgB,GAAG,oBAAoB,CAAC;IAChD,QAAQ,EAAE,QAAQ,CAAA;CACrB,CAAC;AAEF,qBAAa,YAAY;IACrB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAS;gBAGX,KAAK,EAAE,KAAK,EAAE,IAAI,GAAE,YAAiB;IAQjD;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAS/B;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa7C;;;OAGG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAetD;;;OAGG;IACG,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoCrE;;;OAGG;IACG,+BAA+B,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoC9E;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAI7C;AAGD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,MAAM,MAAM,YAAY,GAAG;IAEvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAK;IAC7B,KAAK,EAAE,IAAI,CAAC;IACZ,KAAK,EAAE,MAAM,CAAA;CAChB,GAAG;IACA,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,gBAAgB,GAAG,oBAAoB,CAAC;IAChD,QAAQ,EAAE,QAAQ,CAAA;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAK;IAC5B,KAAK,EAAE,IAAI,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;CACpB,GAAG;IACA,KAAK,EAAE,KAAK,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,sBAAsB,MAAM,EAAE,CAAC;IACvC,QAAQ,EAAE,QAAQ,CAAC;CACtB,CAAC;AAEF,qBAAa,YAAY;IACrB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,cAAc,CAAS;gBAGnB,KAAK,EAAE,KAAK,EAAE,IAAI,GAAE,YAAiB;IAQjD;;OAEG;IACH,OAAO,CAAC,WAAW;IAWnB;;OAEG;IACH,OAAO,CAAC,WAAW;IAanB;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAU/B;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa7C;;;OAGG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAetD;;;OAGG;IACG,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoCrE;;;OAGG;IACG,+BAA+B,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoC9E;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM1C;;OAEG;IACG,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;CAgB5D;AAED,eAAe,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -5,14 +5,41 @@ exports.NonceManager = void 0;
5
5
  const uuid_1 = require("uuid");
6
6
  class NonceManager {
7
7
  redis;
8
- length;
9
- ttlSeconds;
8
+ ttlNonce;
10
9
  prefix;
10
+ ttlRateLimit;
11
+ countRateLimit;
11
12
  constructor(redis, opts = {}) {
12
13
  this.redis = redis;
13
- this.length = opts.length ?? 32; // default 32 bytes -> 64 hex chars
14
- this.ttlSeconds = opts.ttlSeconds ?? 60 * 5; // default 5 minutes
14
+ this.ttlNonce = opts.ttlNonce ?? 60; // default 1 minute
15
15
  this.prefix = opts.prefix ?? "nonce:";
16
+ this.ttlRateLimit = opts.ttlRateLimit ?? 60; // default 1 minute
17
+ this.countRateLimit = opts.countRateLimit ?? 5; // default 5 times
18
+ }
19
+ /**
20
+ * makes option parameters available in the server in environment files
21
+ */
22
+ getEnvValue(name, fallback) {
23
+ const val = process.env[name];
24
+ const num = Number(val);
25
+ if (val === undefined || isNaN(num)) {
26
+ return fallback;
27
+ }
28
+ return num;
29
+ }
30
+ /**
31
+ * extracts client IP from request headers
32
+ */
33
+ getClientIp(req) {
34
+ const forwardedFor = req.headers.get("x-forwarded-for");
35
+ if (forwardedFor) {
36
+ return forwardedFor.split(",")[0].trim();
37
+ }
38
+ const realIp = req.headers.get("x-real-ip");
39
+ if (realIp) {
40
+ return realIp;
41
+ }
42
+ return "unknown";
16
43
  }
17
44
  /**
18
45
  * generates a new, secure nonce,
@@ -22,7 +49,8 @@ class NonceManager {
22
49
  async create() {
23
50
  const nonce = (0, uuid_1.v4)();
24
51
  const key = this.prefix + nonce;
25
- await this.redis.set(key, "1", { ex: this.ttlSeconds });
52
+ const ttl = this.getEnvValue("NONCE_TTL_SECONDS", this.ttlNonce);
53
+ await this.redis.set(key, "1", { ex: ttl });
26
54
  return nonce;
27
55
  }
28
56
  /**
@@ -123,6 +151,22 @@ class NonceManager {
123
151
  const key = this.prefix + nonce;
124
152
  await this.redis.del(key);
125
153
  }
154
+ /**
155
+ * rate limits requests from the same IP address
156
+ */
157
+ async rateLimiter(req) {
158
+ const ip = this.getClientIp(req);
159
+ const key = `rate:${ip}`;
160
+ const requests = (await this.redis.incr(key)) ?? 0;
161
+ if (requests === 1) {
162
+ await this.redis.expire(key, this.getEnvValue("RATE_LIMIT_TTL_SECONDS", this.ttlRateLimit)); // counter runs 60s
163
+ }
164
+ if (requests > this.getEnvValue("RATE_LIMIT_COUNT", this.countRateLimit)) {
165
+ const response = Response.json({ error: "Too many requests" }, { status: 429 });
166
+ return { valid: false, ip, requests, reason: `too-many-requests: ${requests}`, response: response };
167
+ }
168
+ return { valid: true, ip, requests };
169
+ }
126
170
  }
127
171
  exports.NonceManager = NonceManager;
128
172
  exports.default = NonceManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tozielinski/next-upstash-nonce",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Create, store, verify and delete nonces in Redis by Upstash for Next.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",