@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/CHANGELOG.md +45 -0
- package/LICENSE +135 -0
- package/README.md +240 -0
- package/dist/client/index.d.mts +75 -0
- package/dist/client/index.d.ts +75 -0
- package/dist/client/index.js +115 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +106 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/constants/index.d.mts +28 -0
- package/dist/constants/index.d.ts +28 -0
- package/dist/constants/index.js +19 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/constants/index.mjs +15 -0
- package/dist/constants/index.mjs.map +1 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +218 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +206 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/index.d.mts +79 -0
- package/dist/server/index.d.ts +79 -0
- package/dist/server/index.js +114 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +110 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/types/index.d.mts +97 -0
- package/dist/types/index.d.ts +97 -0
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +3 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +123 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/constants/index.ts","../../src/client/index.tsx"],"names":["useRef","useCallback","useEffect","jsxs","Fragment","jsx","Script"],"mappings":";;;;;;;;;;;;AAsBO,IAAM,8BAAA,GAAiC,GAAA;AAKvC,IAAM,gBAAA,GAAoC;AAAA,EAIxB;AAAA,EAEvB,oBAAA,EAAsB;AACxB,CAAA;ACoBO,SAAS,gBAAA,CAAiB;AAAA,EAC/B,MAAA;AAAA,EACA,SAAA,GAAY,gBAAA;AAAA,EACZ,OAAA,GAAU,iBAAA;AAAA,EACV,OAAA,EAAS,WAAA;AAAA,EACT,kBAAkB,gBAAA,CAAiB,oBAAA;AAAA,EACnC,gBAAA;AAAA,EACA;AACF,CAAA,EAA0B;AAExB,EAAA,MAAM,OAAA,GAAU,WAAA,IAAe,OAAA,CAAQ,GAAA,CAAI,8BAAA;AAC3C,EAAA,MAAM,aAAA,GAAgBA,aAAyB,IAAI,CAAA;AACnD,EAAA,MAAM,kBAAA,GAAqBA,aAA8B,IAAI,CAAA;AAG7D,EAAA,MAAM,gBAAA,GAAmBC,kBAAY,YAAY;AAC/C,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,MAAA,CAAO,UAAA,EAAY;AACtD,QAAA,MAAA,CAAO,UAAA,CAAW,MAAM,YAAY;AAClC,UAAA,IAAI;AACF,YAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,CAAO,UAAA,CAAW,QAAQ,OAAA,EAAS,EAAE,QAAQ,CAAA;AAGjE,YAAA,IAAI,cAAc,OAAA,EAAS;AACzB,cAAA,aAAA,CAAc,QAAQ,KAAA,GAAQ,KAAA;AAAA,YAChC;AAGA,YAAA,IAAI,gBAAA,EAAkB;AACpB,cAAA,gBAAA,CAAiB,KAAK,CAAA;AAAA,YACxB;AAAA,UACF,SAAS,KAAA,EAAO;AACd,YAAA,OAAA,CAAQ,KAAA,CAAM,0CAA0C,KAAK,CAAA;AAC7D,YAAA,IAAI,OAAA,IAAW,iBAAiB,KAAA,EAAO;AACrC,cAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,YACf;AAAA,UACF;AAAA,QACF,CAAC,CAAA;AAAA,MACH;AAAA,IACF,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,sBAAsB,KAAK,CAAA;AACzC,MAAA,IAAI,OAAA,IAAW,iBAAiB,KAAA,EAAO;AACrC,QAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,MACf;AAAA,IACF;AAAA,EACF,GAAG,CAAC,OAAA,EAAS,MAAA,EAAQ,gBAAA,EAAkB,OAAO,CAAC,CAAA;AAG/C,EAAAC,eAAA,CAAU,MAAM;AAEd,IAAA,gBAAA,EAAiB;AAGjB,IAAA,kBAAA,CAAmB,OAAA,GAAU,YAAY,MAAM;AAC7C,MAAA,gBAAA,EAAiB;AAAA,IACnB,GAAG,eAAe,CAAA;AAGlB,IAAA,OAAO,MAAM;AACX,MAAA,IAAI,mBAAmB,OAAA,EAAS;AAC9B,QAAA,aAAA,CAAc,mBAAmB,OAAO,CAAA;AAAA,MAC1C;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,gBAAA,EAAkB,eAAe,CAAC,CAAA;AAGtC,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa,aAAA,EAAe;AAC1C,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN;AAAA,OACF;AAAA,IACF;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,uBACEC,eAAA,CAAAC,mBAAA,EAAA,EAEE,QAAA,EAAA;AAAA,oBAAAC,cAAA;AAAA,MAAC,OAAA;AAAA,MAAA;AAAA,QACC,GAAA,EAAK,aAAA;AAAA,QACL,IAAA,EAAK,QAAA;AAAA,QACL,IAAA,EAAM,SAAA;AAAA,QACN,EAAA,EAAI,OAAA;AAAA,QACJ,aAAA,EAAY;AAAA;AAAA,KACd;AAAA,oBAGAA,cAAA;AAAA,MAACC,uBAAA;AAAA,MAAA;AAAA,QACC,GAAA,EAAK,kDAAkD,OAAO,CAAA,CAAA;AAAA,QAC9D,QAAA,EAAS,kBAAA;AAAA,QACT,QAAQ,MAAM;AACZ,UAAA,gBAAA,EAAiB;AAAA,QACnB,CAAA;AAAA,QACA,SAAS,MAAM;AACb,UAAA,OAAA,CAAQ,MAAM,6CAA6C,CAAA;AAC3D,UAAA,IAAI,OAAA,EAAS;AACX,YAAA,OAAA,CAAQ,IAAI,KAAA,CAAM,iCAAiC,CAAC,CAAA;AAAA,UACtD;AAAA,QACF;AAAA;AAAA;AACF,GAAA,EACF,CAAA;AAEJ;AAEA,IAAO,cAAA,GAAQ","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"]}
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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"]}
|