fiberx-backend-toolkit 0.0.50 → 0.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,4 +23,8 @@ export declare const REQUEST_RATE_LIMITTER_OPTIONS: {
23
23
  max_requests: number;
24
24
  message: string;
25
25
  };
26
- export declare const ALPHABET_CORPUS: string[];
26
+ export declare const CHARACTER_CORPUS: string[];
27
+ export declare const ALPHABET_CORPUS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
28
+ export declare const DEFAULT_TOTP_STEP = 30;
29
+ export declare const DEFAULT_TOTP_DIGITS = 6;
30
+ export declare const DEFAULT_TOTP_WINDOW = 1;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ALPHABET_CORPUS = exports.REQUEST_RATE_LIMITTER_OPTIONS = exports.CORS_MAX_AGE_IN_MICRO_SECONDS = exports.CORS_MAX_AGE_IN_SECONDS = exports.CORS_ALLOWED_HEADERS = exports.CORS_ALLOWED_METHODS = exports.DEVICE_ID_HEADERS_NAME = exports.DEVICE_ID_COOKIE_NAME = exports.DEVICE_ID_COOKIE_MAX_AGE = exports.REQUEST_ID_HEADERS_NAME = exports.REQUEST_ID_COOKIE_NAME = exports.REQUEST_ID_COOKIE_MAX_AGE = exports.SEQUELIZE_SEEDER_META_TABLE_NAME = exports.SEQUELIZE_META_TABLE_NAME = exports.SEEDERS_DIR = exports.MIGRATIONS_DIR = exports.MODELS_DIR = exports.SCHEMA_SNAPSHOTS_DIR = exports.SCHEMAS_DIR = exports.ENV_VAR_DIR = exports.LOG_DIR = exports.BASE_DIR = void 0;
6
+ exports.DEFAULT_TOTP_WINDOW = exports.DEFAULT_TOTP_DIGITS = exports.DEFAULT_TOTP_STEP = exports.ALPHABET_CORPUS = exports.CHARACTER_CORPUS = exports.REQUEST_RATE_LIMITTER_OPTIONS = exports.CORS_MAX_AGE_IN_MICRO_SECONDS = exports.CORS_MAX_AGE_IN_SECONDS = exports.CORS_ALLOWED_HEADERS = exports.CORS_ALLOWED_METHODS = exports.DEVICE_ID_HEADERS_NAME = exports.DEVICE_ID_COOKIE_NAME = exports.DEVICE_ID_COOKIE_MAX_AGE = exports.REQUEST_ID_HEADERS_NAME = exports.REQUEST_ID_COOKIE_NAME = exports.REQUEST_ID_COOKIE_MAX_AGE = exports.SEQUELIZE_SEEDER_META_TABLE_NAME = exports.SEQUELIZE_META_TABLE_NAME = exports.SEEDERS_DIR = exports.MIGRATIONS_DIR = exports.MODELS_DIR = exports.SCHEMA_SNAPSHOTS_DIR = exports.SCHEMAS_DIR = exports.ENV_VAR_DIR = exports.LOG_DIR = exports.BASE_DIR = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  // Database constants
9
9
  exports.BASE_DIR = process.cwd();
@@ -33,4 +33,8 @@ exports.REQUEST_RATE_LIMITTER_OPTIONS = {
33
33
  max_requests: 50,
34
34
  message: "⏳ Too many requests from this IP, please try again later"
35
35
  };
36
- exports.ALPHABET_CORPUS = ["6", ";", "s", "[", "w", "*", "n", "K", "h", "U", "#", "P", "T", "&", "M", "2", "}", "x", ")", "{", "|", "i", "%", "m", ":", "E", "q", "?", "0", "@", "1", "v", "A", "W", "<", "y", "Y", "4", "5", ".", "e", "G", ",", "D", "7", "I", "j", ">", "g", "t", "k", "c", "V", "9", "J", "L", "u", "H", "b", "Z", "+", '"', "'", "a", "f"];
36
+ exports.CHARACTER_CORPUS = ["6", ";", "s", "[", "w", "*", "n", "K", "h", "U", "#", "P", "T", "&", "M", "2", "}", "x", ")", "{", "|", "i", "%", "m", ":", "E", "q", "?", "0", "@", "1", "v", "A", "W", "<", "y", "Y", "4", "5", ".", "e", "G", ",", "D", "7", "I", "j", ">", "g", "t", "k", "c", "V", "9", "J", "L", "u", "H", "b", "Z", "+", '"', "'", "a", "f"];
37
+ exports.ALPHABET_CORPUS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
38
+ exports.DEFAULT_TOTP_STEP = 30;
39
+ exports.DEFAULT_TOTP_DIGITS = 6;
40
+ exports.DEFAULT_TOTP_WINDOW = 1;
@@ -45,3 +45,26 @@ export declare enum ErrorHandlingStrategyEnum {
45
45
  THROW_ERROR = 2,
46
46
  CONTROLLER_ERROR = 3
47
47
  }
