@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 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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+
@@ -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
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "commonjs",
5
+ "declaration": true,
6
+ "outDir": "dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "forceConsistentCasingInFileNames": true
10
+ },
11
+ "include": ["src/**/*"]
12
+ }