@silverassist/recaptcha 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,206 @@
1
+ import Script from 'next/script';
2
+ import { useRef, useCallback, useEffect } from 'react';
3
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
4
+
5
+ // src/client/index.tsx
6
+
7
+ // src/constants/index.ts
8
+ var DEFAULT_SCORE_THRESHOLD = 0.5;
9
+ var DEFAULT_TOKEN_REFRESH_INTERVAL = 9e4;
10
+ var RECAPTCHA_CONFIG = {
11
+ /** Google reCAPTCHA verification endpoint */
12
+ verifyUrl: "https://www.google.com/recaptcha/api/siteverify",
13
+ /** Default score threshold for validation */
14
+ defaultScoreThreshold: DEFAULT_SCORE_THRESHOLD,
15
+ /** Default token refresh interval */
16
+ tokenRefreshInterval: DEFAULT_TOKEN_REFRESH_INTERVAL
17
+ };
18
+ function RecaptchaWrapper({
19
+ action,
20
+ inputName = "recaptchaToken",
21
+ inputId = "recaptcha-token",
22
+ siteKey: propSiteKey,
23
+ refreshInterval = RECAPTCHA_CONFIG.tokenRefreshInterval,
24
+ onTokenGenerated,
25
+ onError
26
+ }) {
27
+ const siteKey = propSiteKey ?? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
28
+ const tokenInputRef = useRef(null);
29
+ const refreshIntervalRef = useRef(null);
30
+ const executeRecaptcha = useCallback(async () => {
31
+ if (!siteKey) {
32
+ return;
33
+ }
34
+ try {
35
+ if (typeof window !== "undefined" && window.grecaptcha) {
36
+ window.grecaptcha.ready(async () => {
37
+ try {
38
+ const token = await window.grecaptcha.execute(siteKey, { action });
39
+ if (tokenInputRef.current) {
40
+ tokenInputRef.current.value = token;
41
+ }
42
+ if (onTokenGenerated) {
43
+ onTokenGenerated(token);
44
+ }
45
+ } catch (error) {
46
+ console.error("[reCAPTCHA] Error executing reCAPTCHA:", error);
47
+ if (onError && error instanceof Error) {
48
+ onError(error);
49
+ }
50
+ }
51
+ });
52
+ }
53
+ } catch (error) {
54
+ console.error("[reCAPTCHA] Error:", error);
55
+ if (onError && error instanceof Error) {
56
+ onError(error);
57
+ }
58
+ }
59
+ }, [siteKey, action, onTokenGenerated, onError]);
60
+ useEffect(() => {
61
+ executeRecaptcha();
62
+ refreshIntervalRef.current = setInterval(() => {
63
+ executeRecaptcha();
64
+ }, refreshInterval);
65
+ return () => {
66
+ if (refreshIntervalRef.current) {
67
+ clearInterval(refreshIntervalRef.current);
68
+ }
69
+ };
70
+ }, [executeRecaptcha, refreshInterval]);
71
+ if (!siteKey) {
72
+ if (process.env.NODE_ENV === "development") {
73
+ console.warn(
74
+ "[reCAPTCHA] Site key not configured. Set NEXT_PUBLIC_RECAPTCHA_SITE_KEY environment variable."
75
+ );
76
+ }
77
+ return null;
78
+ }
79
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
80
+ /* @__PURE__ */ jsx(
81
+ "input",
82
+ {
83
+ ref: tokenInputRef,
84
+ type: "hidden",
85
+ name: inputName,
86
+ id: inputId,
87
+ "data-testid": "recaptcha-token-input"
88
+ }
89
+ ),
90
+ /* @__PURE__ */ jsx(
91
+ Script,
92
+ {
93
+ src: `https://www.google.com/recaptcha/api.js?render=${siteKey}`,
94
+ strategy: "afterInteractive",
95
+ onLoad: () => {
96
+ executeRecaptcha();
97
+ },
98
+ onError: () => {
99
+ console.error("[reCAPTCHA] Failed to load reCAPTCHA script");
100
+ if (onError) {
101
+ onError(new Error("Failed to load reCAPTCHA script"));
102
+ }
103
+ }
104
+ }
105
+ )
106
+ ] });
107
+ }
108
+
109
+ // src/server/index.ts
110
+ async function validateRecaptcha(token, expectedAction, options = {}) {
111
+ const {
112
+ scoreThreshold = DEFAULT_SCORE_THRESHOLD,
113
+ secretKey = process.env.RECAPTCHA_SECRET_KEY,
114
+ debug = process.env.NODE_ENV === "development"
115
+ } = options;
116
+ if (!secretKey) {
117
+ if (debug) {
118
+ console.warn(
119
+ "[reCAPTCHA] Secret key not configured. Skipping validation."
120
+ );
121
+ }
122
+ return {
123
+ success: true,
124
+ score: 1,
125
+ skipped: true
126
+ };
127
+ }
128
+ if (!token) {
129
+ return {
130
+ success: false,
131
+ score: 0,
132
+ error: "reCAPTCHA token is missing"
133
+ };
134
+ }
135
+ try {
136
+ const response = await fetch(RECAPTCHA_CONFIG.verifyUrl, {
137
+ method: "POST",
138
+ headers: {
139
+ "Content-Type": "application/x-www-form-urlencoded"
140
+ },
141
+ body: new URLSearchParams({
142
+ secret: secretKey,
143
+ response: token
144
+ })
145
+ });
146
+ if (!response.ok) {
147
+ throw new Error(`HTTP error! status: ${response.status}`);
148
+ }
149
+ const data = await response.json();
150
+ if (debug) {
151
+ console.log("[reCAPTCHA] Verification response:", {
152
+ success: data.success,
153
+ score: data.score,
154
+ action: data.action,
155
+ hostname: data.hostname
156
+ });
157
+ }
158
+ if (!data.success) {
159
+ return {
160
+ success: false,
161
+ score: 0,
162
+ error: `reCAPTCHA verification failed: ${data["error-codes"]?.join(", ") || "Unknown error"}`,
163
+ rawResponse: data
164
+ };
165
+ }
166
+ if (data.score < scoreThreshold) {
167
+ return {
168
+ success: false,
169
+ score: data.score,
170
+ error: `reCAPTCHA score too low: ${data.score} (threshold: ${scoreThreshold})`,
171
+ rawResponse: data
172
+ };
173
+ }
174
+ if (expectedAction && data.action !== expectedAction) {
175
+ return {
176
+ success: false,
177
+ score: data.score,
178
+ error: `reCAPTCHA action mismatch: expected "${expectedAction}", got "${data.action}"`,
179
+ rawResponse: data
180
+ };
181
+ }
182
+ return {
183
+ success: true,
184
+ score: data.score,
185
+ rawResponse: data
186
+ };
187
+ } catch (error) {
188
+ console.error("[reCAPTCHA] Validation error:", error);
189
+ return {
190
+ success: false,
191
+ score: 0,
192
+ error: error instanceof Error ? `reCAPTCHA validation error: ${error.message}` : "reCAPTCHA validation error"
193
+ };
194
+ }
195
+ }
196
+ function isRecaptchaEnabled(secretKey) {
197
+ return !!(secretKey ?? process.env.RECAPTCHA_SECRET_KEY);
198
+ }
199
+ function getRecaptchaToken(formData, fieldName = "recaptchaToken") {
200
+ const token = formData.get(fieldName);
201
+ return typeof token === "string" ? token : null;
202
+ }
203
+
204
+ export { DEFAULT_SCORE_THRESHOLD, DEFAULT_TOKEN_REFRESH_INTERVAL, RECAPTCHA_CONFIG, RecaptchaWrapper, getRecaptchaToken, isRecaptchaEnabled, validateRecaptcha };
205
+ //# sourceMappingURL=index.mjs.map
206
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants/index.ts","../src/client/index.tsx","../src/server/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * reCAPTCHA Configuration Constants\n *\n * Default configuration values for reCAPTCHA v3 integration.\n *\n * @packageDocumentation\n */\n\nimport type { RecaptchaConfig } from \"../types\";\n\n/**\n * Default score threshold for validation\n * Scores below this value are considered suspicious\n * Range: 0.0 (bot) to 1.0 (human)\n */\nexport const DEFAULT_SCORE_THRESHOLD = 0.5;\n\n/**\n * Token refresh interval in milliseconds\n * reCAPTCHA tokens expire after 2 minutes, so we refresh at 90 seconds\n * to ensure tokens are always valid when forms are submitted\n */\nexport const DEFAULT_TOKEN_REFRESH_INTERVAL = 90000;\n\n/**\n * reCAPTCHA v3 configuration constants\n */\nexport const RECAPTCHA_CONFIG: RecaptchaConfig = {\n /** Google reCAPTCHA verification endpoint */\n verifyUrl: \"https://www.google.com/recaptcha/api/siteverify\",\n /** Default score threshold for validation */\n defaultScoreThreshold: DEFAULT_SCORE_THRESHOLD,\n /** Default token refresh interval */\n tokenRefreshInterval: DEFAULT_TOKEN_REFRESH_INTERVAL,\n} as const;\n","/**\n * reCAPTCHA v3 Client Component\n *\n * Loads the Google reCAPTCHA script and generates tokens automatically.\n * Place inside a form to add invisible spam protection.\n *\n * @see https://developers.google.com/recaptcha/docs/v3\n * @packageDocumentation\n */\n\n\"use client\";\n\nimport Script from \"next/script\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { RecaptchaWrapperProps } from \"../types\";\nimport { RECAPTCHA_CONFIG } from \"../constants\";\n\n/**\n * RecaptchaWrapper - Client component for reCAPTCHA v3 integration\n *\n * Features:\n * - Loads reCAPTCHA script automatically\n * - Generates token when script loads\n * - Refreshes token periodically (tokens expire after 2 minutes)\n * - Stores token in hidden input field for form submission\n * - Graceful fallback when not configured\n *\n * @example Basic usage\n * ```tsx\n * <form action={formAction}>\n * <RecaptchaWrapper action=\"contact_form\" />\n * <input name=\"email\" type=\"email\" required />\n * <button type=\"submit\">Submit</button>\n * </form>\n * ```\n *\n * @example Custom input name\n * ```tsx\n * <RecaptchaWrapper\n * action=\"signup\"\n * inputName=\"captchaToken\"\n * inputId=\"signup-captcha\"\n * />\n * ```\n *\n * @example With callbacks\n * ```tsx\n * <RecaptchaWrapper\n * action=\"payment\"\n * onTokenGenerated={(token) => console.log(\"Token:\", token)}\n * onError={(error) => console.error(\"Error:\", error)}\n * />\n * ```\n */\nexport function RecaptchaWrapper({\n action,\n inputName = \"recaptchaToken\",\n inputId = \"recaptcha-token\",\n siteKey: propSiteKey,\n refreshInterval = RECAPTCHA_CONFIG.tokenRefreshInterval,\n onTokenGenerated,\n onError,\n}: RecaptchaWrapperProps) {\n // Use prop siteKey or fall back to environment variable\n const siteKey = propSiteKey ?? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;\n const tokenInputRef = useRef<HTMLInputElement>(null);\n const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null);\n\n // Execute reCAPTCHA and store token\n const executeRecaptcha = useCallback(async () => {\n if (!siteKey) {\n return;\n }\n\n try {\n if (typeof window !== \"undefined\" && window.grecaptcha) {\n window.grecaptcha.ready(async () => {\n try {\n const token = await window.grecaptcha.execute(siteKey, { action });\n\n // Store token in hidden input\n if (tokenInputRef.current) {\n tokenInputRef.current.value = token;\n }\n\n // Call callback if provided\n if (onTokenGenerated) {\n onTokenGenerated(token);\n }\n } catch (error) {\n console.error(\"[reCAPTCHA] Error executing reCAPTCHA:\", error);\n if (onError && error instanceof Error) {\n onError(error);\n }\n }\n });\n }\n } catch (error) {\n console.error(\"[reCAPTCHA] Error:\", error);\n if (onError && error instanceof Error) {\n onError(error);\n }\n }\n }, [siteKey, action, onTokenGenerated, onError]);\n\n // Set up token refresh interval (tokens expire after 2 minutes)\n useEffect(() => {\n // Generate token immediately when component mounts\n executeRecaptcha();\n\n // Set up refresh interval\n refreshIntervalRef.current = setInterval(() => {\n executeRecaptcha();\n }, refreshInterval);\n\n // Cleanup interval on unmount\n return () => {\n if (refreshIntervalRef.current) {\n clearInterval(refreshIntervalRef.current);\n }\n };\n }, [executeRecaptcha, refreshInterval]);\n\n // Don't render anything if site key is not configured\n if (!siteKey) {\n if (process.env.NODE_ENV === \"development\") {\n console.warn(\n \"[reCAPTCHA] Site key not configured. Set NEXT_PUBLIC_RECAPTCHA_SITE_KEY environment variable.\"\n );\n }\n return null;\n }\n\n return (\n <>\n {/* Hidden input to store the token */}\n <input\n ref={tokenInputRef}\n type=\"hidden\"\n name={inputName}\n id={inputId}\n data-testid=\"recaptcha-token-input\"\n />\n\n {/* Load reCAPTCHA script */}\n <Script\n src={`https://www.google.com/recaptcha/api.js?render=${siteKey}`}\n strategy=\"afterInteractive\"\n onLoad={() => {\n executeRecaptcha();\n }}\n onError={() => {\n console.error(\"[reCAPTCHA] Failed to load reCAPTCHA script\");\n if (onError) {\n onError(new Error(\"Failed to load reCAPTCHA script\"));\n }\n }}\n />\n </>\n );\n}\n\nexport default RecaptchaWrapper;\n","/**\n * reCAPTCHA v3 Server-Side Validation\n *\n * Functions for validating reCAPTCHA tokens in Next.js Server Actions.\n *\n * @see https://developers.google.com/recaptcha/docs/verify\n * @packageDocumentation\n */\n\nimport type {\n RecaptchaValidationResult,\n RecaptchaVerifyResponse,\n RecaptchaValidationOptions,\n} from \"../types\";\nimport { RECAPTCHA_CONFIG, DEFAULT_SCORE_THRESHOLD } from \"../constants\";\n\n/**\n * Validate a reCAPTCHA token with Google's API\n *\n * @param token - The reCAPTCHA token from the client\n * @param expectedAction - The expected action name (optional, for extra security)\n * @param options - Additional validation options\n * @returns Validation result with success status and score\n *\n * @example Basic validation\n * ```ts\n * const result = await validateRecaptcha(token, \"contact_form\");\n * if (!result.success) {\n * return { success: false, message: result.error };\n * }\n * ```\n *\n * @example Custom threshold for sensitive forms\n * ```ts\n * const result = await validateRecaptcha(token, \"payment_form\", {\n * scoreThreshold: 0.7, // Higher threshold for payments\n * secretKey: process.env.RECAPTCHA_SECRET_KEY,\n * });\n * ```\n *\n * @example Skip validation in development\n * ```ts\n * const result = await validateRecaptcha(token, \"test_form\", {\n * debug: true, // Enable debug logging\n * });\n * // Returns { success: true, score: 1, skipped: true } if not configured\n * ```\n */\nexport async function validateRecaptcha(\n token: string | null | undefined,\n expectedAction?: string,\n options: RecaptchaValidationOptions = {}\n): Promise<RecaptchaValidationResult> {\n const {\n scoreThreshold = DEFAULT_SCORE_THRESHOLD,\n secretKey = process.env.RECAPTCHA_SECRET_KEY,\n debug = process.env.NODE_ENV === \"development\",\n } = options;\n\n // Check if reCAPTCHA is configured\n if (!secretKey) {\n if (debug) {\n console.warn(\n \"[reCAPTCHA] Secret key not configured. Skipping validation.\"\n );\n }\n return {\n success: true,\n score: 1,\n skipped: true,\n };\n }\n\n // Check if token is provided\n if (!token) {\n return {\n success: false,\n score: 0,\n error: \"reCAPTCHA token is missing\",\n };\n }\n\n try {\n // Verify token with Google\n const response = await fetch(RECAPTCHA_CONFIG.verifyUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n secret: secretKey,\n response: token,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data: RecaptchaVerifyResponse = await response.json();\n\n if (debug) {\n console.log(\"[reCAPTCHA] Verification response:\", {\n success: data.success,\n score: data.score,\n action: data.action,\n hostname: data.hostname,\n });\n }\n\n // Check if verification failed\n if (!data.success) {\n return {\n success: false,\n score: 0,\n error: `reCAPTCHA verification failed: ${data[\"error-codes\"]?.join(\", \") || \"Unknown error\"}`,\n rawResponse: data,\n };\n }\n\n // Check score threshold\n if (data.score < scoreThreshold) {\n return {\n success: false,\n score: data.score,\n error: `reCAPTCHA score too low: ${data.score} (threshold: ${scoreThreshold})`,\n rawResponse: data,\n };\n }\n\n // Check action if provided\n if (expectedAction && data.action !== expectedAction) {\n return {\n success: false,\n score: data.score,\n error: `reCAPTCHA action mismatch: expected \"${expectedAction}\", got \"${data.action}\"`,\n rawResponse: data,\n };\n }\n\n // Validation passed\n return {\n success: true,\n score: data.score,\n rawResponse: data,\n };\n } catch (error) {\n console.error(\"[reCAPTCHA] Validation error:\", error);\n return {\n success: false,\n score: 0,\n error:\n error instanceof Error\n ? `reCAPTCHA validation error: ${error.message}`\n : \"reCAPTCHA validation error\",\n };\n }\n}\n\n/**\n * Check if reCAPTCHA is enabled (secret key is configured)\n *\n * @param secretKey - Optional explicit secret key to check\n * @returns true if reCAPTCHA is configured\n *\n * @example\n * ```ts\n * if (isRecaptchaEnabled()) {\n * // Require reCAPTCHA validation\n * } else {\n * // Skip validation in development\n * }\n * ```\n */\nexport function isRecaptchaEnabled(secretKey?: string): boolean {\n return !!(secretKey ?? process.env.RECAPTCHA_SECRET_KEY);\n}\n\n/**\n * Extract reCAPTCHA token from FormData\n *\n * @param formData - Form data containing the token\n * @param fieldName - Name of the token field (default: \"recaptchaToken\")\n * @returns The token string or null\n *\n * @example\n * ```ts\n * export async function submitForm(prevState: State, formData: FormData) {\n * const token = getRecaptchaToken(formData);\n * const result = await validateRecaptcha(token, \"contact_form\");\n * // ...\n * }\n * ```\n */\nexport function getRecaptchaToken(\n formData: FormData,\n fieldName: string = \"recaptchaToken\"\n): string | null {\n const token = formData.get(fieldName);\n return typeof token === \"string\" ? token : null;\n}\n"]}
@@ -0,0 +1,79 @@
1
+ import { RecaptchaValidationOptions, RecaptchaValidationResult } from '../types/index.mjs';
2
+
3
+ /**
4
+ * reCAPTCHA v3 Server-Side Validation
5
+ *
6
+ * Functions for validating reCAPTCHA tokens in Next.js Server Actions.
7
+ *
8
+ * @see https://developers.google.com/recaptcha/docs/verify
9
+ * @packageDocumentation
10
+ */
11
+
12
+ /**
13
+ * Validate a reCAPTCHA token with Google's API
14
+ *
15
+ * @param token - The reCAPTCHA token from the client
16
+ * @param expectedAction - The expected action name (optional, for extra security)
17
+ * @param options - Additional validation options
18
+ * @returns Validation result with success status and score
19
+ *
20
+ * @example Basic validation
21
+ * ```ts
22
+ * const result = await validateRecaptcha(token, "contact_form");
23
+ * if (!result.success) {
24
+ * return { success: false, message: result.error };
25
+ * }
26
+ * ```
27
+ *
28
+ * @example Custom threshold for sensitive forms
29
+ * ```ts
30
+ * const result = await validateRecaptcha(token, "payment_form", {
31
+ * scoreThreshold: 0.7, // Higher threshold for payments
32
+ * secretKey: process.env.RECAPTCHA_SECRET_KEY,
33
+ * });
34
+ * ```
35
+ *
36
+ * @example Skip validation in development
37
+ * ```ts
38
+ * const result = await validateRecaptcha(token, "test_form", {
39
+ * debug: true, // Enable debug logging
40
+ * });
41
+ * // Returns { success: true, score: 1, skipped: true } if not configured
42
+ * ```
43
+ */
44
+ declare function validateRecaptcha(token: string | null | undefined, expectedAction?: string, options?: RecaptchaValidationOptions): Promise<RecaptchaValidationResult>;
45
+ /**
46
+ * Check if reCAPTCHA is enabled (secret key is configured)
47
+ *
48
+ * @param secretKey - Optional explicit secret key to check
49
+ * @returns true if reCAPTCHA is configured
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * if (isRecaptchaEnabled()) {
54
+ * // Require reCAPTCHA validation
55
+ * } else {
56
+ * // Skip validation in development
57
+ * }
58
+ * ```
59
+ */
60
+ declare function isRecaptchaEnabled(secretKey?: string): boolean;
61
+ /**
62
+ * Extract reCAPTCHA token from FormData
63
+ *
64
+ * @param formData - Form data containing the token
65
+ * @param fieldName - Name of the token field (default: "recaptchaToken")
66
+ * @returns The token string or null
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * export async function submitForm(prevState: State, formData: FormData) {
71
+ * const token = getRecaptchaToken(formData);
72
+ * const result = await validateRecaptcha(token, "contact_form");
73
+ * // ...
74
+ * }
75
+ * ```
76
+ */
77
+ declare function getRecaptchaToken(formData: FormData, fieldName?: string): string | null;
78
+
79
+ export { getRecaptchaToken, isRecaptchaEnabled, validateRecaptcha };
@@ -0,0 +1,79 @@
1
+ import { RecaptchaValidationOptions, RecaptchaValidationResult } from '../types/index.js';
2
+
3
+ /**
4
+ * reCAPTCHA v3 Server-Side Validation
5
+ *
6
+ * Functions for validating reCAPTCHA tokens in Next.js Server Actions.
7
+ *
8
+ * @see https://developers.google.com/recaptcha/docs/verify
9
+ * @packageDocumentation
10
+ */
11
+
12
+ /**
13
+ * Validate a reCAPTCHA token with Google's API
14
+ *
15
+ * @param token - The reCAPTCHA token from the client
16
+ * @param expectedAction - The expected action name (optional, for extra security)
17
+ * @param options - Additional validation options
18
+ * @returns Validation result with success status and score
19
+ *
20
+ * @example Basic validation
21
+ * ```ts
22
+ * const result = await validateRecaptcha(token, "contact_form");
23
+ * if (!result.success) {
24
+ * return { success: false, message: result.error };
25
+ * }
26
+ * ```
27
+ *
28
+ * @example Custom threshold for sensitive forms
29
+ * ```ts
30
+ * const result = await validateRecaptcha(token, "payment_form", {
31
+ * scoreThreshold: 0.7, // Higher threshold for payments
32
+ * secretKey: process.env.RECAPTCHA_SECRET_KEY,
33
+ * });
34
+ * ```
35
+ *
36
+ * @example Skip validation in development
37
+ * ```ts
38
+ * const result = await validateRecaptcha(token, "test_form", {
39
+ * debug: true, // Enable debug logging
40
+ * });
41
+ * // Returns { success: true, score: 1, skipped: true } if not configured
42
+ * ```
43
+ */
44
+ declare function validateRecaptcha(token: string | null | undefined, expectedAction?: string, options?: RecaptchaValidationOptions): Promise<RecaptchaValidationResult>;
45
+ /**
46
+ * Check if reCAPTCHA is enabled (secret key is configured)
47
+ *
48
+ * @param secretKey - Optional explicit secret key to check
49
+ * @returns true if reCAPTCHA is configured
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * if (isRecaptchaEnabled()) {
54
+ * // Require reCAPTCHA validation
55
+ * } else {
56
+ * // Skip validation in development
57
+ * }
58
+ * ```
59
+ */
60
+ declare function isRecaptchaEnabled(secretKey?: string): boolean;
61
+ /**
62
+ * Extract reCAPTCHA token from FormData
63
+ *
64
+ * @param formData - Form data containing the token
65
+ * @param fieldName - Name of the token field (default: "recaptchaToken")
66
+ * @returns The token string or null
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * export async function submitForm(prevState: State, formData: FormData) {
71
+ * const token = getRecaptchaToken(formData);
72
+ * const result = await validateRecaptcha(token, "contact_form");
73
+ * // ...
74
+ * }
75
+ * ```
76
+ */
77
+ declare function getRecaptchaToken(formData: FormData, fieldName?: string): string | null;
78
+
79
+ export { getRecaptchaToken, isRecaptchaEnabled, validateRecaptcha };
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ // src/constants/index.ts
4
+ var DEFAULT_SCORE_THRESHOLD = 0.5;
5
+ var DEFAULT_TOKEN_REFRESH_INTERVAL = 9e4;
6
+ var RECAPTCHA_CONFIG = {
7
+ /** Google reCAPTCHA verification endpoint */
8
+ verifyUrl: "https://www.google.com/recaptcha/api/siteverify",
9
+ /** Default score threshold for validation */
10
+ defaultScoreThreshold: DEFAULT_SCORE_THRESHOLD,
11
+ /** Default token refresh interval */
12
+ tokenRefreshInterval: DEFAULT_TOKEN_REFRESH_INTERVAL
13
+ };
14
+
15
+ // src/server/index.ts
16
+ async function validateRecaptcha(token, expectedAction, options = {}) {
17
+ const {
18
+ scoreThreshold = DEFAULT_SCORE_THRESHOLD,
19
+ secretKey = process.env.RECAPTCHA_SECRET_KEY,
20
+ debug = process.env.NODE_ENV === "development"
21
+ } = options;
22
+ if (!secretKey) {
23
+ if (debug) {
24
+ console.warn(
25
+ "[reCAPTCHA] Secret key not configured. Skipping validation."
26
+ );
27
+ }
28
+ return {
29
+ success: true,
30
+ score: 1,
31
+ skipped: true
32
+ };
33
+ }
34
+ if (!token) {
35
+ return {
36
+ success: false,
37
+ score: 0,
38
+ error: "reCAPTCHA token is missing"
39
+ };
40
+ }
41
+ try {
42
+ const response = await fetch(RECAPTCHA_CONFIG.verifyUrl, {
43
+ method: "POST",
44
+ headers: {
45
+ "Content-Type": "application/x-www-form-urlencoded"
46
+ },
47
+ body: new URLSearchParams({
48
+ secret: secretKey,
49
+ response: token
50
+ })
51
+ });
52
+ if (!response.ok) {
53
+ throw new Error(`HTTP error! status: ${response.status}`);
54
+ }
55
+ const data = await response.json();
56
+ if (debug) {
57
+ console.log("[reCAPTCHA] Verification response:", {
58
+ success: data.success,
59
+ score: data.score,
60
+ action: data.action,
61
+ hostname: data.hostname
62
+ });
63
+ }
64
+ if (!data.success) {
65
+ return {
66
+ success: false,
67
+ score: 0,
68
+ error: `reCAPTCHA verification failed: ${data["error-codes"]?.join(", ") || "Unknown error"}`,
69
+ rawResponse: data
70
+ };
71
+ }
72
+ if (data.score < scoreThreshold) {
73
+ return {
74
+ success: false,
75
+ score: data.score,
76
+ error: `reCAPTCHA score too low: ${data.score} (threshold: ${scoreThreshold})`,
77
+ rawResponse: data
78
+ };
79
+ }
80
+ if (expectedAction && data.action !== expectedAction) {
81
+ return {
82
+ success: false,
83
+ score: data.score,
84
+ error: `reCAPTCHA action mismatch: expected "${expectedAction}", got "${data.action}"`,
85
+ rawResponse: data
86
+ };
87
+ }
88
+ return {
89
+ success: true,
90
+ score: data.score,
91
+ rawResponse: data
92
+ };
93
+ } catch (error) {
94
+ console.error("[reCAPTCHA] Validation error:", error);
95
+ return {
96
+ success: false,
97
+ score: 0,
98
+ error: error instanceof Error ? `reCAPTCHA validation error: ${error.message}` : "reCAPTCHA validation error"
99
+ };
100
+ }
101
+ }
102
+ function isRecaptchaEnabled(secretKey) {
103
+ return !!(secretKey ?? process.env.RECAPTCHA_SECRET_KEY);
104
+ }
105
+ function getRecaptchaToken(formData, fieldName = "recaptchaToken") {
106
+ const token = formData.get(fieldName);
107
+ return typeof token === "string" ? token : null;
108
+ }
109
+
110
+ exports.getRecaptchaToken = getRecaptchaToken;
111
+ exports.isRecaptchaEnabled = isRecaptchaEnabled;
112
+ exports.validateRecaptcha = validateRecaptcha;
113
+ //# sourceMappingURL=index.js.map
114
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants/index.ts","../../src/server/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * reCAPTCHA Configuration Constants\n *\n * Default configuration values for reCAPTCHA v3 integration.\n *\n * @packageDocumentation\n */\n\nimport type { RecaptchaConfig } from \"../types\";\n\n/**\n * Default score threshold for validation\n * Scores below this value are considered suspicious\n * Range: 0.0 (bot) to 1.0 (human)\n */\nexport const DEFAULT_SCORE_THRESHOLD = 0.5;\n\n/**\n * Token refresh interval in milliseconds\n * reCAPTCHA tokens expire after 2 minutes, so we refresh at 90 seconds\n * to ensure tokens are always valid when forms are submitted\n */\nexport const DEFAULT_TOKEN_REFRESH_INTERVAL = 90000;\n\n/**\n * reCAPTCHA v3 configuration constants\n */\nexport const RECAPTCHA_CONFIG: RecaptchaConfig = {\n /** Google reCAPTCHA verification endpoint */\n verifyUrl: \"https://www.google.com/recaptcha/api/siteverify\",\n /** Default score threshold for validation */\n defaultScoreThreshold: DEFAULT_SCORE_THRESHOLD,\n /** Default token refresh interval */\n tokenRefreshInterval: DEFAULT_TOKEN_REFRESH_INTERVAL,\n} as const;\n","/**\n * reCAPTCHA v3 Server-Side Validation\n *\n * Functions for validating reCAPTCHA tokens in Next.js Server Actions.\n *\n * @see https://developers.google.com/recaptcha/docs/verify\n * @packageDocumentation\n */\n\nimport type {\n RecaptchaValidationResult,\n RecaptchaVerifyResponse,\n RecaptchaValidationOptions,\n} from \"../types\";\nimport { RECAPTCHA_CONFIG, DEFAULT_SCORE_THRESHOLD } from \"../constants\";\n\n/**\n * Validate a reCAPTCHA token with Google's API\n *\n * @param token - The reCAPTCHA token from the client\n * @param expectedAction - The expected action name (optional, for extra security)\n * @param options - Additional validation options\n * @returns Validation result with success status and score\n *\n * @example Basic validation\n * ```ts\n * const result = await validateRecaptcha(token, \"contact_form\");\n * if (!result.success) {\n * return { success: false, message: result.error };\n * }\n * ```\n *\n * @example Custom threshold for sensitive forms\n * ```ts\n * const result = await validateRecaptcha(token, \"payment_form\", {\n * scoreThreshold: 0.7, // Higher threshold for payments\n * secretKey: process.env.RECAPTCHA_SECRET_KEY,\n * });\n * ```\n *\n * @example Skip validation in development\n * ```ts\n * const result = await validateRecaptcha(token, \"test_form\", {\n * debug: true, // Enable debug logging\n * });\n * // Returns { success: true, score: 1, skipped: true } if not configured\n * ```\n */\nexport async function validateRecaptcha(\n token: string | null | undefined,\n expectedAction?: string,\n options: RecaptchaValidationOptions = {}\n): Promise<RecaptchaValidationResult> {\n const {\n scoreThreshold = DEFAULT_SCORE_THRESHOLD,\n secretKey = process.env.RECAPTCHA_SECRET_KEY,\n debug = process.env.NODE_ENV === \"development\",\n } = options;\n\n // Check if reCAPTCHA is configured\n if (!secretKey) {\n if (debug) {\n console.warn(\n \"[reCAPTCHA] Secret key not configured. Skipping validation.\"\n );\n }\n return {\n success: true,\n score: 1,\n skipped: true,\n };\n }\n\n // Check if token is provided\n if (!token) {\n return {\n success: false,\n score: 0,\n error: \"reCAPTCHA token is missing\",\n };\n }\n\n try {\n // Verify token with Google\n const response = await fetch(RECAPTCHA_CONFIG.verifyUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n secret: secretKey,\n response: token,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data: RecaptchaVerifyResponse = await response.json();\n\n if (debug) {\n console.log(\"[reCAPTCHA] Verification response:\", {\n success: data.success,\n score: data.score,\n action: data.action,\n hostname: data.hostname,\n });\n }\n\n // Check if verification failed\n if (!data.success) {\n return {\n success: false,\n score: 0,\n error: `reCAPTCHA verification failed: ${data[\"error-codes\"]?.join(\", \") || \"Unknown error\"}`,\n rawResponse: data,\n };\n }\n\n // Check score threshold\n if (data.score < scoreThreshold) {\n return {\n success: false,\n score: data.score,\n error: `reCAPTCHA score too low: ${data.score} (threshold: ${scoreThreshold})`,\n rawResponse: data,\n };\n }\n\n // Check action if provided\n if (expectedAction && data.action !== expectedAction) {\n return {\n success: false,\n score: data.score,\n error: `reCAPTCHA action mismatch: expected \"${expectedAction}\", got \"${data.action}\"`,\n rawResponse: data,\n };\n }\n\n // Validation passed\n return {\n success: true,\n score: data.score,\n rawResponse: data,\n };\n } catch (error) {\n console.error(\"[reCAPTCHA] Validation error:\", error);\n return {\n success: false,\n score: 0,\n error:\n error instanceof Error\n ? `reCAPTCHA validation error: ${error.message}`\n : \"reCAPTCHA validation error\",\n };\n }\n}\n\n/**\n * Check if reCAPTCHA is enabled (secret key is configured)\n *\n * @param secretKey - Optional explicit secret key to check\n * @returns true if reCAPTCHA is configured\n *\n * @example\n * ```ts\n * if (isRecaptchaEnabled()) {\n * // Require reCAPTCHA validation\n * } else {\n * // Skip validation in development\n * }\n * ```\n */\nexport function isRecaptchaEnabled(secretKey?: string): boolean {\n return !!(secretKey ?? process.env.RECAPTCHA_SECRET_KEY);\n}\n\n/**\n * Extract reCAPTCHA token from FormData\n *\n * @param formData - Form data containing the token\n * @param fieldName - Name of the token field (default: \"recaptchaToken\")\n * @returns The token string or null\n *\n * @example\n * ```ts\n * export async function submitForm(prevState: State, formData: FormData) {\n * const token = getRecaptchaToken(formData);\n * const result = await validateRecaptcha(token, \"contact_form\");\n * // ...\n * }\n * ```\n */\nexport function getRecaptchaToken(\n formData: FormData,\n fieldName: string = \"recaptchaToken\"\n): string | null {\n const token = formData.get(fieldName);\n return typeof token === \"string\" ? token : null;\n}\n"]}
@@ -0,0 +1,110 @@
1
+ // src/constants/index.ts
2
+ var DEFAULT_SCORE_THRESHOLD = 0.5;
3
+ var DEFAULT_TOKEN_REFRESH_INTERVAL = 9e4;
4
+ var RECAPTCHA_CONFIG = {
5
+ /** Google reCAPTCHA verification endpoint */
6
+ verifyUrl: "https://www.google.com/recaptcha/api/siteverify",
7
+ /** Default score threshold for validation */
8
+ defaultScoreThreshold: DEFAULT_SCORE_THRESHOLD,
9
+ /** Default token refresh interval */
10
+ tokenRefreshInterval: DEFAULT_TOKEN_REFRESH_INTERVAL
11
+ };
12
+
13
+ // src/server/index.ts
14
+ async function validateRecaptcha(token, expectedAction, options = {}) {
15
+ const {
16
+ scoreThreshold = DEFAULT_SCORE_THRESHOLD,
17
+ secretKey = process.env.RECAPTCHA_SECRET_KEY,
18
+ debug = process.env.NODE_ENV === "development"
19
+ } = options;
20
+ if (!secretKey) {
21
+ if (debug) {
22
+ console.warn(
23
+ "[reCAPTCHA] Secret key not configured. Skipping validation."
24
+ );
25
+ }
26
+ return {
27
+ success: true,
28
+ score: 1,
29
+ skipped: true
30
+ };
31
+ }
32
+ if (!token) {
33
+ return {
34
+ success: false,
35
+ score: 0,
36
+ error: "reCAPTCHA token is missing"
37
+ };
38
+ }
39
+ try {
40
+ const response = await fetch(RECAPTCHA_CONFIG.verifyUrl, {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/x-www-form-urlencoded"
44
+ },
45
+ body: new URLSearchParams({
46
+ secret: secretKey,
47
+ response: token
48
+ })
49
+ });
50
+ if (!response.ok) {
51
+ throw new Error(`HTTP error! status: ${response.status}`);
52
+ }
53
+ const data = await response.json();
54
+ if (debug) {
55
+ console.log("[reCAPTCHA] Verification response:", {
56
+ success: data.success,
57
+ score: data.score,
58
+ action: data.action,
59
+ hostname: data.hostname
60
+ });
61
+ }
62
+ if (!data.success) {
63
+ return {
64
+ success: false,
65
+ score: 0,
66
+ error: `reCAPTCHA verification failed: ${data["error-codes"]?.join(", ") || "Unknown error"}`,
67
+ rawResponse: data
68
+ };
69
+ }
70
+ if (data.score < scoreThreshold) {
71
+ return {
72
+ success: false,
73
+ score: data.score,
74
+ error: `reCAPTCHA score too low: ${data.score} (threshold: ${scoreThreshold})`,
75
+ rawResponse: data
76
+ };
77
+ }
78
+ if (expectedAction && data.action !== expectedAction) {
79
+ return {
80
+ success: false,
81
+ score: data.score,
82
+ error: `reCAPTCHA action mismatch: expected "${expectedAction}", got "${data.action}"`,
83
+ rawResponse: data
84
+ };
85
+ }
86
+ return {
87
+ success: true,
88
+ score: data.score,
89
+ rawResponse: data
90
+ };
91
+ } catch (error) {
92
+ console.error("[reCAPTCHA] Validation error:", error);
93
+ return {
94
+ success: false,
95
+ score: 0,
96
+ error: error instanceof Error ? `reCAPTCHA validation error: ${error.message}` : "reCAPTCHA validation error"
97
+ };
98
+ }
99
+ }
100
+ function isRecaptchaEnabled(secretKey) {
101
+ return !!(secretKey ?? process.env.RECAPTCHA_SECRET_KEY);
102
+ }
103
+ function getRecaptchaToken(formData, fieldName = "recaptchaToken") {
104
+ const token = formData.get(fieldName);
105
+ return typeof token === "string" ? token : null;
106
+ }
107
+
108
+ export { getRecaptchaToken, isRecaptchaEnabled, validateRecaptcha };
109
+ //# sourceMappingURL=index.mjs.map
110
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants/index.ts","../../src/server/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * reCAPTCHA Configuration Constants\n *\n * Default configuration values for reCAPTCHA v3 integration.\n *\n * @packageDocumentation\n */\n\nimport type { RecaptchaConfig } from \"../types\";\n\n/**\n * Default score threshold for validation\n * Scores below this value are considered suspicious\n * Range: 0.0 (bot) to 1.0 (human)\n */\nexport const DEFAULT_SCORE_THRESHOLD = 0.5;\n\n/**\n * Token refresh interval in milliseconds\n * reCAPTCHA tokens expire after 2 minutes, so we refresh at 90 seconds\n * to ensure tokens are always valid when forms are submitted\n */\nexport const DEFAULT_TOKEN_REFRESH_INTERVAL = 90000;\n\n/**\n * reCAPTCHA v3 configuration constants\n */\nexport const RECAPTCHA_CONFIG: RecaptchaConfig = {\n /** Google reCAPTCHA verification endpoint */\n verifyUrl: \"https://www.google.com/recaptcha/api/siteverify\",\n /** Default score threshold for validation */\n defaultScoreThreshold: DEFAULT_SCORE_THRESHOLD,\n /** Default token refresh interval */\n tokenRefreshInterval: DEFAULT_TOKEN_REFRESH_INTERVAL,\n} as const;\n","/**\n * reCAPTCHA v3 Server-Side Validation\n *\n * Functions for validating reCAPTCHA tokens in Next.js Server Actions.\n *\n * @see https://developers.google.com/recaptcha/docs/verify\n * @packageDocumentation\n */\n\nimport type {\n RecaptchaValidationResult,\n RecaptchaVerifyResponse,\n RecaptchaValidationOptions,\n} from \"../types\";\nimport { RECAPTCHA_CONFIG, DEFAULT_SCORE_THRESHOLD } from \"../constants\";\n\n/**\n * Validate a reCAPTCHA token with Google's API\n *\n * @param token - The reCAPTCHA token from the client\n * @param expectedAction - The expected action name (optional, for extra security)\n * @param options - Additional validation options\n * @returns Validation result with success status and score\n *\n * @example Basic validation\n * ```ts\n * const result = await validateRecaptcha(token, \"contact_form\");\n * if (!result.success) {\n * return { success: false, message: result.error };\n * }\n * ```\n *\n * @example Custom threshold for sensitive forms\n * ```ts\n * const result = await validateRecaptcha(token, \"payment_form\", {\n * scoreThreshold: 0.7, // Higher threshold for payments\n * secretKey: process.env.RECAPTCHA_SECRET_KEY,\n * });\n * ```\n *\n * @example Skip validation in development\n * ```ts\n * const result = await validateRecaptcha(token, \"test_form\", {\n * debug: true, // Enable debug logging\n * });\n * // Returns { success: true, score: 1, skipped: true } if not configured\n * ```\n */\nexport async function validateRecaptcha(\n token: string | null | undefined,\n expectedAction?: string,\n options: RecaptchaValidationOptions = {}\n): Promise<RecaptchaValidationResult> {\n const {\n scoreThreshold = DEFAULT_SCORE_THRESHOLD,\n secretKey = process.env.RECAPTCHA_SECRET_KEY,\n debug = process.env.NODE_ENV === \"development\",\n } = options;\n\n // Check if reCAPTCHA is configured\n if (!secretKey) {\n if (debug) {\n console.warn(\n \"[reCAPTCHA] Secret key not configured. Skipping validation.\"\n );\n }\n return {\n success: true,\n score: 1,\n skipped: true,\n };\n }\n\n // Check if token is provided\n if (!token) {\n return {\n success: false,\n score: 0,\n error: \"reCAPTCHA token is missing\",\n };\n }\n\n try {\n // Verify token with Google\n const response = await fetch(RECAPTCHA_CONFIG.verifyUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n secret: secretKey,\n response: token,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data: RecaptchaVerifyResponse = await response.json();\n\n if (debug) {\n console.log(\"[reCAPTCHA] Verification response:\", {\n success: data.success,\n score: data.score,\n action: data.action,\n hostname: data.hostname,\n });\n }\n\n // Check if verification failed\n if (!data.success) {\n return {\n success: false,\n score: 0,\n error: `reCAPTCHA verification failed: ${data[\"error-codes\"]?.join(\", \") || \"Unknown error\"}`,\n rawResponse: data,\n };\n }\n\n // Check score threshold\n if (data.score < scoreThreshold) {\n return {\n success: false,\n score: data.score,\n error: `reCAPTCHA score too low: ${data.score} (threshold: ${scoreThreshold})`,\n rawResponse: data,\n };\n }\n\n // Check action if provided\n if (expectedAction && data.action !== expectedAction) {\n return {\n success: false,\n score: data.score,\n error: `reCAPTCHA action mismatch: expected \"${expectedAction}\", got \"${data.action}\"`,\n rawResponse: data,\n };\n }\n\n // Validation passed\n return {\n success: true,\n score: data.score,\n rawResponse: data,\n };\n } catch (error) {\n console.error(\"[reCAPTCHA] Validation error:\", error);\n return {\n success: false,\n score: 0,\n error:\n error instanceof Error\n ? `reCAPTCHA validation error: ${error.message}`\n : \"reCAPTCHA validation error\",\n };\n }\n}\n\n/**\n * Check if reCAPTCHA is enabled (secret key is configured)\n *\n * @param secretKey - Optional explicit secret key to check\n * @returns true if reCAPTCHA is configured\n *\n * @example\n * ```ts\n * if (isRecaptchaEnabled()) {\n * // Require reCAPTCHA validation\n * } else {\n * // Skip validation in development\n * }\n * ```\n */\nexport function isRecaptchaEnabled(secretKey?: string): boolean {\n return !!(secretKey ?? process.env.RECAPTCHA_SECRET_KEY);\n}\n\n/**\n * Extract reCAPTCHA token from FormData\n *\n * @param formData - Form data containing the token\n * @param fieldName - Name of the token field (default: \"recaptchaToken\")\n * @returns The token string or null\n *\n * @example\n * ```ts\n * export async function submitForm(prevState: State, formData: FormData) {\n * const token = getRecaptchaToken(formData);\n * const result = await validateRecaptcha(token, \"contact_form\");\n * // ...\n * }\n * ```\n */\nexport function getRecaptchaToken(\n formData: FormData,\n fieldName: string = \"recaptchaToken\"\n): string | null {\n const token = formData.get(fieldName);\n return typeof token === \"string\" ? token : null;\n}\n"]}