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 ADDED
@@ -0,0 +1,266 @@
1
+ # Nuxt4 Turnstile
2
+
3
+ [![npm version](https://img.shields.io/npm/v/nuxt4-turnstile.svg)](https://www.npmjs.com/package/nuxt4-turnstile)
4
+ [![Nuxt](https://img.shields.io/badge/Nuxt-4.x-00DC82?logo=nuxt.js)](https://nuxt.com)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt4-turnstile",
3
+ "configKey": "turnstile",
4
+ "compatibility": {
5
+ "nuxt": ">=4.0.0"
6
+ },
7
+ "version": "1.0.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.2",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -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,5 @@
1
+ /**
2
+ * Client plugin to load Cloudflare Turnstile script
3
+ */
4
+ declare const _default: any;
5
+ export default _default;
@@ -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,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json",
3
+ }
@@ -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
@@ -0,0 +1,5 @@
1
+ export { type TurnstileInstance, type TurnstileOptions } from '../dist/runtime/types.js'
2
+
3
+ export { default } from './module.mjs'
4
+
5
+ export { type ModuleOptions } from './module.mjs'
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