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