UNPKG

2factor-sdk

Version:

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

342 lines (333 loc) 10.9 kB
// src/config/base.ts var Base = class { constructor() { this.baseUrl = "https://2factor.in/API/V1/"; } }; // src/utils/schema.ts import { z } from "zod"; var otpSchema = 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 = z.object({ apiKey: z.string(), upstashUrl: z.string().url({ message: "Invalid Redis URL" }).optional(), upstashToken: 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 = z.string().regex(/^[6-9]{1}[0-9]{9}$/, { message: "Invalid phone number" }); var sendOTPSchema = z.object({ phoneNumber: phoneSchema, templateName: z.string().optional(), otpLength: z.number().refine((value) => value === 4 || value === 6, { message: "Invalid OTP length. Must be either 4 or 6." }).optional(), interval: 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: z.number().optional() }).refine((data) => { return data.interval && data.limit || !data.interval && !data.limit; }); var sendAndReturnOTPSchema = z.object({ phoneNumber: phoneSchema, templateName: z.string().optional(), interval: 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: z.number().optional() }).refine((data) => { return data.interval && data.limit || !data.interval && !data.limit; }); var sendCustomOTPSchema = z.object({ phoneNumber: phoneSchema, templateName: z.string().optional(), otp: otpSchema, interval: 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: z.number().optional() }).refine((data) => { return data.interval && data.limit || !data.interval && !data.limit; }); var verifyByUIDSchema = z.object({ otp: otpSchema, UID: z.string().min(36, { message: "Invalid UID length" }).max(36, { message: "Invalid UID length" }) }); var verifyByPhoneSchema = z.object({ otp: otpSchema, phoneNumber: phoneSchema }); var rateLimitSchema = z.object({ url: z.string().url({ message: "Invalid Redis URL" }), token: z.string().min(58, { message: "Invalid token length" }).max(58, { message: "Invalid token length" }), interval: 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: z.number(), phoneNumber: phoneSchema }); // src/config/axios.ts import axios from "axios"; axios.defaults.validateStatus = function(status) { return status >= 100 && status < 500; }; var api = axios.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 import { ZodError } from "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 ZodError) { const allErrors = error.errors.map((err) => `${err.path.join(".")}: ${err.message}`).join(", "); return { error: allErrors, name: error.name }; } }; // src/lib/redis.ts import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; var rateLimit = async (data) => { try { const { interval, limit, phoneNumber, url, token } = data; const redis = new Redis({ url, token }); const result = rateLimitSchema.safeParse(data); if (!result.success) { throw new Error(result.error.message); } const limiter = new Ratelimit({ redis, limiter: 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; } } }; export { TFError, TWOFactor, handleTFError };