From 67ca7f29e61457d977c63375eb005a24dd25d2fd Mon Sep 17 00:00:00 2001
From: Harlan Wilton
Date: Sat, 20 Dec 2025 15:54:28 +1100
Subject: [PATCH 1/6] feat(registry): add Google reCAPTCHA v3 script
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add useScriptGoogleRecaptcha composable for reCAPTCHA v3 integration
- Support enterprise mode and recaptcha.net domain (China)
- Add clientInit for grecaptcha.ready queue pattern
- Add documentation with usage examples
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
.../scripts/utility/google-recaptcha.md | 209 ++++++++++++++++++
src/registry.ts | 19 ++
src/runtime/registry/google-recaptcha.ts | 66 ++++++
src/runtime/types.ts | 2 +
4 files changed, 296 insertions(+)
create mode 100644 docs/content/scripts/utility/google-recaptcha.md
create mode 100644 src/runtime/registry/google-recaptcha.ts
diff --git a/docs/content/scripts/utility/google-recaptcha.md b/docs/content/scripts/utility/google-recaptcha.md
new file mode 100644
index 00000000..81ed39bc
--- /dev/null
+++ b/docs/content/scripts/utility/google-recaptcha.md
@@ -0,0 +1,209 @@
+---
+title: Google reCAPTCHA
+description: Use Google reCAPTCHA v3 in your Nuxt app.
+links:
+ - label: Source
+ icon: i-simple-icons-github
+ to: https://github.com/nuxt/scripts/blob/main/src/runtime/registry/google-recaptcha.ts
+ size: xs
+---
+
+[Google reCAPTCHA](https://www.google.com/recaptcha/about/) protects your site from spam and abuse using advanced risk analysis.
+
+Nuxt Scripts provides a registry script composable `useScriptGoogleRecaptcha` to easily integrate reCAPTCHA v3 in your Nuxt app.
+
+::callout
+This integration supports reCAPTCHA v3 (score-based, invisible) only. For v2 checkbox, use the standard reCAPTCHA integration.
+::
+
+### Loading Globally
+
+::code-group
+
+```ts [Always enabled]
+export default defineNuxtConfig({
+ scripts: {
+ registry: {
+ googleRecaptcha: {
+ siteKey: 'YOUR_SITE_KEY'
+ }
+ }
+ }
+})
+```
+
+```ts [Production only]
+export default defineNuxtConfig({
+ $production: {
+ scripts: {
+ registry: {
+ googleRecaptcha: {
+ siteKey: 'YOUR_SITE_KEY'
+ }
+ }
+ }
+ }
+})
+```
+
+```ts [Environment Variables]
+export default defineNuxtConfig({
+ scripts: {
+ registry: {
+ googleRecaptcha: true,
+ }
+ },
+ runtimeConfig: {
+ public: {
+ scripts: {
+ googleRecaptcha: {
+ // .env
+ // NUXT_PUBLIC_SCRIPTS_GOOGLE_RECAPTCHA_SITE_KEY=
+ siteKey: '',
+ },
+ },
+ },
+ },
+})
+```
+
+::
+
+## useScriptGoogleRecaptcha
+
+The `useScriptGoogleRecaptcha` composable lets you have fine-grain control over when and how reCAPTCHA is loaded on your site.
+
+```ts
+const { proxy } = useScriptGoogleRecaptcha({
+ siteKey: 'YOUR_SITE_KEY'
+})
+
+// Execute reCAPTCHA and get token
+proxy.grecaptcha.ready(async () => {
+ const token = await proxy.grecaptcha.execute('YOUR_SITE_KEY', { action: 'submit' })
+ // Send token to your server for verification
+})
+```
+
+Please follow the [Registry Scripts](/docs/guides/registry-scripts) guide to learn more about advanced usage.
+
+### GoogleRecaptchaApi
+
+```ts
+export interface GoogleRecaptchaApi {
+ grecaptcha: {
+ ready: (callback: () => void) => void
+ execute: (siteKey: string, options: { action: string }) => Promise
+ enterprise?: {
+ ready: (callback: () => void) => void
+ execute: (siteKey: string, options: { action: string }) => Promise
+ }
+ }
+}
+```
+
+### Config Schema
+
+You must provide the options when setting up the script for the first time.
+
+```ts
+export const GoogleRecaptchaOptions = object({
+ /**
+ * Your reCAPTCHA site key from the Google reCAPTCHA admin console.
+ */
+ siteKey: string(),
+ /**
+ * Use reCAPTCHA Enterprise instead of standard reCAPTCHA.
+ */
+ enterprise: optional(boolean()),
+ /**
+ * Load from recaptcha.net instead of google.com (works in China).
+ */
+ recaptchaNet: optional(boolean()),
+ /**
+ * Language code for the reCAPTCHA widget.
+ */
+ hl: optional(string()),
+})
+```
+
+## Example
+
+Using reCAPTCHA v3 to protect a form submission.
+
+::code-group
+
+```vue [ContactForm.vue]
+
+
+
+
+
+```
+
+::
+
+## Enterprise
+
+For reCAPTCHA Enterprise, set the `enterprise` option to `true`:
+
+```ts
+export default defineNuxtConfig({
+ scripts: {
+ registry: {
+ googleRecaptcha: {
+ siteKey: 'YOUR_SITE_KEY',
+ enterprise: true
+ }
+ }
+ }
+})
+```
+
+## China Support
+
+For sites that need to work in China, use `recaptchaNet: true` to load from `recaptcha.net` instead of `google.com`:
+
+```ts
+export default defineNuxtConfig({
+ scripts: {
+ registry: {
+ googleRecaptcha: {
+ siteKey: 'YOUR_SITE_KEY',
+ recaptchaNet: true
+ }
+ }
+ }
+})
+```
+
+## Hiding the Badge
+
+reCAPTCHA v3 displays a badge in the corner of your site. You can hide it with CSS, but you must include attribution in your form:
+
+```css
+.grecaptcha-badge { visibility: hidden; }
+```
+
+```html
+This site is protected by reCAPTCHA and the Google
+ Privacy Policy and
+ Terms of Service apply.
+
+```
diff --git a/src/registry.ts b/src/registry.ts
index 7fbcebda..6a92a965 100644
--- a/src/registry.ts
+++ b/src/registry.ts
@@ -8,6 +8,7 @@ import type { PlausibleAnalyticsInput } from './runtime/registry/plausible-analy
import type { RegistryScript } from './runtime/types'
import type { GoogleAdsenseInput } from './runtime/registry/google-adsense'
import type { ClarityInput } from './runtime/registry/clarity'
+import type { GoogleRecaptchaInput } from './runtime/registry/google-recaptcha'
// avoid nuxt/kit dependency here so we can use in docs
@@ -289,6 +290,24 @@ export async function registry(resolve?: (path: string, opts?: ResolvePathOption
from: await resolve('./runtime/registry/npm'),
},
},
+ {
+ label: 'Google reCAPTCHA',
+ category: 'utility',
+ logo: ``,
+ import: {
+ name: 'useScriptGoogleRecaptcha',
+ from: await resolve('./runtime/registry/google-recaptcha'),
+ },
+ scriptBundling(options?: GoogleRecaptchaInput) {
+ if (!options?.siteKey) {
+ return false
+ }
+ const baseUrl = options?.recaptchaNet
+ ? 'https://www.recaptcha.net/recaptcha'
+ : 'https://www.google.com/recaptcha'
+ return `${baseUrl}/${options?.enterprise ? 'enterprise.js' : 'api.js'}`
+ },
+ },
{
label: 'Google Tag Manager',
category: 'tracking',
diff --git a/src/runtime/registry/google-recaptcha.ts b/src/runtime/registry/google-recaptcha.ts
new file mode 100644
index 00000000..f0d46f63
--- /dev/null
+++ b/src/runtime/registry/google-recaptcha.ts
@@ -0,0 +1,66 @@
+import { withQuery } from 'ufo'
+import { useRegistryScript } from '#nuxt-scripts/utils'
+import type { RegistryScriptInput } from '#nuxt-scripts/types'
+import { object, string, optional, boolean } from '#nuxt-scripts-validator'
+
+export const GoogleRecaptchaOptions = object({
+ siteKey: string(),
+ // Use enterprise.js instead of api.js
+ enterprise: optional(boolean()),
+ // Use recaptcha.net (works in China)
+ recaptchaNet: optional(boolean()),
+ // Language code
+ hl: optional(string()),
+})
+
+export type GoogleRecaptchaInput = RegistryScriptInput
+
+export interface GoogleRecaptchaApi {
+ grecaptcha: {
+ ready: (callback: () => void) => void
+ execute: (siteKey: string, options: { action: string }) => Promise
+ enterprise?: {
+ ready: (callback: () => void) => void
+ execute: (siteKey: string, options: { action: string }) => Promise
+ }
+ }
+}
+
+declare global {
+ interface Window extends GoogleRecaptchaApi {}
+}
+
+export function useScriptGoogleRecaptcha(_options?: GoogleRecaptchaInput) {
+ return useRegistryScript(_options?.key || 'googleRecaptcha', (options) => {
+ const baseUrl = options?.recaptchaNet
+ ? 'https://www.recaptcha.net/recaptcha'
+ : 'https://www.google.com/recaptcha'
+ const scriptPath = options?.enterprise ? 'enterprise.js' : 'api.js'
+
+ return {
+ scriptInput: {
+ src: withQuery(`${baseUrl}/${scriptPath}`, {
+ render: options?.siteKey,
+ hl: options?.hl,
+ }),
+ crossorigin: false,
+ },
+ schema: import.meta.dev ? GoogleRecaptchaOptions : undefined,
+ scriptOptions: {
+ use() {
+ return { grecaptcha: window.grecaptcha }
+ },
+ },
+ clientInit: import.meta.server
+ ? undefined
+ : () => {
+ const w = window as any
+ w.grecaptcha = w.grecaptcha || {}
+ w.grecaptcha.ready = w.grecaptcha.ready || function (cb: () => void) {
+ (w.___grecaptcha_cfg = w.___grecaptcha_cfg || {}).fns
+ = (w.___grecaptcha_cfg.fns || []).concat([cb])
+ }
+ },
+ }
+ }, _options)
+}
diff --git a/src/runtime/types.ts b/src/runtime/types.ts
index ae28b899..7eb55b9e 100644
--- a/src/runtime/types.ts
+++ b/src/runtime/types.ts
@@ -31,6 +31,7 @@ import type { UmamiAnalyticsInput } from './registry/umami-analytics'
import type { RybbitAnalyticsInput } from './registry/rybbit-analytics'
import type { RedditPixelInput } from './registry/reddit-pixel'
import type { PayPalInput } from './registry/paypal'
+import type { GoogleRecaptchaInput } from './registry/google-recaptcha'
import { object } from '#nuxt-scripts-validator'
export type WarmupStrategy = false | 'preload' | 'preconnect' | 'dns-prefetch'
@@ -142,6 +143,7 @@ export interface ScriptRegistry {
googleAdsense?: GoogleAdsenseInput
googleAnalytics?: GoogleAnalyticsInput
googleMaps?: GoogleMapsInput
+ googleRecaptcha?: GoogleRecaptchaInput
lemonSqueezy?: LemonSqueezyInput
googleTagManager?: GoogleTagManagerInput
hotjar?: HotjarInput
From fac2ff7b7a39098db5d29ebbd787add0735df4d6 Mon Sep 17 00:00:00 2001
From: Harlan Wilton
Date: Sat, 20 Dec 2025 16:07:23 +1100
Subject: [PATCH 2/6] fix(google-recaptcha): add enterprise mode queue support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Set up grecaptcha.enterprise.ready queue when enterprise mode is enabled,
so calls before script loads work correctly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
src/runtime/registry/google-recaptcha.ts | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/runtime/registry/google-recaptcha.ts b/src/runtime/registry/google-recaptcha.ts
index f0d46f63..46f05390 100644
--- a/src/runtime/registry/google-recaptcha.ts
+++ b/src/runtime/registry/google-recaptcha.ts
@@ -56,10 +56,16 @@ export function useScriptGoogleRecaptcha(_options?
: () => {
const w = window as any
w.grecaptcha = w.grecaptcha || {}
- w.grecaptcha.ready = w.grecaptcha.ready || function (cb: () => void) {
+ const readyFn = function (cb: () => void) {
(w.___grecaptcha_cfg = w.___grecaptcha_cfg || {}).fns
= (w.___grecaptcha_cfg.fns || []).concat([cb])
}
+ w.grecaptcha.ready = w.grecaptcha.ready || readyFn
+ // Enterprise mode uses grecaptcha.enterprise.ready
+ if (options?.enterprise) {
+ w.grecaptcha.enterprise = w.grecaptcha.enterprise || {}
+ w.grecaptcha.enterprise.ready = w.grecaptcha.enterprise.ready || readyFn
+ }
},
}
}, _options)
From 4ae4f45067027a96a5df01363a4c577b547ae458 Mon Sep 17 00:00:00 2001
From: Harlan Wilton
Date: Sat, 20 Dec 2025 16:43:01 +1100
Subject: [PATCH 3/6] feat(playground): add reCAPTCHA demo page and server
verification docs
---
.../scripts/utility/google-recaptcha.md | 64 +++++++++++++++++++
playground/pages/index.vue | 1 +
.../google-recaptcha/nuxt-scripts.vue | 42 ++++++++++++
3 files changed, 107 insertions(+)
create mode 100644 playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue
diff --git a/docs/content/scripts/utility/google-recaptcha.md b/docs/content/scripts/utility/google-recaptcha.md
index 81ed39bc..0e50ea38 100644
--- a/docs/content/scripts/utility/google-recaptcha.md
+++ b/docs/content/scripts/utility/google-recaptcha.md
@@ -193,6 +193,70 @@ export default defineNuxtConfig({
})
```
+## Server-Side Verification
+
+reCAPTCHA tokens must be verified on your server. Create an API endpoint to validate the token:
+
+::code-group
+
+```ts [server/api/verify-recaptcha.post.ts]
+export default defineEventHandler(async (event) => {
+ const { token } = await readBody(event)
+ const secretKey = process.env.RECAPTCHA_SECRET_KEY
+
+ const response = await $fetch('https://www.google.com/recaptcha/api/siteverify', {
+ method: 'POST',
+ body: new URLSearchParams({
+ secret: secretKey,
+ response: token,
+ }),
+ })
+
+ if (!response.success || response.score < 0.5) {
+ throw createError({
+ statusCode: 400,
+ message: 'reCAPTCHA verification failed',
+ })
+ }
+
+ return { success: true, score: response.score }
+})
+```
+
+```ts [Enterprise - server/api/verify-recaptcha.post.ts]
+export default defineEventHandler(async (event) => {
+ const { token } = await readBody(event)
+ const projectId = process.env.RECAPTCHA_PROJECT_ID
+ const apiKey = process.env.RECAPTCHA_API_KEY
+ const siteKey = process.env.NUXT_PUBLIC_SCRIPTS_GOOGLE_RECAPTCHA_SITE_KEY
+
+ const response = await $fetch(
+ `https://recaptchaenterprise.googleapis.com/v1/projects/${projectId}/assessments?key=${apiKey}`,
+ {
+ method: 'POST',
+ body: {
+ event: { token, siteKey, expectedAction: 'submit' },
+ },
+ }
+ )
+
+ if (!response.tokenProperties?.valid || response.riskAnalysis?.score < 0.5) {
+ throw createError({
+ statusCode: 400,
+ message: 'reCAPTCHA verification failed',
+ })
+ }
+
+ return { success: true, score: response.riskAnalysis.score }
+})
+```
+
+::
+
+::callout{type="warning"}
+Never expose your secret key on the client. Always verify tokens server-side.
+::
+
## Hiding the Badge
reCAPTCHA v3 displays a badge in the corner of your site. You can hide it with CSS, but you must include attribution in your form:
diff --git a/playground/pages/index.vue b/playground/pages/index.vue
index 6deb1828..11fb593e 100644
--- a/playground/pages/index.vue
+++ b/playground/pages/index.vue
@@ -41,6 +41,7 @@ function getPlaygroundPath(script: any): string | null {
'vimeo-player': '/third-parties/vimeo/nuxt-scripts',
'youtube-player': '/third-parties/youtube/nuxt-scripts',
'google-maps': '/third-parties/google-maps/nuxt-scripts',
+ 'google-recaptcha': '/third-parties/google-recaptcha/nuxt-scripts',
'npm': '/npm/js-confetti',
}
diff --git a/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue b/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue
new file mode 100644
index 00000000..30564a4a
--- /dev/null
+++ b/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+ status: {{ status }}
+
+
+
+ Execute reCAPTCHA
+
+
+
+
+ Token: {{ token.slice(0, 50) }}...
+
+
+
+
+
From 9fc606de0b3063ad3d0ec6ac5f4fd8cc5df093e3 Mon Sep 17 00:00:00 2001
From: Harlan Wilton
Date: Sat, 20 Dec 2025 16:49:06 +1100
Subject: [PATCH 4/6] docs(recaptcha): add complete client+server verification
example
---
.../scripts/utility/google-recaptcha.md | 70 ++++++++++++++++---
1 file changed, 60 insertions(+), 10 deletions(-)
diff --git a/docs/content/scripts/utility/google-recaptcha.md b/docs/content/scripts/utility/google-recaptcha.md
index 0e50ea38..6fce66c9 100644
--- a/docs/content/scripts/utility/google-recaptcha.md
+++ b/docs/content/scripts/utility/google-recaptcha.md
@@ -129,34 +129,84 @@ export const GoogleRecaptchaOptions = object({
## Example
-Using reCAPTCHA v3 to protect a form submission.
+Using reCAPTCHA v3 to protect a form submission with server-side verification.
::code-group
```vue [ContactForm.vue]
```
+```ts [server/api/contact.post.ts]
+export default defineEventHandler(async (event) => {
+ const { token, name, email, message } = await readBody(event)
+
+ // Verify reCAPTCHA token
+ const secretKey = process.env.RECAPTCHA_SECRET_KEY
+ const verification = await $fetch('https://www.google.com/recaptcha/api/siteverify', {
+ method: 'POST',
+ body: new URLSearchParams({
+ secret: secretKey,
+ response: token,
+ }),
+ })
+
+ if (!verification.success || verification.score < 0.5) {
+ throw createError({
+ statusCode: 400,
+ message: 'reCAPTCHA verification failed',
+ })
+ }
+
+ // Process the contact form (send email, save to DB, etc.)
+ console.log('Contact form submitted:', { name, email, message, score: verification.score })
+
+ return { success: true }
+})
+```
+
::
## Enterprise
From ee585b618eb9ef3c288111f6de3f59626815eaaf Mon Sep 17 00:00:00 2001
From: Harlan Wilton
Date: Sat, 20 Dec 2025 16:51:05 +1100
Subject: [PATCH 5/6] fix: remove unused variables
---
.../pages/third-parties/google-recaptcha/nuxt-scripts.vue | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue b/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue
index 30564a4a..98ef8b2f 100644
--- a/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue
+++ b/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue
@@ -6,12 +6,11 @@ useHead({
title: 'Google reCAPTCHA',
})
-const { status, proxy, onLoaded } = useScriptGoogleRecaptcha({
+const { status, onLoaded } = useScriptGoogleRecaptcha({
siteKey: '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI', // test key
})
const token = ref(null)
-const score = ref(null)
async function executeRecaptcha() {
onLoaded(async ({ grecaptcha }) => {
From 594de23c85cafc610fa08a3547bbdf7de4d2156d Mon Sep 17 00:00:00 2001
From: Harlan Wilton
Date: Wed, 14 Jan 2026 23:34:34 +1100
Subject: [PATCH 6/6] feat(google-recaptcha): add e2e playground example, test,
and docs for test keys
---
.../scripts/utility/google-recaptcha.md | 27 +++++++++++
.../google-recaptcha/nuxt-scripts.vue | 35 ++++++++++----
.../server/api/recaptcha-verify.post.ts | 27 +++++++++++
test/e2e/basic.test.ts | 23 +++++++++
test/fixtures/basic/pages/tpc/recaptcha.vue | 48 +++++++++++++++++++
.../basic/server/api/recaptcha-verify.post.ts | 18 +++++++
6 files changed, 170 insertions(+), 8 deletions(-)
create mode 100644 playground/server/api/recaptcha-verify.post.ts
create mode 100644 test/fixtures/basic/pages/tpc/recaptcha.vue
create mode 100644 test/fixtures/basic/server/api/recaptcha-verify.post.ts
diff --git a/docs/content/scripts/utility/google-recaptcha.md b/docs/content/scripts/utility/google-recaptcha.md
index 6fce66c9..44b423d5 100644
--- a/docs/content/scripts/utility/google-recaptcha.md
+++ b/docs/content/scripts/utility/google-recaptcha.md
@@ -321,3 +321,30 @@ reCAPTCHA v3 displays a badge in the corner of your site. You can hide it with C
Terms of Service apply.
```
+
+## Test Keys
+
+Google provides test keys for development that always pass verification. Use these for local testing:
+
+| Key Type | Value |
+|----------|-------|
+| Site Key | `6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI` |
+| Secret Key | `6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe` |
+
+::callout{type="info"}
+Test keys will always return `success: true` with a score of `0.9`. See [Google's FAQ](https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do) for more details.
+::
+
+```ts [nuxt.config.ts]
+export default defineNuxtConfig({
+ $development: {
+ scripts: {
+ registry: {
+ googleRecaptcha: {
+ siteKey: '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'
+ }
+ }
+ }
+ }
+})
+```
diff --git a/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue b/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue
index 98ef8b2f..c9d2dee6 100644
--- a/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue
+++ b/playground/pages/third-parties/google-recaptcha/nuxt-scripts.vue
@@ -6,16 +6,29 @@ useHead({
title: 'Google reCAPTCHA',
})
-const { status, onLoaded } = useScriptGoogleRecaptcha({
- siteKey: '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI', // test key
-})
+const SITE_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI' // test key
+
+const { status, onLoaded } = useScriptGoogleRecaptcha({ siteKey: SITE_KEY })
const token = ref(null)
+const verification = ref<{ success: boolean, score?: number, action?: string, errors?: string[] } | null>(null)
+const loading = ref(false)
+
+async function executeAndVerify() {
+ loading.value = true
+ token.value = null
+ verification.value = null
-async function executeRecaptcha() {
onLoaded(async ({ grecaptcha }) => {
- const result = await grecaptcha.execute('6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI', { action: 'submit' })
+ const result = await grecaptcha.execute(SITE_KEY, { action: 'submit' })
token.value = result
+
+ // verify on server
+ verification.value = await $fetch('/api/recaptcha-verify', {
+ method: 'POST',
+ body: { token: result },
+ })
+ loading.value = false
})
}
@@ -24,11 +37,11 @@ async function executeRecaptcha() {
- status: {{ status }}
+ Script status: {{ status }}
-
- Execute reCAPTCHA
+
+ Execute & Verify reCAPTCHA
@@ -36,6 +49,12 @@ async function executeRecaptcha() {
Token: {{ token.slice(0, 50) }}...
+
+
Verification: {{ verification.success ? '✅ Passed' : '❌ Failed' }}
+
Score: {{ verification.score }}
+
Action: {{ verification.action }}
+
Errors: {{ verification.errors.join(', ') }}
+
diff --git a/playground/server/api/recaptcha-verify.post.ts b/playground/server/api/recaptcha-verify.post.ts
new file mode 100644
index 00000000..92a6e759
--- /dev/null
+++ b/playground/server/api/recaptcha-verify.post.ts
@@ -0,0 +1,27 @@
+export default defineEventHandler(async (event) => {
+ const { token } = await readBody(event)
+
+ // Google's test secret key (always passes verification)
+ // https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do
+ const secretKey = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe'
+
+ const response = await $fetch<{
+ success: boolean
+ score?: number
+ action?: string
+ challenge_ts?: string
+ hostname?: string
+ 'error-codes'?: string[]
+ }>('https://www.google.com/recaptcha/api/siteverify', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: new URLSearchParams({ secret: secretKey, response: token }).toString(),
+ })
+
+ return {
+ success: response.success,
+ score: response.score,
+ action: response.action,
+ errors: response['error-codes'],
+ }
+})
diff --git a/test/e2e/basic.test.ts b/test/e2e/basic.test.ts
index 5e54ea81..699121c6 100644
--- a/test/e2e/basic.test.ts
+++ b/test/e2e/basic.test.ts
@@ -182,4 +182,27 @@ describe('third-party-capital', () => {
await page.getByText('trigger').click()
await request
})
+
+ it('expect reCAPTCHA to execute and verify token', {
+ timeout: 15000,
+ }, async () => {
+ const { page } = await createPage('/tpc/recaptcha')
+ await page.waitForTimeout(500)
+
+ // wait for script to load
+ await page.waitForFunction(() => document.querySelector('#status')?.textContent?.trim() === 'loaded', { timeout: 5000 })
+
+ // click execute button (this also verifies via server)
+ await page.click('#execute')
+
+ // wait for token + verification result
+ await page.waitForSelector('#verified', { timeout: 10000 })
+ const token = await page.$eval('#token', el => el.textContent?.trim())
+ const verified = await page.$eval('#verified', el => el.textContent?.trim())
+
+ // token should exist and verification should pass
+ expect(token).toBeTruthy()
+ expect(token!.length).toBeGreaterThan(100)
+ expect(verified).toBe('true')
+ })
})
diff --git a/test/fixtures/basic/pages/tpc/recaptcha.vue b/test/fixtures/basic/pages/tpc/recaptcha.vue
new file mode 100644
index 00000000..53c63a7d
--- /dev/null
+++ b/test/fixtures/basic/pages/tpc/recaptcha.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+ {{ status }}
+
+
+
+
+ {{ token }}
+
+
+ {{ verified }}
+
+
+
diff --git a/test/fixtures/basic/server/api/recaptcha-verify.post.ts b/test/fixtures/basic/server/api/recaptcha-verify.post.ts
new file mode 100644
index 00000000..58717565
--- /dev/null
+++ b/test/fixtures/basic/server/api/recaptcha-verify.post.ts
@@ -0,0 +1,18 @@
+export default defineEventHandler(async (event) => {
+ const { token } = await readBody(event)
+
+ // Google's test secret key (always passes)
+ const secretKey = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe'
+
+ const response = await $fetch<{
+ success: boolean
+ score?: number
+ 'error-codes'?: string[]
+ }>('https://www.google.com/recaptcha/api/siteverify', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: new URLSearchParams({ secret: secretKey, response: token }).toString(),
+ })
+
+ return { success: response.success, score: response.score }
+})