@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-01-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release of `@silverassist/recaptcha`
|
|
13
|
+
- `RecaptchaWrapper` client component for automatic token generation
|
|
14
|
+
- Automatic script loading via Next.js `Script` component
|
|
15
|
+
- Token auto-refresh before expiration (90 seconds default)
|
|
16
|
+
- Hidden input field for form submission
|
|
17
|
+
- Configurable callbacks (`onTokenGenerated`, `onError`)
|
|
18
|
+
- Graceful fallback when not configured
|
|
19
|
+
- `validateRecaptcha` server function for token validation
|
|
20
|
+
- Google API verification
|
|
21
|
+
- Score threshold checking
|
|
22
|
+
- Action verification
|
|
23
|
+
- Debug logging option
|
|
24
|
+
- Skip validation when not configured (dev mode)
|
|
25
|
+
- `isRecaptchaEnabled` helper function
|
|
26
|
+
- `getRecaptchaToken` FormData extraction helper
|
|
27
|
+
- Full TypeScript support with exported types
|
|
28
|
+
- `RecaptchaWrapperProps`
|
|
29
|
+
- `RecaptchaValidationResult`
|
|
30
|
+
- `RecaptchaVerifyResponse`
|
|
31
|
+
- `RecaptchaConfig`
|
|
32
|
+
- `RecaptchaValidationOptions`
|
|
33
|
+
- Subpath exports for tree-shaking
|
|
34
|
+
- `@silverassist/recaptcha/client`
|
|
35
|
+
- `@silverassist/recaptcha/server`
|
|
36
|
+
- `@silverassist/recaptcha/types`
|
|
37
|
+
- `@silverassist/recaptcha/constants`
|
|
38
|
+
- Comprehensive test suite with >80% coverage
|
|
39
|
+
- ESM and CommonJS bundle outputs
|
|
40
|
+
|
|
41
|
+
### Security
|
|
42
|
+
|
|
43
|
+
- Server-side token validation to prevent client-side bypass
|
|
44
|
+
- Action verification to prevent token reuse across different forms
|
|
45
|
+
- Configurable score thresholds for different risk levels
|
package/LICENSE
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# PolyForm Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
In order to get any license under these terms, you must agree
|
|
8
|
+
to them as both strict obligations and conditions to all
|
|
9
|
+
your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the
|
|
14
|
+
software to do everything you might do with the software
|
|
15
|
+
that would otherwise infringe the licensor's copyright
|
|
16
|
+
in it for any permitted purpose. However, you may
|
|
17
|
+
only distribute the software according to [Distribution
|
|
18
|
+
License](#distribution-license) and make changes or new works
|
|
19
|
+
based on the software according to [Changes and New Works
|
|
20
|
+
License](#changes-and-new-works-license).
|
|
21
|
+
|
|
22
|
+
## Distribution License
|
|
23
|
+
|
|
24
|
+
The licensor grants you an additional copyright license
|
|
25
|
+
to distribute copies of the software. Your license
|
|
26
|
+
to distribute covers distributing the software with
|
|
27
|
+
changes and new works permitted by [Changes and New Works
|
|
28
|
+
License](#changes-and-new-works-license).
|
|
29
|
+
|
|
30
|
+
## Notices
|
|
31
|
+
|
|
32
|
+
You must ensure that anyone who gets a copy of any part of
|
|
33
|
+
the software from you also gets a copy of these terms or the
|
|
34
|
+
URL for them above, as well as copies of any plain-text lines
|
|
35
|
+
beginning with `Required Notice:` that the licensor provided
|
|
36
|
+
with the software. For example:
|
|
37
|
+
|
|
38
|
+
> Required Notice: Copyright SilverAssist (https://silverassist.com)
|
|
39
|
+
|
|
40
|
+
## Changes and New Works License
|
|
41
|
+
|
|
42
|
+
The licensor grants you an additional copyright license to
|
|
43
|
+
make changes and new works based on the software for any
|
|
44
|
+
permitted purpose.
|
|
45
|
+
|
|
46
|
+
## Patent License
|
|
47
|
+
|
|
48
|
+
The licensor grants you a patent license for the software that
|
|
49
|
+
covers patent claims the licensor can license, or becomes able
|
|
50
|
+
to license, that you would infringe by using the software.
|
|
51
|
+
|
|
52
|
+
## Noncommercial Purposes
|
|
53
|
+
|
|
54
|
+
Any noncommercial purpose is a permitted purpose.
|
|
55
|
+
|
|
56
|
+
## Personal Uses
|
|
57
|
+
|
|
58
|
+
Personal use for research, experiment, and testing for
|
|
59
|
+
the benefit of public knowledge, personal study, private
|
|
60
|
+
entertainment, hobby projects, amateur pursuits, or religious
|
|
61
|
+
observance, without any anticipated commercial application,
|
|
62
|
+
is use for a permitted purpose.
|
|
63
|
+
|
|
64
|
+
## Noncommercial Organizations
|
|
65
|
+
|
|
66
|
+
Use by any charitable organization, educational institution,
|
|
67
|
+
public research organization, public safety or health
|
|
68
|
+
organization, environmental protection organization,
|
|
69
|
+
or government institution is use for a permitted purpose
|
|
70
|
+
regardless of the source of funding or obligations resulting
|
|
71
|
+
from the funding.
|
|
72
|
+
|
|
73
|
+
## Fair Use
|
|
74
|
+
|
|
75
|
+
You may have "fair use" rights for the software under the
|
|
76
|
+
law. These terms do not limit them.
|
|
77
|
+
|
|
78
|
+
## No Other Rights
|
|
79
|
+
|
|
80
|
+
These terms do not allow you to sublicense or transfer any of
|
|
81
|
+
your licenses to anyone else, or prevent the licensor from
|
|
82
|
+
granting licenses to anyone else. These terms do not imply
|
|
83
|
+
any other licenses.
|
|
84
|
+
|
|
85
|
+
## Patent Defense
|
|
86
|
+
|
|
87
|
+
If you make any written claim that the software infringes or
|
|
88
|
+
contributes to infringement of any patent, your patent license
|
|
89
|
+
for the software granted under these terms ends immediately. If
|
|
90
|
+
your company makes such a claim, your patent license ends
|
|
91
|
+
immediately for work on behalf of your company.
|
|
92
|
+
|
|
93
|
+
## Violations
|
|
94
|
+
|
|
95
|
+
The first time you are notified in writing that you have
|
|
96
|
+
violated any of these terms, or done anything with the software
|
|
97
|
+
not covered by your licenses, your licenses can nonetheless
|
|
98
|
+
continue if you come into full compliance with these terms,
|
|
99
|
+
and take practical steps to correct past violations, within
|
|
100
|
+
32 days of receiving notice. Otherwise, all your licenses
|
|
101
|
+
end immediately.
|
|
102
|
+
|
|
103
|
+
## No Liability
|
|
104
|
+
|
|
105
|
+
***As far as the law allows, the software comes as is, without
|
|
106
|
+
any warranty or condition, and the licensor will not be liable
|
|
107
|
+
to you for any damages arising out of these terms or the use
|
|
108
|
+
or nature of the software, under any kind of legal claim.***
|
|
109
|
+
|
|
110
|
+
## Definitions
|
|
111
|
+
|
|
112
|
+
The **licensor** is the individual or entity offering these
|
|
113
|
+
terms, and the **software** is the software the licensor makes
|
|
114
|
+
available under these terms.
|
|
115
|
+
|
|
116
|
+
**You** refers to the individual or entity agreeing to these
|
|
117
|
+
terms.
|
|
118
|
+
|
|
119
|
+
**Your company** is any legal entity, sole proprietorship,
|
|
120
|
+
or other kind of organization that you work for, plus all
|
|
121
|
+
organizations that have control over, are under the control of,
|
|
122
|
+
or are under common control with that organization. **Control**
|
|
123
|
+
means ownership of substantially all the assets of an entity,
|
|
124
|
+
or the power to direct its management and policies by vote,
|
|
125
|
+
contract, or otherwise. Control can be direct or indirect.
|
|
126
|
+
|
|
127
|
+
**Your licenses** are all the licenses granted to you for the
|
|
128
|
+
software under these terms.
|
|
129
|
+
|
|
130
|
+
**Use** means anything you do with the software requiring one
|
|
131
|
+
of your licenses.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
Required Notice: Copyright 2026 SilverAssist (https://silverassist.com)
|
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# @silverassist/recaptcha
|
|
2
|
+
|
|
3
|
+
Google reCAPTCHA v3 integration for Next.js applications with Server Actions support.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@silverassist/recaptcha)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- ✅ **Client Component**: `RecaptchaWrapper` for automatic token generation
|
|
11
|
+
- ✅ **Server Validation**: `validateRecaptcha` function for Server Actions
|
|
12
|
+
- ✅ **TypeScript Support**: Full type definitions included
|
|
13
|
+
- ✅ **Next.js Optimized**: Works with App Router and Server Actions
|
|
14
|
+
- ✅ **Auto Token Refresh**: Tokens refresh automatically before expiration
|
|
15
|
+
- ✅ **Graceful Degradation**: Works in development without credentials
|
|
16
|
+
- ✅ **Configurable Thresholds**: Custom score thresholds per form
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @silverassist/recaptcha
|
|
22
|
+
# or
|
|
23
|
+
yarn add @silverassist/recaptcha
|
|
24
|
+
# or
|
|
25
|
+
pnpm add @silverassist/recaptcha
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
### 1. Get reCAPTCHA Keys
|
|
31
|
+
|
|
32
|
+
1. Go to [Google reCAPTCHA Admin](https://www.google.com/recaptcha/admin)
|
|
33
|
+
2. Create a new site with reCAPTCHA v3
|
|
34
|
+
3. Get your **Site Key** (public) and **Secret Key** (private)
|
|
35
|
+
|
|
36
|
+
### 2. Add Environment Variables
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# .env.local
|
|
40
|
+
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_site_key_here
|
|
41
|
+
RECAPTCHA_SECRET_KEY=your_secret_key_here
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Client Component
|
|
47
|
+
|
|
48
|
+
Add `RecaptchaWrapper` inside your form:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
"use client";
|
|
52
|
+
|
|
53
|
+
import { RecaptchaWrapper } from "@silverassist/recaptcha";
|
|
54
|
+
|
|
55
|
+
export function ContactForm() {
|
|
56
|
+
return (
|
|
57
|
+
<form action={submitForm}>
|
|
58
|
+
<RecaptchaWrapper action="contact_form" />
|
|
59
|
+
<input name="email" type="email" required />
|
|
60
|
+
<textarea name="message" required />
|
|
61
|
+
<button type="submit">Send</button>
|
|
62
|
+
</form>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Server Action
|
|
68
|
+
|
|
69
|
+
Validate the token in your Server Action:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
"use server";
|
|
73
|
+
|
|
74
|
+
import { validateRecaptcha, getRecaptchaToken } from "@silverassist/recaptcha/server";
|
|
75
|
+
|
|
76
|
+
export async function submitForm(formData: FormData) {
|
|
77
|
+
// Get and validate reCAPTCHA token
|
|
78
|
+
const token = getRecaptchaToken(formData);
|
|
79
|
+
const recaptcha = await validateRecaptcha(token, "contact_form");
|
|
80
|
+
|
|
81
|
+
if (!recaptcha.success) {
|
|
82
|
+
return { success: false, message: recaptcha.error };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Process form data...
|
|
86
|
+
const email = formData.get("email");
|
|
87
|
+
const message = formData.get("message");
|
|
88
|
+
|
|
89
|
+
// Your form processing logic here
|
|
90
|
+
|
|
91
|
+
return { success: true };
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API Reference
|
|
96
|
+
|
|
97
|
+
### RecaptchaWrapper
|
|
98
|
+
|
|
99
|
+
Client component that loads reCAPTCHA and generates tokens.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<RecaptchaWrapper
|
|
103
|
+
action="contact_form" // Required: action name for analytics
|
|
104
|
+
inputName="recaptchaToken" // Optional: hidden input name (default: "recaptchaToken")
|
|
105
|
+
inputId="recaptcha-token" // Optional: hidden input id
|
|
106
|
+
siteKey="..." // Optional: override env variable
|
|
107
|
+
refreshInterval={90000} // Optional: token refresh interval in ms (default: 90000)
|
|
108
|
+
onTokenGenerated={(token) => {}} // Optional: callback when token is generated
|
|
109
|
+
onError={(error) => {}} // Optional: callback on error
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### validateRecaptcha
|
|
114
|
+
|
|
115
|
+
Server-side token validation function.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const result = await validateRecaptcha(
|
|
119
|
+
token, // Token from form
|
|
120
|
+
"contact_form", // Expected action (optional)
|
|
121
|
+
{
|
|
122
|
+
scoreThreshold: 0.5, // Minimum score (default: 0.5)
|
|
123
|
+
secretKey: "...", // Override env variable
|
|
124
|
+
debug: true, // Enable debug logging
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Result type:
|
|
129
|
+
// {
|
|
130
|
+
// success: boolean,
|
|
131
|
+
// score: number,
|
|
132
|
+
// error?: string,
|
|
133
|
+
// skipped?: boolean,
|
|
134
|
+
// rawResponse?: RecaptchaVerifyResponse
|
|
135
|
+
// }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### isRecaptchaEnabled
|
|
139
|
+
|
|
140
|
+
Check if reCAPTCHA is configured.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { isRecaptchaEnabled } from "@silverassist/recaptcha/server";
|
|
144
|
+
|
|
145
|
+
if (isRecaptchaEnabled()) {
|
|
146
|
+
// Validate token
|
|
147
|
+
} else {
|
|
148
|
+
// Skip validation (development)
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### getRecaptchaToken
|
|
153
|
+
|
|
154
|
+
Extract token from FormData.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { getRecaptchaToken } from "@silverassist/recaptcha/server";
|
|
158
|
+
|
|
159
|
+
const token = getRecaptchaToken(formData);
|
|
160
|
+
const token = getRecaptchaToken(formData, "customFieldName");
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Score Thresholds
|
|
164
|
+
|
|
165
|
+
reCAPTCHA v3 returns a score from 0.0 to 1.0:
|
|
166
|
+
|
|
167
|
+
| Score | Meaning |
|
|
168
|
+
|-------|---------|
|
|
169
|
+
| 1.0 | Very likely human |
|
|
170
|
+
| 0.7+ | Likely human |
|
|
171
|
+
| 0.5 | Default threshold |
|
|
172
|
+
| 0.3- | Suspicious |
|
|
173
|
+
| 0.0 | Very likely bot |
|
|
174
|
+
|
|
175
|
+
Adjust threshold based on form sensitivity:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// Standard forms
|
|
179
|
+
await validateRecaptcha(token, "contact", { scoreThreshold: 0.5 });
|
|
180
|
+
|
|
181
|
+
// Sensitive forms (payments, account creation)
|
|
182
|
+
await validateRecaptcha(token, "payment", { scoreThreshold: 0.7 });
|
|
183
|
+
|
|
184
|
+
// Low-risk forms (newsletter signup)
|
|
185
|
+
await validateRecaptcha(token, "newsletter", { scoreThreshold: 0.3 });
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Subpath Imports
|
|
189
|
+
|
|
190
|
+
You can import from specific subpaths for better tree-shaking:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// Main exports (client + server + types)
|
|
194
|
+
import { RecaptchaWrapper, validateRecaptcha } from "@silverassist/recaptcha";
|
|
195
|
+
|
|
196
|
+
// Client only
|
|
197
|
+
import { RecaptchaWrapper } from "@silverassist/recaptcha/client";
|
|
198
|
+
|
|
199
|
+
// Server only
|
|
200
|
+
import { validateRecaptcha, getRecaptchaToken, isRecaptchaEnabled } from "@silverassist/recaptcha/server";
|
|
201
|
+
|
|
202
|
+
// Types only
|
|
203
|
+
import type { RecaptchaValidationResult, RecaptchaWrapperProps } from "@silverassist/recaptcha/types";
|
|
204
|
+
|
|
205
|
+
// Constants only
|
|
206
|
+
import { DEFAULT_SCORE_THRESHOLD, RECAPTCHA_CONFIG } from "@silverassist/recaptcha/constants";
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Development
|
|
210
|
+
|
|
211
|
+
In development, when `RECAPTCHA_SECRET_KEY` is not set, validation is skipped and forms work normally. This allows testing without reCAPTCHA credentials.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const result = await validateRecaptcha(token, "test");
|
|
215
|
+
// Returns: { success: true, score: 1, skipped: true }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## TypeScript
|
|
219
|
+
|
|
220
|
+
Full TypeScript support with exported types:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import type {
|
|
224
|
+
RecaptchaWrapperProps,
|
|
225
|
+
RecaptchaValidationResult,
|
|
226
|
+
RecaptchaVerifyResponse,
|
|
227
|
+
RecaptchaConfig,
|
|
228
|
+
RecaptchaValidationOptions,
|
|
229
|
+
} from "@silverassist/recaptcha";
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
[Polyform Noncommercial License 1.0.0](LICENSE)
|
|
235
|
+
|
|
236
|
+
## Links
|
|
237
|
+
|
|
238
|
+
- [GitHub Repository](https://github.com/SilverAssist/recaptcha)
|
|
239
|
+
- [npm Package](https://www.npmjs.com/package/@silverassist/recaptcha)
|
|
240
|
+
- [Google reCAPTCHA v3 Documentation](https://developers.google.com/recaptcha/docs/v3)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for RecaptchaWrapper client component
|
|
5
|
+
*/
|
|
6
|
+
interface RecaptchaWrapperProps {
|
|
7
|
+
/** Action name for reCAPTCHA analytics (e.g., "contact_form", "signup") */
|
|
8
|
+
action: string;
|
|
9
|
+
/** Name attribute for the hidden input (default: "recaptchaToken") */
|
|
10
|
+
inputName?: string;
|
|
11
|
+
/** ID attribute for the hidden input */
|
|
12
|
+
inputId?: string;
|
|
13
|
+
/** Override site key (default: uses NEXT_PUBLIC_RECAPTCHA_SITE_KEY) */
|
|
14
|
+
siteKey?: string;
|
|
15
|
+
/** Token refresh interval in ms (default: 90000 = 90 seconds) */
|
|
16
|
+
refreshInterval?: number;
|
|
17
|
+
/** Callback when token is generated */
|
|
18
|
+
onTokenGenerated?: (token: string) => void;
|
|
19
|
+
/** Callback when an error occurs */
|
|
20
|
+
onError?: (error: Error) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Global window interface extension for reCAPTCHA
|
|
24
|
+
*/
|
|
25
|
+
declare global {
|
|
26
|
+
interface Window {
|
|
27
|
+
grecaptcha: {
|
|
28
|
+
ready: (callback: () => void) => void;
|
|
29
|
+
execute: (siteKey: string, options: {
|
|
30
|
+
action: string;
|
|
31
|
+
}) => Promise<string>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* RecaptchaWrapper - Client component for reCAPTCHA v3 integration
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Loads reCAPTCHA script automatically
|
|
41
|
+
* - Generates token when script loads
|
|
42
|
+
* - Refreshes token periodically (tokens expire after 2 minutes)
|
|
43
|
+
* - Stores token in hidden input field for form submission
|
|
44
|
+
* - Graceful fallback when not configured
|
|
45
|
+
*
|
|
46
|
+
* @example Basic usage
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <form action={formAction}>
|
|
49
|
+
* <RecaptchaWrapper action="contact_form" />
|
|
50
|
+
* <input name="email" type="email" required />
|
|
51
|
+
* <button type="submit">Submit</button>
|
|
52
|
+
* </form>
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example Custom input name
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <RecaptchaWrapper
|
|
58
|
+
* action="signup"
|
|
59
|
+
* inputName="captchaToken"
|
|
60
|
+
* inputId="signup-captcha"
|
|
61
|
+
* />
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example With callbacks
|
|
65
|
+
* ```tsx
|
|
66
|
+
* <RecaptchaWrapper
|
|
67
|
+
* action="payment"
|
|
68
|
+
* onTokenGenerated={(token) => console.log("Token:", token)}
|
|
69
|
+
* onError={(error) => console.error("Error:", error)}
|
|
70
|
+
* />
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
declare function RecaptchaWrapper({ action, inputName, inputId, siteKey: propSiteKey, refreshInterval, onTokenGenerated, onError, }: RecaptchaWrapperProps): react_jsx_runtime.JSX.Element | null;
|
|
74
|
+
|
|
75
|
+
export { RecaptchaWrapper, RecaptchaWrapper as default };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for RecaptchaWrapper client component
|
|
5
|
+
*/
|
|
6
|
+
interface RecaptchaWrapperProps {
|
|
7
|
+
/** Action name for reCAPTCHA analytics (e.g., "contact_form", "signup") */
|
|
8
|
+
action: string;
|
|
9
|
+
/** Name attribute for the hidden input (default: "recaptchaToken") */
|
|
10
|
+
inputName?: string;
|
|
11
|
+
/** ID attribute for the hidden input */
|
|
12
|
+
inputId?: string;
|
|
13
|
+
/** Override site key (default: uses NEXT_PUBLIC_RECAPTCHA_SITE_KEY) */
|
|
14
|
+
siteKey?: string;
|
|
15
|
+
/** Token refresh interval in ms (default: 90000 = 90 seconds) */
|
|
16
|
+
refreshInterval?: number;
|
|
17
|
+
/** Callback when token is generated */
|
|
18
|
+
onTokenGenerated?: (token: string) => void;
|
|
19
|
+
/** Callback when an error occurs */
|
|
20
|
+
onError?: (error: Error) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Global window interface extension for reCAPTCHA
|
|
24
|
+
*/
|
|
25
|
+
declare global {
|
|
26
|
+
interface Window {
|
|
27
|
+
grecaptcha: {
|
|
28
|
+
ready: (callback: () => void) => void;
|
|
29
|
+
execute: (siteKey: string, options: {
|
|
30
|
+
action: string;
|
|
31
|
+
}) => Promise<string>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* RecaptchaWrapper - Client component for reCAPTCHA v3 integration
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Loads reCAPTCHA script automatically
|
|
41
|
+
* - Generates token when script loads
|
|
42
|
+
* - Refreshes token periodically (tokens expire after 2 minutes)
|
|
43
|
+
* - Stores token in hidden input field for form submission
|
|
44
|
+
* - Graceful fallback when not configured
|
|
45
|
+
*
|
|
46
|
+
* @example Basic usage
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <form action={formAction}>
|
|
49
|
+
* <RecaptchaWrapper action="contact_form" />
|
|
50
|
+
* <input name="email" type="email" required />
|
|
51
|
+
* <button type="submit">Submit</button>
|
|
52
|
+
* </form>
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example Custom input name
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <RecaptchaWrapper
|
|
58
|
+
* action="signup"
|
|
59
|
+
* inputName="captchaToken"
|
|
60
|
+
* inputId="signup-captcha"
|
|
61
|
+
* />
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example With callbacks
|
|
65
|
+
* ```tsx
|
|
66
|
+
* <RecaptchaWrapper
|
|
67
|
+
* action="payment"
|
|
68
|
+
* onTokenGenerated={(token) => console.log("Token:", token)}
|
|
69
|
+
* onError={(error) => console.error("Error:", error)}
|
|
70
|
+
* />
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
declare function RecaptchaWrapper({ action, inputName, inputId, siteKey: propSiteKey, refreshInterval, onTokenGenerated, onError, }: RecaptchaWrapperProps): react_jsx_runtime.JSX.Element | null;
|
|
74
|
+
|
|
75
|
+
export { RecaptchaWrapper, RecaptchaWrapper as default };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
6
|
+
|
|
7
|
+
var Script = require('next/script');
|
|
8
|
+
var react = require('react');
|
|
9
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
10
|
+
|
|
11
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
|
|
13
|
+
var Script__default = /*#__PURE__*/_interopDefault(Script);
|
|
14
|
+
|
|
15
|
+
var DEFAULT_TOKEN_REFRESH_INTERVAL = 9e4;
|
|
16
|
+
var RECAPTCHA_CONFIG = {
|
|
17
|
+
/** Default token refresh interval */
|
|
18
|
+
tokenRefreshInterval: DEFAULT_TOKEN_REFRESH_INTERVAL
|
|
19
|
+
};
|
|
20
|
+
function RecaptchaWrapper({
|
|
21
|
+
action,
|
|
22
|
+
inputName = "recaptchaToken",
|
|
23
|
+
inputId = "recaptcha-token",
|
|
24
|
+
siteKey: propSiteKey,
|
|
25
|
+
refreshInterval = RECAPTCHA_CONFIG.tokenRefreshInterval,
|
|
26
|
+
onTokenGenerated,
|
|
27
|
+
onError
|
|
28
|
+
}) {
|
|
29
|
+
const siteKey = propSiteKey ?? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
|
|
30
|
+
const tokenInputRef = react.useRef(null);
|
|
31
|
+
const refreshIntervalRef = react.useRef(null);
|
|
32
|
+
const executeRecaptcha = react.useCallback(async () => {
|
|
33
|
+
if (!siteKey) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
if (typeof window !== "undefined" && window.grecaptcha) {
|
|
38
|
+
window.grecaptcha.ready(async () => {
|
|
39
|
+
try {
|
|
40
|
+
const token = await window.grecaptcha.execute(siteKey, { action });
|
|
41
|
+
if (tokenInputRef.current) {
|
|
42
|
+
tokenInputRef.current.value = token;
|
|
43
|
+
}
|
|
44
|
+
if (onTokenGenerated) {
|
|
45
|
+
onTokenGenerated(token);
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("[reCAPTCHA] Error executing reCAPTCHA:", error);
|
|
49
|
+
if (onError && error instanceof Error) {
|
|
50
|
+
onError(error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("[reCAPTCHA] Error:", error);
|
|
57
|
+
if (onError && error instanceof Error) {
|
|
58
|
+
onError(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, [siteKey, action, onTokenGenerated, onError]);
|
|
62
|
+
react.useEffect(() => {
|
|
63
|
+
executeRecaptcha();
|
|
64
|
+
refreshIntervalRef.current = setInterval(() => {
|
|
65
|
+
executeRecaptcha();
|
|
66
|
+
}, refreshInterval);
|
|
67
|
+
return () => {
|
|
68
|
+
if (refreshIntervalRef.current) {
|
|
69
|
+
clearInterval(refreshIntervalRef.current);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}, [executeRecaptcha, refreshInterval]);
|
|
73
|
+
if (!siteKey) {
|
|
74
|
+
if (process.env.NODE_ENV === "development") {
|
|
75
|
+
console.warn(
|
|
76
|
+
"[reCAPTCHA] Site key not configured. Set NEXT_PUBLIC_RECAPTCHA_SITE_KEY environment variable."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
82
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
83
|
+
"input",
|
|
84
|
+
{
|
|
85
|
+
ref: tokenInputRef,
|
|
86
|
+
type: "hidden",
|
|
87
|
+
name: inputName,
|
|
88
|
+
id: inputId,
|
|
89
|
+
"data-testid": "recaptcha-token-input"
|
|
90
|
+
}
|
|
91
|
+
),
|
|
92
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
93
|
+
Script__default.default,
|
|
94
|
+
{
|
|
95
|
+
src: `https://www.google.com/recaptcha/api.js?render=${siteKey}`,
|
|
96
|
+
strategy: "afterInteractive",
|
|
97
|
+
onLoad: () => {
|
|
98
|
+
executeRecaptcha();
|
|
99
|
+
},
|
|
100
|
+
onError: () => {
|
|
101
|
+
console.error("[reCAPTCHA] Failed to load reCAPTCHA script");
|
|
102
|
+
if (onError) {
|
|
103
|
+
onError(new Error("Failed to load reCAPTCHA script"));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
] });
|
|
109
|
+
}
|
|
110
|
+
var client_default = RecaptchaWrapper;
|
|
111
|
+
|
|
112
|
+
exports.RecaptchaWrapper = RecaptchaWrapper;
|
|
113
|
+
exports.default = client_default;
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
|
115
|
+
//# sourceMappingURL=index.js.map
|