Initial commit

This commit is contained in:
2026-08-02 20:55:23 +02:00
parent 411f484b40
commit d558d18d9a
295 changed files with 9 additions and 5880 deletions
-30
View File
@@ -1,30 +0,0 @@
declare global {
interface Window {
turnstile?: {
render: (el: HTMLElement, opts: Record<string, unknown>) => string;
reset: (id?: string) => void;
};
}
}
export type TurnstileProps = {
/** Called with the token once solved, or null on expiry/error. */
onToken: (token: null | string) => void;
/**
* Public Turnstile site key. The client project reads it server-side from
* SiteIntegrations (turnstileSiteKey) and passes it in — the widget is a pure
* client component and can't read Payload itself.
*/
siteKey: string;
theme?: 'auto' | 'dark' | 'light';
};
/**
* Cloudflare Turnstile widget — reusable across any form. Renders the challenge
* and reports the token via onToken. The token must then be verified
* server-side (see verifyTurnstile) before a submission is trusted.
*
* siteKey is a prop rather than an env var so all Turnstile config lives in one
* place (SiteIntegrations), consistent with the plugin's panel-managed model.
* The script is injected directly (no next/script dependency) so the widget
* stays framework-agnostic.
*/
export declare function Turnstile({ onToken, siteKey, theme }: TurnstileProps): import("react/jsx-runtime").JSX.Element | null;
-64
View File
@@ -1,64 +0,0 @@
'use client';
import { jsx as _jsx } from "react/jsx-runtime";
import { useEffect, useRef } from 'react';
const SCRIPT_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
/** Loads the Turnstile script once, shared across all widget instances. */ function ensureScript() {
if (typeof document === 'undefined') {
return;
}
if (document.querySelector(`script[src="${SCRIPT_SRC}"]`)) {
return;
}
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
/**
* Cloudflare Turnstile widget — reusable across any form. Renders the challenge
* and reports the token via onToken. The token must then be verified
* server-side (see verifyTurnstile) before a submission is trusted.
*
* siteKey is a prop rather than an env var so all Turnstile config lives in one
* place (SiteIntegrations), consistent with the plugin's panel-managed model.
* The script is injected directly (no next/script dependency) so the widget
* stays framework-agnostic.
*/ export function Turnstile({ onToken, siteKey, theme = 'auto' }) {
const ref = useRef(null);
const widgetId = useRef(null);
useEffect(()=>{
if (!siteKey) {
return;
}
ensureScript();
function render() {
if (!ref.current || !window.turnstile || widgetId.current) {
return;
}
widgetId.current = window.turnstile.render(ref.current, {
callback: (token)=>onToken(token),
'error-callback': ()=>onToken(null),
'expired-callback': ()=>onToken(null),
sitekey: siteKey,
theme
});
}
render();
// Script may load after mount — retry briefly until ready.
const interval = setInterval(render, 300);
return ()=>clearInterval(interval);
}, [
siteKey,
theme,
onToken
]);
if (!siteKey) {
return null;
}
return /*#__PURE__*/ _jsx("div", {
ref: ref
});
}
//# sourceMappingURL=Turnstile.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/turnstile/Turnstile.tsx"],"sourcesContent":["'use client'\nimport { useEffect, useRef } from 'react'\n\ndeclare global {\n interface Window {\n turnstile?: {\n render: (el: HTMLElement, opts: Record<string, unknown>) => string\n reset: (id?: string) => void\n }\n }\n}\n\nconst SCRIPT_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'\n\n/** Loads the Turnstile script once, shared across all widget instances. */\nfunction ensureScript(): void {\n if (typeof document === 'undefined') {return}\n if (document.querySelector(`script[src=\"${SCRIPT_SRC}\"]`)) {return}\n const script = document.createElement('script')\n script.src = SCRIPT_SRC\n script.async = true\n script.defer = true\n document.head.appendChild(script)\n}\n\nexport type TurnstileProps = {\n /** Called with the token once solved, or null on expiry/error. */\n onToken: (token: null | string) => void\n /**\n * Public Turnstile site key. The client project reads it server-side from\n * SiteIntegrations (turnstileSiteKey) and passes it in — the widget is a pure\n * client component and can't read Payload itself.\n */\n siteKey: string\n theme?: 'auto' | 'dark' | 'light'\n}\n\n/**\n * Cloudflare Turnstile widget — reusable across any form. Renders the challenge\n * and reports the token via onToken. The token must then be verified\n * server-side (see verifyTurnstile) before a submission is trusted.\n *\n * siteKey is a prop rather than an env var so all Turnstile config lives in one\n * place (SiteIntegrations), consistent with the plugin's panel-managed model.\n * The script is injected directly (no next/script dependency) so the widget\n * stays framework-agnostic.\n */\nexport function Turnstile({ onToken, siteKey, theme = 'auto' }: TurnstileProps) {\n const ref = useRef<HTMLDivElement>(null)\n const widgetId = useRef<null | string>(null)\n\n useEffect(() => {\n if (!siteKey) {return}\n\n ensureScript()\n\n function render() {\n if (!ref.current || !window.turnstile || widgetId.current) {return}\n widgetId.current = window.turnstile.render(ref.current, {\n callback: (token: string) => onToken(token),\n 'error-callback': () => onToken(null),\n 'expired-callback': () => onToken(null),\n sitekey: siteKey,\n theme,\n })\n }\n\n render()\n // Script may load after mount — retry briefly until ready.\n const interval = setInterval(render, 300)\n return () => clearInterval(interval)\n }, [siteKey, theme, onToken])\n\n if (!siteKey) {return null}\n\n return <div ref={ref} />\n}\n"],"names":["useEffect","useRef","SCRIPT_SRC","ensureScript","document","querySelector","script","createElement","src","async","defer","head","appendChild","Turnstile","onToken","siteKey","theme","ref","widgetId","render","current","window","turnstile","callback","token","sitekey","interval","setInterval","clearInterval","div"],"mappings":"AAAA;;AACA,SAASA,SAAS,EAAEC,MAAM,QAAQ,QAAO;AAWzC,MAAMC,aAAa;AAEnB,yEAAyE,GACzE,SAASC;IACP,IAAI,OAAOC,aAAa,aAAa;QAAC;IAAM;IAC5C,IAAIA,SAASC,aAAa,CAAC,CAAC,YAAY,EAAEH,WAAW,EAAE,CAAC,GAAG;QAAC;IAAM;IAClE,MAAMI,SAASF,SAASG,aAAa,CAAC;IACtCD,OAAOE,GAAG,GAAGN;IACbI,OAAOG,KAAK,GAAG;IACfH,OAAOI,KAAK,GAAG;IACfN,SAASO,IAAI,CAACC,WAAW,CAACN;AAC5B;AAcA;;;;;;;;;CASC,GACD,OAAO,SAASO,UAAU,EAAEC,OAAO,EAAEC,OAAO,EAAEC,QAAQ,MAAM,EAAkB;IAC5E,MAAMC,MAAMhB,OAAuB;IACnC,MAAMiB,WAAWjB,OAAsB;IAEvCD,UAAU;QACR,IAAI,CAACe,SAAS;YAAC;QAAM;QAErBZ;QAEA,SAASgB;YACP,IAAI,CAACF,IAAIG,OAAO,IAAI,CAACC,OAAOC,SAAS,IAAIJ,SAASE,OAAO,EAAE;gBAAC;YAAM;YAClEF,SAASE,OAAO,GAAGC,OAAOC,SAAS,CAACH,MAAM,CAACF,IAAIG,OAAO,EAAE;gBACtDG,UAAU,CAACC,QAAkBV,QAAQU;gBACrC,kBAAkB,IAAMV,QAAQ;gBAChC,oBAAoB,IAAMA,QAAQ;gBAClCW,SAASV;gBACTC;YACF;QACF;QAEAG;QACA,2DAA2D;QAC3D,MAAMO,WAAWC,YAAYR,QAAQ;QACrC,OAAO,IAAMS,cAAcF;IAC7B,GAAG;QAACX;QAASC;QAAOF;KAAQ;IAE5B,IAAI,CAACC,SAAS;QAAC,OAAO;IAAI;IAE1B,qBAAO,KAACc;QAAIZ,KAAKA;;AACnB"}
-2
View File
@@ -1,2 +0,0 @@
export { Turnstile } from './Turnstile.js';
export type { TurnstileProps } from './Turnstile.js';
-6
View File
@@ -1,6 +0,0 @@
'use client';
// Client-only exports — the Turnstile widget. Kept separate from index.ts so
// the server-only verify never leaks into a browser bundle.
export { Turnstile } from './Turnstile.js';
//# sourceMappingURL=client.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/turnstile/client.ts"],"sourcesContent":["'use client'\n// Client-only exports — the Turnstile widget. Kept separate from index.ts so\n// the server-only verify never leaks into a browser bundle.\nexport { Turnstile } from './Turnstile.js'\nexport type { TurnstileProps } from './Turnstile.js'\n"],"names":["Turnstile"],"mappings":"AAAA;AACA,6EAA6E;AAC7E,4DAA4D;AAC5D,SAASA,SAAS,QAAQ,iBAAgB"}
-1
View File
@@ -1 +0,0 @@
export { verifyTurnstile } from './verify.js';
-5
View File
@@ -1,5 +0,0 @@
// Server-only exports. verify.ts imports 'server-only', so this must never be
// imported from a client component — use ./client for the widget instead.
export { verifyTurnstile } from './verify.js';
//# sourceMappingURL=index.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/turnstile/index.ts"],"sourcesContent":["// Server-only exports. verify.ts imports 'server-only', so this must never be\n// imported from a client component — use ./client for the widget instead.\nexport { verifyTurnstile } from './verify.js'\n"],"names":["verifyTurnstile"],"mappings":"AAAA,8EAA8E;AAC9E,0EAA0E;AAC1E,SAASA,eAAe,QAAQ,cAAa"}
-23
View File
@@ -1,23 +0,0 @@
import 'server-only';
import type { BasePayload } from 'payload';
type VerifyTurnstileArgs = {
/** Optional client IP for stricter verification. */
ip?: string;
/** Payload instance — used to read the secret from SiteIntegrations. */
payload: BasePayload;
/** Token produced by the client-side widget. */
token: string;
};
/**
* Verifies a Turnstile token with Cloudflare, server-side only.
*
* The secret comes from the SiteIntegrations global (editor-managed, per the
* plugin's "secrets in the panel" model), read via the Local API which bypasses
* access control. `server-only` guarantees this never reaches the browser
* bundle, keeping the secret off the client.
*
* Returns false on any failure (missing secret/token, network error, rejected
* challenge) — callers treat false as "do not trust this submission".
*/
export declare function verifyTurnstile({ ip, payload, token, }: VerifyTurnstileArgs): Promise<boolean>;
export {};
-41
View File
@@ -1,41 +0,0 @@
import 'server-only';
import { getSiteIntegrations } from '../payload/index.js';
/**
* Verifies a Turnstile token with Cloudflare, server-side only.
*
* The secret comes from the SiteIntegrations global (editor-managed, per the
* plugin's "secrets in the panel" model), read via the Local API which bypasses
* access control. `server-only` guarantees this never reaches the browser
* bundle, keeping the secret off the client.
*
* Returns false on any failure (missing secret/token, network error, rejected
* challenge) — callers treat false as "do not trust this submission".
*/ export async function verifyTurnstile({ ip, payload, token }) {
if (!token) {
return false;
}
const integrations = await getSiteIntegrations(payload);
const secret = integrations.turnstileSecretKey;
if (!secret) {
return false;
}
const body = new URLSearchParams({
response: token,
secret
});
if (ip) {
body.append('remoteip', ip);
}
try {
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
body,
method: 'POST'
});
const data = await res.json();
return data.success;
} catch {
return false;
}
}
//# sourceMappingURL=verify.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/turnstile/verify.ts"],"sourcesContent":["import 'server-only'\n\nimport type { BasePayload } from 'payload'\n\nimport { getSiteIntegrations } from '../payload/index.js'\n\ntype SiteverifyResponse = {\n challenge_ts?: string\n 'error-codes'?: string[]\n hostname?: string\n success: boolean\n}\n\ntype IntegrationsWithTurnstile = {\n turnstileSecretKey?: null | string\n}\n\ntype VerifyTurnstileArgs = {\n /** Optional client IP for stricter verification. */\n ip?: string\n /** Payload instance — used to read the secret from SiteIntegrations. */\n payload: BasePayload\n /** Token produced by the client-side widget. */\n token: string\n}\n\n/**\n * Verifies a Turnstile token with Cloudflare, server-side only.\n *\n * The secret comes from the SiteIntegrations global (editor-managed, per the\n * plugin's \"secrets in the panel\" model), read via the Local API which bypasses\n * access control. `server-only` guarantees this never reaches the browser\n * bundle, keeping the secret off the client.\n *\n * Returns false on any failure (missing secret/token, network error, rejected\n * challenge) — callers treat false as \"do not trust this submission\".\n */\nexport async function verifyTurnstile({\n ip,\n payload,\n token,\n}: VerifyTurnstileArgs): Promise<boolean> {\n if (!token) {return false}\n\n const integrations = await getSiteIntegrations<IntegrationsWithTurnstile>(payload)\n const secret = integrations.turnstileSecretKey\n if (!secret) {return false}\n\n const body = new URLSearchParams({ response: token, secret })\n if (ip) {body.append('remoteip', ip)}\n\n try {\n const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {\n body,\n method: 'POST',\n })\n const data = (await res.json()) as SiteverifyResponse\n return data.success\n } catch {\n return false\n }\n}\n"],"names":["getSiteIntegrations","verifyTurnstile","ip","payload","token","integrations","secret","turnstileSecretKey","body","URLSearchParams","response","append","res","fetch","method","data","json","success"],"mappings":"AAAA,OAAO,cAAa;AAIpB,SAASA,mBAAmB,QAAQ,sBAAqB;AAsBzD;;;;;;;;;;CAUC,GACD,OAAO,eAAeC,gBAAgB,EACpCC,EAAE,EACFC,OAAO,EACPC,KAAK,EACe;IACpB,IAAI,CAACA,OAAO;QAAC,OAAO;IAAK;IAEzB,MAAMC,eAAe,MAAML,oBAA+CG;IAC1E,MAAMG,SAASD,aAAaE,kBAAkB;IAC9C,IAAI,CAACD,QAAQ;QAAC,OAAO;IAAK;IAE1B,MAAME,OAAO,IAAIC,gBAAgB;QAAEC,UAAUN;QAAOE;IAAO;IAC3D,IAAIJ,IAAI;QAACM,KAAKG,MAAM,CAAC,YAAYT;IAAG;IAEpC,IAAI;QACF,MAAMU,MAAM,MAAMC,MAAM,6DAA6D;YACnFL;YACAM,QAAQ;QACV;QACA,MAAMC,OAAQ,MAAMH,IAAII,IAAI;QAC5B,OAAOD,KAAKE,OAAO;IACrB,EAAE,OAAM;QACN,OAAO;IACT;AACF"}