48
+ export type GenerateSecretOptionsType = {
49
+ name: string;
50
+ issuer: string;
51
+ length?: number;
52
+ };
53
+ export type VerifyResultType = {
54
+ valid: boolean;
55
+ counter?: number;
56
+ };
57
+ export type VerifyOptionsType = {
58
+ secret: string;
59
+ token: string;
60
+ };
61
+ export interface TOTPServiceOptions {
62
+ STEP?: number;
63
+ DIGITS?: number;
64
+ WINDOW?: number;
65
+ ALGORITHM?: "SHA1" | "SHA256" | "SHA512";
66
+ }
67
+ export interface GenerateSecretResult {
68
+ secret_key: string;
69
+ otpauth_url: string;
70
+ }
@@ -8,3 +8,4 @@ var ErrorHandlingStrategyEnum;
8
8
  ErrorHandlingStrategyEnum[ErrorHandlingStrategyEnum["THROW_ERROR"] = 2] = "THROW_ERROR";
9
9
  ErrorHandlingStrategyEnum[ErrorHandlingStrategyEnum["CONTROLLER_ERROR"] = 3] = "CONTROLLER_ERROR";
10
10
  })(ErrorHandlingStrategyEnum || (exports.ErrorHandlingStrategyEnum = ErrorHandlingStrategyEnum = {}));
