UNPKG

2factor-sdk

Version:

A TypeScript SDK for sending and validating OTPs with support for rate limiting.

381 lines (370 loc) 13 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { TFError: () => TFError, TWOFactor: () => TWOFactor, handleTFError: () => handleTFError }); module.exports = __toCommonJS(index_exports); // src/config/base.ts var Base = class { constructor() { this.baseUrl = "https://2factor.in/API/V1/"; } }; // src/utils/schema.ts var import_zod = require("zod"); var otpSchema = import_zod.z.string().refine( (val) => { const isNumeric = /^\d+$/.test(val); const validLength = val.length === 4 || val.length === 6; return isNumeric && validLength; }, { message: "OTP must be either 4 or 6 digits and contain only numbers" } ); var constructorSchema = import_zod.z.object({ apiKey: import_zod.z.string(), upstashUrl: import_zod.z.string().url({ message: "Invalid Redis URL" }).optional(), upstashToken: import_zod.z.string().min(58, { message: "Invalid token length" }).max(58, { message: "Invalid token length" }).optional() }).refine((data) => { return data.upstashUrl && data.upstashToken || !data.upstashUrl && !data.upstashToken; }); var phoneSchema = import_zod.z.string().regex(/^[6-9]{1}[0-9]{9}$/, { message: "Invalid phone number" }); var sendOTPSchema = import_zod.z.object({ phoneNumber: phoneSchema, templateName: import_zod.z.string().optional(), otpLength: import_zod.z.number().refine((value) => value === 4 || value === 6, { message: "Invalid OTP length. Must be either 4 or 6." }).optional(), interval: import_zod.z.string().regex(/^\d+\s[smhd]$/, { message: "Invalid format. Must be in the format '<number> <unit>' where unit is one of s, m, h, or d." }).optional(), limit: import_zod.z.number().optional() }).refine((data) => { return data.interval && data.limit || !data.interval && !data.limit; }); var sendAndReturnOTPSchema = import_zod.z.object({ phoneNumber: phoneSchema, templateName: import_zod.z.string().optional(), interval: import_zod.z.string().regex(/^\d+\s[smhd]$/, { message: "Invalid format. Must be in the format '<number> <unit>' where unit is one of s, m, h, or d." }).optional(), limit: import_zod.z.number().optional() }).refine((data) => { return data.interval && data.limit || !data.interval && !data.limit; }); var sendCustomOTPSchema = import_zod.z.object({ phoneNumber: phoneSchema, templateName: import_zod.z.string().optional(), otp: otpSchema, interval: import_zod.z.string().regex(/^\d+\s[smhd]$/, { message: "Invalid format. Must be in the format '<number> <unit>' where unit is one of s, m, h, or d." }).optional(), limit: import_zod.z.number().optional() }).refine((data) => { return data.interval && data.limit || !data.interval && !data.limit; }); var verifyByUIDSchema = import_zod.z.object({ otp: otpSchema, UID: import_zod.z.string().min(36, { message: "Invalid UID length" }).max(36, { message: "Invalid UID length" }) }); var verifyByPhoneSchema = import_zod.z.object({ otp: otpSchema, phoneNumber: phoneSchema }); var rateLimitSchema = import_zod.z.object({ url: import_zod.z.string().url({ message: "Invalid Redis URL" }), token: import_zod.z.string().min(58, { message: "Invalid token length" }).max(58, { message: "Invalid token length" }), interval: import_zod.z.string().regex(/^\d+\s[smhd]$/, { message: "Invalid format. Must be in the format '<number> <unit>' where unit is one of s, m, h, or d." }), limit: import_zod.z.number(), phoneNumber: phoneSchema }); // src/config/axios.ts var import_axios = __toESM(require("axios"), 1); import_axios.default.defaults.validateStatus = function(status) { return status >= 100 && status < 500; }; var api = import_axios.default.create({ baseURL: "https://2factor.in/API/V1/", headers: { "Content-Type": "application/json" } }); // src/config/urls.ts var gen4DigitOTPUrl = (data) => { const { phoneNumber, templateName, apiKey } = data; return `${apiKey}/SMS/${phoneNumber}/AUTOGEN3/${templateName}`; }; var gen6DigitOTPUrl = (data) => { const { phoneNumber, templateName, apiKey } = data; return `${apiKey}/SMS/${phoneNumber}/AUTOGEN/${templateName}`; }; var genAndReturnOTPUrl = (data) => { const { phoneNumber, templateName, apiKey } = data; return `${apiKey}/SMS/${phoneNumber}/AUTOGEN2/${templateName}`; }; var genCustomOTPUrl = (data) => { const { phoneNumber, templateName, apiKey, otp } = data; return `${apiKey}/SMS/${phoneNumber}/${otp}/${templateName}`; }; var genVerifybyUIDUrl = (data) => { const { apiKey, otp, UID } = data; return `${apiKey}/SMS/VERIFY/${UID}/${otp}`; }; var genVerifybyPhoneUrl = (data) => { const { apiKey, otp, phone } = data; return `${apiKey}/SMS/VERIFY3/${phone}/${otp}`; }; // src/lib/error.ts var import_zod2 = require("zod"); // src/config/errors.ts var ERROR_CODES = { INVALID_PHONE_NUMBER: "The phone number provided is not valid. Please check the number format and try again.", INVALID_API_KEY: "The API key provided is invalid. Please verify your API key and try again.", INVALID_OTP_LENGTH: "The OTP length is incorrect. Please ensure the OTP is of the correct length and try again.", OTP_MISMATCHED: "The OTP you entered does not match. Please check the OTP and try again.", OTP_EXPIRED: "The OTP has expired. Please request a new OTP and try again.", OTP_LIMIT: "Too many requests sent. Please try again later." }; // src/lib/error.ts var TFError = class extends Error { constructor(message) { super(message); this.name = "TFError"; } }; var handleTFError = (error) => { if (error instanceof TFError) { switch (error.message) { case "INVALID_PHONE_NUMBER": return { error: ERROR_CODES.INVALID_PHONE_NUMBER, name: error.name }; case "INVALID_OTP_LENGTH": return { error: ERROR_CODES.INVALID_OTP_LENGTH, name: error.name }; case "OTP_MISMATCHED": return { error: ERROR_CODES.OTP_MISMATCHED, name: error.name }; case "OTP_EXPIRED": return { error: ERROR_CODES.OTP_EXPIRED, name: error.name }; case "OTP limit reached": return { error: ERROR_CODES.OTP_LIMIT, name: error.name }; } } if (error instanceof import_zod2.ZodError) { const allErrors = error.errors.map((err) => `${err.path.join(".")}: ${err.message}`).join(", "); return { error: allErrors, name: error.name }; } }; // src/lib/redis.ts var import_ratelimit = require("@upstash/ratelimit"); var import_redis = require("@upstash/redis"); var rateLimit = async (data) => { try { const { interval, limit, phoneNumber, url, token } = data; const redis = new import_redis.Redis({ url, token }); const result = rateLimitSchema.safeParse(data); if (!result.success) { throw new Error(result.error.message); } const limiter = new import_ratelimit.Ratelimit({ redis, limiter: import_ratelimit.Ratelimit.slidingWindow(limit, interval) }); const { success } = await limiter.limit(phoneNumber); if (!success) { throw new TFError("OTP limit reached"); } return; } catch (err) { throw err; } }; // src/core/TF.ts var TWOFactor = class _TWOFactor extends Base { constructor(data) { const { apiKey, upstashToken, upstashUrl } = data; super(); this.apiKey = apiKey; this.upstashToken = upstashToken; this.upstashUrl = upstashUrl; } static init(data) { const result = constructorSchema.safeParse(data); if (!result.success) { throw new Error(result.error.message); } return new _TWOFactor(data); } async sendOTP(data) { try { const { phoneNumber, templateName, otpLength = 4, interval, limit } = data; sendOTPSchema.parse(data); if (this.upstashUrl && this.upstashToken) { await rateLimit({ interval: interval || "30 s", limit: limit || 1, phoneNumber, token: this.upstashToken, url: this.upstashUrl }); } const urlOptions = { phoneNumber, templateName, apiKey: this.apiKey }; const url = otpLength && otpLength === 4 ? gen4DigitOTPUrl(urlOptions) : gen6DigitOTPUrl(urlOptions); const response = await api.get(url); if (response && response.data) { const data2 = response.data; if (data2.Status === "Success") { return { UID: data2.Details }; } else if (data2.Status === "Error") { throw new TFError(data2.Details); } } } catch (error) { throw error; } } async sendAndReturnOTP(data) { try { const { phoneNumber, templateName, interval, limit } = data; sendAndReturnOTPSchema.parse(data); if (this.upstashUrl && this.upstashToken) { await rateLimit({ interval: interval || "30 s", limit: limit || 1, phoneNumber, token: this.upstashToken, url: this.upstashUrl }); } const urlOptions = { phoneNumber, templateName, apiKey: this.apiKey }; const url = genAndReturnOTPUrl(urlOptions); const response = await api.get(url); if (response && response.data) { const data2 = response.data; if (data2.Status === "Success") { return { OTP: data2.OTP, UID: data2.Details }; } else if (data2.Status === "Error") { throw new TFError(data2.Details); } } } catch (error) { throw error; } } async sendCustomOTP(data) { try { const { phoneNumber, templateName, otp, interval, limit } = data; sendCustomOTPSchema.parse(data); if (this.upstashUrl && this.upstashToken) { await rateLimit({ interval: interval || "30 s", limit: limit || 1, phoneNumber, token: this.upstashToken, url: this.upstashUrl }); } const urlOptions = { phoneNumber, templateName, apiKey: this.apiKey, otp }; const url = genCustomOTPUrl(urlOptions); const response = await api.get(url); if (response && response.data) { const data2 = response.data; if (data2.Status === "Success") { return { UID: data2.Details }; } else if (data2.Status === "Error") { throw new TFError(data2.Details); } } } catch (error) { throw error; } } async verifyByUID(data) { try { const { otp, UID } = data; verifyByUIDSchema.parse(data); const urlOptions = { apiKey: this.apiKey, otp, UID }; const url = genVerifybyUIDUrl(urlOptions); const response = await api.get(url); if (response && response.data) { const data2 = response.data; if (data2.Status === "Success") { return true; } else if (data2.Status === "Error") { return; } } } catch (err) { throw err; } } async verifyByPhone(data) { try { const { otp, phoneNumber } = data; verifyByPhoneSchema.parse(data); const urlOptions = { apiKey: this.apiKey, otp, phone: phoneNumber }; const url = genVerifybyPhoneUrl(urlOptions); const response = await api.get(url); if (response && response.data) { const data2 = response.data; if (data2.Status === "Success") { return true; } else if (data2.Status === "Error") { return; } } } catch (err) { throw err; } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { TFError, TWOFactor, handleTFError });