@onderwijsin/nuxt-turnstile 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stichting Onderwijs in
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # @onderwijsin/nuxt-turnstile
2
+
3
+ Nuxt 4 integration for action-aware Cloudflare Turnstile protection. The module registers
4
+ `@nuxtjs/turnstile` and `@nuxt/ui`, exposes the auto-imported `useTurnstile()` composable, and
5
+ provides server helpers for validating single-use Turnstile tokens before a protected operation.
6
+
7
+ ## Installation and configuration
8
+
9
+ ```sh
10
+ pnpm add @onderwijsin/nuxt-turnstile
11
+ ```
12
+
13
+ ```ts
14
+ export default defineNuxtConfig({
15
+ modules: ["@onderwijsin/nuxt-turnstile"],
16
+ turnstile: {
17
+ siteKey: process.env.TURNSTILE_SITE_KEY ?? "",
18
+ secretKey: process.env.TURNSTILE_SECRET_KEY ?? ""
19
+ }
20
+ });
21
+ ```
22
+
23
+ The module's options are:
24
+
25
+ | Option | Default | Purpose |
26
+ | ----------------- | ----------------- | ------------------------------------------------- |
27
+ | `enabled` | `true` | Enables or disables module setup |
28
+ | `siteKey` | `""` | Public key used by the Turnstile widget |
29
+ | `secretKey` | `""` | Server-only key used for verification |
30
+ | `adminToken` | `""` | Optional trusted token that bypasses verification |
31
+ | `adminHeaderName` | `"x-admin-token"` | Header accepted for `adminToken` |
32
+
33
+ `siteKey` is public and is also passed to `@nuxtjs/turnstile`. Keep `secretKey` and `adminToken`
34
+ private. In production, prefer `NUXT_TURNSTILE_SECRET_KEY` for the private runtime value. The
35
+ module's dependency registration also requires the consuming app's normal Nuxt UI stylesheet setup.
36
+
37
+ ## Protecting a form
38
+
39
+ Render `NuxtTurnstile`, choose a stable action for the operation, wait for a token, and forward it
40
+ in `x-turnstile-token`. `useTurnstile()` is auto-imported and provides token lifecycle helpers plus
41
+ Nuxt UI toast feedback.
42
+
43
+ ```vue
44
+ <script setup lang="ts">
45
+ import { TURNSTILE_TOKEN_HEADER } from "@onderwijsin/nuxt-turnstile/runtime";
46
+
47
+ const token = ref<string>();
48
+ const widget = useTemplateRef<{ reset: () => void }>("turnstile");
49
+ const { getTokenWithRetry, isEnabled, showMissingTokenErrorHint, captureTurnstileError } =
50
+ useTurnstile();
51
+
52
+ async function onSubmit() {
53
+ const currentToken = await getTokenWithRetry();
54
+ if (isEnabled.value && !currentToken) {
55
+ showMissingTokenErrorHint();
56
+ return;
57
+ }
58
+
59
+ try {
60
+ await $fetch("/api/user/session", {
61
+ method: "POST",
62
+ body: { email: "user@example.com" },
63
+ headers: currentToken ? { [TURNSTILE_TOKEN_HEADER]: currentToken } : undefined
64
+ });
65
+ } catch (error) {
66
+ if (!captureTurnstileError(error)) throw error;
67
+ } finally {
68
+ widget.value?.reset();
69
+ }
70
+ }
71
+ </script>
72
+
73
+ <template>
74
+ <form @submit.prevent="onSubmit">
75
+ <NuxtTurnstile
76
+ ref="turnstile"
77
+ v-model="token"
78
+ :options="{ action: 'magic-link', appearance: 'interaction-only' }"
79
+ />
80
+ <UButton type="submit" label="Send magic link" />
81
+ </form>
82
+ </template>
83
+ ```
84
+
85
+ Use `getToken()` when the widget is already ready, or `getTokenWithRetry()` when submission may race
86
+ with widget initialization. Reset the widget after every processed submission because Turnstile
87
+ tokens are single-use.
88
+
89
+ ## Protecting a server route
90
+
91
+ Call `assertTurnstileToken` before application validation, persistence, delivery, or other protected
92
+ work. The expected action must match the widget action.
93
+
94
+ ```ts
95
+ import { z } from "zod";
96
+ import { assertTurnstileToken } from "@onderwijsin/nuxt-turnstile/runtime";
97
+
98
+ export default defineEventHandler(async (event) => {
99
+ await assertTurnstileToken(event, "magic-link");
100
+ const body = await readValidatedBody(event, z.object({ email: z.email() }).parse);
101
+
102
+ return await sendMagicLink(body.email);
103
+ });
104
+ ```
105
+
106
+ The helper reads `x-turnstile-token` and verifies it through `@nuxtjs/turnstile`. It rejects
107
+ missing, failed, unavailable, or mismatched-action tokens. In development, an absent secret leaves
108
+ local forms usable; in production, it produces `TURNSTILE_SERVER_MISCONFIGURED`.
109
+
110
+ ## Trusted administrator bypass
111
+
112
+ Configure `adminToken` for trusted server-to-server or administrative requests. The configured
113
+ `adminHeaderName` and `Authorization: Bearer <token>` are both accepted. This bypass is checked
114
+ before Turnstile verification; do not expose the token in public runtime config, client code, logs,
115
+ or application aliases.
116
+
117
+ ## Runtime exports
118
+
119
+ The `@onderwijsin/nuxt-turnstile/runtime` subpath is the explicit runtime API and does not require
120
+ loading the Nuxt module entrypoint. It exports:
121
+
122
+ - `TURNSTILE_TOKEN_HEADER` (`"x-turnstile-token"`)
123
+ - `assertTurnstileToken`
124
+ - `createTurnstileError`
125
+ - `createTurnstileErrorData`
126
+ - `isErrorWithStatusCode`
127
+ - `TurnstileErrorCode` and `TurnstileErrorData` types
128
+
129
+ Stable error codes are `TURNSTILE_TOKEN_MISSING`, `TURNSTILE_VALIDATION_FAILED`,
130
+ `TURNSTILE_ACTION_MISMATCH`, `TURNSTILE_VALIDATION_UNAVAILABLE`, and
131
+ `TURNSTILE_SERVER_MISCONFIGURED`. Apply authorization and business validation in the consuming route
132
+ after Turnstile succeeds; the module does not replace the route's existing submit or business logic.
133
+
134
+ ## Compatibility
135
+
136
+ - Nuxt 4
137
+ - Node.js 22+
138
+ - Node and Cloudflare Workers-compatible server runtime
139
+ - No Sentry dependency or telemetry is included
@@ -0,0 +1,19 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ /** Whether the module is enabled. @default true */
5
+ enabled?: boolean;
6
+ /** Public site key consumed by the Turnstile widget. @default "" */
7
+ siteKey?: string;
8
+ /** Secret key used by server-side Turnstile verification. @default "" */
9
+ secretKey?: string;
10
+ /** Optional administrator token that bypasses Turnstile verification. @default "" */
11
+ adminToken?: string;
12
+ /** Header accepted for the administrator token. @default "x-admin-token" */
13
+ adminHeaderName?: string;
14
+ }
15
+
16
+ /** Registers shared Turnstile configuration, client helpers, and server validation. */
17
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
18
+
19
+ export { _default as default };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@onderwijsin/nuxt-turnstile",
3
+ "configKey": "turnstile",
4
+ "version": "0.2.0",
5
+ "compatibility": {
6
+ "nuxt": "^4.0.0"
7
+ },
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.3",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -0,0 +1,149 @@
1
+ import { defineNuxtModule, useLogger, createResolver, addTypeTemplate, addImportsDir, addServerScanDir } from '@nuxt/kit';
2
+ import { defu } from 'defu';
3
+ import { z } from 'zod';
4
+
5
+ var NUMBER_CHAR_RE = /\d/;
6
+ var STR_SPLITTERS = ["-", "_", "/", "."];
7
+ function isUppercase(char = "") {
8
+ if (NUMBER_CHAR_RE.test(char)) {
9
+ return void 0;
10
+ }
11
+ return char !== char.toLowerCase();
12
+ }
13
+ function splitByCase(str, separators) {
14
+ const splitters = STR_SPLITTERS;
15
+ const parts = [];
16
+ let buff = "";
17
+ let previousUpper;
18
+ let previousSplitter;
19
+ for (const char of str) {
20
+ const isSplitter = splitters.includes(char);
21
+ if (isSplitter === true) {
22
+ parts.push(buff);
23
+ buff = "";
24
+ previousUpper = void 0;
25
+ continue;
26
+ }
27
+ const isUpper = isUppercase(char);
28
+ if (previousSplitter === false) {
29
+ if (previousUpper === false && isUpper === true) {
30
+ parts.push(buff);
31
+ buff = char;
32
+ previousUpper = isUpper;
33
+ continue;
34
+ }
35
+ if (previousUpper === true && isUpper === false && buff.length > 1) {
36
+ const lastChar = buff.at(-1);
37
+ parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
38
+ buff = lastChar + char;
39
+ previousUpper = isUpper;
40
+ continue;
41
+ }
42
+ }
43
+ buff += char;
44
+ previousUpper = isUpper;
45
+ previousSplitter = isSplitter;
46
+ }
47
+ parts.push(buff);
48
+ return parts;
49
+ }
50
+ function kebabCase(str, joiner) {
51
+ return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join("-") : "";
52
+ }
53
+ var enabled = z.boolean().default(true);
54
+ function resolveModuleName(moduleKey) {
55
+ return `@onderwijsin/nuxt-${kebabCase(moduleKey)}`;
56
+ }
57
+ function resolveLoggerScope(moduleKey) {
58
+ return kebabCase(moduleKey);
59
+ }
60
+ function transpileRuntime(nuxt, runtimeDir) {
61
+ nuxt.options.build.transpile.push(runtimeDir);
62
+ }
63
+ function moduleSetup(MODULE_NAME, options, log) {
64
+ const start = () => log.start(`Loading module ${MODULE_NAME}`);
65
+ const end = () => log.success(`Module ${MODULE_NAME} Loaded`);
66
+ const isEnabled = () => {
67
+ if ("enabled" in options && options.enabled === false) {
68
+ log.info(`Module ${MODULE_NAME} is disabled. Skipping setup...`);
69
+ return false;
70
+ }
71
+ return true;
72
+ };
73
+ return { start, end, isEnabled };
74
+ }
75
+ function validateModuleOptions(options, schema, log) {
76
+ const result = schema.safeParse(options);
77
+ if (result.success) {
78
+ return result.data;
79
+ }
80
+ log.info(z.prettifyError(result.error));
81
+ throw new Error("Invalid module options \u261D. Exiting.");
82
+ }
83
+
84
+ const version = "0.2.0";
85
+
86
+ const turnstileOptionsSchema = z.object({
87
+ enabled,
88
+ siteKey: z.string(),
89
+ secretKey: z.string(),
90
+ adminToken: z.string(),
91
+ adminHeaderName: z.string().min(1)
92
+ });
93
+
94
+ const MODULE_KEY = "turnstile";
95
+ const MODULE_NAME = resolveModuleName(MODULE_KEY);
96
+ const DEFAULTS = {
97
+ enabled: true,
98
+ siteKey: "",
99
+ secretKey: "",
100
+ adminToken: "",
101
+ adminHeaderName: "x-admin-token"
102
+ };
103
+ const module$1 = defineNuxtModule({
104
+ meta: {
105
+ name: MODULE_NAME,
106
+ configKey: MODULE_KEY,
107
+ version,
108
+ compatibility: { nuxt: "^4.0.0" }
109
+ },
110
+ defaults: DEFAULTS,
111
+ moduleDependencies: {
112
+ "@nuxt/ui": { version: ">=4.0.0" },
113
+ "@nuxtjs/turnstile": { version: ">=1.1.3" }
114
+ },
115
+ setup(rawOptions, nuxt) {
116
+ const log = useLogger(resolveLoggerScope(MODULE_KEY));
117
+ const { start, end, isEnabled } = moduleSetup(MODULE_NAME, rawOptions, log);
118
+ start();
119
+ const options = validateModuleOptions(rawOptions, turnstileOptionsSchema, log);
120
+ const resolver = createResolver(import.meta.url);
121
+ const runtimeDir = resolver.resolve("./runtime");
122
+ addTypeTemplate({
123
+ filename: "types/turnstile-config.d.ts",
124
+ src: resolver.resolve(runtimeDir, "types/config.d.ts")
125
+ });
126
+ if (!isEnabled()) return;
127
+ nuxt.options.runtimeConfig.turnstile = defu(nuxt.options.runtimeConfig.turnstile, {
128
+ secretKey: options.secretKey,
129
+ adminToken: options.adminToken,
130
+ adminHeaderName: options.adminHeaderName
131
+ });
132
+ nuxt.options.runtimeConfig.public.turnstile = defu(
133
+ nuxt.options.runtimeConfig.public.turnstile,
134
+ {
135
+ siteKey: options.siteKey
136
+ }
137
+ );
138
+ if (!nuxt.options.turnstile) {
139
+ nuxt.options.turnstile = {};
140
+ }
141
+ nuxt.options.turnstile.siteKey ??= options.siteKey;
142
+ transpileRuntime(nuxt, runtimeDir);
143
+ addImportsDir(resolver.resolve(runtimeDir, "app", "composables"));
144
+ addServerScanDir(resolver.resolve(runtimeDir, "server"));
145
+ end();
146
+ }
147
+ });
148
+
149
+ export { module$1 as default };
@@ -0,0 +1,19 @@
1
+ /** Minimal template-ref contract exposed by `NuxtTurnstile`. */
2
+ export interface TurnstileResetInstance {
3
+ reset: () => void;
4
+ }
5
+ /**
6
+ * Provides token lifecycle, retry, reset, and Nuxt UI feedback helpers.
7
+ * @returns Turnstile state and lifecycle helpers.
8
+ */
9
+ export declare function useTurnstile(): {
10
+ token: import("vue").Ref<string | undefined, string | undefined>;
11
+ isEnabled: import("vue").ComputedRef<boolean>;
12
+ getToken: () => string | undefined;
13
+ getTokenWithRetry: (retries?: number, delayMs?: number) => Promise<string | undefined>;
14
+ isReady: () => boolean;
15
+ reset: (instance?: TurnstileResetInstance) => void;
16
+ showPendingHint: () => void;
17
+ showMissingTokenErrorHint: () => void;
18
+ captureTurnstileError: (error: unknown) => boolean;
19
+ };
@@ -0,0 +1,59 @@
1
+ import { computed, ref } from "vue";
2
+ import { useRuntimeConfig, useToast } from "#imports";
3
+ import { turnstileErrorDataSchema } from "../../types/errors.js";
4
+ export function useTurnstile() {
5
+ const runtimeConfig = useRuntimeConfig();
6
+ const token = ref();
7
+ const toast = useToast();
8
+ const isEnabled = computed(() => Boolean(runtimeConfig.public.turnstile?.siteKey?.trim()));
9
+ const getToken = () => token.value?.trim() || void 0;
10
+ async function getTokenWithRetry(retries = 12, delayMs = 250) {
11
+ if (!isEnabled.value) return void 0;
12
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
13
+ const current = getToken();
14
+ if (current) return current;
15
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
16
+ }
17
+ return void 0;
18
+ }
19
+ function reset(instance) {
20
+ token.value = void 0;
21
+ if (isEnabled.value) instance?.reset();
22
+ }
23
+ function showPendingHint() {
24
+ toast.add({
25
+ title: "Please wait",
26
+ description: "Security check in progress",
27
+ color: "warning"
28
+ });
29
+ }
30
+ function showMissingTokenErrorHint() {
31
+ toast.add({
32
+ title: "Security check failed",
33
+ description: "Refresh the page and try again",
34
+ color: "error"
35
+ });
36
+ }
37
+ function captureTurnstileError(error) {
38
+ const data = extractErrorData(error);
39
+ if (!turnstileErrorDataSchema.safeParse(data).success) return false;
40
+ showMissingTokenErrorHint();
41
+ return true;
42
+ }
43
+ return {
44
+ token,
45
+ isEnabled,
46
+ getToken,
47
+ getTokenWithRetry,
48
+ isReady: () => !isEnabled.value || Boolean(getToken()),
49
+ reset,
50
+ showPendingHint,
51
+ showMissingTokenErrorHint,
52
+ captureTurnstileError
53
+ };
54
+ }
55
+ function extractErrorData(error) {
56
+ if (!error || typeof error !== "object" || !("data" in error)) return void 0;
57
+ const data = error.data;
58
+ return data && typeof data === "object" && "data" in data ? data.data : data;
59
+ }
@@ -0,0 +1,2 @@
1
+ /** Header used to forward a browser Turnstile token to a protected API route. */
2
+ export declare const TURNSTILE_TOKEN_HEADER = "x-turnstile-token";
@@ -0,0 +1 @@
1
+ export const TURNSTILE_TOKEN_HEADER = "x-turnstile-token";
@@ -0,0 +1,3 @@
1
+ export { TURNSTILE_TOKEN_HEADER } from "./constants.js";
2
+ export { assertTurnstileToken, createTurnstileError, createTurnstileErrorData, isErrorWithStatusCode } from "./server/utils/turnstile.js";
3
+ export type { TurnstileErrorCode, TurnstileErrorData } from "./types/errors.js";
@@ -0,0 +1,7 @@
1
+ export { TURNSTILE_TOKEN_HEADER } from "./constants.js";
2
+ export {
3
+ assertTurnstileToken,
4
+ createTurnstileError,
5
+ createTurnstileErrorData,
6
+ isErrorWithStatusCode
7
+ } from "./server/utils/turnstile.js";
@@ -0,0 +1,32 @@
1
+ import type { H3Event } from "h3";
2
+ import type { TurnstileErrorCode, TurnstileErrorData } from "../../types/errors.js";
3
+ /**
4
+ * Validates the Turnstile token from a protected request.
5
+ * @param event - H3 request event.
6
+ * @param expectedAction - Action expected from the Turnstile response.
7
+ * @returns Resolves when the token is valid.
8
+ */
9
+ export declare function assertTurnstileToken(event: H3Event, expectedAction: string): Promise<void>;
10
+ /** Creates a stable H3 error for Turnstile failures.
11
+ * @param statusCode - HTTP status code.
12
+ * @param statusMessage - Human-readable status message.
13
+ * @param code - Stable Turnstile error code.
14
+ * @param expectedAction - Optional expected action.
15
+ * @returns An H3 error with normalized Turnstile data.
16
+ */
17
+ export declare function createTurnstileError(statusCode: number, statusMessage: string, code: TurnstileErrorCode, expectedAction?: string): Error;
18
+ /**
19
+ * Creates the serializable Turnstile error payload.
20
+ * @param code - Stable Turnstile error code.
21
+ * @param expectedAction - Optional expected action.
22
+ * @returns Serializable error data.
23
+ */
24
+ export declare function createTurnstileErrorData(code: TurnstileErrorCode, expectedAction?: string): TurnstileErrorData;
25
+ /**
26
+ * Narrows unknown exceptions to errors carrying an HTTP status code.
27
+ * @param error - Unknown exception.
28
+ * @returns Whether the exception has a numeric status code.
29
+ */
30
+ export declare function isErrorWithStatusCode(error: unknown): error is {
31
+ statusCode: number;
32
+ };
@@ -0,0 +1,71 @@
1
+ import { createError, getRequestHeader } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { verifyTurnstileToken } from "@nuxtjs/turnstile/runtime/server/utils/verify.js";
4
+ import { isAdmin } from "module-utils/server";
5
+ import { attempt } from "module-utils/shared";
6
+ import { TURNSTILE_TOKEN_HEADER } from "../../constants.js";
7
+ export async function assertTurnstileToken(event, expectedAction) {
8
+ const config = useRuntimeConfig(event);
9
+ const turnstileConfig = config.turnstile;
10
+ if (isAdmin(event, turnstileConfig?.adminToken, turnstileConfig?.adminHeaderName ?? "x-admin-token"))
11
+ return;
12
+ const secretKey = turnstileConfig?.secretKey?.trim();
13
+ if (!secretKey) {
14
+ if (!import.meta.dev)
15
+ throw createTurnstileError(
16
+ 500,
17
+ "Turnstile secret key is missing",
18
+ "TURNSTILE_SERVER_MISCONFIGURED",
19
+ expectedAction
20
+ );
21
+ return;
22
+ }
23
+ const token = getRequestHeader(event, TURNSTILE_TOKEN_HEADER)?.trim();
24
+ if (!token)
25
+ throw createTurnstileError(
26
+ 400,
27
+ "Turnstile token is missing",
28
+ "TURNSTILE_TOKEN_MISSING",
29
+ expectedAction
30
+ );
31
+ const result = await attempt(() => verifyTurnstileToken(token));
32
+ if (result.error !== null) {
33
+ if (isErrorWithStatusCode(result.error)) throw result.error;
34
+ throw createError({
35
+ statusCode: 502,
36
+ statusMessage: "Turnstile validation could not be performed",
37
+ data: createTurnstileErrorData("TURNSTILE_VALIDATION_UNAVAILABLE", expectedAction),
38
+ cause: result.error
39
+ });
40
+ }
41
+ const verification = result.data;
42
+ if (!verification.success)
43
+ throw createTurnstileError(
44
+ 403,
45
+ "Turnstile validation failed",
46
+ "TURNSTILE_VALIDATION_FAILED",
47
+ expectedAction
48
+ );
49
+ if (verification.action && verification.action !== expectedAction)
50
+ throw createTurnstileError(
51
+ 403,
52
+ "Turnstile action does not match",
53
+ "TURNSTILE_ACTION_MISMATCH",
54
+ expectedAction
55
+ );
56
+ }
57
+ export function createTurnstileError(statusCode, statusMessage, code, expectedAction) {
58
+ return createError({
59
+ statusCode,
60
+ statusMessage,
61
+ data: createTurnstileErrorData(code, expectedAction)
62
+ });
63
+ }
64
+ export function createTurnstileErrorData(code, expectedAction) {
65
+ return expectedAction ? { code, expectedAction } : { code };
66
+ }
67
+ export function isErrorWithStatusCode(error) {
68
+ return Boolean(
69
+ error && typeof error === "object" && "statusCode" in error && typeof error.statusCode === "number"
70
+ );
71
+ }
@@ -0,0 +1,15 @@
1
+ declare module "nuxt/schema" {
2
+ interface RuntimeConfig {
3
+ turnstile: {
4
+ secretKey: string;
5
+ adminToken: string;
6
+ adminHeaderName: string;
7
+ };
8
+ }
9
+
10
+ interface PublicRuntimeConfig {
11
+ turnstile: { siteKey: string };
12
+ }
13
+ }
14
+
15
+ export {};
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ export declare const turnstileErrorCodeSchema: z.ZodEnum<{
3
+ TURNSTILE_TOKEN_MISSING: "TURNSTILE_TOKEN_MISSING";
4
+ TURNSTILE_VALIDATION_FAILED: "TURNSTILE_VALIDATION_FAILED";
5
+ TURNSTILE_ACTION_MISMATCH: "TURNSTILE_ACTION_MISMATCH";
6
+ TURNSTILE_VALIDATION_UNAVAILABLE: "TURNSTILE_VALIDATION_UNAVAILABLE";
7
+ TURNSTILE_SERVER_MISCONFIGURED: "TURNSTILE_SERVER_MISCONFIGURED";
8
+ }>;
9
+ export declare const turnstileErrorDataSchema: z.ZodObject<{
10
+ code: z.ZodEnum<{
11
+ TURNSTILE_TOKEN_MISSING: "TURNSTILE_TOKEN_MISSING";
12
+ TURNSTILE_VALIDATION_FAILED: "TURNSTILE_VALIDATION_FAILED";
13
+ TURNSTILE_ACTION_MISMATCH: "TURNSTILE_ACTION_MISMATCH";
14
+ TURNSTILE_VALIDATION_UNAVAILABLE: "TURNSTILE_VALIDATION_UNAVAILABLE";
15
+ TURNSTILE_SERVER_MISCONFIGURED: "TURNSTILE_SERVER_MISCONFIGURED";
16
+ }>;
17
+ expectedAction: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strip>;
19
+ export type TurnstileErrorCode = z.infer<typeof turnstileErrorCodeSchema>;
20
+ export type TurnstileErrorData = z.infer<typeof turnstileErrorDataSchema>;
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ export const turnstileErrorCodeSchema = z.enum([
3
+ "TURNSTILE_TOKEN_MISSING",
4
+ "TURNSTILE_VALIDATION_FAILED",
5
+ "TURNSTILE_ACTION_MISMATCH",
6
+ "TURNSTILE_VALIDATION_UNAVAILABLE",
7
+ "TURNSTILE_SERVER_MISCONFIGURED"
8
+ ]);
9
+ export const turnstileErrorDataSchema = z.object({
10
+ code: turnstileErrorCodeSchema,
11
+ expectedAction: z.string().optional()
12
+ });
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.mjs'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@onderwijsin/nuxt-turnstile",
3
+ "version": "0.2.0",
4
+ "description": "Nuxt Turnstile integration with client helpers and server-side validation.",
5
+ "homepage": "https://github.com/onderwijsin/nuxt-modules#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/onderwijsin/nuxt-modules/issues"
8
+ },
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/onderwijsin/nuxt-modules.git",
13
+ "directory": "modules/turnstile"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/module.mjs",
20
+ "types": "./dist/types.d.mts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/types.d.mts",
24
+ "import": "./dist/module.mjs"
25
+ },
26
+ "./runtime": {
27
+ "types": "./dist/runtime/index.d.ts",
28
+ "import": "./dist/runtime/index.js"
29
+ }
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@nuxt/kit": "4.5.2",
36
+ "@nuxt/schema": "4.5.2",
37
+ "@nuxt/ui": "4.10.0",
38
+ "@nuxtjs/turnstile": "1.1.3",
39
+ "defu": "6.1.7",
40
+ "h3": "1.15.11",
41
+ "zod": "4.4.3"
42
+ },
43
+ "devDependencies": {
44
+ "@nuxt/module-builder": "1.0.3",
45
+ "@nuxt/test-utils": "4.1.0",
46
+ "nuxt": "4.5.2",
47
+ "typescript": "5.9.3",
48
+ "unbuild": "3.6.1",
49
+ "module-utils": "0.0.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=22"
53
+ },
54
+ "scripts": {
55
+ "build": "nuxt-module-build build",
56
+ "typecheck": "tsc --noEmit --project tsconfig.json",
57
+ "dev": "npm run dev:prepare && nuxt dev playground",
58
+ "dev:build": "nuxt build playground",
59
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build && nuxt prepare playground"
60
+ }
61
+ }