@riocrypto/common-server 1.0.2696 → 1.0.2698

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/build/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from "./middlewares/require-min-admin-role";
9
9
  export * from "./middlewares/verify-csrf-token";
10
10
  export * from "./middlewares/dynamic-rate-limiter";
11
11
  export * from "./middlewares/auth-context";
12
+ export * from "./middlewares/verify-turnstile";
12
13
  export * from "./services/apiKey";
13
14
  export * from "./services/password";
14
15
  export * from "./services/logger";
package/build/index.js CHANGED
@@ -25,6 +25,7 @@ __exportStar(require("./middlewares/require-min-admin-role"), exports);
25
25
  __exportStar(require("./middlewares/verify-csrf-token"), exports);
26
26
  __exportStar(require("./middlewares/dynamic-rate-limiter"), exports);
27
27
  __exportStar(require("./middlewares/auth-context"), exports);
28
+ __exportStar(require("./middlewares/verify-turnstile"), exports);
28
29
  __exportStar(require("./services/apiKey"), exports);
29
30
  __exportStar(require("./services/password"), exports);
30
31
  __exportStar(require("./services/logger"), exports);
@@ -0,0 +1,44 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ interface TurnstileConfig {
3
+ /**
4
+ * The Turnstile secret key (from Cloudflare dashboard)
5
+ * Can be obtained from: https://dash.cloudflare.com/?to=/:account/turnstile
6
+ */
7
+ secretKey: string;
8
+ /**
9
+ * Field name in request body that contains the turnstile token
10
+ * Default: "turnstileToken"
11
+ */
12
+ tokenField?: string;
13
+ /**
14
+ * Whether to skip verification in development/test environments
15
+ * Default: false
16
+ */
17
+ skipInDevelopment?: boolean;
18
+ /**
19
+ * Custom error message when verification fails
20
+ */
21
+ errorMessage?: string;
22
+ /**
23
+ * Logger function for errors
24
+ */
25
+ logger?: {
26
+ warn: (msg: object) => void;
27
+ error: (msg: object) => void;
28
+ };
29
+ }
30
+ /**
31
+ * Creates a middleware that verifies Cloudflare Turnstile tokens
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const verifyTurnstile = createTurnstileMiddleware({
36
+ * secretKey: process.env.TURNSTILE_SECRET_KEY!,
37
+ * skipInDevelopment: process.env.NODE_ENV !== 'production',
38
+ * });
39
+ *
40
+ * app.post('/api/auth/send-code', verifyTurnstile, sendCodeHandler);
41
+ * ```
42
+ */
43
+ export declare function createTurnstileMiddleware(config: TurnstileConfig): (req: Request, res: Response, next: NextFunction) => Promise<void>;
44
+ export { TurnstileConfig };
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.createTurnstileMiddleware = void 0;
16
+ const axios_1 = __importDefault(require("axios"));
17
+ const TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
18
+ /**
19
+ * Normalize IP address (handle IPv4-mapped IPv6)
20
+ */
21
+ function normalizeIp(ip) {
22
+ if (!ip)
23
+ return undefined;
24
+ if (ip.startsWith("::ffff:")) {
25
+ return ip.substring(7);
26
+ }
27
+ return ip;
28
+ }
29
+ /**
30
+ * Get client IP from request, prioritizing Cloudflare headers
31
+ */
32
+ function getClientIp(req) {
33
+ const cfConnectingIp = req.headers["cf-connecting-ip"];
34
+ const xForwardedFor = req.headers["x-forwarded-for"];
35
+ if (cfConnectingIp) {
36
+ return normalizeIp(cfConnectingIp);
37
+ }
38
+ if (xForwardedFor) {
39
+ return normalizeIp(xForwardedFor.split(",")[0].trim());
40
+ }
41
+ return normalizeIp(req.ip);
42
+ }
43
+ /**
44
+ * Creates a middleware that verifies Cloudflare Turnstile tokens
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const verifyTurnstile = createTurnstileMiddleware({
49
+ * secretKey: process.env.TURNSTILE_SECRET_KEY!,
50
+ * skipInDevelopment: process.env.NODE_ENV !== 'production',
51
+ * });
52
+ *
53
+ * app.post('/api/auth/send-code', verifyTurnstile, sendCodeHandler);
54
+ * ```
55
+ */
56
+ function createTurnstileMiddleware(config) {
57
+ const { secretKey, tokenField = "turnstileToken", skipInDevelopment = false, errorMessage = "Human verification failed. Please try again.", logger, } = config;
58
+ if (!secretKey) {
59
+ throw new Error("[Turnstile] Secret key is required. Get it from https://dash.cloudflare.com/?to=/:account/turnstile");
60
+ }
61
+ return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
62
+ var _a;
63
+ // Skip in development if configured
64
+ if (skipInDevelopment) {
65
+ return next();
66
+ }
67
+ const token = (_a = req.body) === null || _a === void 0 ? void 0 : _a[tokenField];
68
+ if (!token) {
69
+ logger === null || logger === void 0 ? void 0 : logger.warn({
70
+ message: "Turnstile token missing from request",
71
+ path: req.path,
72
+ ip: getClientIp(req),
73
+ });
74
+ res.status(400).json({
75
+ error: "Verification token is required.",
76
+ });
77
+ return;
78
+ }
79
+ try {
80
+ const remoteIp = getClientIp(req);
81
+ const response = yield axios_1.default.post(TURNSTILE_VERIFY_URL, new URLSearchParams(Object.assign({ secret: secretKey, response: token }, (remoteIp && { remoteip: remoteIp }))), {
82
+ headers: {
83
+ "Content-Type": "application/x-www-form-urlencoded",
84
+ },
85
+ timeout: 10000, // 10 second timeout
86
+ });
87
+ if (response.data.success) {
88
+ // Verification successful
89
+ return next();
90
+ }
91
+ // Verification failed
92
+ logger === null || logger === void 0 ? void 0 : logger.warn({
93
+ message: "Turnstile verification failed",
94
+ path: req.path,
95
+ ip: remoteIp,
96
+ errorCodes: response.data["error-codes"],
97
+ });
98
+ res.status(403).json({
99
+ error: errorMessage,
100
+ codes: response.data["error-codes"],
101
+ });
102
+ }
103
+ catch (error) {
104
+ logger === null || logger === void 0 ? void 0 : logger.error({
105
+ message: "Error verifying Turnstile token",
106
+ path: req.path,
107
+ ip: getClientIp(req),
108
+ error: error instanceof Error ? error.message : String(error),
109
+ });
110
+ // On error, we could either:
111
+ // 1. Block the request (more secure)
112
+ // 2. Allow it through (better UX but less secure)
113
+ // Here we block to be safe
114
+ res.status(500).json({
115
+ error: "Verification service unavailable. Please try again.",
116
+ });
117
+ }
118
+ });
119
+ }
120
+ exports.createTurnstileMiddleware = createTurnstileMiddleware;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common-server",
3
- "version": "1.0.2696",
3
+ "version": "1.0.2698",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",