@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 +14 -0
- package/README.md +61 -4
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -5
- package/package.json +1 -1
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
|
[](https://opensource.org/licenses/MIT)
|
|
3
3
|
[](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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
-
|
|
9
|
-
ttlSeconds;
|
|
8
|
+
ttlNonce;
|
|
10
9
|
prefix;
|
|
10
|
+
ttlRateLimit;
|
|
11
|
+
countRateLimit;
|
|
11
12
|
constructor(redis, opts = {}) {
|
|
12
13
|
this.redis = redis;
|
|
13
|
-
this.
|
|
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
|
-
|
|
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