UNPKG

@kentcdodds/tmp-remix-utils

Version:

This package contains simple utility functions to use with [Remix.run](https://remix.run).

89 lines (88 loc) 3.1 kB
import CryptoJS from "crypto-js"; export class SpamError extends Error {} const DEFAULT_NAME_FIELD_NAME = "name__confirm"; const DEFAULT_VALID_FROM_FIELD_NAME = "from__confirm"; export class Honeypot { config; generatedEncryptionSeed = this.randomValue(); constructor(config = {}) { this.config = config; } getInputProps({ validFromTimestamp = Date.now() } = {}) { return { nameFieldName: this.nameFieldName, validFromFieldName: this.validFromFieldName, encryptedValidFrom: this.encrypt(validFromTimestamp.toString()), }; } 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 = 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 CryptoJS.lib.WordArray.random(128 / 8).toString(); } encrypt(value) { return CryptoJS.AES.encrypt(value, this.encryptionSeed).toString(); } decrypt(value) { return CryptoJS.AES.decrypt(value, this.encryptionSeed).toString( CryptoJS.enc.Utf8 ); } 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; } }