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
});
;