@silverassist/recaptcha 0.2.0 → 0.2.1

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.
@@ -1,12 +1,15 @@
1
1
  import { RecaptchaValidationOptions, RecaptchaValidationResult } from '../types/index.mjs';
2
2
 
3
3
  /**
4
- * reCAPTCHA v3 Server-Side Validation
4
+ * @module @silverassist/recaptcha/server
5
+ * @description reCAPTCHA v3 Server-Side Validation - Functions for validating
6
+ * reCAPTCHA tokens in Next.js Server Actions.
5
7
  *
6
- * Functions for validating reCAPTCHA tokens in Next.js Server Actions.
7
- *
8
- * @see https://developers.google.com/recaptcha/docs/verify
9
- * @packageDocumentation
8
+ * @author Miguel Colmenares <me@miguelcolmenares.com>
9
+ * @license Polyform-Noncommercial-1.0.0
10
+ * @version 0.2.1
11
+ * @see {@link https://developers.google.com/recaptcha/docs/verify|Google reCAPTCHA Verify Documentation}
12
+ * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}
10
13
  */
11
14
 
12
15
  /**
@@ -1,12 +1,15 @@
1
1
  import { RecaptchaValidationOptions, RecaptchaValidationResult } from '../types/index.js';
2
2
 
3
3
  /**
4
- * reCAPTCHA v3 Server-Side Validation
4
+ * @module @silverassist/recaptcha/server
5
+ * @description reCAPTCHA v3 Server-Side Validation - Functions for validating
6
+ * reCAPTCHA tokens in Next.js Server Actions.
5
7
  *
6
- * Functions for validating reCAPTCHA tokens in Next.js Server Actions.
7
- *
8
- * @see https://developers.google.com/recaptcha/docs/verify
9
- * @packageDocumentation
8
+ * @author Miguel Colmenares <me@miguelcolmenares.com>
9
+ * @license Polyform-Noncommercial-1.0.0
10
+ * @version 0.2.1
11
+ * @see {@link https://developers.google.com/recaptcha/docs/verify|Google reCAPTCHA Verify Documentation}
12
+ * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}
10
13
  */
11
14
 
12
15
  /**
@@ -106,6 +106,27 @@ function getRecaptchaToken(formData, fieldName = "recaptchaToken") {
106
106
  const token = formData.get(fieldName);
107
107
  return typeof token === "string" ? token : null;
108
108
  }
109
+ /**
110
+ * @module @silverassist/recaptcha/constants
111
+ * @description reCAPTCHA Configuration Constants - Default configuration values
112
+ * for reCAPTCHA v3 integration.
113
+ *
114
+ * @author Miguel Colmenares <me@miguelcolmenares.com>
115
+ * @license Polyform-Noncommercial-1.0.0
116
+ * @version 0.2.1
117
+ * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}
118
+ */
119
+ /**
120
+ * @module @silverassist/recaptcha/server
121
+ * @description reCAPTCHA v3 Server-Side Validation - Functions for validating
122
+ * reCAPTCHA tokens in Next.js Server Actions.
123
+ *
124
+ * @author Miguel Colmenares <me@miguelcolmenares.com>
125
+ * @license Polyform-Noncommercial-1.0.0
126
+ * @version 0.2.1
127
+ * @see {@link https://developers.google.com/recaptcha/docs/verify|Google reCAPTCHA Verify Documentation}
128
+ * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}
129
+ */
109
130
 
110
131
  exports.getRecaptchaToken = getRecaptchaToken;
111
132
  exports.isRecaptchaEnabled = isRecaptchaEnabled;
