@tozielinski/next-upstash-nonce 1.3.0 → 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,19 @@
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
+
10
+ ## [1.3.1](https://github.com/tozielinski/next-upstash-nonce/compare/v1.3.0...v1.3.1) (2025-11-22)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * enable type generation ([6df83aa](https://github.com/tozielinski/next-upstash-nonce/commit/6df83aa769f367481231b21f7cac0d3c0297af40))
16
+
3
17
  ## [1.3.0](https://github.com/tozielinski/next-upstash-nonce/compare/v1.2.3...v1.3.0) (2025-11-22)
4
18
 
5
19
 
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(
@@ -52,9 +52,38 @@ export async function POST(req: Request) {
52
52
 
53
53
  const valid = await nonceManager.verifyAndDelete(nonce);
54
54
 
55
+ // valid will be true if nonce was found and deleted
56
+ // false if nonce was not found or expired
57
+
55
58
  return NextResponse.json({nonce: nonce, valid: valid});
56
59
  }
57
60
  ```
61
+ or more simple
62
+ ```typescript
63
+ 'use server'
64
+ import {NextResponse} from "next/server";
65
+ import {nonceManager} from "@/[wherever you store your nonceManager instance]";
66
+ import {NonceCheckResult} from "@tozielinski/next-upstash-nonce";
67
+
68
+ export async function POST(req: Request) {
69
+ const result: NonceCheckResult = await nonceManager.verifyAndDeleteNonceFromRequest(req);
70
+
71
+ // result will be {nonce: string, valid: true} or
72
+ // {valid false, reason: string, response: NextResponse}
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
+ // };
83
+
84
+ return NextResponse.json({nonce: result.nonce, valid: result.valid});
85
+ }
86
+ ```
58
87
  ### Use it in your client side
59
88
  ```typescript
60
89
  'use client'
@@ -64,7 +93,7 @@ import {createNonce} from "@/[wherever you store your server action]";
64
93
 
65
94
  export default function NonceSecuredComponent() {
66
95
  const [running, setRunning] = useState(false);
67
- const [message, setMessage] = useState("");
96
+ const [message, setMessage] = useState('');
68
97
 
69
98
  const handleClick = async () => {
70
99
  if (running) return;
@@ -104,4 +133,32 @@ export default function NonceSecuredComponent() {
104
133
  )
105
134
  }
106
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";
107
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
+ ```
@@ -0,0 +1,78 @@
1
+ import { Redis } from "@upstash/redis";
2
+ export type NonceOptions = {
3
+ ttlNonce?: number;
4
+ prefix?: string;
5
+ ttlRateLimit?: number;
6
+ countRateLimit?: number;
7
+ };
8
+ export type NonceCheckResult = {
9
+ valid: true;
10
+ nonce: string;
11
+ } | {
12
+ valid: false;
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}`;
25
+ response: Response;
26
+ };
27
+ export declare class NonceManager {
28
+ private redis;
29
+ private ttlNonce;
30
+ private prefix;
31
+ private ttlRateLimit;
32
+ private countRateLimit;
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;
42
+ /**
43
+ * generates a new, secure nonce,
44
+ * inserts it into Redis with a TTL,
45
+ * and returns the nonce string.
46
+ */
47
+ create(): Promise<string>;
48
+ /**
49
+ * verifies a nonce and deletes it from Redis,
50
+ * returning true if the nonce exists and has not expired.
51
+ */
52
+ verify(nonce: string): Promise<boolean>;
53
+ /**
54
+ * verifies a nonce and deletes it from Redis,
55
+ * returning true if the nonce exists and has not expired.
56
+ */
57
+ verifyAndDelete(nonce: string): Promise<boolean>;
58
+ /**
59
+ * verifies a nonce from the Header of a request
60
+ * returns a NonceCheckResult if the nonce exists and has not expired.
61
+ */
62
+ verifyNonceFromRequest(req: Request): Promise<NonceCheckResult>;
63
+ /**
64
+ * verifies a nonce from the Header of a request and deletes it from Redis
65
+ * returns a NonceCheckResult if the nonce exists and has not expired.
66
+ */
67
+ verifyAndDeleteNonceFromRequest(req: Request): Promise<NonceCheckResult>;
68
+ /**
69
+ * Optional: delete a nonce from Redis manually
70
+ */
71
+ delete(nonce: string): Promise<void>;
72
+ /**
73
+ * rate limits requests from the same IP address
74
+ */
75
+ rateLimiter(req: Request): Promise<RateLimitResult>;
76
+ }
77
+ export default NonceManager;
78
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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;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.0",
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",