2factor-sdk
Version: 
A TypeScript SDK for sending and validating OTPs with support for rate limiting.
381 lines (370 loc) • 13 kB
JavaScript
;
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
});