@@ -1 +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"]}
1
+ {"version":3,"sources":["../../src/constants/index.ts","../../src/server/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @module @silverassist/recaptcha/constants\n * @description reCAPTCHA Configuration Constants - Default configuration values\n * for reCAPTCHA v3 integration.\n *\n * @author Miguel Colmenares <me@miguelcolmenares.com>\n * @license Polyform-Noncommercial-1.0.0\n * @version 0.2.1\n * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}\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 * @module @silverassist/recaptcha/server\n * @description reCAPTCHA v3 Server-Side Validation - Functions for validating\n * reCAPTCHA tokens in Next.js Server Actions.\n *\n * @author Miguel Colmenares <me@miguelcolmenares.com>\n * @license Polyform-Noncommercial-1.0.0\n * @version 0.2.1\n * @see {@link https://developers.google.com/recaptcha/docs/verify|Google reCAPTCHA Verify Documentation}\n * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}\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"]}
@@ -104,6 +104,27 @@ function getRecaptchaToken(formData, fieldName = "recaptchaToken") {
104
104
  const token = formData.get(fieldName);
105
105
  return typeof token === "string" ? token : null;
106
106
  }
107
+ /**
108
+ * @module @silverassist/recaptcha/constants
109
+ * @description reCAPTCHA Configuration Constants - Default configuration values
110
+ * for reCAPTCHA v3 integration.
111
+ *
112
+ * @author Miguel Colmenares <me@miguelcolmenares.com>
113
+ * @license Polyform-Noncommercial-1.0.0
114
+ * @version 0.2.1
115
+ * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}
116
+ */
117
+ /**
118
+ * @module @silverassist/recaptcha/server
119
+ * @description reCAPTCHA v3 Server-Side Validation - Functions for validating
120
+ * reCAPTCHA tokens in Next.js Server Actions.
121
+ *
122
+ * @author Miguel Colmenares <me@miguelcolmenares.com>
123
+ * @license Polyform-Noncommercial-1.0.0
124
+ * @version 0.2.1
125
+ * @see {@link https://developers.google.com/recaptcha/docs/verify|Google reCAPTCHA Verify Documentation}
126
+ * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}
127
+ */
107
128
 
108
129
  export { getRecaptchaToken, isRecaptchaEnabled, validateRecaptcha };
109
130
  //# sourceMappingURL=index.mjs.map
@@ -1 +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"]}
1
+ {"version":3,"sources":["../../src/constants/index.ts","../../src/server/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @module @silverassist/recaptcha/constants\n * @description reCAPTCHA Configuration Constants - Default configuration values\n * for reCAPTCHA v3 integration.\n *\n * @author Miguel Colmenares <me@miguelcolmenares.com>\n * @license Polyform-Noncommercial-1.0.0\n * @version 0.2.1\n * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}\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 * @module @silverassist/recaptcha/server\n * @description reCAPTCHA v3 Server-Side Validation - Functions for validating\n * reCAPTCHA tokens in Next.js Server Actions.\n *\n * @author Miguel Colmenares <me@miguelcolmenares.com>\n * @license Polyform-Noncommercial-1.0.0\n * @version 0.2.1\n * @see {@link https://developers.google.com/recaptcha/docs/verify|Google reCAPTCHA Verify Documentation}\n * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}\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"]}
@@ -1,9 +1,12 @@
1
1
  /**
2
- * reCAPTCHA v3 Integration Types
2
+ * @module @silverassist/recaptcha/types
3
+ * @description reCAPTCHA v3 Integration Types - Type definitions for Google
4
+ * reCAPTCHA v3 integration with Next.js.
3
5
  *
4
- * Type definitions for Google reCAPTCHA v3 integration with Next.js.
5
- *
6
- * @packageDocumentation
6
+ * @author Miguel Colmenares <me@miguelcolmenares.com>
7
+ * @license Polyform-Noncommercial-1.0.0
8
+ * @version 0.2.1
9
+ * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}
7
10
  */
8
11
  /**
9
12
  * reCAPTCHA v3 verification API response from Google
@@ -53,9 +56,29 @@ interface RecaptchaWrapperProps {
53
56
  siteKey?: string;
54
57
  /** Token refresh interval in ms (default: 90000 = 90 seconds) */
55
58
  refreshInterval?: number;