11
+ ;
@@ -14,7 +14,7 @@ class EncryptorDecryptorUtil {
14
14
  static instance;
15
15
  encrypt_secret_key;
16
16
  corpus_shift_key;
17
- corpus = constants_1.ALPHABET_CORPUS;
17
+ corpus = constants_1.CHARACTER_CORPUS;
18
18
  algorithm = "aes-256-cbc";
19
19
  iv_length = 16;
20
20
  jwt_secret_key;
@@ -9,7 +9,6 @@ const dayjs_1 = __importDefault(require("dayjs"));
9
9
  const isSameOrAfter_1 = __importDefault(require("dayjs/plugin/isSameOrAfter"));
10
10
  const bcrypt_1 = __importDefault(require("bcrypt"));
11
11
  const axios_1 = __importDefault(require("axios"));
12
- // import speakeasy from "speakeasy";
13
12
  const env_manager_util_1 = __importDefault(require("../utils/env_manager_util"));
14
13
  dayjs_1.default.extend(isSameOrAfter_1.default);
15
14
  class InputValidatorUtil {
@@ -0,0 +1,21 @@
1
+ import { GenerateSecretOptionsType, VerifyResultType, VerifyOptionsType, TOTPServiceOptions, GenerateSecretResult } from "../types/util_type";
2
+ declare class TOTPServiceUtil {
3
+ readonly name = "totp_service_util";
4
+ private static instance;
5
+ private logger;
6
+ private readonly STEP;
7
+ private readonly DIGITS;
8
+ private readonly WINDOW;
9
+ private readonly ALGORITHM;
10
+ private readonly SECRET_KEY_REGEX;
11
+ private constructor();
12
+ static getInstance(options?: TOTPServiceOptions): TOTPServiceUtil;
13
+ private base32Decode;
14
+ private base32Encode;
15
+ private generateForCounter;
16
+ generateSecret(options: GenerateSecretOptionsType): GenerateSecretResult;
17
+ verify(options: VerifyOptionsType & {
18
+ last_used_counter?: number;
19
+ }): VerifyResultType;
20
+ }
21
+ export default TOTPServiceUtil;
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const crypto_1 = __importDefault(require("crypto"));
7
+ const constants_1 = require("../config/constants");
8
+ const logger_util_1 = __importDefault(require("./logger_util"));
9
+ const input_validator_util_1 = __importDefault(require("./input_validator_util"));
10
+ class TOTPServiceUtil {
11
+ name = "totp_service_util";
12
+ static instance;
13
+ logger = new logger_util_1.default(this.name);
14
+ // ===============================
15
+ // CONFIG
16
+ // ===============================
17
+ STEP;
18
+ DIGITS;
19
+ WINDOW;
20
+ ALGORITHM;
21
+ SECRET_KEY_REGEX = (/^[A-Z2-7]+$/i);
22
+ constructor(options = {}) {
23
+ this.STEP = options.STEP ?? constants_1.DEFAULT_TOTP_STEP;
24
+ this.DIGITS = options.DIGITS ?? constants_1.DEFAULT_TOTP_DIGITS;
25
+ this.WINDOW = options.WINDOW ?? constants_1.DEFAULT_TOTP_WINDOW;
26
+ this.ALGORITHM = options.ALGORITHM?.toLowerCase() ?? "sha1";
27
+ }
28
+ // ✅ Global access point
29
+ static getInstance(options = {}) {
30
+ if (!TOTPServiceUtil.instance) {
31
+ TOTPServiceUtil.instance = new TOTPServiceUtil(options);
32
+ }
33
+ return TOTPServiceUtil.instance;
34
+ }
35
+ // ===============================
36
+ // BASE32 DECODE
37
+ // ===============================
38
+ base32Decode(input) {
39
+ if (!/^[A-Z2-7]+=*$/i.test(input)) {
40
+ throw new Error("Invalid base32 secret");
41
+ }
42
+ const alphabet = constants_1.ALPHABET_CORPUS;
43
+ const cleaned = input.replace(/=+$/, "").toUpperCase();
44
+ let bits = 0;
45
+ let value = 0;
46
+ const output = [];
47
+ for (const char of cleaned) {
48
+ const idx = alphabet.indexOf(char);
49
+ if (idx === -1) {
50
+ throw new Error("Invalid base32 character while decoding base 32 for totp service util");
51
+ }
52
+ value = (value << 5) | idx;
53
+ bits += 5;
54
+ if (bits >= 8) {
55
+ output.push((value >>> (bits - 8)) & 255);
56
+ bits -= 8;
57
+ }
58
+ }
59
+ return Buffer.from(output);
60
+ }
61
+ // ===============================
62
+ // BASE32 ENCODE
63
+ // ===============================
64
+ base32Encode(buffer) {
65
+ const alphabet = constants_1.ALPHABET_CORPUS;
66
+ let bits = 0;
67
+ let value = 0;
68
+ let output = "";
69
+ for (const byte of buffer) {
70
+ value = (value << 8) | byte;
71
+ bits += 8;
72
+ while (bits >= 5) {
73
+ output += alphabet[(value >>> (bits - 5)) & 31];
74
+ bits -= 5;
75
+ }
76
+ }
77
+ if (bits > 0) {
78
+ output += alphabet[(value << (5 - bits)) & 31];
79
+ }
80
+ return output;
81
+ }
82
+ // ===============================
83
+ // INTERNAL: GENERATE OTP
84
+ // ===============================
85
+ generateForCounter(secret, counter) {
86
+ const buffer = Buffer.alloc(8);
87
+ buffer.writeBigUInt64BE(BigInt(counter));
88
+ const hmac = crypto_1.default
89
+ .createHmac(this.ALGORITHM, secret)
90
+ .update(buffer)
91
+ .digest();
92
+ const offset = hmac[hmac.length - 1] & 0xf;
93
+ const code = ((hmac[offset] & 0x7f) << 24) |
94
+ ((hmac[offset + 1] & 0xff) << 16) |
95
+ ((hmac[offset + 2] & 0xff) << 8) |
96
+ (hmac[offset + 3] & 0xff);
97
+ const otp = code % 10 ** this.DIGITS;
98
+ return otp.toString().padStart(this.DIGITS, "0");
99
+ }
100
+ // ===============================
101
+ // PUBLIC: GENERATE SECRET
102
+ // ===============================
103
+ generateSecret(options) {
104
+ const { name, issuer, length = 20 } = options;
105
+ const buffer = crypto_1.default.randomBytes(length);
106
+ const base32 = this.base32Encode(buffer);
107
+ const encoder_issuer = encodeURIComponent(issuer);
108
+ const encoded_name = encodeURIComponent(name);
109
+ const otpauth_url = `otpauth://totp/${encoder_issuer}:${encoded_name}` +
110
+ `?secret=${base32}` +
111
+ `&issuer=${encoder_issuer}` +
112
+ `&algorithm=${this.ALGORITHM.toUpperCase()}` +
113
+ `&digits=${this.DIGITS}` +
114
+ `&period=${this.STEP}`;
115
+ return {
116
+ secret_key: base32,
117
+ otpauth_url,
118
+ };
119
+ }
120
+ // ===============================
121
+ // PUBLIC: VERIFY TOKEN
122
+ // ===============================
123
+ verify(options) {
124
+ const { secret, token, last_used_counter } = options;
125
+ if (!/^\d+$/.test(token)) {
126
+ return { valid: false };
127
+ }
128
+ if (token.length !== this.DIGITS) {
129
+ return { valid: false };
130
+ }
131
+ if (!this.SECRET_KEY_REGEX.test(secret)) {
132
+ throw new Error("Invalid TOTP secret format");
133
+ }
134
+ let key;
135
+ try {
136
+ key = this.base32Decode(secret);
137
+ }
138
+ catch (error) {
139
+ this.logger.error("Failed to decode TOTP secret");
140
+ return { valid: false };
141
+ }
142
+ const time = Math.floor(Date.now() / 1000 / this.STEP);
143
+ for (let error_window = -this.WINDOW; error_window <= this.WINDOW; error_window++) {
144
+ const counter = time + error_window;
145
+ const generated = this.generateForCounter(key, counter);
146
+ if (input_validator_util_1.default.isSafeHashCompare(generated, token)) {
147
+ // 🔒 Replay protection
148
+ if (typeof last_used_counter === "number" &&
149
+ counter <= last_used_counter) {
150
+ return { valid: false };
151
+ }
152
+ return { valid: true, counter };
153
+ }
154
+ }
155
+ return { valid: false };
156
+ }
157
+ }
158
+ exports.default = TOTPServiceUtil;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fiberx-backend-toolkit",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "description": "A TypeScript backend toolkit providing shared domain logic, infrastructure helpers, and utilities for FiberX server-side applications and services.",
5
5
  "type": "commonjs",
6
6
  "main": "./dist/index.js",