fiberx-backend-toolkit 0.0.49 → 0.0.51

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;
@@ -15,7 +15,7 @@ declare class AuthenicationMiddleWare<TRequestInfo extends DefaultRequestInfo =
15
15
  protected loadMemberLoginChallenge(member_id: number | string, challenge_id: number | string, request_info?: TRequestInfo, is_2fa_validated?: boolean): Promise<Model | null>;
16
16
  protected getActorPermissions(actor_id: number | string, role_ids: (number | string)[]): Promise<string[]>;
17
17
  protected validateHasPermission(request_info: TRequestInfo): Promise<boolean>;
18
- setPermissionName(permission_name: string): RequestHandler;
18
+ setPermissionName: (permission_name: string) => RequestHandler;
19
19
  requireNoAuth(req: Request, res: Response, next: NextFunction): Promise<void | Response>;
20
20
  requirePartialAuth(req: Request, res: Response, next: NextFunction): Promise<void | Response>;
21
21
  requireFullAuth(req: Request, res: Response, next: NextFunction): Promise<void | Response>;
@@ -77,7 +77,7 @@ class AuthenicationMiddleWare {
77
77
  // -----------------------------------
78
78
  // GENERIC MIDDLEWARES
79
79
  // -----------------------------------
80
- setPermissionName(permission_name) {
80
+ setPermissionName = (permission_name) => {
81
81
  return async (req, res, next) => {
82
82
  req.permission_name = permission_name;
83
83
  this.logger.info(`[${this.name}] 🔐 Route permission set as ${permission_name} for request ${req.request_id}`);
@@ -90,7 +90,7 @@ class AuthenicationMiddleWare {
90
90
  this.logger.success(`[${this.name}] ✅ Permission granted for request ${req.request_id} with required permission ${permission_name}`);
91
91
  next();
92
92
  };
93
- }
93
+ };
94
94
  async requireNoAuth(req, res, next) {
95
95
  if (!this.options?.requireNoAuthMiddleWareMethod) {
96
96
  return next();
@@ -161,12 +161,6 @@ __decorate([
161
161
  __metadata("design:paramtypes", [Object]),
162
162
  __metadata("design:returntype", Promise)
163
163
  ], AuthenicationMiddleWare.prototype, "validateHasPermission", null);
164
- __decorate([
165
- main_1.SafeExecuteUtil.safeExecuteThrow("authentication_middle_ware"),
166
- __metadata("design:type", Function),
167
- __metadata("design:paramtypes", [String]),
168
- __metadata("design:returntype", Function)
169
- ], AuthenicationMiddleWare.prototype, "setPermissionName", null);
170
164
  __decorate([
171
165
  main_1.SafeExecuteUtil.safeExecuteThrow("authentication_middle_ware"),
172
166
  __metadata("design:type", Function),
@@ -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,19 @@
1
+ import { GenerateSecretOptionsType, VerifyResultType, VerifyOptionsType, TOTPServiceOptions, GenerateSecretResult } from "../types/util_type";
2
+ declare class TOTPServiceUtil {
3
+ readonly name = "totp_service_util";
4
+ private logger;
5
+ private readonly STEP;
6
+ private readonly DIGITS;
7
+ private readonly WINDOW;
8
+ private readonly ALGORITHM;
9
+ private readonly SECRET_KEY_REGEX;
10
+ constructor(options?: TOTPServiceOptions);
11
+ private base32Decode;
12
+ private base32Encode;
13
+ private generateForCounter;
14
+ generateSecret(options: GenerateSecretOptionsType): GenerateSecretResult;
15
+ verify(options: VerifyOptionsType & {
16
+ last_used_counter?: number;
17
+ }): VerifyResultType;
18
+ }
19
+ export default TOTPServiceUtil;
@@ -0,0 +1,150 @@
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
+ logger = new logger_util_1.default(this.name);
13
+ // ===============================
14
+ // CONFIG
15
+ // ===============================
16
+ STEP;
17
+ DIGITS;
18
+ WINDOW;
19
+ ALGORITHM;
20
+ SECRET_KEY_REGEX = (/^[A-Z2-7]+$/i);
21
+ constructor(options = {}) {
22
+ this.STEP = options.STEP ?? constants_1.DEFAULT_TOTP_STEP;
23
+ this.DIGITS = options.DIGITS ?? constants_1.DEFAULT_TOTP_DIGITS;
24
+ this.WINDOW = options.WINDOW ?? constants_1.DEFAULT_TOTP_WINDOW;
25
+ this.ALGORITHM = options.ALGORITHM?.toLowerCase() ?? "sha1";
26
+ }
27
+ // ===============================
28
+ // BASE32 DECODE
29
+ // ===============================
30
+ base32Decode(input) {
31
+ if (!/^[A-Z2-7]+=*$/i.test(input)) {
32
+ throw new Error("Invalid base32 secret");
33
+ }
34
+ const alphabet = constants_1.ALPHABET_CORPUS;
35
+ const cleaned = input.replace(/=+$/, "").toUpperCase();
36
+ let bits = 0;
37
+ let value = 0;
38
+ const output = [];
39
+ for (const char of cleaned) {
40
+ const idx = alphabet.indexOf(char);
41
+ if (idx === -1) {
42
+ throw new Error("Invalid base32 character while decoding base 32 for totp service util");
43
+ }
44
+ value = (value << 5) | idx;
45
+ bits += 5;
46
+ if (bits >= 8) {
47
+ output.push((value >>> (bits - 8)) & 255);
48
+ bits -= 8;
49
+ }
50
+ }
51
+ return Buffer.from(output);
52
+ }
53
+ // ===============================
54
+ // BASE32 ENCODE
55
+ // ===============================
56
+ base32Encode(buffer) {
57
+ const alphabet = constants_1.ALPHABET_CORPUS;
58
+ let bits = 0;
59
+ let value = 0;
60
+ let output = "";
61
+ for (const byte of buffer) {
62
+ value = (value << 8) | byte;
63
+ bits += 8;
64
+ while (bits >= 5) {
65
+ output += alphabet[(value >>> (bits - 5)) & 31];
66
+ bits -= 5;
67
+ }
68
+ }
69
+ if (bits > 0) {
70
+ output += alphabet[(value << (5 - bits)) & 31];
71
+ }
72
+ return output;
73
+ }
74
+ // ===============================
75
+ // INTERNAL: GENERATE OTP
76
+ // ===============================
77
+ generateForCounter(secret, counter) {
78
+ const buffer = Buffer.alloc(8);
79
+ buffer.writeBigUInt64BE(BigInt(counter));
80
+ const hmac = crypto_1.default
81
+ .createHmac(this.ALGORITHM, secret)
82
+ .update(buffer)
83
+ .digest();
84
+ const offset = hmac[hmac.length - 1] & 0xf;
85
+ const code = ((hmac[offset] & 0x7f) << 24) |
86
+ ((hmac[offset + 1] & 0xff) << 16) |
87
+ ((hmac[offset + 2] & 0xff) << 8) |
88
+ (hmac[offset + 3] & 0xff);
89
+ const otp = code % 10 ** this.DIGITS;
90
+ return otp.toString().padStart(this.DIGITS, "0");
91
+ }
92
+ // ===============================
93
+ // PUBLIC: GENERATE SECRET
94
+ // ===============================
95
+ generateSecret(options) {
96
+ const { name, issuer, length = 20 } = options;
97
+ const buffer = crypto_1.default.randomBytes(length);
98
+ const base32 = this.base32Encode(buffer);
99
+ const encoder_issuer = encodeURIComponent(issuer);
100
+ const encoded_name = encodeURIComponent(name);
101
+ const otpauth_url = `otpauth://totp/${encoder_issuer}:${encoded_name}` +
102
+ `?secret=${base32}` +
103
+ `&issuer=${encoder_issuer}` +
104
+ `&algorithm=${this.ALGORITHM.toUpperCase()}` +
105
+ `&digits=${this.DIGITS}` +
106
+ `&period=${this.STEP}`;
107
+ return {
108
+ secret_key: base32,
109
+ otpauth_url,
110
+ };
111
+ }
112
+ // ===============================
113
+ // PUBLIC: VERIFY TOKEN
114
+ // ===============================
115
+ verify(options) {
116
+ const { secret, token, last_used_counter } = options;
117
+ if (!/^\d+$/.test(token)) {
118
+ return { valid: false };
119
+ }
120
+ if (token.length !== this.DIGITS) {
121
+ return { valid: false };
122
+ }
123
+ if (!this.SECRET_KEY_REGEX.test(secret)) {
124
+ throw new Error("Invalid TOTP secret format");
125
+ }
126
+ let key;
127
+ try {
128
+ key = this.base32Decode(secret);
129
+ }
130
+ catch (error) {
131
+ this.logger.error("Failed to decode TOTP secret");
132
+ return { valid: false };
133
+ }
134
+ const time = Math.floor(Date.now() / 1000 / this.STEP);
135
+ for (let error_window = -this.WINDOW; error_window <= this.WINDOW; error_window++) {
136
+ const counter = time + error_window;
137
+ const generated = this.generateForCounter(key, counter);
138
+ if (input_validator_util_1.default.isSafeHashCompare(generated, token)) {
139
+ // 🔒 Replay protection
140
+ if (typeof last_used_counter === "number" &&
141
+ counter <= last_used_counter) {
142
+ return { valid: false };
143
+ }
144
+ return { valid: true, counter };
145
+ }
146
+ }
147
+ return { valid: false };
148
+ }
149
+ }
150
+ exports.default = TOTPServiceUtil;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fiberx-backend-toolkit",
3
- "version": "0.0.49",
3
+ "version": "0.0.51",
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",