diff --git a/.env.development.enc b/.env.development.enc
index a384960e..80cba719 100644
--- a/.env.development.enc
+++ b/.env.development.enc
@@ -1,99 +1,101 @@
-----BEGIN AGE ENCRYPTED FILE-----
-YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBub2VRQzZSenNIVHNLVE53
-ZHpURXFzcDdJRUtnZlNTRnl0UWJVUXdPeUg0CnFxWXdKSFJTUjk3WldvL2dlTHNE
-TVZHWENDRHJkeG52MG5BcmRkQjZuL3cKLT4gWDI1NTE5IFNDRS92OEdOcmF0c3lw
-S21BR2VLRXcvMmt2ZFM1YmkrQ1JPRWhhUEVwencKZUFIWGZEQjAyd2F4U2ZaN1VZ
-Y3dxUlBmZ3lLMzdqNi83ajJKayt6NWFiSQotPiBYMjU1MTkgbnNLRTZqWlZsOFUz
-VkFkUU5jdzZzMHk1d0p2Uk9NbzJaaUhmdjNNa0MwVQoxNDVEMU5lUjI1UDZYTjFw
-NDJZZndPTjJFSGJJMWxYQnJmZ0FzbGdKWm5BCi0+IFgyNTUxOSBRbGE0WGRxUXNX
-VUR1Q1hrZzBqR0tGMCtyRmJQWGRiMUJpVHVnNWkzZ1I4CmNqZ3d5cU5DdkV4eUk5
-MGtoM05HNXEzNHYxck5NMnZUaXZkNDVEZUJ5U3MKLT4gWDI1NTE5IHFuMXgwTytj
-ZG9ja3dNZGFWVmM0WnNjK1ZpeWEwMkl5ck9QWnhRZzI3aHMKSFRCNDVaNUdvUDV5
-VWtwRmtiZUphR2lHMlZBZEFpaGxRR0lxaTQ0UjF2QQotPiBYMjU1MTkgS1pPMXZB
-dC9UY01GN2hadjZoT0EwVkFLY1VDR291MzJiemhacVRwN0NpMApHL0xKL1hGVmh2
-TGxnZVRiZWFPaEMwT3NVWndoZ2ROSm9vWnJTVmJTV1E0Ci0+IFgyNTUxOSBzR3Fu
-UCtLejBVTm5VUm1DR2RmRHZJUnN4WHNnaDlGbDVDeE1zUzNmN2hvCmtKTmtLZXlT
-ZWJEaklEUFk5WmVJbUFTaU9vZnpiQ1ZwakFKbjVuc3lpUU0KLT4gWDI1NTE5IGV0
-VkFnNzVtRXZHb1JBQmx6ekhPODlrOGlQalZOMVdwWGMwaDl1QW53MkUKTEFqd2Ev
-MEhpYW5oQTloeDNINHpCYUFzaU5kZk90eXR4VUhac2p6OC9LRQotPiBYMjU1MTkg
-ZXJVUVBMVE1DcDMycXhHSm5vK1JneTdnY3VLOUdpemtUWXovZWFKTjlTOApqSnMz
-RDdsYVViaDdhZUN0Y3ZTMFJjQU82SUgyRkprS0ZlcS9zd1RwbDRNCi0+IFgyNTUx
-OSBVWTFrVGV2Z0FoemkwdEVZTDJIL0FaQlROdHY0aEZVb3ZwM2hFWFNGS1gwCjVz
-MHdxQ0FKR2pKak0rcjBCUmltNENPN1BrQWRna3JZZmFaVUZmVXRJbU0KLS0tIGpk
-dkY1bVFlNVZQNHNvZmdwakJadWRRS0V0Y2hjRjFwL044QjN4anVFSGMKf8fLStbp
-wQeMv2cANX78hlpsngMds7m+CwvlnnXVkHmjt6n047N11vbTwg/qiTHkOCFwbrnX
-rZ9YH9I4Zqj1T92JYPU9KrPx5WuUE0wGIjXv5T9SoDsVOSgNQnOnLiTqFutNJCbj
-3fI1AnzdU7aqL8Gdt2b7GjeAjq7zWUMBKq0J2fufjb+55Ul0TY9JWgBrG2oZB1GM
-AjSxwpJZ48V5loAchd9Zt0QMw+gIi42eGQBoh8wf5HgyvgW8nxivho/jo4hzJvM9
-g8utIj7wslw0RvfE9x34xNZMUZESa3RBlypjodFyGzfd6T/ii/AGM6MJw04BDIsC
-H+6XeJdkb+beJnr7yv8CKc80cXB8bmpD3RqvUJmB1JWJFdLIBijrSg/UPKK68w1t
-ySXAUj4LtB9PY2SijszPnw1xiwcQdpPkGIKMs9GqB5tMyIbQlt/RhAdHb/sxOObB
-MlrHTLw4xWLcsyhFujAhxj06lkRTAJexYhqCod1QJboulSkDzLi4SGTJqr7MR2/P
-vgMrMs41jg+tjQKi3NN5HniU4U5jzREwdTh4pBgeH6Zni3r8bp9dcyWG+dNff+Zl
-7CLh5PAfqa/79LnBy8Tq5UVuj5rE9bU6PahllOubjbRlZkJsvKHbbvqOpUI8vy6O
-Jxv1RtY0qGkq534Ez+0NKeMWmvFLw4v2evOhz3TQ/Mm0FzlQTtPFnSt7RARv12Ob
-n2bEdEjPrWOQvlgLP6rHQ3oQKLQpvR63d/lswewtjaIHecgks4VbBBRqG02wWV2R
-zdnfEETwVk5QRCf5KYDvhwPcLEBTyF9f11voj9BcqLh0c5sb8ZquNGpfJFpZnVC6
-kifdflePiVb5uWwKrRTyFkpTq9mB3fNO52n/b1+j3F6/Kon+oE+ojAsu+HUfx0sj
-HSX4eZcq8RbMvldRXgagvJbF7jtZwHnzYJbOmbDVQQmbmnShZ4qWwEDfBMON6hsk
-wTwSR5f05Ov5CjqB/b1XD2JPXuy7bNuebz/roEHV0ntZCw5hn2shsJ5my3tT8gg5
-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
-95k=
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlcSt5OEZvY3o0VG1BZkNz
+eEV4TG1LOXN6V2FzdlZwcExBZVR1K1kxMEdjCjNnb3llSTZ5Tk13MUVIN0NHWEFD
+Zllxa1NZaVo1Nyt3MFV1NXpzSUV6dzQKLT4gWDI1NTE5IFV0WitzWXJUV093MEUr
+NEJsRU83d3lUVDFGbEZXMzNnZ010eE16MHFVaVEKZWx4TkhGd0hDajdYais3ZmFW
+bGVYZ05xeXJnS21mcngzZThxTFlwK2lMTQotPiBYMjU1MTkgQlk5bEdiMGh0VGc4
+dFlZWjB4bHdZZHZNUXE2YUR6aVo4NmZ3c3F4SE1rSQpSYmF0b2VES1ZFUnBRdTFT
+emJlNmYrZDNpT3lYeWJkdWhKNXExMlFUZlg0Ci0+IFgyNTUxOSBnUFpvKzNHRGtq
+V3lhOUtEbmZORXl0ajUvYlo1QnBoTjJTMkYwWjhGTzB3CnJVR2Vrb3IyVnpGbW1t
+UkZHcG5NaTdvZ2dESml5Zi9BWjk0TFdmTk4raTQKLT4gWDI1NTE5IEtYTWNldVdx
+TnpPdnJSb2pTZWFiQmN5ejVuenNpemtVcXY1L2M0RlliVmsKcjNEWlhFWFlQSnVS
+NGpidkdXRUVKNUlYK0hZMDFZc3hUdjY5ZmRDcWMyMAotPiBYMjU1MTkgMlJrRldQ
+YzBhN1lPSW1FTVJMZGd6Y2QvNDNMQzIxdnMraDhwMmo1UnJ3ZwpYR0VOaFBTMnpS
+S3JlcGVUZ0F5enVjK1J4Wm54ZVQva1hFdEZQMFUxTU1NCi0+IFgyNTUxOSBvVXor
+Nk54dG1aOVc0YjhCeHh1SU5reHRlQld4NDIyMm1kMWdjNjhvS2lnCjdXTURqN2lq
+S2J4NzFodjhXQkhMRlBDV1RoeHd4b1h1ak1uakV1dlBZT2MKLT4gWDI1NTE5IFJG
+bG1IelpPQnF4Vnp4cGRYejZXRmdRTmN3NE5ENnF4TEQwMFlicUdzRmsKcjc3bUVT
+QWJEUXQzK3Bad3BCWnZvOFpiSk9VYVFzRGs1WlorVklobVQ0awotPiBYMjU1MTkg
+OHlXMzZ0RkNvZ05EUUw0WllOa1JZTGc2ZHJRY0ZCQy81eTM3SXd1V1poawp2ZE5i
+cVZhc0JCWlhsdG4rVkltZHF3NDNiR1gxQXVsZ1lqTWc1d21VOVc0Ci0+IFgyNTUx
+OSBxUnptd2dlVFBBaUE0cDZ2blZ2Tm1ITE5RSVBlNGJSd1FJUG1GbTNzeGhrCjRh
+eXpWc2k0TEtobDNtS3pJWWM0emFqRzMySWhscHVxL1NKZU5FaDB5ZzgKLS0tIGts
+ZVZiNnFNSjUvMjh2bVI0TXZZeHFWc1o2MytwSytRTkJwTTVoVXJ3Z1UK4621nIw+
+WLp2ggb3x/DPU58z9wbyP73W3Y8+lrTEykWug8h5d0UGRqM1T5IDQOs5JrUGdzh8
+49IkCFhZHjovUUCCyjoUw90hcWYgdS8LSQl7VqWVf6SGnC4YgXf/utIY2cP+vYIO
+LkfUULpb8dooiA95XMWyTp0bSh9lJzflAFVCndnsjB2hzr2dz7r0BiPKryPqVQCI
+SEdnOp0HtV0Ns/PMU28bdosdybc0b3JId9Z0dVJm1OSi2lXUAsfLaC4K051US593
+uFOxpGuyHkNOeFz5j6S0SesWqoJrEzS+EZ3ipZX6AX/tLXVNxtffHJjaJwP6IUfm
+efgRiK+c0nl6srcnFn6YKwchMIUmn0eO3fLClsP5uTT5IShc5XpRm358IRfZeWjx
+13Wtv+RKxBP/4Hmb0q7Tl0A/Lq6hb9r4sPlnYSSVy9+Krl3JDkVGJr7MNmfsmq9G
+WoB2j+i6en7Hy7AQjeBgkzzLg03xWUMMTZopdHvCg2pJhozRzJIgyKiNAZknmrjC
+edwOoKdBl2n6f6qTkIRCM0PQ8muVkPoF6qRTWsIleV0pHK/B4azG9x8VG7PTT64s
+EilRoxvZAE2YGMJqmU+TqRXT6Mhvg/k8/VbULMp1xO9NJedn/rhqBkYjc2ISlC4R
+JQ8V4Of8xmTY+s9jEqVD+ZDhG4RoJOhpZkWKSwRKB0LsibhKSiCruIQUGev6ImVk
+uX0hQhOvR4J1D/JB7MclQHXX8stWfM3T8JBE7OzWLvHqUiTimRQmIzOlHgOsd+8g
+uFl8t7flN/wlLRsMT7cYmE07o4+e4gWpkmPsS5Hh/VrvgjKr71rHZHSxvUfytAhx
+cs1aQeuyN15XM09QthK1y+640pJzhFGjaqBzugK72qNyfK5tz3gZtP7zVIPgJhW5
+wtxDdxEeBLA79fMa67Sx0E41V3F22rWwSgXzBTyAh/hrXEYmS47c4BRSQDT48uqK
+FGf4D/wkTOLCFUsdsV0fNAX11wvpZKH8pOXWUDpZ4fK5NBX9rXev3EriCt4SxPTr
+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
+YpKL4GjNzw==
-----END AGE ENCRYPTED FILE-----
diff --git a/.env.example b/.env.example
index 9472d3e0..13f0a111 100644
--- a/.env.example
+++ b/.env.example
@@ -4,8 +4,8 @@ SECRET_API_TOKEN=
FASTLY_KEY=
FASTLY_SERVICE_ID=
MAILERLITE_TOKEN=
-RECAPTCHA_KEY=
-RECAPTCHA_SECRET_KEY=
+TURNSTILE_SITE_KEY=
+TURNSTILE_SECRET_KEY=
ROLLBAR_TOKEN=
PIPEDRIVE_TOKEN=
SLACK_TOKEN=
diff --git a/Dockerfile b/Dockerfile
index 6e329b8c..17b3ef66 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,7 +7,7 @@
FROM node:22-alpine AS base
ARG PUBLIC_HOSTNAME
-ARG RECAPTCHA_KEY
+ARG TURNSTILE_SITE_KEY
ARG KNOWLEDGE_BASE_URL
ARG LINKEDIN_PARTNER_ID
ARG LLMS_BLOB_BASE_URL
@@ -58,7 +58,7 @@ RUN --mount=type=secret,id=DATOCMS_API_TOKEN,env=DATOCMS_API_TOKEN \
npx gql.tada generate schema https://graphql.datocms.com --header "X-Exclude-Invalid: true" --header "Authorization: Bearer $DATOCMS_API_TOKEN"
# Build the actual Astro standalone production server
-ENV RECAPTCHA_KEY=$RECAPTCHA_KEY
+ENV TURNSTILE_SITE_KEY=$TURNSTILE_SITE_KEY
ENV PUBLIC_HOSTNAME=$PUBLIC_HOSTNAME
ENV KNOWLEDGE_BASE_URL=$KNOWLEDGE_BASE_URL
ENV LINKEDIN_PARTNER_ID=$LINKEDIN_PARTNER_ID
diff --git a/astro.config.ts b/astro.config.ts
index f4e8976b..b8a24689 100644
--- a/astro.config.ts
+++ b/astro.config.ts
@@ -67,9 +67,12 @@ export default defineConfig({
context: 'server',
access: 'secret',
}),
- RECAPTCHA_SECRET_KEY: envField.string({
+ // Cloudflare Turnstile secret for the forms. Unset → verification is skipped
+ // for that environment (see `~/lib/turnstile`).
+ TURNSTILE_SECRET_KEY: envField.string({
context: 'server',
access: 'secret',
+ optional: true,
}),
SLACK_TOKEN: envField.string({
context: 'server',
@@ -107,9 +110,12 @@ export default defineConfig({
context: 'server',
access: 'secret',
}),
- RECAPTCHA_KEY: envField.string({
+ // Cloudflare Turnstile sitekey for the forms, baked at build time. Unset → no
+ // widget is rendered (see `~/lib/loadTurnstile`).
+ TURNSTILE_SITE_KEY: envField.string({
context: 'client',
access: 'public',
+ optional: true,
}),
KNOWLEDGE_BASE_URL: envField.string({
context: 'client',
diff --git a/cubo.json5 b/cubo.json5
index 815db7d3..3296c600 100644
--- a/cubo.json5
+++ b/cubo.json5
@@ -6,7 +6,7 @@
build: {
args: {
PUBLIC_HOSTNAME: 'www.datocms.com',
- RECAPTCHA_KEY: '6Lda4UkgAAAAAE14pGAmMkbHKJCKOzmRoVc9_DE5',
+ TURNSTILE_SITE_KEY: '0x4AAAAAAErF5qflV0JALrvh',
KNOWLEDGE_BASE_URL: 'https://knowledge.datocms.com',
LINKEDIN_PARTNER_ID: '6497930',
LLMS_BLOB_BASE_URL: 'https://llms-txt.datocms.com',
@@ -80,7 +80,7 @@
build: {
args: {
PUBLIC_HOSTNAME: 'www.staging-datocms.com',
- RECAPTCHA_KEY: '6Lda4UkgAAAAAE14pGAmMkbHKJCKOzmRoVc9_DE5',
+ TURNSTILE_SITE_KEY: '0x4AAAAAAErF0MvgFAYEMfsH',
KNOWLEDGE_BASE_URL: 'https://knowledge.datocms.com',
LLMS_BLOB_BASE_URL: 'https://llms-txt.datocms.com',
},
@@ -139,7 +139,7 @@
build: {
args: {
PUBLIC_HOSTNAME: 'www-2.staging-datocms.com',
- RECAPTCHA_KEY: '6Lda4UkgAAAAAE14pGAmMkbHKJCKOzmRoVc9_DE5',
+ TURNSTILE_SITE_KEY: '0x4AAAAAAErF0MvgFAYEMfsH',
KNOWLEDGE_BASE_URL: 'https://knowledge.datocms.com',
LLMS_BLOB_BASE_URL: 'https://llms-txt.datocms.com',
},
diff --git a/package-lock.json b/package-lock.json
index 4349e33f..ce97b06a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -63,7 +63,6 @@
"react-dom-confetti": "^0.2.0",
"react-hook-form": "^7.53.0",
"react-hot-toast": "^2.4.1",
- "react-recaptcha-hook": "^1.2.3",
"react-simple-maps": "^3.0.0",
"react-spring": "^9.7.4",
"react-textarea-autosize": "^8.5.9",
@@ -22847,19 +22846,6 @@
"async-limiter": "~1.0.0"
}
},
- "node_modules/react-recaptcha-hook": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/react-recaptcha-hook/-/react-recaptcha-hook-1.2.3.tgz",
- "integrity": "sha512-Uq6JoPQsuCNUpDfWGlanFlqhobyr8ajPxmlM56fLp54iHASxX27VuMFUV2ExSEEEqoQLm+bzXY8/IgI0eFp1Ag==",
- "license": "MIT",
- "dependencies": {
- "react-script-hook": "^1.2.1"
- },
- "peerDependencies": {
- "react": "^16.8.6 || ^17 || ^18",
- "react-dom": "^16.8.6 || ^17 || ^18"
- }
- },
"node_modules/react-reconciler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
@@ -22896,16 +22882,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/react-script-hook": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/react-script-hook/-/react-script-hook-1.7.2.tgz",
- "integrity": "sha512-fhyCEfXb94fag34UPRF0zry1XGwmVY+79iibWwTqAoOiCzYJQOYTiWJ7CnqglA9tMSV8g45cQpHCMcBwr7dwhA==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.6 || 17 - 18",
- "react-dom": "^16.8.6 || 17 - 18"
- }
- },
"node_modules/react-select": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
diff --git a/package.json b/package.json
index 198482df..f9cd5c86 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,6 @@
"react-dom-confetti": "^0.2.0",
"react-hook-form": "^7.53.0",
"react-hot-toast": "^2.4.1",
- "react-recaptcha-hook": "^1.2.3",
"react-simple-maps": "^3.0.0",
"react-spring": "^9.7.4",
"react-textarea-autosize": "^8.5.9",
diff --git a/src/actions/forms/submitDvLegacyCmsRequest.ts b/src/actions/forms/submitDvLegacyCmsRequest.ts
index 153f14e0..dc136d8d 100644
--- a/src/actions/forms/submitDvLegacyCmsRequest.ts
+++ b/src/actions/forms/submitDvLegacyCmsRequest.ts
@@ -3,7 +3,7 @@ import { z } from 'astro:schema';
import { hasMarketingConsent } from '~/lib/consent';
import { trackConversion } from '~/lib/linkedin';
import { logErrorToRollbar } from '~/lib/logToRollbar';
-import { isRecaptchaTokenValid } from '~/lib/recaptcha';
+import { isTurnstileTokenValid } from '~/lib/turnstile';
import { isSpam } from '~/lib/spam';
import {
createLead,
@@ -26,14 +26,16 @@ export default defineAction({
currentCms: z.string(),
country: z.string(),
body: z.string(),
- token: z.string(),
+ // Empty when no Turnstile sitekey is configured for the environment; the
+ // verifier decides whether that is acceptable.
+ token: z.string().optional(),
}),
handler: async ({ token, ...input }, { request, cookies }) => {
try {
- if (!(await isRecaptchaTokenValid(token))) {
+ if (!(await isTurnstileTokenValid(token, { action: 'lp-modernize-cms' }))) {
throw new ActionError({
code: 'UNAUTHORIZED',
- message: 'Invalid recaptcha token',
+ message: 'Invalid anti-bot token',
});
}
diff --git a/src/actions/forms/submitDvRequest.ts b/src/actions/forms/submitDvRequest.ts
index 88784918..de409782 100644
--- a/src/actions/forms/submitDvRequest.ts
+++ b/src/actions/forms/submitDvRequest.ts
@@ -3,7 +3,7 @@ import { z } from 'astro:schema';
import { hasMarketingConsent } from '~/lib/consent';
import { trackConversion } from '~/lib/linkedin';
import { logErrorToRollbar } from '~/lib/logToRollbar';
-import { isRecaptchaTokenValid } from '~/lib/recaptcha';
+import { isTurnstileTokenValid } from '~/lib/turnstile';
import { isSpam } from '~/lib/spam';
import {
createLead,
@@ -26,14 +26,16 @@ export default defineAction({
currentCms: z.string(),
country: z.string(),
body: z.string(),
- token: z.string(),
+ // Empty when no Turnstile sitekey is configured for the environment; the
+ // verifier decides whether that is acceptable.
+ token: z.string().optional(),
}),
handler: async ({ token, ...input }, { request, cookies }) => {
try {
- if (!(await isRecaptchaTokenValid(token))) {
+ if (!(await isTurnstileTokenValid(token, { action: 'lp-nuxt2eol' }))) {
throw new ActionError({
code: 'UNAUTHORIZED',
- message: 'Invalid recaptcha token',
+ message: 'Invalid anti-bot token',
});
}
diff --git a/src/actions/forms/submitMigcsRequest.ts b/src/actions/forms/submitMigcsRequest.ts
index f2fbb80b..446621b1 100644
--- a/src/actions/forms/submitMigcsRequest.ts
+++ b/src/actions/forms/submitMigcsRequest.ts
@@ -3,7 +3,7 @@ import { z } from 'astro:schema';
import { hasMarketingConsent } from '~/lib/consent';
import { trackConversion } from '~/lib/linkedin';
import { logErrorToRollbar } from '~/lib/logToRollbar';
-import { isRecaptchaTokenValid } from '~/lib/recaptcha';
+import { isTurnstileTokenValid } from '~/lib/turnstile';
import { isSpam } from '~/lib/spam';
import {
createLead,
@@ -25,14 +25,16 @@ export default defineAction({
currentCms: z.string(),
country: z.string(),
body: z.string(),
- token: z.string(),
+ // Empty when no Turnstile sitekey is configured for the environment; the
+ // verifier decides whether that is acceptable.
+ token: z.string().optional(),
}),
handler: async ({ token, ...input }, { request, cookies }) => {
try {
- if (!(await isRecaptchaTokenValid(token))) {
+ if (!(await isTurnstileTokenValid(token, { action: 'lp-migcs' }))) {
throw new ActionError({
code: 'UNAUTHORIZED',
- message: 'Invalid recaptcha token',
+ message: 'Invalid anti-bot token',
});
}
diff --git a/src/actions/forms/submitPartnerProgramRequest.ts b/src/actions/forms/submitPartnerProgramRequest.ts
index c546453a..47571819 100644
--- a/src/actions/forms/submitPartnerProgramRequest.ts
+++ b/src/actions/forms/submitPartnerProgramRequest.ts
@@ -5,7 +5,7 @@ import { hasMarketingConsent } from '~/lib/consent';
import { sendToFrontChannel } from '~/lib/front';
import { trackConversion } from '~/lib/linkedin';
import { logErrorToRollbar } from '~/lib/logToRollbar';
-import { isRecaptchaTokenValid } from '~/lib/recaptcha';
+import { isTurnstileTokenValid } from '~/lib/turnstile';
import { isSpam } from '~/lib/spam';
import {
createLead,
@@ -29,15 +29,17 @@ export default defineAction({
email: z.string(),
country: z.string(),
body: z.string(),
- token: z.string(),
+ // Empty when no Turnstile sitekey is configured for the environment; the
+ // verifier decides whether that is acceptable.
+ token: z.string().optional(),
}),
handler: async ({ token, ...input }, { request, cookies }) => {
try {
- // Step 1: Validate reCAPTCHA token
- if (!(await isRecaptchaTokenValid(token))) {
+ // Step 1: Validate Turnstile token
+ if (!(await isTurnstileTokenValid(token, { action: 'partner-program' }))) {
throw new ActionError({
code: 'UNAUTHORIZED',
- message: 'Invalid recaptcha token',
+ message: 'Invalid anti-bot token',
});
}
diff --git a/src/actions/forms/submitSalesRequest.ts b/src/actions/forms/submitSalesRequest.ts
index bfa3d9fc..8ec20a73 100644
--- a/src/actions/forms/submitSalesRequest.ts
+++ b/src/actions/forms/submitSalesRequest.ts
@@ -5,7 +5,7 @@ import { hasMarketingConsent } from '~/lib/consent';
import { sendToFrontChannel } from '~/lib/front';
import { trackConversion } from '~/lib/linkedin';
import { logErrorToRollbar } from '~/lib/logToRollbar';
-import { isRecaptchaTokenValid } from '~/lib/recaptcha';
+import { isTurnstileTokenValid } from '~/lib/turnstile';
import { isSpam } from '~/lib/spam';
import {
createLead,
@@ -30,15 +30,17 @@ export default defineAction({
useCase: z.string(),
body: z.string(),
issueType: z.enum(['sales', 'enterprise']).optional(),
- token: z.string(),
+ // Empty when no Turnstile sitekey is configured for the environment; the
+ // verifier decides whether that is acceptable.
+ token: z.string().optional(),
}),
handler: async ({ token, ...input }, { request, cookies }) => {
try {
- // Step 1: Validate reCAPTCHA token
- if (!(await isRecaptchaTokenValid(token))) {
+ // Step 1: Validate Turnstile token
+ if (!(await isTurnstileTokenValid(token, { action: 'contact' }))) {
throw new ActionError({
code: 'UNAUTHORIZED',
- message: 'Invalid recaptcha token',
+ message: 'Invalid anti-bot token',
});
}
diff --git a/src/actions/forms/submitSupportRequest.ts b/src/actions/forms/submitSupportRequest.ts
index f2fbf5f9..2f6203f1 100644
--- a/src/actions/forms/submitSupportRequest.ts
+++ b/src/actions/forms/submitSupportRequest.ts
@@ -3,7 +3,7 @@ import { FRONT_CHANNEL_URL_SUPPORT } from 'astro:env/server';
import { z } from 'astro:schema';
import { sendToFrontChannel } from '~/lib/front';
import { logErrorToRollbar } from '~/lib/logToRollbar';
-import { isRecaptchaTokenValid } from '~/lib/recaptcha';
+import { isTurnstileTokenValid } from '~/lib/turnstile';
import { isSpam } from '~/lib/spam';
export default defineAction({
@@ -16,15 +16,17 @@ export default defineAction({
errorId: z.string().optional(),
uploads: z.instanceof(File).array().optional(),
issueType: z.string().optional(),
- token: z.string(),
+ // Empty when no Turnstile sitekey is configured for the environment; the
+ // verifier decides whether that is acceptable.
+ token: z.string().optional(),
}),
handler: async ({ token, ...input }) => {
try {
- // Step 1: Validate reCAPTCHA token
- if (!(await isRecaptchaTokenValid(token))) {
+ // Step 1: Validate Turnstile token
+ if (!(await isTurnstileTokenValid(token, { action: 'support' }))) {
throw new ActionError({
code: 'UNAUTHORIZED',
- message: 'Invalid recaptcha token',
+ message: 'Invalid anti-bot token',
});
}
diff --git a/src/actions/inviteEmailToSlackChannel/index.ts b/src/actions/inviteEmailToSlackChannel/index.ts
index 3782f550..3a2aea2b 100644
--- a/src/actions/inviteEmailToSlackChannel/index.ts
+++ b/src/actions/inviteEmailToSlackChannel/index.ts
@@ -3,7 +3,7 @@ import { ActionError, defineAction } from 'astro:actions';
import { SLACK_TOKEN } from 'astro:env/server';
import { z } from 'astro:schema';
import { logErrorToRollbar } from '~/lib/logToRollbar';
-import { isRecaptchaTokenValid } from '~/lib/recaptcha';
+import { isTurnstileTokenValid } from '~/lib/turnstile';
interface ErrorResponse {
data: {
@@ -35,15 +35,17 @@ export default defineAction({
email: z
.string({ invalid_type_error: 'Please, enter your email! 😊' })
.email('Please, enter a valid email! 😊'),
- token: z.string(),
+ // Empty when no Turnstile sitekey is configured for the environment; the
+ // verifier decides whether that is acceptable.
+ token: z.string().optional(),
}),
handler: async (input) => {
const { email, token } = input;
- if (!(await isRecaptchaTokenValid(token))) {
+ if (!(await isTurnstileTokenValid(token, { action: 'slack-invite' }))) {
throw new ActionError({
code: 'UNAUTHORIZED',
- message: 'Invalid recaptcha token',
+ message: 'Invalid anti-bot token',
});
}
diff --git a/src/actions/sendFeedbackAboutDocPage/index.ts b/src/actions/sendFeedbackAboutDocPage/index.ts
index 4461df92..a56d6b91 100644
--- a/src/actions/sendFeedbackAboutDocPage/index.ts
+++ b/src/actions/sendFeedbackAboutDocPage/index.ts
@@ -2,7 +2,7 @@ import { ActionError, defineAction } from 'astro:actions';
import { z } from 'astro:schema';
import buildBasecampClient from '~/lib/basecamp';
import { logErrorToRollbar } from '~/lib/logToRollbar';
-import { isRecaptchaTokenValid } from '~/lib/recaptcha';
+import { isTurnstileTokenValid } from '~/lib/turnstile';
const SUPPORT_TEAM = 33592869;
const TRIAGE_COLUMN = 9255769099;
@@ -15,16 +15,18 @@ export default defineAction({
reaction: z.enum(['positive', 'negative']),
notes: z.string().optional(),
email: z.string().email().optional(),
- token: z.string(),
+ // Empty when no Turnstile sitekey is configured for the environment; the
+ // verifier decides whether that is acceptable.
+ token: z.string().optional(),
}),
handler: async (input) => {
const { url, reaction, notes, email, token } = input;
try {
- if (!(await isRecaptchaTokenValid(token))) {
+ if (!(await isTurnstileTokenValid(token, { action: 'docs-feedback' }))) {
throw new ActionError({
code: 'UNAUTHORIZED',
- message: 'Invalid recaptcha token',
+ message: 'Invalid anti-bot token',
});
}
diff --git a/src/actions/subscribeToNewsletter/index.ts b/src/actions/subscribeToNewsletter/index.ts
index 4f514cc2..29f9d15d 100644
--- a/src/actions/subscribeToNewsletter/index.ts
+++ b/src/actions/subscribeToNewsletter/index.ts
@@ -4,7 +4,7 @@ import { MAILERLITE_TOKEN } from 'astro:env/server';
import { z } from 'astro:schema';
import { format } from 'date-fns';
import { logErrorToRollbar } from '~/lib/logToRollbar';
-import { isRecaptchaTokenValid } from '~/lib/recaptcha';
+import { isTurnstileTokenValid } from '~/lib/turnstile';
export default defineAction({
accept: 'form',
@@ -12,16 +12,18 @@ export default defineAction({
email: z
.string({ invalid_type_error: 'Please, enter your email! 😊' })
.email('Please, enter a valid email! 😊'),
- token: z.string(),
+ // Empty when no Turnstile sitekey is configured for the environment; the
+ // verifier decides whether that is acceptable.
+ token: z.string().optional(),
}),
handler: async (input) => {
const { email, token } = input;
try {
- if (!(await isRecaptchaTokenValid(token))) {
+ if (!(await isTurnstileTokenValid(token, { action: 'newsletter' }))) {
throw new ActionError({
code: 'UNAUTHORIZED',
- message: 'Invalid recaptcha token',
+ message: 'Invalid anti-bot token',
});
}
diff --git a/src/components/form/Form/ReactComponent.tsx b/src/components/form/Form/ReactComponent.tsx
index bc5aa745..ee5589cd 100644
--- a/src/components/form/Form/ReactComponent.tsx
+++ b/src/components/form/Form/ReactComponent.tsx
@@ -7,18 +7,22 @@ import {
type FieldValues,
type SubmitHandler,
} from 'react-hook-form';
-import { useRecaptcha } from 'react-recaptcha-hook';
import toast, { Toaster } from 'react-hot-toast';
import s from './style.module.css';
-import { RECAPTCHA_KEY } from 'astro:env/client';
import { ButtonReactComponent } from '~/components/Button/ReactComponent';
import { isActionError, isInputError } from 'astro:actions';
+import { TurnstileError, useTurnstile } from './useTurnstile';
type Props