dymo-api
Version:
Flow system for Dymo API.
1,260 lines • 53.6 kB
JavaScript
"use strict";
const axios = require("axios");
const path = require("path");
const React = require("react");
const twToCss = require("tw-to-css");
const render = require("@react-email/render");
const config = {
lib: {
name: "Dymo API"
}
};
const customError = (code, message) => {
return Object.assign(new Error(), { code, message: `[${config.lib.name}] ${message}` });
};
const validBaseURL = (baseUrl) => {
if (/^(https:\/\/api\.tpeoficial\.com$|http:\/\/(localhost:\d+|dymoapi:\d+))$/.test(baseUrl)) return baseUrl;
else throw new Error("[Dymo API] Invalid URL. It must be https://api.tpeoficial.com or start with http://localhost or http://dymoapi followed by a port.");
};
class FallbackDataGenerator {
static generateFallbackData(method, inputData) {
switch (method) {
case "isValidData":
case "isValidDataRaw":
return this.generateDataValidationAnalysis(inputData);
case "isValidEmail":
return this.generateEmailValidatorResponse(inputData);
case "isValidIP":
return this.generateIPValidatorResponse(inputData);
case "isValidPhone":
return this.generatePhoneValidatorResponse(inputData);
case "protectReq":
return this.generateHTTPRequest(inputData);
case "sendEmail":
return this.generateEmailStatus();
case "getRandom":
return this.generateSRNSummary(inputData);
case "extractWithTextly":
return this.generateExtractWithTextly(inputData);
case "getPrayerTimes":
return this.generatePrayerTimes(inputData);
case "satinize":
case "satinizer":
return this.generateSatinizedInputAnalysis(inputData);
case "isValidPwd":
return this.generatePasswordValidationResult(inputData);
default:
throw new Error(`Unknown method for fallback: ${method}`);
}
}
static validateURL(url) {
if (!url) return false;
const urlRegex = /^https?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?$/;
return urlRegex.test(url);
}
static validateEmail(email) {
if (!email) return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static validateDomain(domain) {
if (!domain) return false;
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*$/;
if (!domainRegex.test(domain)) return false;
const parts = domain.split(".");
return parts.length >= 2 && parts[parts.length - 1].length > 0;
}
static validateCreditCard(creditCard) {
if (!creditCard) return false;
const cardNumber = typeof creditCard === "string" ? creditCard : creditCard?.pan || "";
if (!cardNumber) return false;
const cardRegex = /^\d{13,19}$/;
if (!cardRegex.test(cardNumber.replace(/\s/g, ""))) return false;
const digits = cardNumber.replace(/\s/g, "").split("").map(Number);
let sum = 0;
let isEven = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = digits[i];
if (isEven) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
static validateIP(ip) {
if (!ip) return false;
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}
static validatePhone(phone) {
if (!phone) return false;
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
return phoneRegex.test(phone.replace(/[^\d+]/g, ""));
}
static validateWallet(wallet) {
if (!wallet) return false;
const bitcoinRegex = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
const ethereumRegex = /^0x[a-fA-F0-9]{40}$/;
return bitcoinRegex.test(wallet) || ethereumRegex.test(wallet);
}
static validateIBAN(iban) {
if (!iban) return false;
const ibanRegex = /^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/;
return ibanRegex.test(iban.replace(/\s/g, "").toUpperCase());
}
static extractDomain(url) {
if (!url) return "";
try {
return new URL(url).hostname;
} catch {
return "";
}
}
static generateDataValidationAnalysis(inputData) {
return {
url: {
valid: this.validateURL(inputData?.url),
fraud: false,
freeSubdomain: false,
customTLD: false,
url: inputData?.url || "",
domain: this.extractDomain(inputData?.url),
plugins: {
blocklist: false,
compromiseDetector: false,
mxRecords: [],
nsfw: false,
reputation: "unknown",
riskScore: 0,
torNetwork: false,
typosquatting: 0,
urlShortener: false
}
},
email: this.generateEmailDataAnalysis(inputData?.email),
phone: this.generatePhoneDataAnalysis(inputData?.phone),
domain: {
valid: this.validateDomain(inputData?.domain),
fraud: false,
freeSubdomain: false,
customTLD: false,
domain: inputData?.domain || "",
plugins: {
blocklist: false,
compromiseDetector: false,
mxRecords: [],
nsfw: false,
reputation: "unknown",
riskScore: 0,
torNetwork: false,
typosquatting: 0,
urlShortener: false
}
},
creditCard: {
valid: this.validateCreditCard(inputData?.creditCard),
fraud: false,
test: false,
type: "unknown",
creditCard: typeof inputData?.creditCard === "string" ? inputData.creditCard : inputData?.creditCard?.pan || "",
plugins: {
blocklist: false,
riskScore: 0
}
},
ip: this.generateIPDataAnalysis(inputData?.ip),
wallet: {
valid: this.validateWallet(inputData?.wallet),
fraud: false,
wallet: inputData?.wallet || "",
type: "unknown",
plugins: {
blocklist: false,
riskScore: 0,
torNetwork: false
}
},
userAgent: {
valid: false,
fraud: false,
userAgent: inputData?.userAgent || "",
bot: true,
device: { type: "unknown", brand: "unknown" },
plugins: {
blocklist: false,
riskScore: 0
}
},
iban: {
valid: this.validateIBAN(inputData?.iban),
fraud: false,
iban: inputData?.iban || "",
plugins: {
blocklist: false,
riskScore: 0
}
}
};
}
static generateEmailValidatorResponse(inputData) {
return {
email: inputData?.email || "",
allow: this.validateEmail(inputData?.email),
reasons: this.validateEmail(inputData?.email) ? [] : ["INVALID"],
response: this.generateEmailDataAnalysis(inputData?.email)
};
}
static generateEmailDataAnalysis(email) {
return {
valid: this.validateEmail(email),
fraud: false,
proxiedEmail: false,
freeSubdomain: false,
corporate: false,
email: email || "",
realUser: "",
didYouMean: null,
noReply: false,
customTLD: false,
domain: "",
roleAccount: false,
plugins: {
mxRecords: [],
blocklist: false,
compromiseDetector: false,
nsfw: false,
reputation: "unknown",
riskScore: 0,
torNetwork: false,
typosquatting: 0,
urlShortener: false
}
};
}
static generateIPValidatorResponse(inputData) {
return {
ip: inputData?.ip || "",
allow: this.validateIP(inputData?.ip),
reasons: this.validateIP(inputData?.ip) ? [] : ["INVALID"],
response: this.generateIPDataAnalysis(inputData?.ip)
};
}
static generateIPDataAnalysis(ip) {
const isValid = this.validateIP(ip);
return {
valid: isValid,
type: isValid ? "IPv4" : "Invalid",
class: isValid ? "A" : "Unknown",
fraud: false,
ip: ip || "",
continent: "",
continentCode: "",
country: "",
countryCode: "",
region: "",
regionName: "",
city: "",
district: "",
zipCode: "",
lat: 0,
lon: 0,
timezone: "",
offset: 0,
currency: "",
isp: "",
org: "",
as: "",
asname: "",
mobile: false,
proxy: true,
hosting: false,
plugins: {
blocklist: false,
riskScore: 0
}
};
}
static generatePhoneValidatorResponse(inputData) {
return {
phone: inputData?.phone || "",
allow: this.validatePhone(inputData?.phone),
reasons: this.validatePhone(inputData?.phone) ? [] : ["INVALID"],
response: this.generatePhoneDataAnalysis(inputData?.phone)
};
}
static generatePhoneDataAnalysis(phone) {
const phoneNumber = phone?.phone || phone;
const isValid = this.validatePhone(phoneNumber);
return {
valid: isValid,
fraud: false,
phone: phone?.phone || "",
prefix: "",
number: "",
lineType: "Unknown",
carrierInfo: {
carrierName: "",
accuracy: 0,
carrierCountry: "",
carrierCountryCode: ""
},
country: "",
countryCode: "",
plugins: {
blocklist: false,
riskScore: 0
}
};
}
static generateHTTPRequest(inputData) {
return {
method: inputData?.method || "GET",
url: inputData?.url || "",
headers: inputData?.headers || {},
body: inputData?.body || null,
allow: false,
reasons: ["FRAUD"],
protected: true
};
}
static generateEmailStatus() {
return {
status: false,
error: "API unavailable - using fallback response"
};
}
static generateSRNSummary(inputData) {
const quantity = inputData?.quantity || 1;
const values = Array.from({ length: quantity }, () => ({
integer: 0,
float: 0
}));
return {
values,
executionTime: 0
};
}
static generateExtractWithTextly(inputData) {
return {
data: inputData?.data || "",
extracted: {},
error: "API unavailable - using fallback response"
};
}
static generatePrayerTimes(inputData) {
return {
error: "API unavailable - using fallback response"
};
}
static generateSatinizedInputAnalysis(inputData) {
return {
input: inputData?.input || "",
formats: {
ascii: false,
bitcoinAddress: false,
cLikeIdentifier: false,
coordinates: false,
crediCard: false,
date: false,
discordUsername: false,
doi: false,
domain: false,
e164Phone: false,
email: false,
emoji: false,
hanUnification: false,
hashtag: false,
hyphenWordBreak: false,
ipv6: false,
ip: false,
jiraTicket: false,
macAddress: false,
name: false,
number: false,
panFromGstin: false,
password: false,
port: false,
tel: false,
text: false,
semver: false,
ssn: false,
uuid: false,
url: false,
urlSlug: false,
username: false
},
includes: {
spaces: false,
hasSql: false,
hasNoSql: false,
letters: false,
uppercase: false,
lowercase: false,
symbols: false,
digits: false
}
};
}
static generatePasswordValidationResult(inputData) {
return {
valid: false,
password: inputData?.password || "",
details: [
{
validation: "length",
message: "API unavailable - using fallback response"
}
]
};
}
}
const getPrayerTimes = async ({
axiosClient,
resilienceManager,
data
}) => {
const { lat, lon } = data;
if (lat === void 0 || lon === void 0) throw customError(1e3, "You must provide a latitude and longitude.");
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("getPrayerTimes", data);
return await resilienceManager.executeWithResilience(
axiosClient,
{
method: "GET",
url: "/public/islam/prayertimes",
params: data
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
} else {
const response = await axiosClient.get("/public/islam/prayertimes", { params: data });
return response.data;
}
};
const isValidPwd = async ({
axiosClient,
resilienceManager,
data
}) => {
let { email, password, bannedWords, min, max } = data;
if (password === void 0) throw customError(1e3, "You must specify at least the password.");
const params = { password: encodeURIComponent(password) };
if (email) {
if (!/^[a-zA-Z0-9._\-+]+@?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(email)) throw customError(1500, "If you provide an email address it must be valid.");
params.email = encodeURIComponent(email);
}
if (bannedWords) {
if (typeof bannedWords === "string") bannedWords = bannedWords.slice(1, -1).trim().split(",").map((item) => item.trim());
if (!Array.isArray(bannedWords) || bannedWords.length > 10) throw customError(1500, "If you provide a list of banned words; the list may not exceed 10 words and must be of array type.");
if (!bannedWords.every((word) => typeof word === "string") || new Set(bannedWords).size !== bannedWords.length) throw customError(1500, "If you provide a list of banned words; all elements must be non-repeated strings.");
params.bannedWords = bannedWords;
}
if (min !== void 0 && (!Number.isInteger(min) || min < 8 || min > 32)) throw customError(1500, "If you provide a minimum it must be valid.");
if (max !== void 0 && (!Number.isInteger(max) || max < 32 || max > 100)) throw customError(1500, "If you provide a maximum it must be valid.");
if (min !== void 0) params.min = min;
if (max !== void 0) params.max = max;
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("isValidPwd", data);
return await resilienceManager.executeWithResilience(
axiosClient,
{
method: "GET",
url: "/public/validPwd",
params
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
} else {
const response = await axiosClient.get("/public/validPwd", { params });
return response.data;
}
};
const satinize = async ({
axiosClient,
resilienceManager,
input
}) => {
if (input === void 0) throw customError(1e3, "You must specify at least the input.");
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("satinize", input);
return await resilienceManager.executeWithResilience(
axiosClient,
{
method: "GET",
url: "/public/inputSatinizer",
params: { input: encodeURIComponent(input) }
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
} else {
const response = await axiosClient.get("/public/inputSatinizer", { params: { input: encodeURIComponent(input) } });
return response.data;
}
};
const extractWithTextly = async ({
axiosClient,
resilienceManager,
data
}) => {
if (!axiosClient.defaults.headers?.Authorization) throw customError(3e3, "Invalid private token.");
if (!data.data) throw customError(1500, "No data provided.");
if (!data.format) throw customError(1500, "No format provided.");
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("extractWithTextly", data);
return await resilienceManager.executeWithResilience(
axiosClient,
{
method: "POST",
url: "/private/textly/extract",
data
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
} else {
const response = await axiosClient.post("/private/textly/extract", data, { headers: { "Content-Type": "application/json" } });
return response.data;
}
};
const getRandom = async ({
axiosClient,
resilienceManager,
data
}) => {
if (!axiosClient.defaults.headers?.Authorization) throw customError(3e3, "Invalid private token.");
if (data.min === void 0 || data.max === void 0) throw customError(1500, "Both 'min' and 'max' parameters must be defined.");
if (data.min >= data.max) throw customError(1500, "'min' must be less than 'max'.");
if (data.min < -1e9 || data.min > 1e9) throw customError(1500, "'min' must be an integer in the interval [-1000000000, 1000000000].");
if (data.max < -1e9 || data.max > 1e9) throw customError(1500, "'max' must be an integer in the interval [-1000000000, 1000000000].");
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("getRandom", data);
return await resilienceManager.executeWithResilience(
axiosClient,
{
method: "POST",
url: "/private/srng",
data
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
} else {
const response = await axiosClient.post("/private/srng", data, { headers: { "Content-Type": "application/json" } });
return response.data;
}
};
const isValidDataRaw = async ({
axiosClient,
resilienceManager,
data
}) => {
if (!axiosClient.defaults.headers?.Authorization) throw customError(3e3, "Invalid private token.");
if (!Object.keys(data).some((key) => ["url", "email", "phone", "domain", "creditCard", "ip", "wallet", "userAgent", "iban"].includes(key) && data.hasOwnProperty(key))) throw customError(1500, "You must provide at least one parameter.");
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("isValidDataRaw", data);
return await resilienceManager.executeWithResilience(
axiosClient,
{
method: "POST",
url: "/private/secure/verify",
data
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
} else {
const response = await axiosClient.post("/private/secure/verify", data, { headers: { "Content-Type": "application/json" } });
return response.data;
}
};
const isValidEmail = async ({
axiosClient,
resilienceManager,
email,
rules
}) => {
if (!axiosClient.defaults.headers?.Authorization) throw customError(3e3, "Invalid private token.");
if (rules.deny.length === 0) throw customError(1500, "You must provide at least one deny rule.");
if (rules.mode === "DRY_RUN") {
console.warn("[Dymo API] DRY_RUN mode is enabled. No requests with real data will be processed until you switch to LIVE mode.");
return {
email: typeof email === "string" ? email : "",
allow: true,
reasons: [],
response: "CHANGE TO LIVE MODE"
};
}
const plugins = [
rules.deny.includes("NO_MX_RECORDS") ? "mxRecords" : void 0,
rules.deny.includes("NO_REACHABLE") ? "reachable" : void 0,
rules.deny.includes("HIGH_RISK_SCORE") ? "riskScore" : void 0,
rules.deny.includes("NO_GRAVATAR") ? "gravatar" : void 0
].filter(Boolean);
let responseEmail;
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("isValidEmail", email);
const response = await resilienceManager.executeWithResilience(
axiosClient,
{
method: "POST",
url: "/private/secure/verify",
data: { email, plugins }
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
responseEmail = response.email;
} else {
const response = await axiosClient.post("/private/secure/verify", { email, plugins }, { headers: { "Content-Type": "application/json" } });
responseEmail = response.data.email;
}
if (!responseEmail || !responseEmail.valid) {
return {
email: responseEmail?.email || (typeof email === "string" ? email : ""),
allow: false,
reasons: ["INVALID"],
response: responseEmail
};
}
const reasons = [];
if (rules.deny.includes("FRAUD") && responseEmail.fraud) reasons.push("FRAUD");
if (rules.deny.includes("PROXIED_EMAIL") && responseEmail.proxiedEmail) reasons.push("PROXIED_EMAIL");
if (rules.deny.includes("FREE_SUBDOMAIN") && responseEmail.freeSubdomain) reasons.push("FREE_SUBDOMAIN");
if (rules.deny.includes("PERSONAL_EMAIL") && !responseEmail.corporate) reasons.push("PERSONAL_EMAIL");
if (rules.deny.includes("CORPORATE_EMAIL") && responseEmail.corporate) reasons.push("CORPORATE_EMAIL");
if (rules.deny.includes("NO_MX_RECORDS") && responseEmail.plugins?.mxRecords?.length === 0) reasons.push("NO_MX_RECORDS");
if (rules.deny.includes("NO_REPLY_EMAIL") && responseEmail.noReply) reasons.push("NO_REPLY_EMAIL");
if (rules.deny.includes("ROLE_ACCOUNT") && responseEmail.roleAccount) reasons.push("ROLE_ACCOUNT");
if (rules.deny.includes("NO_REACHABLE") && responseEmail.plugins?.reachable === false) reasons.push("NO_REACHABLE");
if (rules.deny.includes("HIGH_RISK_SCORE") && (responseEmail.plugins?.riskScore ?? 0) >= 80) reasons.push("HIGH_RISK_SCORE");
if (rules.deny.includes("NO_GRAVATAR") && !responseEmail.plugins?.gravatar) reasons.push("NO_GRAVATAR");
return {
email: responseEmail.email,
allow: reasons.length === 0,
reasons,
response: responseEmail
};
};
const isValidIP = async ({
axiosClient,
resilienceManager,
ip,
rules
}) => {
if (!axiosClient.defaults.headers?.Authorization) throw customError(3e3, "Invalid private token.");
if (rules.deny.length === 0) throw customError(1500, "You must provide at least one deny rule.");
if (rules.mode === "DRY_RUN") {
console.warn("[Dymo API] DRY_RUN mode is enabled. No requests with real data will be processed until you switch to LIVE mode.");
return {
ip: typeof ip === "string" ? ip : "",
allow: true,
reasons: [],
response: "CHANGE TO LIVE MODE"
};
}
const plugins = [
rules.deny.includes("TOR_NETWORK") ? "torNetwork" : void 0,
rules.deny.includes("HIGH_RISK_SCORE") ? "riskScore" : void 0
].filter(Boolean);
let responseIP;
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("isValidIP", ip);
const response = await resilienceManager.executeWithResilience(
axiosClient,
{
method: "POST",
url: "/private/secure/verify",
data: { ip, plugins }
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
responseIP = response.ip;
} else {
const response = await axiosClient.post("/private/secure/verify", { ip, plugins }, { headers: { "Content-Type": "application/json" } });
responseIP = response.data.ip;
}
if (!responseIP || !responseIP.valid) {
return {
ip: responseIP?.ip || (typeof ip === "string" ? ip : ""),
allow: false,
reasons: ["INVALID"],
response: responseIP
};
}
const reasons = [];
if (rules.deny.includes("FRAUD") && responseIP.fraud) reasons.push("FRAUD");
if (rules.deny.includes("TOR_NETWORK") && responseIP.plugins?.torNetwork) reasons.push("TOR_NETWORK");
if (rules.deny.includes("HIGH_RISK_SCORE") && (responseIP.plugins?.riskScore ?? 0) >= 80) reasons.push("HIGH_RISK_SCORE");
for (const rule of rules.deny) {
if (rule.startsWith("COUNTRY:")) {
const block = rule.split(":")[1];
if (responseIP.countryCode === block) reasons.push(`COUNTRY:${block}`);
}
}
return {
ip: responseIP.ip,
allow: reasons.length === 0,
reasons,
response: responseIP
};
};
const isValidPhone = async ({
axiosClient,
resilienceManager,
phone,
rules
}) => {
if (!axiosClient.defaults.headers?.Authorization) throw customError(3e3, "Invalid private token.");
if (rules.deny.length === 0) throw customError(1500, "You must provide at least one deny rule.");
if (rules.mode === "DRY_RUN") {
console.warn("[Dymo API] DRY_RUN mode is enabled. No requests with real data will be processed until you switch to LIVE mode.");
return {
phone: typeof phone === "string" ? phone : "",
allow: true,
reasons: [],
response: "CHANGE TO LIVE MODE"
};
}
const plugins = [
rules.deny.includes("HIGH_RISK_SCORE") ? "riskScore" : void 0
].filter(Boolean);
let responsePhone;
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("isValidPhone", phone);
const response = await resilienceManager.executeWithResilience(
axiosClient,
{
method: "POST",
url: "/private/secure/verify",
data: { phone, plugins }
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
responsePhone = response.phone;
} else {
const response = await axiosClient.post("/private/secure/verify", { phone, plugins }, { headers: { "Content-Type": "application/json" } });
responsePhone = response.data.phone;
}
if (!responsePhone || !responsePhone.valid) {
return {
phone: responsePhone?.phone || (typeof phone === "string" ? phone : ""),
allow: false,
reasons: ["INVALID"],
response: responsePhone
};
}
const reasons = [];
if (rules.deny.includes("FRAUD") && responsePhone.fraud) reasons.push("FRAUD");
if (rules.deny.includes("HIGH_RISK_SCORE") && (responsePhone.plugins?.riskScore ?? 0) >= 80) reasons.push("HIGH_RISK_SCORE");
for (const rule of rules.deny) {
if (rule.startsWith("COUNTRY:")) {
const block = rule.split(":")[1];
if (responsePhone.countryCode === block) reasons.push(`COUNTRY:${block}`);
}
}
return {
phone: responsePhone.phone,
allow: reasons.length === 0,
reasons,
response: responsePhone
};
};
const getUserAgent = (req) => {
return req.headers?.["user-agent"] || req.headers?.["User-Agent"];
};
const getIp = (req) => {
return req.ip || req.headers?.["x-forwarded-for"] || req.connection?.remoteAddress || req.socket?.remoteAddress || req.req?.socket?.remoteAddress;
};
const handleRequest = (req) => {
return {
body: req.body,
userAgent: getUserAgent(req),
ip: getIp(req)
};
};
const protectReq = async ({
axiosClient,
resilienceManager,
req,
rules
}) => {
if (!axiosClient.defaults.headers?.Authorization) throw customError(3e3, "Invalid private token.");
const reqData = handleRequest(req);
if (!reqData.userAgent || !reqData.ip) throw customError(1500, "You must provide user agent and ip.");
if (rules.mode === "DRY_RUN") {
console.warn("[Dymo API] DRY_RUN mode is enabled. No requests with real data will be processed until you switch to LIVE mode.");
return {
ip: reqData.ip,
userAgent: reqData.userAgent,
allow: true,
reasons: []
};
}
const requestData = {
ip: reqData.ip,
userAgent: reqData.userAgent,
allowBots: rules.allowBots,
deny: rules.deny
};
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("protectReq", req);
return await resilienceManager.executeWithResilience(
axiosClient,
{
method: "POST",
url: "/private/waf/verifyRequest",
data: requestData
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
} else {
const response = await axiosClient.post("/private/waf/verifyRequest", requestData, { headers: { "Content-Type": "application/json" } });
return response.data;
}
};
const fs = {};
const convertTailwindToInlineCss = (htmlContent) => {
return htmlContent.replace(/class="([^"]+)"( style="([^"]+)")?/g, (match, classList, _, existingStyle) => {
const compiledStyles = twToCss.twi(classList, { minify: true, merge: true });
return match.replace(/class="[^"]+"/, "").replace(/ style="[^"]+"/, "").concat(` style="${existingStyle ? `${existingStyle.trim().slice(0, -1)}; ${compiledStyles}` : compiledStyles}"`);
});
};
const sendEmail = async ({
axiosClient,
resilienceManager,
data
}) => {
if (!axiosClient.defaults.headers?.Authorization) throw customError(3e3, "Invalid private token.");
if (!data.from) throw customError(1500, "You must provide an email address from which the following will be sent.");
if (!data.to) throw customError(1500, "You must provide an email to be sent to.");
if (!data.subject) throw customError(1500, "You must provide a subject for the email to be sent.");
if (!data.html && !data.react && !React.isValidElement(data.react)) throw customError(1500, "You must provide HTML or a React component.");
if (data.html && data.react) throw customError(1500, "You must provide only HTML or a React component, not both.");
try {
if (data.react) {
data.html = await render.render(data.react);
delete data.react;
}
if (data.options && data.options.composeTailwindClasses) {
data.html = convertTailwindToInlineCss(data.html);
delete data.options.composeTailwindClasses;
}
} catch (error) {
throw customError(1500, `An error occurred while rendering your React component. Details: ${error}`);
}
let totalSize = 0;
if (data.attachments && Array.isArray(data.attachments)) {
const processedAttachments = await Promise.all(
data.attachments.map(async (attachment) => {
if (attachment.path && attachment.content || !attachment.path && !attachment.content) throw customError(1500, "You must provide either 'path' or 'content', not both.");
let contentBuffer;
if (attachment.path) contentBuffer = await fs.readFile(path.resolve(attachment.path));
else if (attachment.content) contentBuffer = attachment.content instanceof Buffer ? attachment.content : Buffer.from(attachment.content);
totalSize += Buffer.byteLength(contentBuffer);
if (totalSize > 40 * 1024 * 1024) throw customError(1500, "Attachments exceed the maximum allowed size of 40 MB.");
return {
filename: attachment.filename || path.basename(attachment.path || ""),
content: contentBuffer,
cid: attachment.cid || attachment.filename
};
})
);
data.attachments = processedAttachments;
}
if (resilienceManager) {
const fallbackData = FallbackDataGenerator.generateFallbackData("sendEmail", data);
return await resilienceManager.executeWithResilience(
axiosClient,
{
method: "POST",
url: "/private/sender/sendEmail",
data
},
resilienceManager.getConfig().fallbackEnabled ? fallbackData : void 0
);
} else {
const response = await axiosClient.post("/private/sender/sendEmail", data);
return response.data;
}
};
class RateLimitManager {
constructor() {
this.tracker = {};
}
static getInstance() {
if (!RateLimitManager.instance) RateLimitManager.instance = new RateLimitManager();
return RateLimitManager.instance;
}
/**
* Parses a header value that could be a number or "unlimited".
* Returns undefined if the value is "unlimited", null, undefined, or invalid.
*/
parseHeaderValue(value) {
if (value === null || value === void 0) return void 0;
if (typeof value !== "string" && typeof value !== "number") return void 0;
const strValue = String(value).trim().toLowerCase();
if (!strValue || strValue === "unlimited") return void 0;
const parsed = parseInt(strValue, 10);
return isNaN(parsed) || parsed < 0 ? void 0 : parsed;
}
updateRateLimit(clientId, headers) {
if (!this.tracker[clientId]) this.tracker[clientId] = {};
const limitInfo = this.tracker[clientId];
const limitRequests = headers["x-ratelimit-limit-requests"];
const remainingRequests = headers["x-ratelimit-remaining-requests"];
const resetRequests = headers["x-ratelimit-reset-requests"];
const retryAfter = headers["retry-after"];
const parsedLimit = this.parseHeaderValue(limitRequests);
const parsedRemaining = this.parseHeaderValue(remainingRequests);
const parsedRetryAfter = this.parseHeaderValue(retryAfter);
if (parsedLimit !== void 0) limitInfo.limit = parsedLimit;
if (parsedRemaining !== void 0) limitInfo.remaining = parsedRemaining;
if (typeof remainingRequests === "string" && remainingRequests.trim().toLowerCase() === "unlimited") limitInfo.isUnlimited = true;
if (resetRequests && typeof resetRequests === "string") limitInfo.resetTime = resetRequests;
if (parsedRetryAfter !== void 0) limitInfo.retryAfter = parsedRetryAfter;
limitInfo.lastUpdated = Date.now();
}
isRateLimited(clientId) {
const limitInfo = this.tracker[clientId];
if (!limitInfo) return false;
if (limitInfo.isUnlimited) return false;
return limitInfo.remaining !== void 0 && limitInfo.remaining <= 0;
}
getRetryAfter(clientId) {
return this.tracker[clientId]?.retryAfter;
}
clearExpiredLimits() {
const now = Date.now();
Object.keys(this.tracker).forEach((clientId) => {
const limitInfo = this.tracker[clientId];
if (limitInfo.lastUpdated && now - limitInfo.lastUpdated > 3e5) delete this.tracker[clientId];
});
}
}
class ResilienceManager {
constructor(config2 = {}, clientId = "default") {
this.config = {
fallbackEnabled: config2.fallbackEnabled ?? false,
retryAttempts: Math.max(0, config2.retryAttempts ?? 2),
// Number of additional retries
retryDelay: Math.max(0, config2.retryDelay ?? 1e3)
};
this.clientId = clientId;
this.rateLimitManager = RateLimitManager.getInstance();
}
async executeWithResilience(axiosClient, requestConfig, fallbackData) {
let lastError;
const totalAttempts = 1 + this.config.retryAttempts;
this.rateLimitManager.clearExpiredLimits();
if (this.rateLimitManager.isRateLimited(this.clientId)) {
const retryAfter = this.rateLimitManager.getRetryAfter(this.clientId);
if (retryAfter) {
console.warn(`[Dymo API] Client ${this.clientId} is rate limited. Waiting ${retryAfter} seconds...`);
await this.sleep(retryAfter * 1e3);
}
}
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
try {
const response = await axiosClient.request(requestConfig);
this.rateLimitManager.updateRateLimit(this.clientId, response.headers);
if (response.status === 429) {
const retryAfter = this.rateLimitManager.getRetryAfter(this.clientId);
if (retryAfter) {
console.warn(`[Dymo API] Rate limited. Waiting ${retryAfter} seconds (no retries)`);
await this.sleep(retryAfter * 1e3);
}
throw new Error(`Rate limited (429) - not retrying`);
}
return response.data;
} catch (error) {
lastError = error;
let shouldRetry = this.shouldRetry(error);
const isLastAttempt = attempt === totalAttempts;
if (error.response?.status === 429) shouldRetry = false;
if (!shouldRetry || isLastAttempt) {
if (this.config.fallbackEnabled && fallbackData) {
console.warn(`[Dymo API] Request failed after ${attempt} attempts. Using fallback data.`);
return fallbackData;
}
throw error;
}
const delay = this.config.retryDelay * Math.pow(2, attempt - 1);
console.warn(`[Dymo API] Attempt ${attempt} failed. Retrying in ${delay}ms...`);
await this.sleep(delay);
}
}
throw lastError;
}
shouldRetry(error) {
const statusCode = error.response?.status;
const isNetworkError = !error.response && error.code !== "ECONNABORTED";
const isServerError = statusCode && statusCode >= 500;
return isNetworkError || isServerError;
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
getConfig() {
return { ...this.config };
}
getClientId() {
return this.clientId;
}
}
class DymoAPI {
/**
* @param {Object} options - Options to create the DymoAPI instance.
* @param {string} [options.rootApiKey] - The root API key.
* @param {string} [options.apiKey] - The API key.
* @param {string} [options.baseUrl] - Whether to use a local server instead of the cloud server.
* @param {Object} [options.serverEmailConfig] - The server email config.
* @param {Object} [options.rules] - The rules.
* @param {Object} [options.resilience] - The resilience config.
* @description
* This is the main class to interact with the Dymo API. It should be
* instantiated with the root API key and the API key. The root API key is
* used to fetch the tokens and the API key is used to authenticate the
* requests. Requests are retried once by default with exponential backoff.
* @example
* const dymoApi = new DymoAPI({
* rootApiKey: "6bfb7675-6b69-4f8d-9f43-5a6f7f02c6c5",
* apiKey: "dm_4c8b7675-6b69-4f8d-9f43-5a6f7f02c6c5"
* });
*/
constructor({
rootApiKey = null,
apiKey = null,
baseUrl = "https://api.tpeoficial.com",
serverEmailConfig = void 0,
rules = {},
resilience = {}
} = {}) {
this.rules = {
email: { mode: "LIVE", deny: ["FRAUD", "INVALID", "NO_MX_RECORDS", "NO_REPLY_EMAIL"] },
ip: { mode: "LIVE", deny: ["FRAUD", "INVALID", "TOR_NETWORK"] },
phone: { mode: "LIVE", deny: ["FRAUD", "INVALID"] },
sensitiveInfo: { mode: "LIVE", deny: ["EMAIL", "PHONE", "CREDIT_CARD"] },
waf: { mode: "LIVE", allowBots: ["CURL", "CATEGORY:SEARCH_ENGINE", "CATEGORY:PREVIEW"], deny: ["FRAUD", "TOR_NETWORK"] },
...rules
};
this.rootApiKey = rootApiKey;
this.apiKey = apiKey;
this.serverEmailConfig = serverEmailConfig;
this.baseUrl = baseUrl;
const clientId = this.apiKey || this.rootApiKey || "anonymous";
this.resilienceManager = new ResilienceManager(resilience, clientId);
this.axiosClient = axios.create({
baseURL: `${validBaseURL(this.baseUrl)}/v1`,
headers: {
"User-Agent": "DymoAPISDK/1.0.0",
"X-Dymo-SDK-Env": "Node",
"X-Dymo-SDK-Version": "1.2.38"
}
});
if (this.rootApiKey || this.apiKey) this.axiosClient.defaults.headers.Authorization = `Bearer ${this.rootApiKey || this.apiKey}`;
}
// FUNCTIONS / Private.
/**
* Validates the given data against the configured validation settings.
*
* This method requires either the root API key or the API key to be set.
* If neither is set, it will throw an error.
*
* @deprecated Use `isValidDataRaw` instead. This feature will be modified soon.
* @param {Object} data - The data to be validated.
* @param {string} [data.url] - Optional URL to be validated.
* @param {string} [data.email] - Optional email address to be validated.
* @param {Interfaces.PhoneData} [data.phone] - Optional phone number data to be validated.
* @param {string} [data.domain] - Optional domain name to be validated.
* @param {string|Interfaces.CreditCardData} [data.creditCard] - Optional credit card number or data to be validated.
* @param {string} [data.ip] - Optional IP address to be validated.
* @param {string} [data.wallet] - Optional wallet address to be validated.
* @param {string} [data.userAgent] - Optional user agent string to be validated.
* @param {string} [data.iban] - Optional IBAN to be validated.
* @param {Interfaces.VerifyPlugins[]} [data.plugins] - Optional array of verification plugins to be used.
* @returns {Promise<Interfaces.DataValidationAnalysis>} A promise that resolves to the response from the server.
* @throws Will throw an error if there is an issue with the validation process.
*
* [Documentation](https://docs.tpeoficial.com/docs/dymo-api/private/data-verifier)
*/
async isValidData(data) {
return await isValidDataRaw({ axiosClient: this.axiosClient, resilienceManager: this.resilienceManager, data });
}
/**
* Validates the given data against the configured validation settings.
*
* This method requires either the root API key or the API key to be set.
* If neither is set, it will throw an error.
*
* @param {Object} data - The data to be validated.
* @param {string} [data.url] - Optional URL to be validated.
* @param {string} [data.email] - Optional email address to be validated.
* @param {Interfaces.PhoneData} [data.phone] - Optional phone number data to be validated.
* @param {string} [data.domain] - Optional domain name to be validated.
* @param {string|Interfaces.CreditCardData} [data.creditCard] - Optional credit card number or data to be validated.
* @param {string} [data.ip] - Optional IP address to be validated.
* @param {string} [data.wallet] - Optional wallet address to be validated.
* @param {string} [data.userAgent] - Optional user agent string to be validated.
* @param {string} [data.iban] - Optional IBAN to be validated.
* @param {Interfaces.VerifyPlugins[]} [data.plugins] - Optional array of verification plugins to be used.
* @returns {Promise<Interfaces.DataValidationAnalysis>} A promise that resolves to the response from the server.
* @throws Will throw an error if there is an issue with the validation process.
*
* [Documentation](https://docs.tpeoficial.com/docs/dymo-api/private/data-verifier)
*/
async isValidDataRaw(data) {
return await isValidDataRaw({ axiosClient: this.axiosClient, resilienceManager: this.resilienceManager, data });
}
/**
* Validates the given email against the configured rules.
*
* This method requires either the root API key or the API key to be set.
* If neither is set, it will throw an error.
*
* @param {string} [email] - Email address to validate.
* @param {NegativeEmailRules[]} [rules] - Optional rules for validation. Some rules are premium features.
* @important
* **⚠️ NO_MX_RECORDS, HIGH_RISK_SCORE and NO_REACHABLE are [PREMIUM](https://docs.tpeoficial.com/docs/dymo-api/private/data-verifier) features.**
* @returns {Promise<Interfaces.EmailValidatorResponse>} Resolves with the validation response.
* @throws Will throw an error if validation cannot be performed.
*
* @example
* const valid = await dymoClient.isValidEmail("user@example.com", { deny: ["FRAUD", "NO_MX_RECORDS"] });
*
* @see [Documentation](https://docs.tpeoficial.com/docs/dymo-api/private/email-validation)
*/
async isValidEmail(email, rules = this.rules.email) {
return await isValidEmail({ axiosClient: this.axiosClient, resilienceManager: this.resilienceManager, email, rules });
}
/**
* Validates the given IP against the configured rules.
*
* This method requires either the root API key or the API key to be set.
* If neither is set, it will throw an error.
*
* @param {string} [ip] - IP address to validate.
* @param {NegativeIPRules[]} [rules] - Optional rules for validation. Some rules are premium features.
* @important
* **⚠️ TOR_NETWORK and HIGH_RISK_SCORE are [PREMIUM](https://docs.tpeoficial.com/docs/dymo-api/private/data-verifier) features.**
* @returns {Promise<Interfaces.IPValidatorResponse>} Resolves with the validation response.
* @throws Will throw an error if validation cannot be performed.
*
* @example
* const valid = await isValidIP("52.94.236.248", { deny: ["FRAUD", "TOR_NETWORK", "COUNTRY:RU"] });
*
* @see [Documentation](https://docs.tpeoficial.com/docs/dymo-api/private/ip-validation)
*/
async isValidIP(ip, rules = this.rules.ip) {
return await isValidIP({ axiosClient: this.axiosClient, resilienceManager: this.resilienceManager, ip, rules });
}
/**
* Validates the given phone against the configured rules.
*
* This method requires either the root API key or the API key to be set.
* If neither is set, it will throw an error.
*
* @param {string} [phone] - Phone number to validate.
* @param {NegativePhoneRules[]} [rules] - Optional rules for validation. Some rules are premium features.
* @important
* **⚠️ HIGH_RISK_SCORE is a [PREMIUM](https://docs.tpeoficial.com/docs/dymo-api/private/data-verifier) feature.**
* @returns {Promise<Interfaces.PhoneValidatorResponse>} Resolves with the validation response.
* @throws Will throw an error if validation cannot be performed.
*
* @example
* const valid = await dymoClient.isValidPhone("+34617509462", { deny: ["FRAUD", "INVALID"] });
*
* @see [Documentation](https://docs.tpeoficial.com/docs/dymo-api/private/phone-validation)
*/
async isValidPhone(phone, rules = this.rules.phone) {
return await isValidPhone({ axiosClient: this.axiosClient, resilienceManager: this.resilienceManager, phone, rules });
}
/**
* Protects the given request against the configured rules.
*
* This method requires either the root API key or the API key to be set.
* If neither is set, it will throw an error.
*
* @param {Object} req - The request object to be protected.
* @param {Interfaces.WafRules} [rules] - Optional rules for protection. Some rules are premium features.
* @returns {Promise<Interfaces.HTTPRequest>} Resolves with the protected request.
* @important
* **⚠️ This is a [PREMIUM](https://docs.tpeoficial.com/docs/dymo-api/private/data-verifier) and BETA feature.**
* @throws Will throw an error if protection cannot be performed.
*
* @example
* const protectedReq = await dymoClient.protectReq(req);
*
* @see [Documentation](https://docs.tpeoficial.com/docs/dymo-api/private/data-verifier)
*/
async protectReq(req, rules = this.rules.waf) {
return await protectReq({ axiosClient: this.axiosClient, resilienceManager: this.resilienceManager, req, rules });
}
/**
* Sends an email using the configured email client settings.
*
* This method requires either the root API key or the server email config to be set.
* If neither is set, it will throw an error.
*
* @param {Object} data - The email data to be sent.
* @param {string} data.from - The email address from which the email will be sent.
* @param {string} data.to - The email address to which the email will be sent.
* @param {string} data.subject - The subject of the email.
* @param {string} [data.html] - The HTML content of the email.
* @param {React.ReactElement} [data.react] - The React component to be rendered as the email content.
* @param {Object} [data.options] - Content configuration options.
* @param {"high" | "normal" | "low" | undefined} [data.options.priority="normal"] - Email priority (default: normal).
* @param {boolean} [data.options.waitToResponse=true] - Wait until the email is sent (default: true).
* @param {boolean} [data.options.composeTailwindClasses] - Whether to compose tailwind classes.
* @param {Attachment[]} [data.attachments] - An array of attachments to be included in the email.
* @param {string} data.attachments[].filename - The name of the attached file.
* @param {string} [data.attachments[].path] - The path or URL of the attached file. Either this or `content` must be provided.
* @param {Buffer} [data.attachments[].content] - The content of the attached file as a Buffer. Either this or `path` must be provided.
* @param {string} [data.attachments[].cid] - The CID (Content-ID) of the attached file, used for inline images.
* @returns {Promise<Interfaces.EmailStatus>} A promise that resolves to the response from the server.
* @throws Will throw an error if there is an issue with the email sending process.
*
* [Documentation](https://docs.tpeoficial.com/docs/dymo-api/private/sender-send-email/getting-started)
*/
async sendEmail(data) {
if (!this.serverEmailConfig && !this.rootApiKey) console.error(`[${config.lib.name}] You must configure the email client settings.`);
return await sendEmail({ axiosClient: this.axiosClient, resilienceManager: this.resilienceManager, data: { serverEmailConfig: this.serverEmailConfig, ...data } });
}
/**
* Generates a random number between the provided min and max values.
*
* This method requires either the root API key or the API key to be set.
* If neither is set, it will throw an error.
*
* @param {Interfaces.SRNG} data - The data to be sent.
* @param {number} data.min - The minimum value of the range.
* @param {number} data.max - The maximum value of the range.
* @param {number} [data.quantity] - The number of random values to generate. Defaults to 1 if not provided.
* @returns {Promise<Interfaces.SRNSummary>} A promise that resolves to the response from the server.
* @throws Will throw an error if there is an issue with the random number generation process.
*
* [Documentation](https://docs.tpeoficial.com/docs/dymo-api/private/secure-random-number-generator)
*/
async getRandom(data) {
return await getRandom({ axiosClient: this.axiosClient, resilienceManager: this.resilienceManager, data });
}
/**
* Extracts structured data from a given string using Textly.
*
* This method requires either the root API key or the API key to be set.
* If neither is set, it will throw an error.
*
* @param {Interfaces.ExtractWithTextly} data - The data to be sent, containing the string to be processed and the format schema.
* @returns {Promise<any>} A promise that resolves to the extracted structured data.
* @throws Will throw an error if there is an issue with the extraction process.
*
* [Documentation](https://docs.tpeoficial.com/docs/dymo-api/private/textly/text-extraction)
*/
async extractWithTextly(data) {
return await extractWithTextly({ axiosClient: this.axiosClient, resilienceManager: this.resilienceManager, data });
}
// FUNCTIONS / Public.
/**
* Retrieves the prayer times for the given location.
*
* This method requires a latitude and longitude to be provided in the
* data object. If either of these are not provided, it will