UNPKG

remix-utils-rt

Version:

This package contains simple utility functions to use with [React Router](https://reactrouter.com/home).

114 lines 4.16 kB
import { decrypt, encrypt, randomString } from "../common/crypto.js"; /** * The error thrown when the Honeypot fails, meaning some automated bot filled * the form and the request is probably spam. */ export class SpamError extends Error { name = "SpamError"; } const DEFAULT_NAME_FIELD_NAME = "name__confirm"; const DEFAULT_VALID_FROM_FIELD_NAME = "from__confirm"; /** * Module used to implement a Honeypot. * A Honeypot is a visually hidden input that is used to detect spam bots. This * field is expected to be left empty by users because they don't see it, but * bots will fill it falling in the honeypot trap. */ export class Honeypot { generatedEncryptionSeed = this.randomValue(); config; constructor(config = {}) { this.config = config; } /** * Get the HoneypotInputProps to be used in your forms. * @param options The options for the input props. * @param options.validFromTimestamp Since when the timestamp is valid. * @returns The props to be used in the form. */ async getInputProps({ validFromTimestamp = Date.now(), } = {}) { return { nameFieldName: this.nameFieldName, validFromFieldName: this.validFromFieldName, encryptedValidFrom: await this.encrypt(validFromTimestamp.toString()), }; } async check(formData) { let nameFieldName = this.config.nameFieldName ?? DEFAULT_NAME_FIELD_NAME; if (this.config.randomizeNameFieldName) { let actualName = this.getRandomizedNameFieldName(nameFieldName, formData); if (actualName) nameFieldName = actualName; } if (!this.shouldCheckHoneypot(formData, nameFieldName)) return; if (!formData.has(nameFieldName)) { throw new SpamError("Missing honeypot input"); } let honeypotValue = formData.get(nameFieldName); if (honeypotValue !== "") throw new SpamError("Honeypot input not empty"); if (!this.validFromFieldName) return; let validFrom = formData.get(this.validFromFieldName); if (!validFrom) throw new SpamError("Missing honeypot valid from input"); let time = await this.decrypt(validFrom); if (!time) throw new SpamError("Invalid honeypot valid from input"); if (!this.isValidTimeStamp(Number(time))) { throw new SpamError("Invalid honeypot valid from input"); } if (this.isFuture(Number(time))) { throw new SpamError("Honeypot valid from is in future"); } } get nameFieldName() { let fieldName = this.config.nameFieldName ?? DEFAULT_NAME_FIELD_NAME; if (!this.config.randomizeNameFieldName) return fieldName; return `${fieldName}_${this.randomValue()}`; } get validFromFieldName() { if (this.config.validFromFieldName === undefined) { return DEFAULT_VALID_FROM_FIELD_NAME; } return this.config.validFromFieldName; } get encryptionSeed() { return this.config.encryptionSeed ?? this.generatedEncryptionSeed; } getRandomizedNameFieldName(nameFieldName, formData) { for (let key of formData.keys()) { if (!key.startsWith(nameFieldName)) continue; return key; } } shouldCheckHoneypot(formData, nameFieldName) { return (formData.has(nameFieldName) || Boolean(this.validFromFieldName && formData.has(this.validFromFieldName))); } randomValue() { return randomString(); } encrypt(value) { return encrypt(value, this.encryptionSeed); } decrypt(value) { return decrypt(value, this.encryptionSeed); } isFuture(timestamp) { return timestamp > Date.now(); } isValidTimeStamp(timestampp) { if (Number.isNaN(timestampp)) return false; if (timestampp <= 0) return false; if (timestampp >= Number.MAX_SAFE_INTEGER) return false; return true; } } //# sourceMappingURL=honeypot.js.map