remix-utils-rt
Version:
This package contains simple utility functions to use with [React Router](https://reactrouter.com/home).
114 lines • 4.16 kB
JavaScript
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