@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
JavaScript
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;
}
}