8 |
9 |
10 |
11 |
24 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/playground/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | title: string;
4 | }
5 |
6 | const {title} = Astro.props;
7 | ---
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {title}
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Astro Turnstile
2 |
3 | [](https://badge.fury.io/js/astro-turnstile)
4 |
5 | Astro Turnstile is a package that provides a simple way to add a turnstile to your Astro site. It is a simple and easy to use package that can be added to your site with just a few lines of code.
6 |
7 |
8 | ## Installation
9 |
10 | To see how to get started, check out the [package README](./package/README.md)
11 |
12 | ## Licensing
13 |
14 | [MIT Licensed](./LICENSE). Made with ❤️ by [Hunter Bertoson](https://github.com/hkbertoson).
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "private": true,
4 | "packageManager": "pnpm@10.11.0",
5 | "engines": {
6 | "node": ">=20.10.0"
7 | },
8 | "scripts": {
9 | "playground:dev": "pnpm --filter playground dev",
10 | "changeset": "changeset",
11 | "release": "node scripts/release.mjs",
12 | "ci:version": "pnpm changeset version",
13 | "ci:publish": "pnpm changeset publish",
14 | "lint": "biome check .",
15 | "lint:fix": "biome check --write ."
16 | },
17 | "devDependencies": {
18 | "@biomejs/biome": "1.9.2",
19 | "@changesets/cli": "^2.27.10"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "astro dev",
9 | "build": "astro check && astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/tailwind": "^5.1.4",
15 | "@astrojs/node": "^9.0.0",
16 | "astro": "^5.0.2",
17 | "astro-turnstile": "workspace:*",
18 | "tailwindcss": "^3.4.16"
19 | },
20 | "devDependencies": {
21 | "@astrojs/check": "^0.9.4",
22 | "@types/node": "^22.10.1",
23 | "typescript": "^5.7.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/playground/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/package/src/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { AstroIntegration } from "astro";
2 | import type { AstroTurnstileOptions } from "./schema";
3 |
4 | /**
5 | * # Astro Turnstile integration.
6 | *
7 | * @description An [Astro](https://astro.build) integration that enables ease of use within Astro for Cloudflare Turnstile Captcha.
8 | * @requires `TURNSTILE_SECRET_KEY` .env variable - The secret key for the Turnstile API.
9 | * @requires `TURNSTILE_SITE_KEY` .env variable - The site key for the Turnstile API.
10 | * @see [Cloudflare: Get started with Turnstile](https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key) For instructions on how to get your Turnstile API keys.
11 | * @param {string} options.endpointPath - The path to use for the injected Turnstile API endpoint.
12 | * @param {boolean} options.verbose - Enable verbose logging.
13 | * @returns {AstroIntegration & {}} The Astro Turnstile integration.
14 | */
15 | export default function astroTurnstile(
16 | options?: AstroTurnstileOptions,
17 | ): AstroIntegration & {};
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 Cyber Logical Development
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout Repo
16 | uses: actions/checkout@v4
17 |
18 | - name: Setup pnpm
19 | uses: pnpm/action-setup@v4
20 |
21 | - name: Setup Node.js 20.x
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: 20
25 | cache: "pnpm"
26 |
27 | - name: Install dependencies
28 | run: pnpm install
29 |
30 | - name: Create Release Pull Request or Publish to npm
31 | id: changesets
32 | uses: changesets/action@v1
33 | with:
34 | version: pnpm ci:version
35 | commit: "[ci]: release"
36 | title: "[ci] Release"
37 | publish: pnpm ci:publish
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
41 |
--------------------------------------------------------------------------------
/package/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "astro-turnstile",
3 | "version": "2.1.0",
4 | "description": "Astro cloudflare turnstile integration",
5 | "author": {
6 | "email": "dev@hunterbertoson.tech",
7 | "name": "Hunter Bertoson",
8 | "url": "https://github.com/hkbertoson/astro-turnstile"
9 | },
10 | "license": "MIT",
11 | "keywords": [
12 | "astro-integration",
13 | "astro-component",
14 | "withastro",
15 | "astro",
16 | "turnstile"
17 | ],
18 | "homepage": "https://github.com/hkbertoson/astro-turnstile",
19 | "publishConfig": {
20 | "access": "public"
21 | },
22 | "sideEffects": false,
23 | "files": [
24 | "src",
25 | "lib"
26 | ],
27 | "exports": {
28 | ".": {
29 | "types": "./src/index.d.ts",
30 | "default": "./src/index.ts"
31 | },
32 | "./schema": "./src/schema.ts",
33 | "./server": "./lib/server.ts",
34 | "./components/TurnstileWidget": "./lib/components/TurnstileWidget.astro",
35 | "./components/TurnstileForm": "./lib/components/TurnstileForm.astro"
36 | },
37 | "scripts": {},
38 | "type": "module",
39 | "peerDependencies": {
40 | "astro": ">=5.0.2"
41 | },
42 | "dependencies": {
43 | "@matthiesenxyz/astrodtsbuilder": "^0.2.0",
44 | "astro-integration-kit": "^0.18.0",
45 | "vite": "^6.0.5"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/playground/src/components/Card.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | title: string;
4 | body: string;
5 | href: string;
6 | }
7 |
8 | const { href, title, body } = Astro.props;
9 | ---
10 |
11 |
22 |
62 |
--------------------------------------------------------------------------------
/scripts/release.mjs:
--------------------------------------------------------------------------------
1 | import { spawn } from "node:child_process";
2 | import { resolve } from "node:path";
3 |
4 | /**
5 | *
6 | * @param {string} command
7 | * @param {...Array} args
8 | *
9 | * @returns {Promise}
10 | */
11 | const run = async (command, ...args) => {
12 | const cwd = resolve();
13 | return new Promise((resolve) => {
14 | const cmd = spawn(command, args, {
15 | stdio: ["inherit", "pipe", "pipe"], // Inherit stdin, pipe stdout, pipe stderr
16 | shell: true,
17 | cwd,
18 | });
19 |
20 | let output = "";
21 |
22 | cmd.stdout.on("data", (data) => {
23 | process.stdout.write(data.toString());
24 | output += data.toString();
25 | });
26 |
27 | cmd.stderr.on("data", (data) => {
28 | process.stderr.write(data.toString());
29 | });
30 |
31 | cmd.on("close", () => {
32 | resolve(output);
33 | });
34 | });
35 | };
36 |
37 | const main = async () => {
38 | await run("pnpm changeset version");
39 | await run("git add .");
40 | await run('git commit -m "chore: update version"');
41 | await run("git push");
42 | await run("pnpm changeset publish");
43 | await run("git push --follow-tags");
44 | const tag = (await run("git describe --abbrev=0")).replace("\n", "");
45 | await run(
46 | `gh release create ${tag} --title ${tag} --notes "Please refer to [CHANGELOG.md](https://github.com/hkbertoson/astro-turnstile/blob/main/package/CHANGELOG.md) for details."`,
47 | );
48 | };
49 |
50 | main();
51 |
--------------------------------------------------------------------------------
/playground/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import TurnstileWidget from 'astro-turnstile:components/TurnstileWidget';
3 | import TurnstileForm from 'astro-turnstile/components/TurnstileForm';
4 | import Layout from '../layouts/Layout.astro';
5 |
6 | if (Astro.request.method === 'POST') {
7 | console.log('success');
8 | }
9 | ---
10 |
11 |
12 |
13 |
21 |
29 |
30 |
31 |
68 |
69 |
--------------------------------------------------------------------------------
/package/lib/server.ts:
--------------------------------------------------------------------------------
1 | import { TURNSTILE_SECRET_KEY } from "astro:env/server";
2 | import type { APIRoute } from "astro";
3 |
4 | // Ensure this route is not prerendered by Astro
5 | export const prerender = false;
6 |
7 | /**
8 | * The Astro-Turnstile API endpoint for verifying tokens.
9 | */
10 | export const POST: APIRoute = async ({ request }) => {
11 | // Parse the incoming form data
12 | const data = await request.formData();
13 |
14 | // Get the Turnstile token and the connecting IP
15 | const turnstileToken = data.get("cf-turnstile-response");
16 | const connectingIP = request.headers.get("CF-Connecting-IP");
17 |
18 | // Ensure the secret key and token are present
19 | if (!TURNSTILE_SECRET_KEY || !turnstileToken) {
20 | return new Response(null, {
21 | status: 400,
22 | statusText:
23 | "[Astro-Turnstile] Missing secret key or token, please contact the site administrator",
24 | });
25 | }
26 |
27 | // Validate the token
28 | const formData = new FormData();
29 |
30 | // Add the secret key and token to the form data
31 | formData.append("secret", TURNSTILE_SECRET_KEY);
32 | formData.append("response", turnstileToken);
33 |
34 | // If there is a connecting IP, add it to the form data
35 | connectingIP && formData.append("remoteip", connectingIP);
36 |
37 | // Send the token to the Turnstile API for verification
38 | const result = await fetch(
39 | "https://challenges.cloudflare.com/turnstile/v0/siteverify",
40 | {
41 | body: formData,
42 | method: "POST",
43 | },
44 | );
45 |
46 | // Parse the outcome
47 | const outcome = await result.json();
48 |
49 | // Return the outcome
50 | if (outcome.success) {
51 | return new Response(null, {
52 | status: 200,
53 | statusText: "[Astro-Turnstile] Token verification successful",
54 | });
55 | }
56 |
57 | // Return an error message if the token is invalid
58 | return new Response(null, {
59 | status: 400,
60 | statusText: "[Astro-Turnstile] Unable to verify token",
61 | });
62 | };
63 |
64 | /**
65 | * The Astro-Turnstile API endpoint for all other requests.
66 | */
67 | export const ALL: APIRoute = async () => {
68 | // Return a 405 error for all other requests than POST
69 | return new Response(
70 | JSON.stringify({ error: "Method not allowed" }, null, 2),
71 | {
72 | status: 405,
73 | statusText: "method not allowed",
74 | headers: {
75 | "content-type": "application/json",
76 | },
77 | },
78 | );
79 | };
80 |
--------------------------------------------------------------------------------
/playground/README.md:
--------------------------------------------------------------------------------
1 | # Astro Starter Kit: Basics
2 |
3 | ```sh
4 | npm create astro@latest -- --template basics
5 | ```
6 |
7 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
8 | [](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
9 | [](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
10 |
11 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
12 |
13 | 
14 |
15 | ## 🚀 Project Structure
16 |
17 | Inside of your Astro project, you'll see the following folders and files:
18 |
19 | ```text
20 | /
21 | ├── public/
22 | │ └── favicon.svg
23 | ├── src/
24 | │ ├── components/
25 | │ │ └── Card.astro
26 | │ ├── layouts/
27 | │ │ └── Layout.astro
28 | │ └── pages/
29 | │ └── index.astro
30 | └── package.json
31 | ```
32 |
33 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
34 |
35 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
36 |
37 | Any static assets, like images, can be placed in the `public/` directory.
38 |
39 | ## 🧞 Commands
40 |
41 | All commands are run from the root of the project, from a terminal:
42 |
43 | | Command | Action |
44 | | :------------------------ | :----------------------------------------------- |
45 | | `npm install` | Installs dependencies |
46 | | `npm run dev` | Starts local dev server at `localhost:4321` |
47 | | `npm run build` | Build your production site to `./dist/` |
48 | | `npm run preview` | Preview your build locally, before deploying |
49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
50 | | `npm run astro -- --help` | Get help using the Astro CLI |
51 |
52 | ## 👀 Want to learn more?
53 |
54 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
55 |
--------------------------------------------------------------------------------
/package/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import type { MiddlewareHandler } from "astro";
2 |
3 | const COMPONENT_IDENTIFIER = 'data-turnstile-container';
4 | const TURNSTILE_SCRIPT = `
5 | `;
11 |
12 | const MAX_SCAN_BYTES = 16384; // 16KB
13 | let bytesScanned = 0;
14 |
15 | export const onRequest: MiddlewareHandler = async (_, next) => {
16 | const response = await next();
17 |
18 | const contentType = response.headers.get('Content-Type');
19 | if (!contentType?.includes('text/html') || !response.body) {
20 | return response;
21 | }
22 |
23 | const transformedBody = response.body
24 | .pipeThrough(new TextDecoderStream())
25 | .pipeThrough(createFastScriptInjectionTransform())
26 | .pipeThrough(new TextEncoderStream());
27 |
28 | return new Response(transformedBody, {
29 | status: response.status,
30 | statusText: response.statusText,
31 | headers: response.headers
32 | });
33 | };
34 |
35 | function createFastScriptInjectionTransform(): TransformStream {
36 | let hasInjected = false;
37 | let hasFoundComponent = false;
38 | let buffer = '';
39 |
40 | return new TransformStream({
41 | transform(chunk: string, controller) {
42 | // Fast path: already injected or scanned too much
43 | if (hasInjected || bytesScanned > MAX_SCAN_BYTES) {
44 | controller.enqueue(chunk);
45 | return;
46 | }
47 |
48 | bytesScanned += chunk.length;
49 |
50 | // Fast path: haven't found component yet
51 | if (!hasFoundComponent) {
52 | // Look for the data attribute
53 | if (chunk.includes(COMPONENT_IDENTIFIER)) {
54 | hasFoundComponent = true;
55 | buffer = chunk;
56 | } else {
57 | controller.enqueue(chunk);
58 | return;
59 | }
60 | } else {
61 | buffer += chunk;
62 | }
63 |
64 | const headCloseIndex = buffer.indexOf('');
65 | if (headCloseIndex === -1) {
66 | if (buffer.length > MAX_SCAN_BYTES) {
67 | controller.enqueue(buffer);
68 | buffer = '';
69 | hasInjected = true;
70 | }
71 | return;
72 | }
73 |
74 | const injectedContent =
75 | buffer.slice(0, headCloseIndex) +
76 | TURNSTILE_SCRIPT +
77 | buffer.slice(headCloseIndex);
78 |
79 | controller.enqueue(injectedContent);
80 | hasInjected = true;
81 | buffer = '';
82 | },
83 |
84 | flush(controller) {
85 | if (buffer) {
86 | controller.enqueue(buffer);
87 | }
88 | bytesScanned = 0;
89 | }
90 | });
91 | }
--------------------------------------------------------------------------------
/package/src/stubs.ts:
--------------------------------------------------------------------------------
1 | import astroDtsBuilder from '@matthiesenxyz/astrodtsbuilder';
2 | import {name} from '../package.json';
3 |
4 | // Create the config DTS file
5 | const config = astroDtsBuilder();
6 |
7 | // Add a note to the top of the file
8 | config.addSingleLineNote(
9 | `This file is generated by '${name}' and should not be modified manually.`
10 | );
11 |
12 | // Add the module to the file
13 | config.addModule('virtual:astro-turnstile/config', {
14 | defaultExport: {
15 | typeDef: `import("${name}/schema").AstroTurnstileConfig`,
16 | singleLineDescription: 'The Turnstile configuration options',
17 | },
18 | });
19 |
20 | // Create the components DTS file
21 | const components = astroDtsBuilder();
22 |
23 | // Add a note to the top of the file
24 | components.addSingleLineNote(
25 | `This file is generated by '${name}' and should not be modified manually.`
26 | );
27 |
28 | // Add the module to the file
29 | components.addModule('astro-turnstile:components/TurnstileWidget', {
30 | defaultExport: {
31 | typeDef: `typeof import('${name}/components/TurnstileWidget.astro').TurnstileWidget`,
32 | multiLineDescription: [
33 | '# Turnstile Verification Widget',
34 | '@description An [Astro](https://astro.build) component that is used to render a Turnstile verification widget. This widget is used to verify that a user is human.',
35 | `@param {"auto"|"light"|"dark"} theme - The theme for the widget. (default: "auto")`,
36 | `@param {"normal"|"compact"|"flexible"} size - The size for the widget. (default: "normal")`,
37 | `@param {string} margin - The margin for the widget element. (default: '0.5rem')`,
38 | '@example',
39 | '```tsx',
40 | '---',
41 | `import TurnstileWidget from '${name}:components/TurnstileWidget';`,
42 | '---',
43 | "',
47 | ],
48 | },
49 | });
50 |
51 | // Add the module to the file
52 | components.addModule('astro-turnstile:components/TurnstileForm', {
53 | defaultExport: {
54 | typeDef: `typeof import('${name}/components/TurnstileForm.astro').TurnstileForm`,
55 | multiLineDescription: [
56 | '# Turnstile Verification Form',
57 | '@description An [Astro](https://astro.build) component that is used to render a Turnstile verification form. This form includes a Turnstile verification widget and a submit button.',
58 | '@slot default - Any unassigned content will be placed here.',
59 | "@slot header - `
...
`",
60 | "@slot buttons - `
...
`",
61 | "@slot footer - `
...
`",
62 | `@param {"auto"|"light"|"dark"} theme - The theme for the widget. (default: "auto")`,
63 | `@param {"normal"|"compact"|"flexible"} size - The size for the widget. (default: "normal")`,
64 | `@param {string} margin - The margin for the widget element. (default: '0.5rem')`,
65 | `@param {"multipart/form-data"|"application/x-www-form-urlencoded"|"submit"} enctype - The form enctype. (default: 'application/x-www-form-urlencoded')`,
66 | ],
67 | },
68 | });
69 |
70 | // Export the DTS files
71 | export default {
72 | config: config.makeAstroInjectedType('config.d.ts'),
73 | components: components.makeAstroInjectedType('components.d.ts'),
74 | };
75 |
--------------------------------------------------------------------------------
/package/src/strings.ts:
--------------------------------------------------------------------------------
1 | export const loggerStrings = {
2 | setup: 'Turnstile integration setup...',
3 | configSiteMissing: `Astro Config Error: 'site' is not defined, it is recommended to define 'site' in your Astro config. (https://docs.astro.build/en/reference/configuration-reference/#site)\nFalling back to 'window.location.origin'.`,
4 | updateConfig: 'Updating Astro config with Turnstile environment variables...',
5 | injectMiddleware: 'Injecting Turnstile middleware...',
6 | injectRoute: (value: string) => `Injecting Turnstile route at ${value}...`,
7 | virtualImports: 'Adding Virtual Import modules...',
8 | setupComplete: 'Turnstile integration setup complete.',
9 | addDevToolbarApp: 'Adding Turnstile Dev Toolbar App for testing...',
10 | injectTypes: 'Injecting Turnstile types...',
11 | };
12 |
13 | export const ErrorMessages = {
14 | demoSecret:
15 | 'Turnstile Secret Key is set to a demo value. Please replace it with a valid secret key.',
16 | demoSiteKey:
17 | 'Turnstile Site Key is set to a demo value. Please replace it with a valid site key.',
18 | };
19 |
20 | type astroHooks = import('astro').HookParameters<'astro:config:setup'>;
21 |
22 | export const envDefaults = {
23 | secretKey: (command: astroHooks['command']): string | undefined => {
24 | if (command === 'dev' || command === 'preview') {
25 | return '1x0000000000000000000000000000000AA';
26 | }
27 | return undefined;
28 | },
29 | siteKey: (command: astroHooks['command']): string | undefined => {
30 | if (command === 'dev' || command === 'preview') {
31 | return '1x00000000000000000000AA';
32 | }
33 | return undefined;
34 | },
35 | };
36 | // Define the SVG icons for the toolbar app
37 | export const svgIcons = {
38 | turnstile: ``,
39 | close: ``,
40 | };
41 |
42 | // Define the raw HTML elements for the app window
43 | export const windowHtmlElements = {
44 | button: ``,
45 | successBanner: `
Token Verification Successful
`,
46 | errorBanner: `
"Unable to verify token"
`,
47 | };
48 |
--------------------------------------------------------------------------------
/package/lib/components/TurnstileForm.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import config from 'virtual:astro-turnstile/config';
3 | import type {HTMLAttributes} from 'astro/types';
4 | import Widget, {type Props as WidgetProps} from './TurnstileWidget.astro';
5 |
6 | interface Props extends WidgetProps, HTMLAttributes<'form'> {
7 | enctype?:
8 | | 'multipart/form-data'
9 | | 'application/x-www-form-urlencoded'
10 | | 'submit'
11 | | undefined;
12 | }
13 |
14 | const {
15 | theme,
16 | size,
17 | margin,
18 | action,
19 | method,
20 | buttonLabel = 'Submit',
21 | enctype = 'application/x-www-form-urlencoded',
22 | ...rest
23 | } = Astro.props;
24 |
25 | const siteUrl = Astro.site;
26 | ---
27 |
28 |
52 |
53 |
135 |
--------------------------------------------------------------------------------
/package/lib/components/TurnstileWidget.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import {TURNSTILE_SITE_KEY} from 'astro:env/client';
3 | import {AstroError} from 'astro/errors';
4 |
5 | export interface Props {
6 | /**
7 | * Theme of the Turnstile widget as per the [Turnstile documentation](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#appearance-modes)
8 | * @default "auto"
9 | */
10 | theme?: 'auto' | 'light' | 'dark' | undefined;
11 | /**
12 | * Size of the Turnstile widget as per the [Turnstile documentation](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#appearance-modes)
13 | * @default "normal"
14 | */
15 | size?: 'normal' | 'compact' | 'flexible' | undefined;
16 | /**
17 | * Margin of the Turnstile widget (CSS margin property)
18 | * @default "0.5rem"
19 | */
20 | margin?: string | undefined;
21 |
22 | /**
23 | * Label for Button
24 | * @default "Submit"
25 | */
26 | buttonLabel?: string | undefined;
27 | }
28 |
29 | const {theme = 'auto', size = 'normal', margin = '0.5rem'} = Astro.props;
30 |
31 | // Define the dimensions type for the different sizes
32 | type Dimensions = {height: string; width: string; minWidth: string};
33 |
34 | // Define the sizes object with the different style settings
35 | type Sizes = Record;
36 |
37 | // Define the style settings for the different sizes
38 | const sizes: Sizes = {
39 | normal: {height: '65px', width: '300px', minWidth: '300px'},
40 | flexible: {height: '65px', width: '100%', minWidth: '300px'},
41 | compact: {height: '140px', width: '150px', minWidth: '150px'},
42 | };
43 |
44 | // Define a function to get the style settings based on set the size
45 | const getStyleSettings = (size: string): Dimensions => {
46 | // Get the style settings based on the size
47 | const styleSettings = sizes[size];
48 |
49 | // If the style settings exist, return the style settings
50 | if (styleSettings) {
51 | return styleSettings;
52 | }
53 |
54 | // If the style settings do not exist, throw an error
55 | throw new AstroError(
56 | `Invalid size: ${size}`,
57 | `'size' must be one of the following: ${Object.keys(sizes).join(', ')}.`,
58 | );
59 | };
60 |
61 | // Destructure the height, width, and minWidth from the style settings
62 | const {
63 | height: turnstileHeight,
64 | width: turnstileWidth,
65 | minWidth: turnstileMinWidth,
66 | } = getStyleSettings(size);
67 | ---
68 |
69 |
76 |
77 |
78 |
94 |
95 |
112 |
--------------------------------------------------------------------------------
/package/src/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "astro/zod";
2 |
3 | /**
4 | * Map of error messages for the endpoint path refinement function.
5 | */
6 | const endpointErrorMessages = {
7 | startsWith:
8 | "The endpoint path must start with a forward slash. (e.g. '/your-path')",
9 | urlSafe:
10 | "The endpoint path must only contain URL-safe characters. (e.g. '/your-path')",
11 | endsWith:
12 | "The endpoint path must not end with a forward slash. (e.g. '/your-path')",
13 | };
14 |
15 | /**
16 | * Super refine function for the endpoint path.
17 | * @param {string} arg - The value to refine.
18 | * @param {z.RefinementCtx} ctx - The refinement context.
19 | * @returns {void}
20 | */
21 | function endpointPathSuperRefine(arg: string, ctx: z.RefinementCtx): void {
22 | const code = z.ZodIssueCode.custom;
23 |
24 | if (!arg.startsWith("/"))
25 | ctx.addIssue({
26 | code,
27 | message: `${endpointErrorMessages.startsWith} Error: ${arg}`,
28 | });
29 |
30 | if (!/^[a-zA-Z0-9\-_\/]+$/.test(arg))
31 | ctx.addIssue({
32 | code,
33 | message: `${endpointErrorMessages.urlSafe} Error: ${arg}`,
34 | });
35 |
36 | if (arg.endsWith("/"))
37 | ctx.addIssue({
38 | code,
39 | message: `${endpointErrorMessages.endsWith} Error: ${arg}`,
40 | });
41 | }
42 |
43 | /**
44 | * Astro-Turnstile configuration options schema.
45 | */
46 | export const AstroTurnstileOptionsSchema = z
47 | .object({
48 | /**
49 | * The path to the injected Turnstile API endpoint.
50 | * @type {string}
51 | * @default "/verify"
52 | */
53 | endpointPath: z
54 | .string()
55 | .optional()
56 | .default("/verify")
57 | .superRefine((arg, ctx) => endpointPathSuperRefine(arg, ctx))
58 | .describe(
59 | 'The path to the injected Turnstile API endpoint. (default: "/verify")',
60 | ),
61 | /**
62 | * Disable the client-side script injection.
63 | *
64 | * By default, the client-side script is injected into the Astro project on every page. In some cases, you may want to disable this behavior, and manually inject the script where needed. This option allows you to disable the client-side script injection.
65 | *
66 | * **Note:** If you disable the client-side script injection, you will need to manually inject the Turnstile client-side script into your Astro project.
67 | * @example On any Layout that you want to use Turnstile, you can add the following script tag to the head:
68 | * ```html
69 | *
70 | *
71 | *
72 | *
73 | *
74 | *
75 | * ```
76 | * @see [Cloudflare: Turnstile Explicitly render the Turnstile widget (onloadTurnstileCallback)](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget) For more information on the Turnstile client-side script. Or if you want to inject their script manually.
77 | * @type {boolean}
78 | * @default false
79 | */
80 | disableClientScript: z
81 | .boolean()
82 | .optional()
83 | .default(false)
84 | .describe("Disable the client-side script injection. (default: false)"),
85 | /**
86 | * Disable the Astro Turnstile Dev Toolbar App.
87 | *
88 | * @default false
89 | */
90 | disableDevToolbar: z
91 | .boolean()
92 | .optional()
93 | .default(false)
94 | .describe("Disable the Astro Turnstile Dev Toolbar. (default: false)"),
95 | /**
96 | * Enable verbose logging.
97 | * @type {boolean}
98 | * @default false
99 | */
100 | verbose: z
101 | .boolean()
102 | .optional()
103 | .default(false)
104 | .describe("Enable verbose logging. (default: false)"),
105 | })
106 | .optional()
107 | .default({});
108 |
109 | /**
110 | * Astro-Turnstile configuration options type.
111 | */
112 | export type AstroTurnstileOptions = typeof AstroTurnstileOptionsSchema._input;
113 |
114 | /**
115 | * Astro-Turnstile configuration options type used by the `virtual:astro-turnstile/config` module.
116 | */
117 | export type AstroTurnstileConfig = z.infer;
118 |
--------------------------------------------------------------------------------
/package/src/toolbar.ts:
--------------------------------------------------------------------------------
1 | import {TURNSTILE_SITE_KEY} from 'astro:env/client';
2 | import {defineToolbarApp} from 'astro/toolbar';
3 | import {svgIcons, windowHtmlElements} from './strings';
4 |
5 | export default defineToolbarApp({
6 | init(canvas, app, server) {
7 | // Create a new Astro Dev Toolbar Window
8 | const appWindow = document.createElement('astro-dev-toolbar-window');
9 |
10 | // Set the window's initial size
11 | appWindow.style.width = '600px';
12 | appWindow.style.height = '300px';
13 |
14 | // Create a close button for the window
15 | const closeButton = document.createElement('button');
16 |
17 | // Set the close button's inner HTML to an SVG icon
18 | closeButton.innerHTML = svgIcons.close;
19 |
20 | // Style the close button
21 | closeButton.style.position = 'absolute';
22 | closeButton.style.top = '0.5rem';
23 | closeButton.style.right = '0.5rem';
24 | closeButton.style.padding = '0';
25 | closeButton.style.width = '2rem';
26 | closeButton.style.height = '2rem';
27 | closeButton.style.cursor = 'pointer';
28 | closeButton.style.border = 'none';
29 | closeButton.style.backgroundColor = 'transparent';
30 | closeButton.style.borderRadius = '50%';
31 | closeButton.style.zIndex = '1000';
32 |
33 | // Add an event listener to the close button to toggle the app's state
34 | closeButton.onclick = () => {
35 | app.toggleState({state: false});
36 | };
37 |
38 | // Create a Header for the app window
39 | const header = document.createElement('astro-dev-overlay-card');
40 |
41 | // Style the heading
42 | header.style.textAlign = 'center';
43 | header.style.marginTop = '0';
44 | header.style.fontSize = '1.5rem';
45 | header.style.position = 'absolute';
46 | header.style.width = '90%';
47 | header.style.height = '100px';
48 | header.style.top = '0';
49 |
50 | // Create a Heading for the app window
51 | const heading = document.createElement('h1');
52 |
53 | // Set the heading's inner HTML
54 | heading.textContent = 'Astro Turnstile';
55 |
56 | // Style the heading
57 | heading.style.textAlign = 'center';
58 | heading.style.marginTop = '1rem';
59 | heading.style.fontWeight = 'bold';
60 | heading.style.marginBottom = '0';
61 | heading.style.fontSize = '1.5rem';
62 |
63 | // Create a Description for the app window
64 | const description = document.createElement('span');
65 |
66 | // Style the description
67 | description.style.textAlign = 'center';
68 | description.style.marginTop = '0';
69 | description.style.fontSize = '1rem';
70 |
71 | // Set the description's inner HTML
72 | description.textContent = 'Quickly run tests on your Turnstile config.';
73 |
74 | // Create the testing button and add Turnstile integration
75 | const testElement = document.createElement('form');
76 |
77 | // Style the form
78 | testElement.style.display = 'flex';
79 | testElement.style.flexDirection = 'column';
80 | testElement.style.justifyContent = 'center';
81 | testElement.style.alignItems = 'center';
82 | testElement.style.height = '100%';
83 | testElement.style.width = '100%';
84 | testElement.style.padding = '1rem';
85 | testElement.id = 'turnstile-dev-toolbar-form';
86 | testElement.innerHTML = windowHtmlElements.button;
87 | // Append the script to the form
88 | appWindow.appendChild(testElement);
89 |
90 | // Add an event listener to the form
91 | testElement.addEventListener('submit', async (event) => {
92 | // Prevent the default form submission
93 | event.preventDefault();
94 |
95 | // Send the verification request to the server
96 | server.send('sendverify', {key: TURNSTILE_SITE_KEY});
97 | });
98 |
99 | // Append the headings to the header
100 | header.appendChild(heading);
101 | header.appendChild(description);
102 |
103 | // Append the elements to the app window
104 | appWindow.appendChild(closeButton);
105 | appWindow.appendChild(header);
106 | appWindow.appendChild(testElement);
107 |
108 | // Append the app window to the canvas
109 | canvas.appendChild(appWindow);
110 |
111 | // Listen for the response from the server
112 | server.on('verifyresponse', async (data: {success: boolean}) => {
113 | // Create a new element to display the response
114 | const responseElement = document.createElement('div');
115 |
116 | // Style the response element
117 | responseElement.innerHTML = data.success
118 | ? windowHtmlElements.successBanner
119 | : windowHtmlElements.errorBanner;
120 |
121 | // Append the response element to the app window
122 | appWindow.appendChild(responseElement);
123 |
124 | if (data.success) {
125 | app.toggleNotification({state: true, level: 'info'});
126 | } else {
127 | app.toggleNotification({state: true, level: 'error'});
128 | }
129 |
130 | // Remove the response element after 5 seconds
131 | setTimeout(() => {
132 | responseElement.remove();
133 | app.toggleNotification({state: false});
134 | }, 5000);
135 | });
136 | },
137 | });
138 |
--------------------------------------------------------------------------------
/package/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # astro-turnstile
2 |
3 | ## 2.1.0
4 |
5 | ### Minor Changes
6 |
7 | - 31d452f: Updated to use data attribute instead of comment
8 | - 9a444e1: Updated script to only inject on pages where component is rendered
9 |
10 | ## 2.0.1
11 |
12 | ### Patch Changes
13 |
14 | - 8de5ce3: Button Update
15 |
16 | ## 2.0.0
17 |
18 | ### Major Changes
19 |
20 | - e5a7b61: Upgrade to Astro V5
21 |
22 | ## 1.2.0
23 |
24 | ### Minor Changes
25 |
26 | - 9921553: Changeset for Type Updates
27 | - 508f6ad: Refactored component imports for TurnstileWidget and TurnstileForm:
28 | BREAKING: Direct package imports from 'astro-turnstile/components/TurnstileWidget' and 'astro-turnstile/components/TurnstileForm'.
29 |
30 | ```ts
31 | // Previously
32 | import { TurnstileForm } from "astro-turnstile/components";
33 | import { TurnstileWidget } from "astro-turnstile/components";
34 |
35 | // Virtual modules Before
36 | import { TurnstileForm } from "astro-turnstile:components";
37 | import { TurnstileWidget } from "astro-turnstile:components";
38 |
39 | // Now
40 | import TurnstileForm from "astro-turnstile/components/TurnstileForm";
41 | import TurnstileWidget from "astro-turnstile/components/TurnstileWidget";
42 |
43 | // Virtual modules now
44 | import TurnstileForm from "astro-turnstile:components/TurnstileForm";
45 | import TurnstileWidget from "astro-turnstile:components/TurnstileWidget";
46 | ```
47 |
48 | - 16ae506: Updated Imports
49 |
50 | ## 1.1.2
51 |
52 | ### Patch Changes
53 |
54 | - 05537b2: [Fix/Docs]:
55 |
56 | - Update Type `AstroTurnstileOptions` to reflect the correct default values by switching from `z.infer` to a `typeof Schema._input` which properly shows the type as it would be used by the enduser
57 |
58 | ```ts
59 | // Previously
60 | type AstroTurnstileOptions = {
61 | endpointPath: string;
62 | disableClientScript: boolean;
63 | disableDevToolbar: boolean;
64 | verbose: boolean;
65 | };
66 |
67 | // Now
68 | type AstroTurnstileOptions =
69 | | {
70 | endpointPath?: string | undefined;
71 | disableClientScript?: boolean | undefined;
72 | disableDevToolbar?: boolean | undefined;
73 | verbose?: boolean | undefined;
74 | }
75 | | undefined;
76 | ```
77 |
78 | - Update readme to include instructions and more information about what is available to users from the integration
79 |
80 | ## 1.1.1
81 |
82 | ### Patch Changes
83 |
84 | - 2c91682: Updated readme
85 |
86 | ## 1.1.0
87 |
88 | ### Minor Changes
89 |
90 | - a200211: [refactor components]: Simplify Logic and breakout widget into its own component.
91 |
92 | - Moved Widget to its own component that can be used with custom implementations.
93 | - Refactored Form component to add slots, and adjust the configuration of how logic is handled.
94 | - Updated API endpoint to give responses in the status text to use within the custom Form component.
95 |
96 | ## 1.0.1
97 |
98 | ### Patch Changes
99 |
100 | - b08da43: Updated readme
101 |
102 | ## 1.0.0
103 |
104 | ### Major Changes
105 |
106 | - a30c366: First Release
107 |
108 | ## 0.3.0
109 |
110 | ### Minor Changes
111 |
112 | - 5dbd778: [Refactor]: Update integration logic and handling and create a reusable component
113 |
114 | - BREAKING: Minimum version of Astro is now `v4.14` due to the use of the new injectTypes helper functions.
115 | - NEW: Auto injection of Turnstile Client API script.
116 | - NEW: You can now change the endpoint path of your Astro-Turnstile install for Verifying Tokens.
117 | - NEW: Virtual component module for users with full types `astro-turnstile:components`.
118 | - NEW: Added new Astro DevToolbarApp.
119 | - NEW: Virtual modules `virtual:astro-turnstile/config` and `astro-turnstile:components` automatic `.d.ts` generation providing a fully typed ecosystem.
120 | - CHANGED: The options have changed, the `TURNSTILE_SECRET_KEY` and `TURNSTILE_SITE_KEY` are now environment variables only, and integration options change functionality:
121 |
122 | ```ts
123 | type AstroTurnstileOptions = {
124 | endpointPath: string;
125 | disableClientScript: boolean;
126 | disableDevToolbar: boolean;
127 | verbose: boolean;
128 | };
129 | ```
130 |
131 | - NEW: Reusable form component:
132 |
133 | ```tsx
134 | // src/pages/index.astro
135 | ---
136 | import { TurnstileForm } from "astro-turnstile:components";
137 | ---
138 |
139 |
140 |
141 |
142 |
146 |
147 | {/*
148 | Note: You do not need to place a submit button in
149 | as there is already one present as part of the component.
150 | */}
151 |
152 |
153 | ```
154 |
155 | ## 0.2.0
156 |
157 | ### Minor Changes
158 |
159 | - 154366f: Playground and Integration Updates
160 |
161 | ## 0.1.1
162 |
163 | ### Patch Changes
164 |
165 | - 23b88a0: Update Readme
166 |
167 | ## 0.1.0
168 |
169 | ### Minor Changes
170 |
171 | - 75f9d29: Inital Release
172 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct - Astro Turnstile
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to make participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to a positive environment for our
15 | community include:
16 |
17 | * Demonstrating empathy and kindness toward other people
18 | * Being respectful of differing opinions, viewpoints, and experiences
19 | * Giving and gracefully accepting constructive feedback
20 | * Accepting responsibility and apologizing to those affected by our mistakes,
21 | and learning from the experience
22 | * Focusing on what is best not just for us as individuals, but for the
23 | overall community
24 |
25 | Examples of unacceptable behavior include:
26 |
27 | * The use of sexualized language or imagery, and sexual attention or
28 | advances
29 | * Trolling, insulting or derogatory comments, and personal or political attacks
30 | * Public or private harassment
31 | * Publishing others' private information, such as a physical or email
32 | address, without their explicit permission
33 | * Other conduct which could reasonably be considered inappropriate in a
34 | professional setting
35 |
36 | ## Our Responsibilities
37 |
38 | Project maintainers are responsible for clarifying and enforcing our standards of
39 | acceptable behavior and will take appropriate and fair corrective action in
40 | response to any behavior that they deem inappropriate,
41 | threatening, offensive, or harmful.
42 |
43 | Project maintainers have the right and responsibility to remove, edit, or reject
44 | comments, commits, code, wiki edits, issues, and other contributions that are
45 | not aligned to this Code of Conduct, and will
46 | communicate reasons for moderation decisions when appropriate.
47 |
48 | ## Scope
49 |
50 | This Code of Conduct applies within all community spaces, and also applies when
51 | an individual is officially representing the community in public spaces.
52 | Examples of representing our community include using an official e-mail address,
53 | posting via an official social media account, or acting as an appointed
54 | representative at an online or offline event.
55 |
56 | ## Enforcement
57 |
58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
59 | reported to the community leaders responsible for enforcement at .
60 | All complaints will be reviewed and investigated promptly and fairly.
61 |
62 | All community leaders are obligated to respect the privacy and security of the
63 | reporter of any incident.
64 |
65 | ## Enforcement Guidelines
66 |
67 | Community leaders will follow these Community Impact Guidelines in determining
68 | the consequences for any action they deem in violation of this Code of Conduct:
69 |
70 | ### 1. Correction
71 |
72 | **Community Impact**: Use of inappropriate language or other behavior deemed
73 | unprofessional or unwelcome in the community.
74 |
75 | **Consequence**: A private, written warning from community leaders, providing
76 | clarity around the nature of the violation and an explanation of why the
77 | behavior was inappropriate. A public apology may be requested.
78 |
79 | ### 2. Warning
80 |
81 | **Community Impact**: A violation through a single incident or series
82 | of actions.
83 |
84 | **Consequence**: A warning with consequences for continued behavior. No
85 | interaction with the people involved, including unsolicited interaction with
86 | those enforcing the Code of Conduct, for a specified period of time. This
87 | includes avoiding interactions in community spaces as well as external channels
88 | like social media. Violating these terms may lead to a temporary or
89 | permanent ban.
90 |
91 | ### 3. Temporary Ban
92 |
93 | **Community Impact**: A serious violation of community standards, including
94 | sustained inappropriate behavior.
95 |
96 | **Consequence**: A temporary ban from any sort of interaction or public
97 | communication with the community for a specified period of time. No public or
98 | private interaction with the people involved, including unsolicited interaction
99 | with those enforcing the Code of Conduct, is allowed during this period.
100 | Violating these terms may lead to a permanent ban.
101 |
102 | ### 4. Permanent Ban
103 |
104 | **Community Impact**: Demonstrating a pattern of violation of community
105 | standards, including sustained inappropriate behavior, harassment of an
106 | individual, or aggression toward or disparagement of classes of individuals.
107 |
108 | **Consequence**: A permanent ban from any sort of public interaction within
109 | the community.
110 |
111 | ## Attribution
112 |
113 | This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version
114 | [1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and
115 | [2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md),
116 | and was generated by [contributing-gen](https://github.com/bttger/contributing-gen).
--------------------------------------------------------------------------------
/package/README.md:
--------------------------------------------------------------------------------
1 | # `Astro Turnstile`
2 |
3 | This is an [Astro integration](https://docs.astro.build/en/guides/integrations-guide/) that allows you to add a turnstile to your Astro site.
4 |
5 | ## Usage
6 |
7 | ### Prerequisites
8 |
9 | Before you can use this integration, you need to have a Cloudflare account. You can sign up for a free account [here](https://www.cloudflare.com/products/turnstile/).
10 |
11 | ### Getting Started
12 |
13 | First, you need to create a new site in your Cloudflare account. You can do this by following the instructions [here](https://developers.cloudflare.com/turnstile/getting-started/create-site).
14 |
15 | Once you have created a site, you will be given a site key and a secret key. You will need this key to configure the integration.
16 |
17 | ### Installation
18 |
19 | Install the integration **automatically** using the Astro CLI:
20 |
21 | ```bash
22 | pnpm astro add astro-turnstile
23 | ```
24 |
25 | ```bash
26 | npx astro add astro-turnstile
27 | ```
28 |
29 | ```bash
30 | yarn astro add astro-turnstile
31 | ```
32 |
33 | ```bun
34 | bunx astro add astro-turnstile
35 | ```
36 |
37 | Or install it **manually**:
38 |
39 | 1. Install the required dependencies
40 |
41 | ```bash
42 | pnpm add astro-turnstile
43 | ```
44 |
45 | ```bash
46 | npm install astro-turnstile
47 | ```
48 |
49 | ```bash
50 | yarn add astro-turnstile
51 | ```
52 |
53 | ```bun
54 | bun add astro-turnstile
55 | ```
56 |
57 | 2. Add the integration to your astro config
58 |
59 | ```diff
60 | +import astroTurnstile from "astro-turnstile";
61 |
62 | export default defineConfig({
63 | integrations: [
64 | + astroTurnstile(),
65 | ],
66 | });
67 | ```
68 |
69 | ### Configuration
70 |
71 | #### `.env` File
72 |
73 | You will need to add these 2 values to your `.env` file:
74 |
75 | - `TURNSTILE_SITE_KEY` (required): Your Turnstile site key
76 | - `TURNSTILE_SECRET_KEY` (required): Your Turnstile secret key - this should be kept secret
77 |
78 | #### Astro Config Options
79 |
80 | **`verbose`**
81 | - Type: `boolean`
82 | - Default: `false`
83 |
84 | Enable verbose logging.
85 |
86 | **`disableClientScript`**
87 | - Type: `boolean`
88 | - Default: `false`
89 |
90 | Disable the client-side script injection.
91 |
92 | By default, the client-side script is injected into the Astro project on every page. In some cases, you may want to disable this behavior, and manually inject the script where needed. This option allows you to disable the client-side script injection.
93 |
94 | Note: If you disable the client-side script injection, you will need to manually inject the Turnstile client-side script into your Astro project.
95 |
96 | **`disableDevToolbar`**
97 | - Type: `boolean`
98 | - Default: `false`
99 |
100 | Disable the Astro Turnstile Dev Toolbar App.
101 |
102 | **`endpointPath`**
103 | - Type: `string`
104 | - Default: `/verify`
105 |
106 | The path to the injected Turnstile API endpoint.
107 |
108 | ### Usage
109 |
110 | The following components are made available to the end user:
111 |
112 | - **`TurnstileWidget`** - The main widget component for displaying the Turnstile captcha field in forms
113 | - Available Props:
114 | - **`theme`**:
115 | - Type: `"auto"` | `"light"` | `"dark"` | `undefined`
116 | - Default: `"auto"`
117 | - **`size`**:
118 | - Type: `"normal"` | `"compact"` | `"flexible"` | `undefined`
119 | - Default: `"normal"`
120 | - **`margin`**:
121 | - Type: `string` | `undefined`
122 | - Default: `"0.5rem"`
123 |
124 | - `TurnstileForm` - A helper form element that assists you in building your forms with Turnstile verification built in
125 | - Available Props:
126 | - (All the props from Widget)
127 | - **`enctype`**
128 | - Type: `"multipart/form-data"` | `"application/x-www-form-urlencoded"` | `"submit"` | `undefined`
129 | - Default: `"application/x-www-form-urlencoded"`
130 | - **`action`**
131 | - Type: `string` | `null` | `undefined`
132 | - **`method`**
133 | - Type: `string` | `null` | `undefined`
134 |
135 | These components can be accessed by either of the following methods:
136 |
137 | ```ts
138 | // Option 1: Runtime virtual module
139 | import TurnstileWidget from 'astro-turnstile:components/TurnstileWidget';
140 | import TurnstileForm from 'astro-turnstile:components/TurnstileForm';
141 |
142 | // Option 2: Direct package exports
143 | import TurnstileWidget from 'astro-turnstile/components/TurnstileWidget';
144 | import TurnstileForm from 'astro-turnstile/components/TurnstileForm';
145 |
146 | ```
147 |
148 | ## Contributing
149 |
150 | This package is structured as a monorepo:
151 |
152 | - `playground` contains code for testing the package
153 | - `package` contains the actual package
154 |
155 | Install dependencies using pnpm:
156 |
157 | ```bash
158 | pnpm i --frozen-lockfile
159 | ```
160 |
161 | Start the playground:
162 |
163 | ```bash
164 | pnpm playground:dev
165 | ```
166 |
167 | You can now edit files in `package`. Please note that making changes to those files may require restarting the playground dev server.
168 |
169 | ## Licensing
170 |
171 | [MIT Licensed](https://github.com/hkbertoson/astro-turnstile/blob/main/LICENSE). Made with ❤️ by [Hunter Bertoson](https://github.com/hkbertoson).
172 |
173 | ## Acknowledgements
174 |
175 | [Astro](https://astro.build/)
176 | [Turnstile](https://www.cloudflare.com/products/turnstile/)
177 | [Florian Lefebvre](https://github.com/florian-lefebvre)
178 |
--------------------------------------------------------------------------------
/package/src/integration.ts:
--------------------------------------------------------------------------------
1 | import {
2 | addVirtualImports,
3 | createResolver,
4 | defineIntegration,
5 | } from 'astro-integration-kit';
6 | import {envField} from 'astro/config';
7 | import {AstroError} from 'astro/errors';
8 | import {loadEnv} from 'vite';
9 | import {name} from '../package.json';
10 | import {AstroTurnstileOptionsSchema as optionsSchema} from './schema.ts';
11 | import {
12 | ErrorMessages,
13 | envDefaults,
14 | loggerStrings,
15 | svgIcons,
16 | } from './strings.ts';
17 | import Dts from './stubs.ts';
18 |
19 | // Load the Turnstile environment variables for the Server runtime and Verification
20 | // that the environment variables are NOT set to Turnstile demo values during build.
21 | const env = loadEnv('TURNSTILE_', process.cwd());
22 |
23 | export const astroTurnstile = defineIntegration({
24 | name,
25 | optionsSchema,
26 | setup({
27 | name,
28 | options,
29 | options: {endpointPath, verbose, disableClientScript, disableDevToolbar},
30 | }) {
31 | const {resolve} = createResolver(import.meta.url);
32 | return {
33 | hooks: {
34 | 'astro:config:setup': (params) => {
35 | // Destructure the params object
36 | const {
37 | logger,
38 | updateConfig,
39 | injectScript,
40 | injectRoute,
41 | command,
42 | config,
43 | addDevToolbarApp,
44 | addMiddleware,
45 | } = params;
46 |
47 | // Log startup message
48 | verbose && logger.info(loggerStrings.setup);
49 |
50 | // Update the User's Astro config ('astro:env') with the required Turnstile
51 | // environment variables and Set the 'checkOrigin' security option to true
52 | verbose && logger.info(loggerStrings.updateConfig);
53 | updateConfig({
54 | env: {
55 | validateSecrets: true,
56 | schema: {
57 | TURNSTILE_SECRET_KEY: envField.string({
58 | access: 'secret',
59 | context: 'server',
60 | optional: false,
61 | // The default value is only usable in 'dev'/'preview' and should be replaced with the actual secret key.
62 | // See https://developers.cloudflare.com/turnstile/troubleshooting/testing/#dummy-sitekeys-and-secret-keys for more information.
63 | default: envDefaults.secretKey(command),
64 | }),
65 | TURNSTILE_SITE_KEY: envField.string({
66 | access: 'public',
67 | context: 'client',
68 | optional: false,
69 | // The default value is only usable in 'dev'/'preview' and should be replaced with the actual secret key.
70 | // See https://developers.cloudflare.com/turnstile/troubleshooting/testing/#dummy-sitekeys-and-secret-keys for more information.
71 | default: envDefaults.siteKey(command),
72 | }),
73 | },
74 | },
75 | });
76 |
77 | // Helper function to check if the environment variables are set to Turnstile demo values
78 | function checkKeys(o: {
79 | key: string;
80 | knownKeys: string[];
81 | error: string;
82 | }) {
83 | if (o.knownKeys.includes(o.key)) {
84 | throw new AstroError(o.error);
85 | }
86 | }
87 |
88 | // If environment variables are set to Turnstile demo values during build, error
89 | if (command === 'build') {
90 | // Check TURNSTILE_SECRET_KEY
91 | if (env.TURNSTILE_SECRET_KEY) {
92 | checkKeys({
93 | key: env.TURNSTILE_SECRET_KEY,
94 | knownKeys: [
95 | '1x0000000000000000000000000000000AA',
96 | '2x0000000000000000000000000000000AA',
97 | '3x0000000000000000000000000000000AA',
98 | ],
99 | error: ErrorMessages.demoSecret,
100 | });
101 | }
102 |
103 | // Check TURNSTILE_SITE_KEY
104 | if (env.TURNSTILE_SITE_KEY) {
105 | checkKeys({
106 | key: env.TURNSTILE_SITE_KEY,
107 | knownKeys: [
108 | '1x00000000000000000000AA',
109 | '1x00000000000000000000BB',
110 | '2x00000000000000000000AB',
111 | '2x00000000000000000000BB',
112 | '3x00000000000000000000FF',
113 | ],
114 | error: ErrorMessages.demoSiteKey,
115 | });
116 | }
117 | }
118 |
119 | // Check if the Astro config has a 'site' property
120 | if (!config.site) {
121 | logger.warn(loggerStrings.configSiteMissing);
122 | }
123 |
124 | // Inject the required Turnstile client-side script if not disabled
125 | if (!disableClientScript) {
126 | verbose && logger.info(loggerStrings.injectMiddleware);
127 | addMiddleware({entrypoint: resolve('./middleware.ts'), order: 'post'});
128 | }
129 |
130 | // Add Development Toolbar App for Astro Turnstile testing
131 | if (!disableDevToolbar) {
132 | verbose && logger.info(loggerStrings.addDevToolbarApp);
133 | addDevToolbarApp({
134 | name: 'Astro Turnstile',
135 | id: 'astro-turnstile-dev-toolbar',
136 | icon: svgIcons.turnstile,
137 | entrypoint: resolve('./toolbar.ts'),
138 | });
139 | }
140 |
141 | // Inject the required Turnstile server-side route
142 | verbose && logger.info(loggerStrings.injectRoute(endpointPath));
143 | injectRoute({
144 | pattern: endpointPath,
145 | entrypoint: `${name}/server`,
146 | prerender: false,
147 | });
148 |
149 | // Add Virtual Imports for resolving the Astro Turnstile Options during runtime
150 | verbose && logger.info(loggerStrings.virtualImports);
151 | addVirtualImports(params, {
152 | name,
153 | imports: {
154 | 'virtual:astro-turnstile/config': `export default ${JSON.stringify(
155 | options
156 | )}`,
157 | 'astro-turnstile:components/TurnstileWidget': `import Widget from '${name}/components/TurnstileWidget'; export default Widget;`,
158 | 'astro-turnstile:components/TurnstileForm': `import Form from '${name}/components/TurnstileForm'; export default Form;`,
159 | },
160 | });
161 |
162 |
163 |
164 | // Log completion message
165 | verbose && logger.info(loggerStrings.setupComplete);
166 | },
167 | 'astro:config:done': ({injectTypes, logger}) => {
168 | // Inject the required Turnstile types for the Astro config and components
169 | verbose && logger.info(loggerStrings.injectTypes);
170 | injectTypes(Dts.config);
171 | injectTypes(Dts.components);
172 | },
173 | 'astro:server:setup': async ({toolbar, logger}) => {
174 | // Add a Server Event Listener for the 'sendverify' event from the Astro Dev Toolbar app
175 | if (!disableDevToolbar) {
176 | verbose &&
177 | logger.info('Adding Server Event Listener Dev Toolbar App...');
178 | toolbar.on('sendverify', async (data: {key: string}) => {
179 | const formData = new FormData();
180 |
181 | // Get the Turnstile site token from the environment variables
182 | // or use a dummy token for testing
183 | const siteToken =
184 | env.TURNSTILE_SITE_KEY || '1x00000000000000000000AA';
185 |
186 | // Add the secret key and token to the form data
187 | formData.append('secret', data.key);
188 | formData.append('response', siteToken);
189 |
190 | // Send the token to the Turnstile API for verification
191 | try {
192 | const response = await fetch(
193 | 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
194 | {
195 | method: 'POST',
196 | body: formData,
197 | headers: {
198 | 'content-type': 'application/x-www-form-urlencoded',
199 | },
200 | }
201 | );
202 |
203 | // Send the verification response back to the Astro Dev Toolbar app
204 | toolbar.send('verifyresponse', {
205 | success: response.ok,
206 | });
207 | } catch (error) {
208 | // Send the verification response back to the Astro Dev Toolbar app
209 | toolbar.send('verifyresponse', {success: false});
210 | }
211 | });
212 | }
213 | },
214 | },
215 | };
216 | },
217 | });
218 |
219 | export default astroTurnstile;
220 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributing to Astro Turnstile
3 |
4 | First off, thanks for taking the time to contribute! ❤️
5 |
6 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
7 |
8 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
9 | > - Star the project
10 | > - Tweet about it
11 | > - Refer this project in your project's readme
12 | > - Mention the project at local meetups and tell your friends/colleagues
13 |
14 |
15 | ## Table of Contents
16 |
17 | - [Code of Conduct](#code-of-conduct)
18 | - [I Have a Question](#i-have-a-question)
19 | - [I Want To Contribute](#i-want-to-contribute)
20 | - [Reporting Bugs](#reporting-bugs)
21 | - [Suggesting Enhancements](#suggesting-enhancements)
22 | - [Your First Code Contribution](#your-first-code-contribution)
23 | - [Improving The Documentation](#improving-the-documentation)
24 | - [Styleguides](#styleguides)
25 | - [Commit Messages](#commit-messages)
26 |
27 |
28 | ## Code of Conduct
29 |
30 | This project and everyone participating in it is governed by the
31 | [Astro Turnstile Code of Conduct](https://github.com/hkbertoson/astro-turnstileblob/master/CODE_OF_CONDUCT.md).
32 | By participating, you are expected to uphold this code. Please report unacceptable behavior
33 | to .
34 |
35 |
36 | ## I Have a Question
37 |
38 | > If you want to ask a question, we assume that you have read the available [Documentation]().
39 |
40 | Before you ask a question, it is best to search for existing [Issues](https://github.com/hkbertoson/astro-turnstile/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
41 |
42 | If you then still feel the need to ask a question and need clarification, we recommend the following:
43 |
44 | - Open an [Issue](https://github.com/hkbertoson/astro-turnstile/issues/new).
45 | - Provide as much context as you can about what you're running into.
46 | - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
47 |
48 | We will then take care of the issue as soon as possible.
49 |
50 |
64 |
65 | ## I Want To Contribute
66 |
67 | > ### Legal Notice
68 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
69 |
70 | ### Reporting Bugs
71 |
72 |
73 | #### Before Submitting a Bug Report
74 |
75 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
76 |
77 | - Make sure that you are using the latest version.
78 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/hkbertoson/astro-turnstile). If you are looking for support, you might want to check [this section](#i-have-a-question)).
79 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/hkbertoson/astro-turnstileissues?q=label%3Abug).
80 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
81 | - Collect information about the bug:
82 | - Stack trace (Traceback)
83 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
84 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
85 | - Possibly your input and the output
86 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions?
87 |
88 |
89 | #### How Do I Submit a Good Bug Report?
90 |
91 | > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to .
92 |
93 |
94 | We use GitHub issues to track bugs and errors. If you run into an issue with the project:
95 |
96 | - Open an [Issue](https://github.com/hkbertoson/astro-turnstile/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
97 | - Explain the behavior you would expect and the actual behavior.
98 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
99 | - Provide the information you collected in the previous section.
100 |
101 | Once it's filed:
102 |
103 | - The project team will label the issue accordingly.
104 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
105 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
106 |
107 |
108 |
109 |
110 | ### Suggesting Enhancements
111 |
112 | This section guides you through submitting an enhancement suggestion for Astro Turnstile, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
113 |
114 |
115 | #### Before Submitting an Enhancement
116 |
117 | - Make sure that you are using the latest version.
118 | - Read the [documentation](https://github.com/hkbertoson/astro-turnstile) carefully and find out if the functionality is already covered, maybe by an individual configuration.
119 | - Perform a [search](https://github.com/hkbertoson/astro-turnstile/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
120 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
121 |
122 |
123 | #### How Do I Submit a Good Enhancement Suggestion?
124 |
125 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/hkbertoson/astro-turnstile/issues).
126 |
127 | - Use a **clear and descriptive title** for the issue to identify the suggestion.
128 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
129 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
130 | - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
131 | - **Explain why this enhancement would be useful** to most Astro Turnstile users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
132 |
133 |
134 |
135 |
136 |
140 |
141 |
142 |
146 |
147 |
148 |
149 |
152 |
153 |
154 | ## Attribution
155 | This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)!
156 |
--------------------------------------------------------------------------------