@tozielinski/next-upstash-nonce 0.1.4
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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +67 -0
- package/package.json +35 -0
- package/src/index.ts +82 -0
- package/tsconfig.json +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Torsten Zielinski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# next-upstash-nonce
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
Create, store, verify and delete nonces in Redis by Upstash for Next.js
|
|
6
|
+
|
|
7
|
+
# Quick Start
|
|
8
|
+
### Install the package:
|
|
9
|
+
```bash
|
|
10
|
+
npm install @tozielinski/next-upstash-nonce
|
|
11
|
+
```
|
|
12
|
+
### Create database
|
|
13
|
+
Create a new redis database on [upstash](https://console.upstash.com/)
|
|
14
|
+
### Create a NonceManager Instance
|
|
15
|
+
```typescript
|
|
16
|
+
import { NonceManager } from '@tozielinski/next-upstash-nonce'
|
|
17
|
+
import { Redis } from "@upstash/redis";
|
|
18
|
+
|
|
19
|
+
const redis = new Redis({
|
|
20
|
+
url: process.env.UPSTASH_REDIS_REST_URL as string,
|
|
21
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN as string,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const nonceManager = new NonceManager(redis, {ttlSeconds: 60* 5});
|
|
25
|
+
```
|
|
26
|
+
### Create a ServerAction, to use it from client side
|
|
27
|
+
```typescript
|
|
28
|
+
'use server'
|
|
29
|
+
|
|
30
|
+
import {nonceManager} from "@/[wherever you store your nonceManager instance]";
|
|
31
|
+
|
|
32
|
+
export async function createNonce(): Promise<string> {
|
|
33
|
+
return await nonceManager.create();
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
### Secure your API endpoint
|
|
37
|
+
```typescript
|
|
38
|
+
'use server'
|
|
39
|
+
|
|
40
|
+
import {NextResponse} from "next/server";
|
|
41
|
+
import {nonceManager} from "@/[wherever you store your nonceManager instance]";
|
|
42
|
+
|
|
43
|
+
export async function POST(req: Request) {
|
|
44
|
+
const nonce = req.headers.get("x-api-nonce");
|
|
45
|
+
|
|
46
|
+
if (!nonce) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: "Missing nonce", valid: false },
|
|
49
|
+
{ status: 401 }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const valid = await nonceManager.verifyAndDelete(nonce);
|
|
54
|
+
|
|
55
|
+
return NextResponse.json({nonce: nonce, valid: valid});
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
### Use it in your client side
|
|
59
|
+
```typescript
|
|
60
|
+
'use client'
|
|
61
|
+
|
|
62
|
+
import {useState} from "react";
|
|
63
|
+
import {createNonce} from "@/[wherever you store your server action]";
|
|
64
|
+
|
|
65
|
+
export default function NonceSecuredComponent() {
|
|
66
|
+
const [running, setRunning] = useState(false);
|
|
67
|
+
const [message, setMessage] = useState("");
|
|
68
|
+
|
|
69
|
+
const handleClick = async () => {
|
|
70
|
+
if (running) return;
|
|
71
|
+
setRunning(true);
|
|
72
|
+
setMessage("Starting SSA...");
|
|
73
|
+
|
|
74
|
+
const nonce = await createNonce();
|
|
75
|
+
|
|
76
|
+
const res = await fetch('/api/[name of your API endpoint]', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
'X-API-Nonce': nonce,
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({success: true}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (res.ok) {
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
setMessage(`Nonce: ${data.nonce} Valid: ${data.valid}` || "No nonce received");
|
|
88
|
+
setRunning(false);
|
|
89
|
+
} else
|
|
90
|
+
setMessage(res.statusText);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div>
|
|
95
|
+
<button
|
|
96
|
+
onClick={handleClick}
|
|
97
|
+
disabled={running}
|
|
98
|
+
className="px-6 py-3 rounded-xl bg-blue-600 text-white disabled:opacity-50"
|
|
99
|
+
>
|
|
100
|
+
{running ? "Running..." : "Start SSA"}
|
|
101
|
+
</button>
|
|
102
|
+
<p>{message}</p>
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Redis } from "@upstash/redis";
|
|
2
|
+
export type NonceOptions = {
|
|
3
|
+
length?: number;
|
|
4
|
+
ttlSeconds?: number;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare class NonceManager {
|
|
8
|
+
private redis;
|
|
9
|
+
private length;
|
|
10
|
+
private ttlSeconds;
|
|
11
|
+
private prefix;
|
|
12
|
+
constructor(redis: Redis, opts?: NonceOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Generiert einen neuen, kryptographisch sicheren Nonce,
|
|
15
|
+
* speichert ihn in Redis und gibt ihn zurück.
|
|
16
|
+
*/
|
|
17
|
+
create(): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Verifiziert einen Nonce: prüft ob vorhanden, und löscht ihn atomisch.
|
|
20
|
+
* Gibt true zurück, wenn Validierung erfolgreich war (Nonce existierte und wurde entfernt).
|
|
21
|
+
*/
|
|
22
|
+
verifyAndDelete(nonce: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Optional: entferne einen Nonce ohne Verifizierung
|
|
25
|
+
*/
|
|
26
|
+
delete(nonce: string): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export default NonceManager;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use server';
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.NonceManager = void 0;
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
class NonceManager {
|
|
10
|
+
constructor(redis, opts = {}) {
|
|
11
|
+
this.redis = redis;
|
|
12
|
+
this.length = opts.length ?? 32; // default 32 bytes -> 64 hex chars
|
|
13
|
+
this.ttlSeconds = opts.ttlSeconds ?? 60 * 5; // default 5 minutes
|
|
14
|
+
this.prefix = opts.prefix ?? "nonce:";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generiert einen neuen, kryptographisch sicheren Nonce,
|
|
18
|
+
* speichert ihn in Redis und gibt ihn zurück.
|
|
19
|
+
*/
|
|
20
|
+
async create() {
|
|
21
|
+
const buffer = crypto_1.default.randomBytes(this.length);
|
|
22
|
+
const nonce = buffer.toString("hex");
|
|
23
|
+
const key = this.prefix + nonce;
|
|
24
|
+
console.log("creating nonce:", nonce);
|
|
25
|
+
// set with ttl (nx not required — collisions extremely unlikely)
|
|
26
|
+
await this.redis.set(key, "1", { ex: this.ttlSeconds });
|
|
27
|
+
return nonce;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Verifiziert einen Nonce: prüft ob vorhanden, und löscht ihn atomisch.
|
|
31
|
+
* Gibt true zurück, wenn Validierung erfolgreich war (Nonce existierte und wurde entfernt).
|
|
32
|
+
*/
|
|
33
|
+
async verifyAndDelete(nonce) {
|
|
34
|
+
if (!nonce)
|
|
35
|
+
return false;
|
|
36
|
+
// const key = this.prefix + nonce;
|
|
37
|
+
//
|
|
38
|
+
// const script = `
|
|
39
|
+
// local v = redis.call('GET', KEYS[1])
|
|
40
|
+
// if v then
|
|
41
|
+
// redis.call('DEL', KEYS[1])
|
|
42
|
+
// return v
|
|
43
|
+
// end
|
|
44
|
+
// return nil
|
|
45
|
+
// `;
|
|
46
|
+
try {
|
|
47
|
+
const res = await this.redis.get(`nonce:${nonce}`);
|
|
48
|
+
// const res = await (this.redis as any).eval(script, { keys: [key] });
|
|
49
|
+
if (res)
|
|
50
|
+
await this.redis.del(`nonce:${nonce}`);
|
|
51
|
+
return res !== null;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error("verifyAndDelete error:", err);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Optional: entferne einen Nonce ohne Verifizierung
|
|
60
|
+
*/
|
|
61
|
+
async delete(nonce) {
|
|
62
|
+
const key = this.prefix + nonce;
|
|
63
|
+
await this.redis.del(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.NonceManager = NonceManager;
|
|
67
|
+
exports.default = NonceManager;
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tozielinski/next-upstash-nonce",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Create, store, verify and delete nonces in Redis by Upstash for Next.js",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/tozielinski/next-upstash-nonce.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"next",
|
|
16
|
+
"nextjs",
|
|
17
|
+
"upstash",
|
|
18
|
+
"nonce",
|
|
19
|
+
"redis"
|
|
20
|
+
],
|
|
21
|
+
"author": "Torsten Zielinski",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/tozielinski/next-upstash-nonce/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/tozielinski/next-upstash-nonce#readme",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@upstash/redis": "1.22.0",
|
|
29
|
+
"crypto": "^1.0.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"typescript": "^5.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { Redis } from "@upstash/redis";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
|
|
6
|
+
export type NonceOptions = {
|
|
7
|
+
length?: number; // bytes
|
|
8
|
+
ttlSeconds?: number; // Time-to-live in Redis
|
|
9
|
+
prefix?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class NonceManager {
|
|
13
|
+
private redis: Redis;
|
|
14
|
+
private length: number;
|
|
15
|
+
private ttlSeconds: number;
|
|
16
|
+
private prefix: string;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
constructor(redis: Redis, opts: NonceOptions = {}) {
|
|
20
|
+
this.redis = redis;
|
|
21
|
+
this.length = opts.length ?? 32; // default 32 bytes -> 64 hex chars
|
|
22
|
+
this.ttlSeconds = opts.ttlSeconds ?? 60 * 5; // default 5 minutes
|
|
23
|
+
this.prefix = opts.prefix ?? "nonce:";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generiert einen neuen, kryptographisch sicheren Nonce,
|
|
29
|
+
* speichert ihn in Redis und gibt ihn zurück.
|
|
30
|
+
*/
|
|
31
|
+
async create(): Promise<string> {
|
|
32
|
+
const buffer = crypto.randomBytes(this.length);
|
|
33
|
+
const nonce = buffer.toString("hex");
|
|
34
|
+
const key = this.prefix + nonce;
|
|
35
|
+
|
|
36
|
+
console.log("creating nonce:", nonce);
|
|
37
|
+
// set with ttl (nx not required — collisions extremely unlikely)
|
|
38
|
+
await this.redis.set(key, "1", { ex: this.ttlSeconds });
|
|
39
|
+
return nonce;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Verifiziert einen Nonce: prüft ob vorhanden, und löscht ihn atomisch.
|
|
45
|
+
* Gibt true zurück, wenn Validierung erfolgreich war (Nonce existierte und wurde entfernt).
|
|
46
|
+
*/
|
|
47
|
+
async verifyAndDelete(nonce: string): Promise<boolean> {
|
|
48
|
+
if (!nonce) return false;
|
|
49
|
+
// const key = this.prefix + nonce;
|
|
50
|
+
//
|
|
51
|
+
// const script = `
|
|
52
|
+
// local v = redis.call('GET', KEYS[1])
|
|
53
|
+
// if v then
|
|
54
|
+
// redis.call('DEL', KEYS[1])
|
|
55
|
+
// return v
|
|
56
|
+
// end
|
|
57
|
+
// return nil
|
|
58
|
+
// `;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const res = await (this.redis as any).get(`nonce:${nonce}`);
|
|
62
|
+
// const res = await (this.redis as any).eval(script, { keys: [key] });
|
|
63
|
+
if (res) await this.redis.del(`nonce:${nonce}`);
|
|
64
|
+
return res !== null;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error("verifyAndDelete error:", err);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Optional: entferne einen Nonce ohne Verifizierung
|
|
74
|
+
*/
|
|
75
|
+
async delete(nonce: string): Promise<void> {
|
|
76
|
+
const key = this.prefix + nonce;
|
|
77
|
+
await this.redis.del(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
export default NonceManager;
|
package/tsconfig.json
ADDED