56
- /** Callback when token is generated */
59
+ /**
60
+ * Callback when token is generated.
61
+ * @remarks Should be memoized with useCallback to prevent unnecessary re-renders.
62
+ * @example
63
+ * ```tsx
64
+ * const handleToken = useCallback((token: string) => {
65
+ * console.log('Token:', token);
66
+ * }, []);
67
+ * <RecaptchaWrapper action="form" onTokenGenerated={handleToken} />
68
+ * ```
69
+ */
57
70
  onTokenGenerated?: (token: string) => void;
58
- /** Callback when an error occurs */
71
+ /**
72
+ * Callback when an error occurs.
73
+ * @remarks Should be memoized with useCallback to prevent unnecessary re-renders.
74
+ * @example
75
+ * ```tsx
76
+ * const handleError = useCallback((error: Error) => {
77
+ * console.error('reCAPTCHA error:', error);
78
+ * }, []);
79
+ * <RecaptchaWrapper action="form" onError={handleError} />
80
+ * ```
81
+ */
59
82
  onError?: (error: Error) => void;
60
83
  /** Enable lazy loading (default: false for backward compatibility) */
61
84
  lazy?: boolean;
@@ -1,9 +1,12 @@
1
1
  /**
2
- * reCAPTCHA v3 Integration Types
2
+ * @module @silverassist/recaptcha/types
3
+ * @description reCAPTCHA v3 Integration Types - Type definitions for Google
4
+ * reCAPTCHA v3 integration with Next.js.
3
5
  *
4
- * Type definitions for Google reCAPTCHA v3 integration with Next.js.
5
- *
6
- * @packageDocumentation
6
+ * @author Miguel Colmenares <me@miguelcolmenares.com>
7
+ * @license Polyform-Noncommercial-1.0.0
8
+ * @version 0.2.1
9
+ * @see {@link https://github.com/SilverAssist/recaptcha|GitHub Repository}
7
10
  */
8
11
  /**
9
12
  * reCAPTCHA v3 verification API response from Google
@@ -53,9 +56,29 @@ interface RecaptchaWrapperProps {
53
56
  siteKey?: string;
54
57
  /** Token refresh interval in ms (default: 90000 = 90 seconds) */
55
58
  refreshInterval?: number;
56
- /** Callback when token is generated */
59
+ /**
60
+ * Callback when token is generated.
61
+ * @remarks Should be memoized with useCallback to prevent unnecessary re-renders.
62
+ * @example
63
+ * ```tsx
64
+ * const handleToken = useCallback((token: string) => {
65
+ * console.log('Token:', token);
66
+ * }, []);
67
+ * <RecaptchaWrapper action="form" onTokenGenerated={handleToken} />
68
+ * ```
69
+ */
57
70
  onTokenGenerated?: (token: string) => void;
58
- /** Callback when an error occurs */
71
+ /**
72
+ * Callback when an error occurs.
73
+ * @remarks Should be memoized with useCallback to prevent unnecessary re-renders.
74
+ * @example
75
+ * ```tsx
76
+ * const handleError = useCallback((error: Error) => {
77
+ * console.error('reCAPTCHA error:', error);
78
+ * }, []);
79
+ * <RecaptchaWrapper action="form" onError={handleError} />
80
+ * ```
81
+ */
59
82
  onError?: (error: Error) => void;
60
83
  /** Enable lazy loading (default: false for backward compatibility) */
61
84
  lazy?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silverassist/recaptcha",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Google reCAPTCHA v3 integration for Next.js applications with Server Actions support",
5
5
  "author": "Miguel Colmenares <me@miguelcolmenares.com>",
6
6
  "license": "Polyform-Noncommercial-1.0.0",
@@ -105,6 +105,7 @@
105
105
  "react-dom": ">=18.0.0"
106
106
  },
107
107
  "devDependencies": {
108
+ "@silverassist/copilot-prompts-kit": "^1.2.0",
108
109
  "@testing-library/jest-dom": "^6.9.1",
109
110
  "@testing-library/react": "^16.3.2",
110
111
  "@types/jest": "^30.0.0",