@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants/index.ts","../../src/client/index.tsx"],"names":["useRef","useCallback","useEffect","jsxs","Fragment","jsx","Script"],"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 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"]}
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ import Script from 'next/script';
4
+ import { useRef, useCallback, useEffect } from 'react';
5
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
+
7
+ var DEFAULT_TOKEN_REFRESH_INTERVAL = 9e4;
8
+ var RECAPTCHA_CONFIG = {
9
+ /** Default token refresh interval */
10
+ tokenRefreshInterval: DEFAULT_TOKEN_REFRESH_INTERVAL
11
+ };
12
+ function RecaptchaWrapper({
13
+ action,
14
+ inputName = "recaptchaToken",
15
+ inputId = "recaptcha-token",
16
+ siteKey: propSiteKey,
17
+ refreshInterval = RECAPTCHA_CONFIG.tokenRefreshInterval,
18
+ onTokenGenerated,
19
+ onError
20
+ }) {
21
+ const siteKey = propSiteKey ?? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
22
+ const tokenInputRef = useRef(null);
23
+ const refreshIntervalRef = useRef(null);
24
+ const executeRecaptcha = useCallback(async () => {
25
+ if (!siteKey) {
26
+ return;
27
+ }
28
+ try {
29
+ if (typeof window !== "undefined" && window.grecaptcha) {
30
+ window.grecaptcha.ready(async () => {
31
+ try {
32
+ const token = await window.grecaptcha.execute(siteKey, { action });
33
+ if (tokenInputRef.current) {
34
+ tokenInputRef.current.value = token;
35
+ }
36
+ if (onTokenGenerated) {
37
+ onTokenGenerated(token);
38
+ }
39
+ } catch (error) {
40
+ console.error("[reCAPTCHA] Error executing reCAPTCHA:", error);
41
+ if (onError && error instanceof Error) {
42
+ onError(error);
43
+ }
44
+ }
45
+ });
46
+ }
47
+ } catch (error) {
48
+ console.error("[reCAPTCHA] Error:", error);
49
+ if (onError && error instanceof Error) {
50
+ onError(error);
51
+ }
52
+ }
53
+ }, [siteKey, action, onTokenGenerated, onError]);
54
+ useEffect(() => {
55
+ executeRecaptcha();
56
+ refreshIntervalRef.current = setInterval(() => {
57
+ executeRecaptcha();
58
+ }, refreshInterval);
59
+ return () => {
60
+ if (refreshIntervalRef.current) {
61
+ clearInterval(refreshIntervalRef.current);
62
+ }
63
+ };
64
+ }, [executeRecaptcha, refreshInterval]);
65
+ if (!siteKey) {
66
+ if (process.env.NODE_ENV === "development") {
67
+ console.warn(
68
+ "[reCAPTCHA] Site key not configured. Set NEXT_PUBLIC_RECAPTCHA_SITE_KEY environment variable."
69
+ );
70
+ }
71
+ return null;
72
+ }
73
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
74
+ /* @__PURE__ */ jsx(
75
+ "input",
76
+ {
77
+ ref: tokenInputRef,
78
+ type: "hidden",
79
+ name: inputName,
80
+ id: inputId,
81
+ "data-testid": "recaptcha-token-input"
82
+ }
83
+ ),
84
+ /* @__PURE__ */ jsx(
85
+ Script,
86
+ {
87
+ src: `https://www.google.com/recaptcha/api.js?render=${siteKey}`,
88
+ strategy: "afterInteractive",
89
+ onLoad: () => {
90
+ executeRecaptcha();
91
+ },
92
+ onError: () => {
93
+ console.error("[reCAPTCHA] Failed to load reCAPTCHA script");
94
+ if (onError) {
95
+ onError(new Error("Failed to load reCAPTCHA script"));
96
+ }
97
+ }
98
+ }
99
+ )
100
+ ] });
101
+ }
102
+ var client_default = RecaptchaWrapper;
103
+
104
+ export { RecaptchaWrapper, client_default as default };
105
+ //# sourceMappingURL=index.mjs.map
106
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants/index.ts","../../src/client/index.tsx"],"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"]}
@@ -0,0 +1,28 @@
1
+ import { RecaptchaConfig } from '../types/index.mjs';
2
+
3
+ /**
4
+ * reCAPTCHA Configuration Constants
5
+ *
6
+ * Default configuration values for reCAPTCHA v3 integration.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ /**
12
+ * Default score threshold for validation
13
+ * Scores below this value are considered suspicious
14
+ * Range: 0.0 (bot) to 1.0 (human)
15
+ */
16
+ declare const DEFAULT_SCORE_THRESHOLD = 0.5;
17
+ /**
18
+ * Token refresh interval in milliseconds
19
+ * reCAPTCHA tokens expire after 2 minutes, so we refresh at 90 seconds
20
+ * to ensure tokens are always valid when forms are submitted
21
+ */
22
+ declare const DEFAULT_TOKEN_REFRESH_INTERVAL = 90000;
23
+ /**
24
+ * reCAPTCHA v3 configuration constants
25
+ */
26
+ declare const RECAPTCHA_CONFIG: RecaptchaConfig;
27
+
28
+ export { DEFAULT_SCORE_THRESHOLD, DEFAULT_TOKEN_REFRESH_INTERVAL, RECAPTCHA_CONFIG };
@@ -0,0 +1,28 @@
1
+ import { RecaptchaConfig } from '../types/index.js';
2
+
3
+ /**
4
+ * reCAPTCHA Configuration Constants
5
+ *
6
+ * Default configuration values for reCAPTCHA v3 integration.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ /**
12
+ * Default score threshold for validation
13
+ * Scores below this value are considered suspicious
14
+ * Range: 0.0 (bot) to 1.0 (human)
15
+ */
16
+ declare const DEFAULT_SCORE_THRESHOLD = 0.5;
17
+ /**
18
+ * Token refresh interval in milliseconds
19
+ * reCAPTCHA tokens expire after 2 minutes, so we refresh at 90 seconds
20
+ * to ensure tokens are always valid when forms are submitted
21
+ */
22
+ declare const DEFAULT_TOKEN_REFRESH_INTERVAL = 90000;
23
+ /**
24
+ * reCAPTCHA v3 configuration constants
25
+ */
26
+ declare const RECAPTCHA_CONFIG: RecaptchaConfig;
27
+
28
+ export { DEFAULT_SCORE_THRESHOLD, DEFAULT_TOKEN_REFRESH_INTERVAL, RECAPTCHA_CONFIG };
@@ -0,0 +1,19 @@
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
+ exports.DEFAULT_SCORE_THRESHOLD = DEFAULT_SCORE_THRESHOLD;
16
+ exports.DEFAULT_TOKEN_REFRESH_INTERVAL = DEFAULT_TOKEN_REFRESH_INTERVAL;
17
+ exports.RECAPTCHA_CONFIG = RECAPTCHA_CONFIG;
18
+ //# sourceMappingURL=index.js.map
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants/index.ts"],"names":[],"mappings":";;;AAeO,IAAM,uBAAA,GAA0B;AAOhC,IAAM,8BAAA,GAAiC;AAKvC,IAAM,gBAAA,GAAoC;AAAA;AAAA,EAE/C,SAAA,EAAW,iDAAA;AAAA;AAAA,EAEX,qBAAA,EAAuB,uBAAA;AAAA;AAAA,EAEvB,oBAAA,EAAsB;AACxB","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"]}
@@ -0,0 +1,15 @@
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
+ export { DEFAULT_SCORE_THRESHOLD, DEFAULT_TOKEN_REFRESH_INTERVAL, RECAPTCHA_CONFIG };
14
+ //# sourceMappingURL=index.mjs.map
15
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants/index.ts"],"names":[],"mappings":";AAeO,IAAM,uBAAA,GAA0B;AAOhC,IAAM,8BAAA,GAAiC;AAKvC,IAAM,gBAAA,GAAoC;AAAA;AAAA,EAE/C,SAAA,EAAW,iDAAA;AAAA;AAAA,EAEX,qBAAA,EAAuB,uBAAA;AAAA;AAAA,EAEvB,oBAAA,EAAsB;AACxB","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"]}
@@ -0,0 +1,46 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { RecaptchaWrapperProps } from './types/index.mjs';
3
+ export { RecaptchaConfig, RecaptchaValidationOptions, RecaptchaValidationResult, RecaptchaVerifyResponse } from './types/index.mjs';
4
+ export { getRecaptchaToken, isRecaptchaEnabled, validateRecaptcha } from './server/index.mjs';
5
+ export { DEFAULT_SCORE_THRESHOLD, DEFAULT_TOKEN_REFRESH_INTERVAL, RECAPTCHA_CONFIG } from './constants/index.mjs';
6
+
7
+ /**
8
+ * RecaptchaWrapper - Client component for reCAPTCHA v3 integration
9
+ *
10
+ * Features:
11
+ * - Loads reCAPTCHA script automatically
12
+ * - Generates token when script loads
13
+ * - Refreshes token periodically (tokens expire after 2 minutes)
14
+ * - Stores token in hidden input field for form submission
15
+ * - Graceful fallback when not configured
16
+ *
17
+ * @example Basic usage
18
+ * ```tsx
19
+ * <form action={formAction}>
20
+ * <RecaptchaWrapper action="contact_form" />
21
+ * <input name="email" type="email" required />
22
+ * <button type="submit">Submit</button>
23
+ * </form>
24
+ * ```
25
+ *
26
+ * @example Custom input name
27
+ * ```tsx
28
+ * <RecaptchaWrapper
29
+ * action="signup"
30
+ * inputName="captchaToken"
31
+ * inputId="signup-captcha"
32
+ * />
33
+ * ```
34
+ *
35
+ * @example With callbacks
36
+ * ```tsx
37
+ * <RecaptchaWrapper
38
+ * action="payment"
39
+ * onTokenGenerated={(token) => console.log("Token:", token)}
40
+ * onError={(error) => console.error("Error:", error)}
41
+ * />
42
+ * ```
43
+ */
44
+ declare function RecaptchaWrapper({ action, inputName, inputId, siteKey: propSiteKey, refreshInterval, onTokenGenerated, onError, }: RecaptchaWrapperProps): react_jsx_runtime.JSX.Element | null;
45
+
46
+ export { RecaptchaWrapper, RecaptchaWrapperProps };
@@ -0,0 +1,46 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { RecaptchaWrapperProps } from './types/index.js';
3
+ export { RecaptchaConfig, RecaptchaValidationOptions, RecaptchaValidationResult, RecaptchaVerifyResponse } from './types/index.js';
4
+ export { getRecaptchaToken, isRecaptchaEnabled, validateRecaptcha } from './server/index.js';
5
+ export { DEFAULT_SCORE_THRESHOLD, DEFAULT_TOKEN_REFRESH_INTERVAL, RECAPTCHA_CONFIG } from './constants/index.js';
6
+
7
+ /**
8
+ * RecaptchaWrapper - Client component for reCAPTCHA v3 integration
9
+ *
10
+ * Features:
11
+ * - Loads reCAPTCHA script automatically
12
+ * - Generates token when script loads
13
+ * - Refreshes token periodically (tokens expire after 2 minutes)
14
+ * - Stores token in hidden input field for form submission
15
+ * - Graceful fallback when not configured
16
+ *
17
+ * @example Basic usage
18
+ * ```tsx
19
+ * <form action={formAction}>
20
+ * <RecaptchaWrapper action="contact_form" />
21
+ * <input name="email" type="email" required />
22
+ * <button type="submit">Submit</button>
23
+ * </form>
24
+ * ```
25
+ *
26
+ * @example Custom input name
27
+ * ```tsx
28
+ * <RecaptchaWrapper
29
+ * action="signup"
30
+ * inputName="captchaToken"
31
+ * inputId="signup-captcha"
32
+ * />
33
+ * ```
34
+ *
35
+ * @example With callbacks
36
+ * ```tsx
37
+ * <RecaptchaWrapper
38
+ * action="payment"
39
+ * onTokenGenerated={(token) => console.log("Token:", token)}
40
+ * onError={(error) => console.error("Error:", error)}
41
+ * />
42
+ * ```
43
+ */
44
+ declare function RecaptchaWrapper({ action, inputName, inputId, siteKey: propSiteKey, refreshInterval, onTokenGenerated, onError, }: RecaptchaWrapperProps): react_jsx_runtime.JSX.Element | null;
45
+
46
+ export { RecaptchaWrapper, RecaptchaWrapperProps };
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ 'use strict';
2
+
3
+ var Script = require('next/script');
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var Script__default = /*#__PURE__*/_interopDefault(Script);
10
+
11
+ // src/client/index.tsx
12
+
13
+ // src/constants/index.ts
14
+ var DEFAULT_SCORE_THRESHOLD = 0.5;
15
+ var DEFAULT_TOKEN_REFRESH_INTERVAL = 9e4;
16
+ var RECAPTCHA_CONFIG = {
17
+ /** Google reCAPTCHA verification endpoint */
18
+ verifyUrl: "https://www.google.com/recaptcha/api/siteverify",
19
+ /** Default score threshold for validation */
20
+ defaultScoreThreshold: DEFAULT_SCORE_THRESHOLD,
21
+ /** Default token refresh interval */
22
+ tokenRefreshInterval: DEFAULT_TOKEN_REFRESH_INTERVAL
23
+ };
24
+ function RecaptchaWrapper({
25
+ action,
26
+ inputName = "recaptchaToken",
27
+ inputId = "recaptcha-token",
28
+ siteKey: propSiteKey,
29
+ refreshInterval = RECAPTCHA_CONFIG.tokenRefreshInterval,
30
+ onTokenGenerated,
31
+ onError
32
+ }) {
33
+ const siteKey = propSiteKey ?? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
34
+ const tokenInputRef = react.useRef(null);
35
+ const refreshIntervalRef = react.useRef(null);
36
+ const executeRecaptcha = react.useCallback(async () => {
37
+ if (!siteKey) {
38
+ return;
39
+ }
40
+ try {
41
+ if (typeof window !== "undefined" && window.grecaptcha) {
42
+ window.grecaptcha.ready(async () => {
43
+ try {
44
+ const token = await window.grecaptcha.execute(siteKey, { action });
45
+ if (tokenInputRef.current) {
46
+ tokenInputRef.current.value = token;
47
+ }
48
+ if (onTokenGenerated) {
49
+ onTokenGenerated(token);
50
+ }
51
+ } catch (error) {
52
+ console.error("[reCAPTCHA] Error executing reCAPTCHA:", error);
53
+ if (onError && error instanceof Error) {
54
+ onError(error);
55
+ }
56
+ }
57
+ });
58
+ }
59
+ } catch (error) {
60
+ console.error("[reCAPTCHA] Error:", error);
61
+ if (onError && error instanceof Error) {
62
+ onError(error);
63
+ }
64
+ }
65
+ }, [siteKey, action, onTokenGenerated, onError]);
66
+ react.useEffect(() => {
67
+ executeRecaptcha();
68
+ refreshIntervalRef.current = setInterval(() => {
69
+ executeRecaptcha();
70
+ }, refreshInterval);
71
+ return () => {
72
+ if (refreshIntervalRef.current) {
73
+ clearInterval(refreshIntervalRef.current);
74
+ }
75
+ };
76
+ }, [executeRecaptcha, refreshInterval]);
77
+ if (!siteKey) {
78
+ if (process.env.NODE_ENV === "development") {
79
+ console.warn(
80
+ "[reCAPTCHA] Site key not configured. Set NEXT_PUBLIC_RECAPTCHA_SITE_KEY environment variable."
81
+ );
82
+ }
83
+ return null;
84
+ }
85
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
86
+ /* @__PURE__ */ jsxRuntime.jsx(
87
+ "input",
88
+ {
89
+ ref: tokenInputRef,
90
+ type: "hidden",
91
+ name: inputName,
92
+ id: inputId,
93
+ "data-testid": "recaptcha-token-input"
94
+ }
95
+ ),
96
+ /* @__PURE__ */ jsxRuntime.jsx(
97
+ Script__default.default,
98
+ {
99
+ src: `https://www.google.com/recaptcha/api.js?render=${siteKey}`,
100
+ strategy: "afterInteractive",
101
+ onLoad: () => {
102
+ executeRecaptcha();
103
+ },
104
+ onError: () => {
105
+ console.error("[reCAPTCHA] Failed to load reCAPTCHA script");
106
+ if (onError) {
107
+ onError(new Error("Failed to load reCAPTCHA script"));
108
+ }
109
+ }
110
+ }
111
+ )
112
+ ] });
113
+ }
114
+
115
+ // src/server/index.ts
116
+ async function validateRecaptcha(token, expectedAction, options = {}) {
117
+ const {
118
+ scoreThreshold = DEFAULT_SCORE_THRESHOLD,
119
+ secretKey = process.env.RECAPTCHA_SECRET_KEY,
120
+ debug = process.env.NODE_ENV === "development"
121
+ } = options;
122
+ if (!secretKey) {
123
+ if (debug) {
124
+ console.warn(
125
+ "[reCAPTCHA] Secret key not configured. Skipping validation."
126
+ );
127
+ }
128
+ return {
129
+ success: true,
130
+ score: 1,
131
+ skipped: true
132
+ };
133
+ }
134
+ if (!token) {
135
+ return {
136
+ success: false,
137
+ score: 0,
138
+ error: "reCAPTCHA token is missing"
139
+ };
140
+ }
141
+ try {
142
+ const response = await fetch(RECAPTCHA_CONFIG.verifyUrl, {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/x-www-form-urlencoded"
146
+ },
147
+ body: new URLSearchParams({
148
+ secret: secretKey,
149
+ response: token
150
+ })
151
+ });
152
+ if (!response.ok) {
153
+ throw new Error(`HTTP error! status: ${response.status}`);
154
+ }
155
+ const data = await response.json();
156
+ if (debug) {
157
+ console.log("[reCAPTCHA] Verification response:", {
158
+ success: data.success,
159
+ score: data.score,
160
+ action: data.action,
161
+ hostname: data.hostname
162
+ });
163
+ }
164
+ if (!data.success) {
165
+ return {
166
+ success: false,
167
+ score: 0,
168
+ error: `reCAPTCHA verification failed: ${data["error-codes"]?.join(", ") || "Unknown error"}`,
169
+ rawResponse: data
170
+ };
171
+ }
172
+ if (data.score < scoreThreshold) {
173
+ return {
174
+ success: false,
175
+ score: data.score,
176
+ error: `reCAPTCHA score too low: ${data.score} (threshold: ${scoreThreshold})`,
177
+ rawResponse: data
178
+ };
179
+ }
180
+ if (expectedAction && data.action !== expectedAction) {
181
+ return {
182
+ success: false,
183
+ score: data.score,
184
+ error: `reCAPTCHA action mismatch: expected "${expectedAction}", got "${data.action}"`,
185
+ rawResponse: data
186
+ };
187
+ }
188
+ return {
189
+ success: true,
190
+ score: data.score,
191
+ rawResponse: data
192
+ };
193
+ } catch (error) {
194
+ console.error("[reCAPTCHA] Validation error:", error);
195
+ return {
196
+ success: false,
197
+ score: 0,
198
+ error: error instanceof Error ? `reCAPTCHA validation error: ${error.message}` : "reCAPTCHA validation error"
199
+ };
200
+ }
201
+ }
202
+ function isRecaptchaEnabled(secretKey) {
203
+ return !!(secretKey ?? process.env.RECAPTCHA_SECRET_KEY);
204
+ }
205
+ function getRecaptchaToken(formData, fieldName = "recaptchaToken") {
206
+ const token = formData.get(fieldName);
207
+ return typeof token === "string" ? token : null;
208
+ }
209
+
210
+ exports.DEFAULT_SCORE_THRESHOLD = DEFAULT_SCORE_THRESHOLD;
211
+ exports.DEFAULT_TOKEN_REFRESH_INTERVAL = DEFAULT_TOKEN_REFRESH_INTERVAL;
212
+ exports.RECAPTCHA_CONFIG = RECAPTCHA_CONFIG;
213
+ exports.RecaptchaWrapper = RecaptchaWrapper;
214
+ exports.getRecaptchaToken = getRecaptchaToken;
215
+ exports.isRecaptchaEnabled = isRecaptchaEnabled;
216
+ exports.validateRecaptcha = validateRecaptcha;
217
+ //# sourceMappingURL=index.js.map
218
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants/index.ts","../src/client/index.tsx","../src/server/index.ts"],"names":["useRef","useCallback","useEffect","jsxs","Fragment","jsx","Script"],"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 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"]}