cloudflare-turnstile-widget
Version:
Framework agnostic widget for Cloudflare's free-to-use CAPTCHA service, Cloudflare Turnstile.
230 lines • 10.3 kB
JavaScript
export class TurnstileWidgetFrame extends HTMLElement {
static _currentIdentifier;
static get currentIdentifier() {
return TurnstileWidgetFrame._currentIdentifier;
}
static set currentIdentifier(identifier) {
TurnstileWidgetFrame._currentIdentifier = identifier;
}
get siteKey() {
return this.getAttribute('site-key');
}
set siteKey(value) {
if (this.getAttribute('site-key') !== value && value) {
this.setAttribute('site-key', value);
}
}
get theme() {
return this.getAttribute('theme');
}
set theme(value) {
if (this.getAttribute('theme') !== value && value) {
this.setAttribute('theme', value);
}
}
get size() {
return this.getAttribute('size');
}
set size(value) {
if (this.getAttribute('size') !== value && value) {
this.setAttribute('size', value);
}
}
get retry() {
return this.getAttribute('retry');
}
set retry(value) {
if (this.getAttribute('retry') !== value && value) {
this.setAttribute('retry', value);
}
}
get refreshExpired() {
return this.getAttribute('refresh-expired');
}
set refreshExpired(value) {
if (this.getAttribute('refresh-expired') !== value && value) {
this.setAttribute('refresh-expired', value);
}
}
/**
* Initializes the component.
*
* @hideconstructor
*/
constructor() {
super();
}
/**
* Setup the component once added to the DOM.
*/
connectedCallback() {
const div = document.createElement('div');
div.style.height = '100%';
this.appendChild(div);
const widgetLoad = (identifier) => {
div.id = identifier;
turnstile.ready(() => {
turnstile.render(div, {
sitekey: this.siteKey,
callback: (token) => {
TurnstileWidgetFrame.messageApplication(TurnstileWidgetFrame.currentIdentifier, 'success', token);
},
'error-callback': (errorCode) => {
TurnstileWidgetFrame.messageApplication(TurnstileWidgetFrame.currentIdentifier, 'error', errorCode);
},
'expired-callback': () => {
TurnstileWidgetFrame.messageApplication(TurnstileWidgetFrame.currentIdentifier, 'expired');
},
'unsupported-callback': () => {
TurnstileWidgetFrame.messageApplication(TurnstileWidgetFrame.currentIdentifier, 'unsupported');
},
theme: this.theme ? this.theme : 'auto',
size: this.size ? this.size : 'normal',
retry: this.retry ? this.retry : 'auto',
'refresh-expired': this.refreshExpired ? this.refreshExpired : 'auto'
});
});
};
TurnstileWidgetFrame.initialize(widgetLoad);
}
/**
* Send a bridged message to the hosting application frame as a widget.
*
* @param identifier Identifier to represent widget.
* @param eventName Name of event to send to application.
* @param detail Custom detail to attach as event detail.
* @param callback (Optional) Callback function to invoke on widget response.
* @param timeout (Optional) Duration to wait for callback.
* @param timeoutCallback (Optional) Callback function to invoke if response timeout is exceeded
* @param callbackIdentifierPrefix (Optional) Prefix to apply on generated widget callback id
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static messageApplication(identifier, eventName, detail, callback, timeout, timeoutCallback, callbackIdentifierPrefix = 'widget-callback') {
if (window === window.parent) {
throw new Error('No parent application to message!');
}
// Build up bridge event with provided detail
const bridgeEvent = {
bridgeEvent: eventName,
detail: detail,
fromApplication: false,
identifier: identifier,
callbackId: undefined
};
if (callback) {
let resolved = null;
// If callback function is specified, setup temporary event for callback response
const callbackId = `${callbackIdentifierPrefix}|${TurnstileWidgetFrame.uuidv4()}`;
bridgeEvent.callbackId = callbackId;
const callbackHandler = (e) => {
if (resolved === null) {
resolved = true;
TurnstileWidgetFrame.removeEventListener(callbackListener);
// eslint-disable-next-line callback-return, @typescript-eslint/no-unsafe-argument
callback(e.content);
}
};
if (timeout) {
setTimeout(() => {
if (resolved === null) {
resolved = false;
TurnstileWidgetFrame.removeEventListener(callbackListener);
if (timeoutCallback) {
timeoutCallback();
}
}
}, timeout);
}
const callbackListener = TurnstileWidgetFrame.addEventListener(callbackId, callbackHandler);
}
window.parent.postMessage(JSON.parse(JSON.stringify(bridgeEvent)), `*`);
}
/**
* Generate a unique identifier string
*/
static uuidv4() {
if (crypto && crypto.randomUUID) {
try {
const uuid = crypto.randomUUID();
if (uuid) {
return uuid;
}
}
catch (error) {
console.warn(error);
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
}
/**
* Asynchronously sends a bridged message to the hosting application frame as a widget, then awaits and returns its response.
* @param identifier Identifier to represent widget.
* @param eventName The event this message correlates to.
* @param detail Custom detail to attach as event detail.
* @param timeout (Optional) Duration to wait for before rejecting the promise. If not specified, will wait indefinitely.
* @param callbackIdentifierPrefix (Optional) Prefix to apply on generated widget callback id
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static messageApplicationAsync(identifier, eventName, detail, timeout = 3000, callbackIdentifierPrefix = 'widget-callback') {
return new Promise((resolve, reject) => {
try {
TurnstileWidgetFrame.messageApplication(identifier, eventName, detail, resolve, timeout, () => reject(new Error(`No response received for '${eventName}' in the given timeout: ${timeout}ms`)), callbackIdentifierPrefix);
}
catch (error) {
reject(error);
}
});
}
/**
* Set up a callback for the `frame-load` event from the widget component in a hosting application.
* Can be used by a widget application to retrieve its identifier from the widget component.
* @param frameLoaded The callback to invoke when the `frame-load` event occurs.
*/
static initialize(frameLoaded) {
// Listen for messages on the widget window. Used for communication with the parent application
const listener = TurnstileWidgetFrame.addEventListener(`frame-load`, (event) => {
TurnstileWidgetFrame.currentIdentifier = event.content;
TurnstileWidgetFrame.removeEventListener(listener);
frameLoaded(event.content);
});
}
/**
* Set up a callback for the specified event from the widget component in a hosting application.
* @param eventName The event name to listen for when receiving bridged messages.
* @param listener The callback to invoke when the specified event occurs.
* @returns Event listener instance that can be used to remove the listener via `Widget.removeEventListener`
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static addEventListener(eventName, listener) {
// Listen for messages on the widget window. Used for communication with the parent application
const messageListener = (event) => {
// Check if received message is a parent application message
if (typeof event.data === `object` && event.data.bridgeEvent && event.data.fromApplication === true && event.data.bridgeEvent === eventName) {
TurnstileWidgetFrame.currentIdentifier = event.data.identifier;
listener({
content: event.data.detail,
callback: event.data.callbackId
? (detail) => {
TurnstileWidgetFrame.messageApplication(event.data.identifier, event.data.callbackId, detail);
}
: undefined
});
}
};
window.addEventListener(`message`, messageListener);
return messageListener;
}
/**
* Remove the listener from the widget component in a hosting application.
* @param messageListener The listener instance created by `Widget.addEventListener` to remove
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static removeEventListener(messageListener) {
window.removeEventListener(`message`, messageListener);
}
}
customElements.define('turnstile-widget-frame', TurnstileWidgetFrame);
//# sourceMappingURL=TurnstileWidgetFrame.js.map