UNPKG

dymo-api

Version:
1,260 lines 53.6 kB
"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