nuxt4-turnstile 1.0.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/README.md +266 -0
- package/dist/module.d.mts +59 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +75 -0
- package/dist/runtime/components/NuxtTurnstile.d.vue.ts +45 -0
- package/dist/runtime/components/NuxtTurnstile.vue +132 -0
- package/dist/runtime/components/NuxtTurnstile.vue.d.ts +45 -0
- package/dist/runtime/composables/useTurnstile.d.ts +21 -0
- package/dist/runtime/composables/useTurnstile.js +77 -0
- package/dist/runtime/plugin.client.d.ts +5 -0
- package/dist/runtime/plugin.client.js +22 -0
- package/dist/runtime/server/api/validate.d.ts +19 -0
- package/dist/runtime/server/api/validate.js +22 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/verifyTurnstileToken.d.ts +41 -0
- package/dist/runtime/server/utils/verifyTurnstileToken.js +53 -0
- package/dist/runtime/types.d.ts +130 -0
- package/dist/runtime/types.js +0 -0
- package/dist/types.d.mts +5 -0
- package/package.json +66 -0
- package/public/screenshot.png +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Nuxt4 Turnstile
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/nuxt4-turnstile)
|
|
4
|
+
[](https://nuxt.com)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Cloudflare Turnstile integration for **Nuxt 4** - A privacy-focused CAPTCHA alternative.
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://raw.githubusercontent.com/bootssecurity/nuxt4-turnstile/main/public/screenshot.png" width="600" alt="Nuxt4 Turnstile Demo">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
## ✨ Features
|
|
14
|
+
|
|
15
|
+
- 🔒 **Privacy-focused** - No tracking, no cookies, no fingerprinting
|
|
16
|
+
- 🚀 **Nuxt 4 Compatible** - Built specifically for Nuxt 4
|
|
17
|
+
- 📦 **Auto-imported** - Components and composables ready to use
|
|
18
|
+
- 🛡️ **Server Validation** - Built-in server-side token verification
|
|
19
|
+
- 🎨 **Customizable** - Theme, size, appearance options
|
|
20
|
+
- ♻️ **Auto-refresh** - Automatically refreshes tokens before expiry
|
|
21
|
+
- 📝 **TypeScript** - Full TypeScript support
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# npm
|
|
27
|
+
npm install nuxt4-turnstile
|
|
28
|
+
|
|
29
|
+
# pnpm
|
|
30
|
+
pnpm add nuxt4-turnstile
|
|
31
|
+
|
|
32
|
+
# bun
|
|
33
|
+
bun add nuxt4-turnstile
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## ⚙️ Configuration
|
|
37
|
+
|
|
38
|
+
Add `nuxt4-turnstile` to your `nuxt.config.ts`:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
export default defineNuxtConfig({
|
|
42
|
+
modules: ['nuxt4-turnstile'],
|
|
43
|
+
|
|
44
|
+
turnstile: {
|
|
45
|
+
siteKey: 'your-site-key', // Get from Cloudflare Dashboard
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
runtimeConfig: {
|
|
49
|
+
turnstile: {
|
|
50
|
+
// Override with NUXT_TURNSTILE_SECRET_KEY env variable
|
|
51
|
+
secretKey: '',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Get Your Keys
|
|
58
|
+
|
|
59
|
+
1. Go to [Cloudflare Turnstile](https://dash.cloudflare.com/?to=/:account/turnstile)
|
|
60
|
+
2. Create a new site
|
|
61
|
+
3. Copy your **Site Key** (public) and **Secret Key** (server-side)
|
|
62
|
+
|
|
63
|
+
### Configuration Options
|
|
64
|
+
|
|
65
|
+
| Option | Type | Default | Description |
|
|
66
|
+
|--------|------|---------|-------------|
|
|
67
|
+
| `siteKey` | `string` | `''` | Your Turnstile site key (required) |
|
|
68
|
+
| `secretKey` | `string` | `''` | Your Turnstile secret key (server-side) |
|
|
69
|
+
| `addValidateEndpoint` | `boolean` | `false` | Add `/_turnstile/validate` endpoint |
|
|
70
|
+
| `appearance` | `'always' \| 'execute' \| 'interaction-only'` | `'always'` | Widget visibility |
|
|
71
|
+
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Widget theme |
|
|
72
|
+
| `size` | `'normal' \| 'compact' \| 'flexible'` | `'normal'` | Widget size |
|
|
73
|
+
| `retry` | `'auto' \| 'never'` | `'auto'` | Retry behavior |
|
|
74
|
+
| `retryInterval` | `number` | `8000` | Retry interval in ms |
|
|
75
|
+
| `refreshExpired` | `number` | `250` | Auto-refresh before expiry (seconds) |
|
|
76
|
+
| `language` | `string` | `'auto'` | Widget language |
|
|
77
|
+
|
|
78
|
+
## 🚀 Usage
|
|
79
|
+
|
|
80
|
+
### Component
|
|
81
|
+
|
|
82
|
+
Use the auto-imported `<NuxtTurnstile>` component:
|
|
83
|
+
|
|
84
|
+
```vue
|
|
85
|
+
<template>
|
|
86
|
+
<form @submit.prevent="onSubmit">
|
|
87
|
+
<NuxtTurnstile v-model="token" />
|
|
88
|
+
<button type="submit" :disabled="!token">Submit</button>
|
|
89
|
+
</form>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<script setup>
|
|
93
|
+
const token = ref('')
|
|
94
|
+
|
|
95
|
+
async function onSubmit() {
|
|
96
|
+
// Send token to your server for verification
|
|
97
|
+
await $fetch('/api/contact', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
body: { token, message: '...' }
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Component Props
|
|
106
|
+
|
|
107
|
+
| Prop | Type | Description |
|
|
108
|
+
|------|------|-------------|
|
|
109
|
+
| `v-model` | `string` | Two-way binding for the token |
|
|
110
|
+
| `element` | `string` | HTML element to use (default: `'div'`) |
|
|
111
|
+
| `options` | `TurnstileOptions` | Override module options |
|
|
112
|
+
| `action` | `string` | Custom action for analytics |
|
|
113
|
+
| `cData` | `string` | Custom data payload |
|
|
114
|
+
|
|
115
|
+
### Component Events
|
|
116
|
+
|
|
117
|
+
| Event | Payload | Description |
|
|
118
|
+
|-------|---------|-------------|
|
|
119
|
+
| `@verify` | `token: string` | Token generated |
|
|
120
|
+
| `@expire` | - | Token expired |
|
|
121
|
+
| `@error` | `error: Error` | Error occurred |
|
|
122
|
+
| `@before-interactive` | - | Before challenge |
|
|
123
|
+
| `@after-interactive` | - | After challenge |
|
|
124
|
+
| `@unsupported` | - | Browser unsupported |
|
|
125
|
+
|
|
126
|
+
### Component Methods (via ref)
|
|
127
|
+
|
|
128
|
+
```vue
|
|
129
|
+
<template>
|
|
130
|
+
<NuxtTurnstile ref="turnstile" v-model="token" />
|
|
131
|
+
<button @click="turnstile?.reset()">Reset</button>
|
|
132
|
+
</template>
|
|
133
|
+
|
|
134
|
+
<script setup>
|
|
135
|
+
const turnstile = ref()
|
|
136
|
+
const token = ref('')
|
|
137
|
+
</script>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
| Method | Description |
|
|
141
|
+
|--------|-------------|
|
|
142
|
+
| `reset()` | Reset widget for re-verification |
|
|
143
|
+
| `remove()` | Remove widget from DOM |
|
|
144
|
+
| `getResponse()` | Get current token |
|
|
145
|
+
| `isExpired()` | Check if token is expired |
|
|
146
|
+
| `execute()` | Execute invisible challenge |
|
|
147
|
+
|
|
148
|
+
## 🛡️ Server Verification
|
|
149
|
+
|
|
150
|
+
### Using the Built-in Endpoint
|
|
151
|
+
|
|
152
|
+
Enable the validation endpoint in your config:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
export default defineNuxtConfig({
|
|
156
|
+
turnstile: {
|
|
157
|
+
siteKey: '...',
|
|
158
|
+
addValidateEndpoint: true,
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Then call it from your client:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const { success } = await $fetch('/_turnstile/validate', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
body: { token }
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Using the Helper Function
|
|
173
|
+
|
|
174
|
+
In your server routes:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// server/api/contact.post.ts
|
|
178
|
+
export default defineEventHandler(async (event) => {
|
|
179
|
+
const { token, message } = await readBody(event)
|
|
180
|
+
|
|
181
|
+
// Verify the token
|
|
182
|
+
const result = await verifyTurnstileToken(token)
|
|
183
|
+
|
|
184
|
+
if (!result.success) {
|
|
185
|
+
throw createError({
|
|
186
|
+
statusCode: 400,
|
|
187
|
+
message: 'Invalid captcha'
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Continue with your logic...
|
|
192
|
+
return { success: true }
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Verification Options
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
await verifyTurnstileToken(token, {
|
|
200
|
+
secretKey: 'override-secret', // Override config secret
|
|
201
|
+
remoteip: '1.2.3.4', // Client IP for security
|
|
202
|
+
action: 'login', // Validate expected action
|
|
203
|
+
cdata: 'user-123', // Validate expected cdata
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## 🔧 Composable
|
|
208
|
+
|
|
209
|
+
Use the `useTurnstile` composable for programmatic access:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const {
|
|
213
|
+
isAvailable, // Is Turnstile loaded?
|
|
214
|
+
siteKey, // Get site key
|
|
215
|
+
verify, // Verify token via endpoint
|
|
216
|
+
render, // Render widget programmatically
|
|
217
|
+
reset, // Reset widget
|
|
218
|
+
remove, // Remove widget
|
|
219
|
+
getResponse, // Get token
|
|
220
|
+
isExpired, // Check expiry
|
|
221
|
+
execute, // Execute invisible challenge
|
|
222
|
+
} = useTurnstile()
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## 🌐 Environment Variables
|
|
226
|
+
|
|
227
|
+
Override configuration with environment variables:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
NUXT_PUBLIC_TURNSTILE_SITE_KEY=your-site-key
|
|
231
|
+
NUXT_TURNSTILE_SECRET_KEY=your-secret-key
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 📝 TypeScript
|
|
235
|
+
|
|
236
|
+
Types are automatically available:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import type {
|
|
240
|
+
TurnstileInstance,
|
|
241
|
+
TurnstileOptions,
|
|
242
|
+
TurnstileVerifyResponse,
|
|
243
|
+
} from 'nuxt4-turnstile'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## 🧪 Testing
|
|
247
|
+
|
|
248
|
+
For testing, use Cloudflare's test keys:
|
|
249
|
+
|
|
250
|
+
| Key | Behavior |
|
|
251
|
+
|-----|----------|
|
|
252
|
+
| `1x00000000000000000000AA` | Always passes |
|
|
253
|
+
| `2x00000000000000000000AB` | Always blocks |
|
|
254
|
+
| `3x00000000000000000000FF` | Forces interactive challenge |
|
|
255
|
+
|
|
256
|
+
Secret test keys:
|
|
257
|
+
|
|
258
|
+
| Key | Behavior |
|
|
259
|
+
|-----|----------|
|
|
260
|
+
| `1x0000000000000000000000000000000AA` | Always passes |
|
|
261
|
+
| `2x0000000000000000000000000000000AA` | Always fails |
|
|
262
|
+
| `3x0000000000000000000000000000000AA` | Yields token spend error |
|
|
263
|
+
|
|
264
|
+
## 📄 License
|
|
265
|
+
|
|
266
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
export { TurnstileInstance, TurnstileOptions } from '../dist/runtime/types.js';
|
|
3
|
+
|
|
4
|
+
interface ModuleOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Turnstile site key (public)
|
|
7
|
+
* Get one at https://dash.cloudflare.com/turnstile
|
|
8
|
+
*/
|
|
9
|
+
siteKey: string;
|
|
10
|
+
/**
|
|
11
|
+
* Turnstile secret key (server-side only)
|
|
12
|
+
* Override with NUXT_TURNSTILE_SECRET_KEY env variable
|
|
13
|
+
*/
|
|
14
|
+
secretKey?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Add a validation endpoint at /_turnstile/validate
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
addValidateEndpoint?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Turnstile widget appearance
|
|
22
|
+
* @default 'auto'
|
|
23
|
+
*/
|
|
24
|
+
appearance?: 'always' | 'execute' | 'interaction-only';
|
|
25
|
+
/**
|
|
26
|
+
* Turnstile widget theme
|
|
27
|
+
* @default 'auto'
|
|
28
|
+
*/
|
|
29
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
30
|
+
/**
|
|
31
|
+
* Turnstile widget size
|
|
32
|
+
* @default 'normal'
|
|
33
|
+
*/
|
|
34
|
+
size?: 'normal' | 'compact' | 'flexible';
|
|
35
|
+
/**
|
|
36
|
+
* Retry behavior
|
|
37
|
+
* @default 'auto'
|
|
38
|
+
*/
|
|
39
|
+
retry?: 'auto' | 'never';
|
|
40
|
+
/**
|
|
41
|
+
* Retry interval in milliseconds
|
|
42
|
+
* @default 8000
|
|
43
|
+
*/
|
|
44
|
+
retryInterval?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Refresh token before expiry (in seconds)
|
|
47
|
+
* @default 250
|
|
48
|
+
*/
|
|
49
|
+
refreshExpired?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Language code for widget
|
|
52
|
+
* @default 'auto'
|
|
53
|
+
*/
|
|
54
|
+
language?: string;
|
|
55
|
+
}
|
|
56
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
57
|
+
|
|
58
|
+
export { _default as default };
|
|
59
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addPlugin, addComponent, addImports, addServerImports, addServerHandler } from '@nuxt/kit';
|
|
2
|
+
import { defu } from 'defu';
|
|
3
|
+
|
|
4
|
+
const module$1 = defineNuxtModule({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "nuxt4-turnstile",
|
|
7
|
+
configKey: "turnstile",
|
|
8
|
+
compatibility: {
|
|
9
|
+
nuxt: ">=4.0.0"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
// Default configuration options
|
|
13
|
+
defaults: {
|
|
14
|
+
siteKey: "",
|
|
15
|
+
secretKey: "",
|
|
16
|
+
addValidateEndpoint: false,
|
|
17
|
+
appearance: "always",
|
|
18
|
+
theme: "auto",
|
|
19
|
+
size: "normal",
|
|
20
|
+
retry: "auto",
|
|
21
|
+
retryInterval: 8e3,
|
|
22
|
+
refreshExpired: 250,
|
|
23
|
+
language: "auto"
|
|
24
|
+
},
|
|
25
|
+
setup(options, nuxt) {
|
|
26
|
+
const resolver = createResolver(import.meta.url);
|
|
27
|
+
if (!options.siteKey) {
|
|
28
|
+
console.warn("[nuxt-turnstile-cf] No siteKey provided. Set turnstile.siteKey in your nuxt.config or NUXT_PUBLIC_TURNSTILE_SITE_KEY env variable.");
|
|
29
|
+
}
|
|
30
|
+
nuxt.options.runtimeConfig.public = defu(nuxt.options.runtimeConfig.public, {
|
|
31
|
+
turnstile: {
|
|
32
|
+
siteKey: options.siteKey,
|
|
33
|
+
appearance: options.appearance,
|
|
34
|
+
theme: options.theme,
|
|
35
|
+
size: options.size,
|
|
36
|
+
retry: options.retry,
|
|
37
|
+
retryInterval: options.retryInterval,
|
|
38
|
+
refreshExpired: options.refreshExpired,
|
|
39
|
+
language: options.language
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
nuxt.options.runtimeConfig.turnstile = defu(
|
|
43
|
+
nuxt.options.runtimeConfig.turnstile,
|
|
44
|
+
{
|
|
45
|
+
secretKey: options.secretKey || ""
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
addPlugin(resolver.resolve("./runtime/plugin.client"));
|
|
49
|
+
addComponent({
|
|
50
|
+
name: "NuxtTurnstile",
|
|
51
|
+
filePath: resolver.resolve("./runtime/components/NuxtTurnstile.vue")
|
|
52
|
+
});
|
|
53
|
+
addImports({
|
|
54
|
+
name: "useTurnstile",
|
|
55
|
+
as: "useTurnstile",
|
|
56
|
+
from: resolver.resolve("./runtime/composables/useTurnstile")
|
|
57
|
+
});
|
|
58
|
+
addServerImports([
|
|
59
|
+
{
|
|
60
|
+
name: "verifyTurnstileToken",
|
|
61
|
+
as: "verifyTurnstileToken",
|
|
62
|
+
from: resolver.resolve("./runtime/server/utils/verifyTurnstileToken")
|
|
63
|
+
}
|
|
64
|
+
]);
|
|
65
|
+
if (options.addValidateEndpoint) {
|
|
66
|
+
addServerHandler({
|
|
67
|
+
route: "/_turnstile/validate",
|
|
68
|
+
method: "post",
|
|
69
|
+
handler: resolver.resolve("./runtime/server/api/validate")
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TurnstileInstance, TurnstileOptions } from '../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/**
|
|
4
|
+
* HTML element to use as container
|
|
5
|
+
*/
|
|
6
|
+
element?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Custom Turnstile options (overrides module config)
|
|
9
|
+
*/
|
|
10
|
+
options?: Partial<TurnstileOptions>;
|
|
11
|
+
/**
|
|
12
|
+
* Custom action for analytics
|
|
13
|
+
*/
|
|
14
|
+
action?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Custom cData payload
|
|
17
|
+
*/
|
|
18
|
+
cData?: string;
|
|
19
|
+
};
|
|
20
|
+
type __VLS_ModelProps = {
|
|
21
|
+
modelValue?: string;
|
|
22
|
+
};
|
|
23
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
24
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, TurnstileInstance, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
25
|
+
"update:modelValue": (value: string) => any;
|
|
26
|
+
} & {
|
|
27
|
+
verify: (token: string) => any;
|
|
28
|
+
expire: () => any;
|
|
29
|
+
error: (error: Error) => any;
|
|
30
|
+
"before-interactive": () => any;
|
|
31
|
+
"after-interactive": () => any;
|
|
32
|
+
unsupported: () => any;
|
|
33
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
34
|
+
onVerify?: ((token: string) => any) | undefined;
|
|
35
|
+
onExpire?: (() => any) | undefined;
|
|
36
|
+
onError?: ((error: Error) => any) | undefined;
|
|
37
|
+
"onBefore-interactive"?: (() => any) | undefined;
|
|
38
|
+
"onAfter-interactive"?: (() => any) | undefined;
|
|
39
|
+
onUnsupported?: (() => any) | undefined;
|
|
40
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
41
|
+
}>, {
|
|
42
|
+
element: string;
|
|
43
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
44
|
+
declare const _default: typeof __VLS_export;
|
|
45
|
+
export default _default;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="element" ref="containerRef" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
element: { type: String, required: false, default: "div" },
|
|
8
|
+
options: { type: Object, required: false },
|
|
9
|
+
action: { type: String, required: false },
|
|
10
|
+
cData: { type: String, required: false }
|
|
11
|
+
});
|
|
12
|
+
const config = useRuntimeConfig();
|
|
13
|
+
const token = defineModel({ type: String, ...{ default: "" } });
|
|
14
|
+
const containerRef = ref(null);
|
|
15
|
+
const widgetId = ref(null);
|
|
16
|
+
const isReady = ref(false);
|
|
17
|
+
const emit = defineEmits(["verify", "expire", "error", "before-interactive", "after-interactive", "unsupported"]);
|
|
18
|
+
const reset = () => {
|
|
19
|
+
if (widgetId.value && window.turnstile) {
|
|
20
|
+
window.turnstile.reset(widgetId.value);
|
|
21
|
+
token.value = "";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const remove = () => {
|
|
25
|
+
if (widgetId.value && window.turnstile) {
|
|
26
|
+
window.turnstile.remove(widgetId.value);
|
|
27
|
+
widgetId.value = null;
|
|
28
|
+
token.value = "";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const getResponse = () => {
|
|
32
|
+
if (widgetId.value && window.turnstile) {
|
|
33
|
+
return window.turnstile.getResponse(widgetId.value);
|
|
34
|
+
}
|
|
35
|
+
return void 0;
|
|
36
|
+
};
|
|
37
|
+
const isExpired = () => {
|
|
38
|
+
if (widgetId.value && window.turnstile) {
|
|
39
|
+
return window.turnstile.isExpired(widgetId.value);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
const execute = () => {
|
|
44
|
+
if (widgetId.value && window.turnstile) {
|
|
45
|
+
window.turnstile.execute(widgetId.value);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const instance = {
|
|
49
|
+
reset,
|
|
50
|
+
remove,
|
|
51
|
+
getResponse,
|
|
52
|
+
isExpired,
|
|
53
|
+
execute
|
|
54
|
+
};
|
|
55
|
+
defineExpose(instance);
|
|
56
|
+
const renderWidget = () => {
|
|
57
|
+
if (!containerRef.value || !window.turnstile) return;
|
|
58
|
+
const publicConfig = config.public.turnstile;
|
|
59
|
+
const widgetOptions = {
|
|
60
|
+
sitekey: publicConfig.siteKey,
|
|
61
|
+
theme: props.options?.theme || publicConfig.theme,
|
|
62
|
+
size: props.options?.size || publicConfig.size,
|
|
63
|
+
appearance: props.options?.appearance || publicConfig.appearance,
|
|
64
|
+
retry: props.options?.retry || publicConfig.retry,
|
|
65
|
+
"retry-interval": props.options?.["retry-interval"] || publicConfig.retryInterval,
|
|
66
|
+
language: props.options?.language || publicConfig.language,
|
|
67
|
+
action: props.action,
|
|
68
|
+
cData: props.cData,
|
|
69
|
+
callback: (responseToken) => {
|
|
70
|
+
token.value = responseToken;
|
|
71
|
+
emit("verify", responseToken);
|
|
72
|
+
},
|
|
73
|
+
"expired-callback": () => {
|
|
74
|
+
token.value = "";
|
|
75
|
+
emit("expire");
|
|
76
|
+
},
|
|
77
|
+
"error-callback": (error) => {
|
|
78
|
+
token.value = "";
|
|
79
|
+
emit("error", error);
|
|
80
|
+
},
|
|
81
|
+
"before-interactive-callback": () => {
|
|
82
|
+
emit("before-interactive");
|
|
83
|
+
},
|
|
84
|
+
"after-interactive-callback": () => {
|
|
85
|
+
emit("after-interactive");
|
|
86
|
+
},
|
|
87
|
+
"unsupported-callback": () => {
|
|
88
|
+
emit("unsupported");
|
|
89
|
+
},
|
|
90
|
+
...props.options
|
|
91
|
+
};
|
|
92
|
+
widgetId.value = window.turnstile.render(containerRef.value, widgetOptions);
|
|
93
|
+
isReady.value = true;
|
|
94
|
+
};
|
|
95
|
+
const refreshExpiredInterval = ref(null);
|
|
96
|
+
const startAutoRefresh = () => {
|
|
97
|
+
const refreshSeconds = config.public.turnstile.refreshExpired;
|
|
98
|
+
if (refreshSeconds && refreshSeconds > 0) {
|
|
99
|
+
refreshExpiredInterval.value = setInterval(() => {
|
|
100
|
+
if (isExpired()) {
|
|
101
|
+
reset();
|
|
102
|
+
}
|
|
103
|
+
}, refreshSeconds * 1e3);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
onMounted(() => {
|
|
107
|
+
if (window.turnstile) {
|
|
108
|
+
renderWidget();
|
|
109
|
+
startAutoRefresh();
|
|
110
|
+
} else {
|
|
111
|
+
const checkTurnstile = setInterval(() => {
|
|
112
|
+
if (window.turnstile) {
|
|
113
|
+
clearInterval(checkTurnstile);
|
|
114
|
+
renderWidget();
|
|
115
|
+
startAutoRefresh();
|
|
116
|
+
}
|
|
117
|
+
}, 100);
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
clearInterval(checkTurnstile);
|
|
120
|
+
if (!window.turnstile) {
|
|
121
|
+
console.error("[nuxt-turnstile-cf] Turnstile script failed to load");
|
|
122
|
+
}
|
|
123
|
+
}, 1e4);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
onBeforeUnmount(() => {
|
|
127
|
+
if (refreshExpiredInterval.value) {
|
|
128
|
+
clearInterval(refreshExpiredInterval.value);
|
|
129
|
+
}
|
|
130
|
+
remove();
|
|
131
|
+
});
|
|
132
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TurnstileInstance, TurnstileOptions } from '../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/**
|
|
4
|
+
* HTML element to use as container
|
|
5
|
+
*/
|
|
6
|
+
element?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Custom Turnstile options (overrides module config)
|
|
9
|
+
*/
|
|
10
|
+
options?: Partial<TurnstileOptions>;
|
|
11
|
+
/**
|
|
12
|
+
* Custom action for analytics
|
|
13
|
+
*/
|
|
14
|
+
action?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Custom cData payload
|
|
17
|
+
*/
|
|
18
|
+
cData?: string;
|
|
19
|
+
};
|
|
20
|
+
type __VLS_ModelProps = {
|
|
21
|
+
modelValue?: string;
|
|
22
|
+
};
|
|
23
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
24
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, TurnstileInstance, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
25
|
+
"update:modelValue": (value: string) => any;
|
|
26
|
+
} & {
|
|
27
|
+
verify: (token: string) => any;
|
|
28
|
+
expire: () => any;
|
|
29
|
+
error: (error: Error) => any;
|
|
30
|
+
"before-interactive": () => any;
|
|
31
|
+
"after-interactive": () => any;
|
|
32
|
+
unsupported: () => any;
|
|
33
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
34
|
+
onVerify?: ((token: string) => any) | undefined;
|
|
35
|
+
onExpire?: (() => any) | undefined;
|
|
36
|
+
onError?: ((error: Error) => any) | undefined;
|
|
37
|
+
"onBefore-interactive"?: (() => any) | undefined;
|
|
38
|
+
"onAfter-interactive"?: (() => any) | undefined;
|
|
39
|
+
onUnsupported?: (() => any) | undefined;
|
|
40
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
41
|
+
}>, {
|
|
42
|
+
element: string;
|
|
43
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
44
|
+
declare const _default: typeof __VLS_export;
|
|
45
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable for programmatic Turnstile access
|
|
3
|
+
*/
|
|
4
|
+
export declare function useTurnstile(): {
|
|
5
|
+
isAvailable: any;
|
|
6
|
+
siteKey: any;
|
|
7
|
+
verify: (token: string) => Promise<{
|
|
8
|
+
success: boolean;
|
|
9
|
+
error?: string;
|
|
10
|
+
}>;
|
|
11
|
+
render: (element: string | HTMLElement, options?: Partial<{
|
|
12
|
+
callback: (token: string) => void;
|
|
13
|
+
expiredCallback: () => void;
|
|
14
|
+
errorCallback: (error: Error) => void;
|
|
15
|
+
}>) => string | null;
|
|
16
|
+
reset: (widgetId?: string) => void;
|
|
17
|
+
remove: (widgetId?: string) => void;
|
|
18
|
+
getResponse: (widgetId?: string) => string | undefined;
|
|
19
|
+
isExpired: (widgetId?: string) => boolean;
|
|
20
|
+
execute: (widgetId?: string) => void;
|
|
21
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function useTurnstile() {
|
|
2
|
+
const config = useRuntimeConfig();
|
|
3
|
+
const publicConfig = config.public.turnstile;
|
|
4
|
+
const isAvailable = computed(() => {
|
|
5
|
+
if (import.meta.client) {
|
|
6
|
+
return !!window.turnstile;
|
|
7
|
+
}
|
|
8
|
+
return false;
|
|
9
|
+
});
|
|
10
|
+
const siteKey = computed(() => publicConfig?.siteKey);
|
|
11
|
+
const verify = async (token) => {
|
|
12
|
+
try {
|
|
13
|
+
const response = await $fetch("/_turnstile/validate", {
|
|
14
|
+
method: "POST",
|
|
15
|
+
body: { token }
|
|
16
|
+
});
|
|
17
|
+
return response;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const render = (element, options) => {
|
|
26
|
+
if (!import.meta.client || !window.turnstile) {
|
|
27
|
+
console.warn("[nuxt4-turnstile] Turnstile not available");
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return window.turnstile.render(element, {
|
|
31
|
+
sitekey: publicConfig?.siteKey,
|
|
32
|
+
theme: publicConfig?.theme,
|
|
33
|
+
size: publicConfig?.size,
|
|
34
|
+
callback: options?.callback,
|
|
35
|
+
"expired-callback": options?.expiredCallback,
|
|
36
|
+
"error-callback": options?.errorCallback
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const reset = (widgetId) => {
|
|
40
|
+
if (import.meta.client && window.turnstile) {
|
|
41
|
+
window.turnstile.reset(widgetId);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const remove = (widgetId) => {
|
|
45
|
+
if (import.meta.client && window.turnstile) {
|
|
46
|
+
window.turnstile.remove(widgetId);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const getResponse = (widgetId) => {
|
|
50
|
+
if (import.meta.client && window.turnstile) {
|
|
51
|
+
return window.turnstile.getResponse(widgetId);
|
|
52
|
+
}
|
|
53
|
+
return void 0;
|
|
54
|
+
};
|
|
55
|
+
const isExpired = (widgetId) => {
|
|
56
|
+
if (import.meta.client && window.turnstile) {
|
|
57
|
+
return window.turnstile.isExpired(widgetId);
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
const execute = (widgetId) => {
|
|
62
|
+
if (import.meta.client && window.turnstile) {
|
|
63
|
+
window.turnstile.execute(widgetId);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
isAvailable,
|
|
68
|
+
siteKey,
|
|
69
|
+
verify,
|
|
70
|
+
render,
|
|
71
|
+
reset,
|
|
72
|
+
remove,
|
|
73
|
+
getResponse,
|
|
74
|
+
isExpired,
|
|
75
|
+
execute
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default defineNuxtPlugin(() => {
|
|
2
|
+
const config = useRuntimeConfig();
|
|
3
|
+
const publicConfig = config.public.turnstile;
|
|
4
|
+
if (!publicConfig?.siteKey) {
|
|
5
|
+
console.warn("[nuxt4-turnstile] No siteKey configured. Turnstile widget will not work.");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (document.querySelector('script[src*="challenges.cloudflare.com/turnstile"]')) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const script = document.createElement("script");
|
|
12
|
+
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
13
|
+
script.async = true;
|
|
14
|
+
script.defer = true;
|
|
15
|
+
script.onload = () => {
|
|
16
|
+
console.debug("[nuxt4-turnstile] Turnstile script loaded");
|
|
17
|
+
};
|
|
18
|
+
script.onerror = () => {
|
|
19
|
+
console.error("[nuxt4-turnstile] Failed to load Turnstile script");
|
|
20
|
+
};
|
|
21
|
+
document.head.appendChild(script);
|
|
22
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_turnstile/validate
|
|
3
|
+
*
|
|
4
|
+
* Validate a Turnstile token from the client
|
|
5
|
+
*
|
|
6
|
+
* Request body:
|
|
7
|
+
* {
|
|
8
|
+
* "token": "turnstile-response-token"
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* Response:
|
|
12
|
+
* {
|
|
13
|
+
* "success": true,
|
|
14
|
+
* "challenge_ts": "2024-01-01T00:00:00Z",
|
|
15
|
+
* "hostname": "example.com"
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
declare const _default: any;
|
|
19
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { verifyTurnstileToken } from "../utils/verifyTurnstileToken.js";
|
|
2
|
+
export default defineEventHandler(async (event) => {
|
|
3
|
+
const body = await readBody(event);
|
|
4
|
+
if (!body?.token) {
|
|
5
|
+
throw createError({
|
|
6
|
+
statusCode: 422,
|
|
7
|
+
statusMessage: "Token not provided"
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
const remoteip = getHeader(event, "cf-connecting-ip") || getHeader(event, "x-forwarded-for")?.split(",")[0] || getHeader(event, "x-real-ip") || void 0;
|
|
11
|
+
const result = await verifyTurnstileToken(body.token, { remoteip });
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
throw createError({
|
|
14
|
+
statusCode: 400,
|
|
15
|
+
statusMessage: "Token verification failed",
|
|
16
|
+
data: {
|
|
17
|
+
errors: result["error-codes"]
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TurnstileVerifyResponse } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Verify a Turnstile token on the server
|
|
4
|
+
*
|
|
5
|
+
* @param token - The token from the client-side widget
|
|
6
|
+
* @param options - Additional verification options
|
|
7
|
+
* @returns Verification response from Cloudflare
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* // In your server route
|
|
12
|
+
* export default defineEventHandler(async (event) => {
|
|
13
|
+
* const { token } = await readBody(event)
|
|
14
|
+
* const result = await verifyTurnstileToken(token)
|
|
15
|
+
*
|
|
16
|
+
* if (!result.success) {
|
|
17
|
+
* throw createError({ statusCode: 400, message: 'Invalid captcha' })
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* // Continue with your logic...
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifyTurnstileToken(token: string, options?: {
|
|
25
|
+
/**
|
|
26
|
+
* Custom secret key (overrides config)
|
|
27
|
+
*/
|
|
28
|
+
secretKey?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Client's IP address (recommended for security)
|
|
31
|
+
*/
|
|
32
|
+
remoteip?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Expected action (if set during widget render)
|
|
35
|
+
*/
|
|
36
|
+
action?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Expected cData (if set during widget render)
|
|
39
|
+
*/
|
|
40
|
+
cdata?: string;
|
|
41
|
+
}): Promise<TurnstileVerifyResponse>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
|
2
|
+
export async function verifyTurnstileToken(token, options) {
|
|
3
|
+
const config = useRuntimeConfig();
|
|
4
|
+
const secretKey = options?.secretKey || config.turnstile?.secretKey;
|
|
5
|
+
if (!secretKey) {
|
|
6
|
+
console.error("[nuxt4-turnstile] No secret key configured for server-side verification");
|
|
7
|
+
return {
|
|
8
|
+
success: false,
|
|
9
|
+
"error-codes": ["missing-secret-key"]
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (!token) {
|
|
13
|
+
return {
|
|
14
|
+
success: false,
|
|
15
|
+
"error-codes": ["missing-input-response"]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const formData = new URLSearchParams();
|
|
20
|
+
formData.append("secret", secretKey);
|
|
21
|
+
formData.append("response", token);
|
|
22
|
+
if (options?.remoteip) {
|
|
23
|
+
formData.append("remoteip", options.remoteip);
|
|
24
|
+
}
|
|
25
|
+
const response = await fetch(TURNSTILE_VERIFY_URL, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
29
|
+
},
|
|
30
|
+
body: formData.toString()
|
|
31
|
+
});
|
|
32
|
+
const result = await response.json();
|
|
33
|
+
if (options?.action && result.action !== options.action) {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
"error-codes": ["action-mismatch"]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (options?.cdata && result.cdata !== options.cdata) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
"error-codes": ["cdata-mismatch"]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("[nuxt4-turnstile] Verification request failed:", error);
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
"error-codes": ["network-error"]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turnstile Widget Instance
|
|
3
|
+
*/
|
|
4
|
+
export interface TurnstileInstance {
|
|
5
|
+
/**
|
|
6
|
+
* Reset the widget to allow re-verification
|
|
7
|
+
*/
|
|
8
|
+
reset: () => void;
|
|
9
|
+
/**
|
|
10
|
+
* Remove the widget from the DOM
|
|
11
|
+
*/
|
|
12
|
+
remove: () => void;
|
|
13
|
+
/**
|
|
14
|
+
* Get the current response token
|
|
15
|
+
*/
|
|
16
|
+
getResponse: () => string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Check if the widget is expired
|
|
19
|
+
*/
|
|
20
|
+
isExpired: () => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Execute the challenge (for invisible mode)
|
|
23
|
+
*/
|
|
24
|
+
execute: () => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Turnstile Widget Options
|
|
28
|
+
*/
|
|
29
|
+
export interface TurnstileOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Site key from Cloudflare dashboard
|
|
32
|
+
*/
|
|
33
|
+
sitekey: string;
|
|
34
|
+
/**
|
|
35
|
+
* Callback when verification succeeds
|
|
36
|
+
*/
|
|
37
|
+
callback?: (token: string) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Callback when token expires
|
|
40
|
+
*/
|
|
41
|
+
'expired-callback'?: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Callback when error occurs
|
|
44
|
+
*/
|
|
45
|
+
'error-callback'?: (error: Error) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Callback before interactive challenge starts
|
|
48
|
+
*/
|
|
49
|
+
'before-interactive-callback'?: () => void;
|
|
50
|
+
/**
|
|
51
|
+
* Callback after interactive challenge completes
|
|
52
|
+
*/
|
|
53
|
+
'after-interactive-callback'?: () => void;
|
|
54
|
+
/**
|
|
55
|
+
* Callback when widget is unsupported
|
|
56
|
+
*/
|
|
57
|
+
'unsupported-callback'?: () => void;
|
|
58
|
+
/**
|
|
59
|
+
* Widget theme
|
|
60
|
+
*/
|
|
61
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
62
|
+
/**
|
|
63
|
+
* Widget size
|
|
64
|
+
*/
|
|
65
|
+
size?: 'normal' | 'compact' | 'flexible';
|
|
66
|
+
/**
|
|
67
|
+
* Widget appearance
|
|
68
|
+
*/
|
|
69
|
+
appearance?: 'always' | 'execute' | 'interaction-only';
|
|
70
|
+
/**
|
|
71
|
+
* Retry behavior
|
|
72
|
+
*/
|
|
73
|
+
retry?: 'auto' | 'never';
|
|
74
|
+
/**
|
|
75
|
+
* Retry interval in ms
|
|
76
|
+
*/
|
|
77
|
+
'retry-interval'?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Refresh before expiry behavior
|
|
80
|
+
*/
|
|
81
|
+
'refresh-expired'?: 'auto' | 'manual' | 'never';
|
|
82
|
+
/**
|
|
83
|
+
* Language code
|
|
84
|
+
*/
|
|
85
|
+
language?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Custom action for analytics
|
|
88
|
+
*/
|
|
89
|
+
action?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Custom data payload
|
|
92
|
+
*/
|
|
93
|
+
cData?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Response field name for form submission
|
|
96
|
+
*/
|
|
97
|
+
'response-field'?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Custom response field name
|
|
100
|
+
*/
|
|
101
|
+
'response-field-name'?: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Turnstile Verification Response
|
|
105
|
+
*/
|
|
106
|
+
export interface TurnstileVerifyResponse {
|
|
107
|
+
success: boolean;
|
|
108
|
+
challenge_ts?: string;
|
|
109
|
+
hostname?: string;
|
|
110
|
+
'error-codes'?: string[];
|
|
111
|
+
action?: string;
|
|
112
|
+
cdata?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Cloudflare Turnstile global object
|
|
116
|
+
*/
|
|
117
|
+
export interface TurnstileWindow {
|
|
118
|
+
turnstile?: {
|
|
119
|
+
render: (element: string | HTMLElement, options: TurnstileOptions) => string;
|
|
120
|
+
reset: (widgetId?: string) => void;
|
|
121
|
+
remove: (widgetId?: string) => void;
|
|
122
|
+
getResponse: (widgetId?: string) => string | undefined;
|
|
123
|
+
isExpired: (widgetId?: string) => boolean;
|
|
124
|
+
execute: (widgetId?: string) => void;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
declare global {
|
|
128
|
+
interface Window extends TurnstileWindow {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
File without changes
|
package/dist/types.d.mts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt4-turnstile",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cloudflare Turnstile integration for Nuxt 4 - A privacy-focused CAPTCHA alternative",
|
|
5
|
+
"repository": "bootssecurity/nuxt4-turnstile",
|
|
6
|
+
"homepage": "https://github.com/bootssecurity/nuxt4-turnstile",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/types.d.mts",
|
|
12
|
+
"import": "./dist/module.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/module.mjs",
|
|
16
|
+
"typesVersions": {
|
|
17
|
+
"*": {
|
|
18
|
+
".": [
|
|
19
|
+
"./dist/types.d.mts"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"public"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prepack": "nuxt-module-build build",
|
|
29
|
+
"dev": "npm run dev:prepare && nuxi dev playground",
|
|
30
|
+
"dev:build": "nuxi build playground",
|
|
31
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
32
|
+
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest watch",
|
|
36
|
+
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@nuxt/kit": "^4.2.2",
|
|
40
|
+
"defu": "^6.1.4"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@nuxt/devtools": "^3.1.1",
|
|
44
|
+
"@nuxt/eslint-config": "^1.12.1",
|
|
45
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
46
|
+
"@nuxt/schema": "^4.2.2",
|
|
47
|
+
"@nuxt/test-utils": "^3.23.0",
|
|
48
|
+
"@types/node": "latest",
|
|
49
|
+
"changelogen": "^0.6.2",
|
|
50
|
+
"eslint": "^9.39.2",
|
|
51
|
+
"nuxt": "^4.2.2",
|
|
52
|
+
"typescript": "~5.9.3",
|
|
53
|
+
"vitest": "^4.0.17",
|
|
54
|
+
"vue-tsc": "^3.2.2"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"nuxt",
|
|
58
|
+
"nuxt4",
|
|
59
|
+
"nuxt-module",
|
|
60
|
+
"cloudflare",
|
|
61
|
+
"turnstile",
|
|
62
|
+
"captcha",
|
|
63
|
+
"security",
|
|
64
|
+
"bot-protection"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
Binary file
|