UNPKG

cloudflare-turnstile-widget

Version:

Framework agnostic widget for Cloudflare's free-to-use CAPTCHA service, Cloudflare Turnstile.

230 lines 10.3 kB
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