core-services-sdk 1.3.2 → 1.3.4
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.
- package/package.json +4 -1
- package/src/core/combine-unique-arrays.js +15 -0
- package/src/core/index.js +5 -0
- package/src/core/normalize-to-array.js +31 -0
- package/src/core/otp-generators.js +113 -0
- package/src/core/regex-utils.js +27 -0
- package/src/core/sanitize-objects.js +50 -0
- package/src/crypto/crypto.js +128 -0
- package/src/crypto/encryption.js +48 -0
- package/src/crypto/index.js +2 -0
- package/src/fastify/error-handlers/with-error-handling.js +5 -1
- package/src/ids/generators.js +73 -0
- package/src/ids/prefixes.js +28 -0
- package/src/index.js +3 -1
- package/src/logger/get-logger.js +64 -0
- package/src/logger/index.js +1 -0
- package/src/mailer/transport.factory.js +25 -1
- package/src/mongodb/index.js +3 -73
- package/src/mongodb/initialize-mongodb.js +73 -0
- package/src/mongodb/validate-mongo-uri.js +32 -0
- package/tests/core/combine-unique-arrays.unit.test.js +35 -0
- package/tests/core/normalize-to-array.unit.test.js +43 -0
- package/tests/core/otp-generators.unit.test.js +93 -0
- package/tests/core/regex-utils.unit.test.js +41 -0
- package/tests/core/sanitize-objects.unit.test.js +78 -0
- package/tests/crypto/crypto.unit.test.js +130 -0
- package/tests/crypto/encryption.unit.test.js +73 -0
- package/tests/ids/generators.unit.test.js +72 -0
- package/tests/ids/prefixes.unit.test.js +42 -0
- package/tests/logger/get-logger.unit.test.js +64 -0
- package/src/fastify/error-handlers/with-error-handling.types.js +0 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-services-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,12 +21,15 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"amqplib": "^0.10.8",
|
|
23
23
|
"aws-sdk": "^2.1692.0",
|
|
24
|
+
"crypto": "^1.0.1",
|
|
24
25
|
"dot": "^1.1.3",
|
|
26
|
+
"fastify": "^5.4.0",
|
|
25
27
|
"http-status": "^2.1.0",
|
|
26
28
|
"mongodb": "^6.17.0",
|
|
27
29
|
"node-fetch": "^3.3.2",
|
|
28
30
|
"nodemailer": "^7.0.5",
|
|
29
31
|
"nodemailer-sendgrid-transport": "^0.2.0",
|
|
32
|
+
"pino": "^9.7.0",
|
|
30
33
|
"uuid": "^11.1.0",
|
|
31
34
|
"xml2js": "^0.6.2"
|
|
32
35
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combines multiple arrays into a single flat array with unique values.
|
|
3
|
+
*
|
|
4
|
+
* Removes duplicate entries and preserves the order of first appearance.
|
|
5
|
+
*
|
|
6
|
+
* @param {...Array<any>} lists - One or more arrays to combine.
|
|
7
|
+
* @returns {Array<any>} A new array containing unique values from all input arrays.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* combineUniqueArrays([1, 2, 3], [3, 4], [5]) // [1, 2, 3, 4, 5]
|
|
11
|
+
* combineUniqueArrays(['a', 'b'], ['b', 'c']) // ['a', 'b', 'c']
|
|
12
|
+
*/
|
|
13
|
+
export const combineUniqueArrays = (...lists) => {
|
|
14
|
+
return Array.from(new Set(lists.flat()))
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes an input value to an array of trimmed, non-empty strings.
|
|
3
|
+
*
|
|
4
|
+
* - If the input is `undefined` or `null`, returns an empty array.
|
|
5
|
+
* - If the input is an array, it trims each string element and filters out empty values.
|
|
6
|
+
* - If the input is a string (or coercible to string), it splits it by commas,
|
|
7
|
+
* trims each item, and filters out empty values.
|
|
8
|
+
*
|
|
9
|
+
* @param {any} value - The value to normalize. Can be a string, array, or any other type.
|
|
10
|
+
* @returns {string[]} An array of trimmed, non-empty strings.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* normalizeToArray('a,b, c , ,d') // ['a', 'b', 'c', 'd']
|
|
14
|
+
* normalizeToArray([' a ', 'b', '', ' ']) // ['a', 'b']
|
|
15
|
+
* normalizeToArray(null) // []
|
|
16
|
+
* normalizeToArray(123) // ['123']
|
|
17
|
+
*/
|
|
18
|
+
export const normalizeToArray = (value) => {
|
|
19
|
+
if (value === undefined || value === null) {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isArray = Array.isArray(value)
|
|
24
|
+
const rawArray = isArray ? value : String(value).split(',')
|
|
25
|
+
|
|
26
|
+
const arrayFiltered = rawArray
|
|
27
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
|
|
30
|
+
return arrayFiltered
|
|
31
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const numeric = '0123456789'
|
|
2
|
+
const symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?'
|
|
3
|
+
const alphaLower = 'abcdefghijklmnopqrstuvwxyz'
|
|
4
|
+
const alphaUpper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
5
|
+
|
|
6
|
+
const alphanumericSymbols = `${alphaLower}${alphaUpper}${numeric}${symbols}`
|
|
7
|
+
|
|
8
|
+
export const OTP_GENERATOR_TYPES = Object.freeze({
|
|
9
|
+
any: 'any',
|
|
10
|
+
alpha: 'alpha',
|
|
11
|
+
numeric: 'numeric',
|
|
12
|
+
symbols: 'symbols',
|
|
13
|
+
alphaLower: 'alphaLower',
|
|
14
|
+
alphaUpper: 'alphaUpper',
|
|
15
|
+
alphanumeric: 'alphanumeric',
|
|
16
|
+
alphanumericSymbols: 'alphanumericSymbols',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const types = Object.freeze({
|
|
20
|
+
[OTP_GENERATOR_TYPES.numeric]: numeric,
|
|
21
|
+
[OTP_GENERATOR_TYPES.symbols]: symbols,
|
|
22
|
+
[OTP_GENERATOR_TYPES.alphaLower]: alphaLower,
|
|
23
|
+
[OTP_GENERATOR_TYPES.alphaUpper]: alphaUpper,
|
|
24
|
+
[OTP_GENERATOR_TYPES.any]: alphanumericSymbols,
|
|
25
|
+
[OTP_GENERATOR_TYPES.alpha]: `${alphaLower}${alphaUpper}`,
|
|
26
|
+
[OTP_GENERATOR_TYPES.alphanumericSymbols]: alphanumericSymbols,
|
|
27
|
+
[OTP_GENERATOR_TYPES.alphanumeric]: `${alphaLower}${alphaUpper}${numeric}`,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generates a one-time password (OTP) code based on a specified type or character set.
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} [options={}] - The options object.
|
|
34
|
+
* @param {number} [options.length=4] - The desired length of the generated code (1-30).
|
|
35
|
+
* @param {string} [options.type='numeric'] - The type of characters to use. One of:
|
|
36
|
+
* 'any', 'alpha', 'numeric', 'symbols', 'alphaLower', 'alphaUpper', 'alphanumeric', 'alphanumericSymbols'.
|
|
37
|
+
* @param {string} [options.charset] - A custom string of characters to use instead of the predefined types.
|
|
38
|
+
* @returns {string} The generated code.
|
|
39
|
+
* @throws {Error} If the length is not a number between 1 and 30.
|
|
40
|
+
* @throws {Error} If charset is provided and is not a non-empty string.
|
|
41
|
+
* @throws {Error} If the type is invalid and no valid charset is provided.
|
|
42
|
+
*/
|
|
43
|
+
export function generateCode({ length = 4, type = 'numeric', charset } = {}) {
|
|
44
|
+
const max = 30
|
|
45
|
+
const min = 1
|
|
46
|
+
|
|
47
|
+
if (typeof length !== 'number' || length < min || length > max) {
|
|
48
|
+
throw new Error(`length must be a number between ${min} and ${max}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (charset !== undefined) {
|
|
52
|
+
if (typeof charset !== 'string') {
|
|
53
|
+
throw new Error('charset must be a string if provided')
|
|
54
|
+
}
|
|
55
|
+
if (charset.length === 0) {
|
|
56
|
+
throw new Error('charset must not be empty')
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const chars = charset || types[type]
|
|
61
|
+
if (!chars) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`type must be one of: ${Object.keys(types)
|
|
64
|
+
.map((t) => `'${t}'`)
|
|
65
|
+
.join(', ')}`,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Array.from(
|
|
70
|
+
{ length },
|
|
71
|
+
() => chars[Math.floor(Math.random() * chars.length)],
|
|
72
|
+
).join('')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generates an OTP code using alphabetic characters (both lowercase and uppercase).
|
|
77
|
+
*
|
|
78
|
+
* @param {number} [length=4] - The desired length of the code.
|
|
79
|
+
* @returns {string} The generated code.
|
|
80
|
+
*/
|
|
81
|
+
export function generateCodeAlpha(length = 4) {
|
|
82
|
+
return generateCode({ length, type: 'alpha' })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generates an OTP code using only numeric digits (0-9).
|
|
87
|
+
*
|
|
88
|
+
* @param {number} [length=4] - The desired length of the code.
|
|
89
|
+
* @returns {string} The generated code.
|
|
90
|
+
*/
|
|
91
|
+
export function generateCodeDigits(length = 4) {
|
|
92
|
+
return generateCode({ length, type: 'numeric' })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generates an OTP code using alphabetic characters and digits.
|
|
97
|
+
*
|
|
98
|
+
* @param {number} [length=4] - The desired length of the code.
|
|
99
|
+
* @returns {string} The generated code.
|
|
100
|
+
*/
|
|
101
|
+
export function generateCodeAlphaNumeric(length = 4) {
|
|
102
|
+
return generateCode({ length, type: 'alphanumeric' })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates an OTP code using alphabetic characters, digits, and symbols.
|
|
107
|
+
*
|
|
108
|
+
* @param {number} [length=4] - The desired length of the code.
|
|
109
|
+
* @returns {string} The generated code.
|
|
110
|
+
*/
|
|
111
|
+
export function generateCodeAlphaNumericSymbols(length = 4) {
|
|
112
|
+
return generateCode({ length, type: 'any' })
|
|
113
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether the given input is a valid regular expression.
|
|
3
|
+
*
|
|
4
|
+
* - If the input is an instance of `RegExp`, returns `true`.
|
|
5
|
+
* - If the input is a string, attempts to create a `RegExp` object from it and returns `true` if valid.
|
|
6
|
+
* - Returns `false` for any other types or if the string is an invalid regex pattern.
|
|
7
|
+
*
|
|
8
|
+
* @param {string | RegExp} pattern - The regex pattern to validate, either as a string or a RegExp object.
|
|
9
|
+
* @returns {boolean} `true` if the pattern is a valid regular expression, otherwise `false`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* isValidRegex('^[a-z]+$') // true
|
|
13
|
+
* isValidRegex(/^[a-z]+$/) // true
|
|
14
|
+
* isValidRegex('[') // false
|
|
15
|
+
* isValidRegex(123) // false
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const isValidRegex = (pattern) => {
|
|
19
|
+
if (pattern instanceof RegExp) return true
|
|
20
|
+
if (typeof pattern !== 'string') return false
|
|
21
|
+
try {
|
|
22
|
+
new RegExp(pattern)
|
|
23
|
+
return true
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a new object containing only the key-value pairs that pass the provided filter function.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} obj - The object to sanitize.
|
|
5
|
+
* @param {(entry: [string, any]) => boolean} filter - A function that determines whether to include each `[key, value]` entry.
|
|
6
|
+
* @returns {Object} A new object with only the filtered entries.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* sanitizeObject({ a: 1, b: undefined }, ([, v]) => v !== undefined) // { a: 1 }
|
|
10
|
+
*/
|
|
11
|
+
export const sanitizeObject = (obj, filter) =>
|
|
12
|
+
Object.fromEntries(Object.entries(obj).filter(filter))
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Removes all properties from the object whose values are `undefined`.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} obj - The object to sanitize.
|
|
18
|
+
* @returns {Object} A new object without `undefined` values.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* sanitizeUndefinedFields({ a: 1, b: undefined }) // { a: 1 }
|
|
22
|
+
*/
|
|
23
|
+
export const sanitizeUndefinedFields = (obj) =>
|
|
24
|
+
sanitizeObject(obj, ([, value]) => value !== undefined)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns a new object containing only the specified allowed properties.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} obj - The original object.
|
|
30
|
+
* @param {string[]} [allowedFields=[]] - An array of property names to retain.
|
|
31
|
+
* @returns {Object} A new object containing only the allowed properties.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* sanitizeObjectAllowProps({ a: 1, b: 2 }, ['a']) // { a: 1 }
|
|
35
|
+
*/
|
|
36
|
+
export const sanitizeObjectAllowProps = (obj, allowedFields = []) =>
|
|
37
|
+
sanitizeObject(obj, ([key]) => allowedFields.includes(key))
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a new object excluding the specified disallowed properties.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} obj - The original object.
|
|
43
|
+
* @param {string[]} [disallowedFields=[]] - An array of property names to exclude.
|
|
44
|
+
* @returns {Object} A new object without the disallowed properties.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* sanitizeObjectDisallowProps({ a: 1, b: 2 }, ['b']) // { a: 1 }
|
|
48
|
+
*/
|
|
49
|
+
export const sanitizeObjectDisallowProps = (obj, disallowedFields = []) =>
|
|
50
|
+
sanitizeObject(obj, ([key]) => !disallowedFields.includes(key))
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { promisify } from 'util'
|
|
2
|
+
import { randomBytes, scrypt } from 'crypto'
|
|
3
|
+
|
|
4
|
+
const scryptPromisify = promisify(scrypt)
|
|
5
|
+
|
|
6
|
+
const keyLength = 64
|
|
7
|
+
const baseFormat = 'hex'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates a cryptographically secure random salt as a Buffer.
|
|
11
|
+
*
|
|
12
|
+
* @param {number} length - The length of the salt in bytes.
|
|
13
|
+
* @returns {Buffer} A Buffer containing random bytes.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const salt = getSalt(16)
|
|
17
|
+
* console.log(salt.toString('hex')) // 'e4b1c9...'
|
|
18
|
+
*/
|
|
19
|
+
export const getSalt = (length) => {
|
|
20
|
+
return randomBytes(length)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generates a cryptographically secure random salt and returns it as a hexadecimal string.
|
|
25
|
+
*
|
|
26
|
+
* @param {number} length - The length of the salt in bytes.
|
|
27
|
+
* @returns {string} The salt encoded as a hexadecimal string.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const saltHex = getSaltHex(16)
|
|
31
|
+
* console.log(saltHex) // 'e4b1c9...'
|
|
32
|
+
*/
|
|
33
|
+
export const getSaltHex = (length) => {
|
|
34
|
+
return getSalt(length).toString(baseFormat)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Encrypts a given expression using scrypt with a provided salt.
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} options - The encryption options.
|
|
41
|
+
* @param {string} options.expression - The expression to encrypt (e.g., a password).
|
|
42
|
+
* @param {string} options.salt - The salt to use (as a string).
|
|
43
|
+
* @param {number} [options.length=64] - The desired key length in bytes for the derived key.
|
|
44
|
+
* @returns {Promise<Buffer>} A Promise that resolves to the derived key as a Buffer.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const buffer = await getEncryptedBuffer({
|
|
48
|
+
* expression: 'my-password',
|
|
49
|
+
* salt: 'somesalt',
|
|
50
|
+
* length: 32
|
|
51
|
+
* })
|
|
52
|
+
* console.log(buffer.toString('hex'))
|
|
53
|
+
*/
|
|
54
|
+
export const getEncryptedBuffer = async ({
|
|
55
|
+
salt,
|
|
56
|
+
expression,
|
|
57
|
+
length = keyLength,
|
|
58
|
+
}) => {
|
|
59
|
+
const encryptedExpression = await scryptPromisify(expression, salt, length)
|
|
60
|
+
|
|
61
|
+
return encryptedExpression
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Encrypts a string expression (e.g., password) using scrypt and returns the result as a hexadecimal string.
|
|
66
|
+
*
|
|
67
|
+
* Optionally appends a `passwordPrivateKey` to the salt for extra security.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} options
|
|
70
|
+
* @param {string} options.expression - The expression to encrypt.
|
|
71
|
+
* @param {string} options.salt - The salt string.
|
|
72
|
+
* @param {string} [options.passwordPrivateKey] - Optional extra key to enhance the salt.
|
|
73
|
+
* @returns {Promise<string>} A Promise that resolves to the encrypted expression as a hex string.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const encrypted = await encrypt({
|
|
77
|
+
* expression: 'my-password',
|
|
78
|
+
* salt: 'abc123',
|
|
79
|
+
* passwordPrivateKey: 'my-secret-key'
|
|
80
|
+
* })
|
|
81
|
+
* console.log(encrypted) // '9af0a1b23c...'
|
|
82
|
+
*/
|
|
83
|
+
export const encrypt = async ({ salt, expression, passwordPrivateKey }) => {
|
|
84
|
+
const encryptedExpression = await getEncryptedBuffer({
|
|
85
|
+
expression,
|
|
86
|
+
salt: `${salt}${passwordPrivateKey ? passwordPrivateKey : ''}`,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return encryptedExpression.toString(baseFormat)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Compares a plain-text password to an already encrypted one using scrypt.
|
|
94
|
+
*
|
|
95
|
+
* Re-encrypts the input password using the provided salt and optional private key,
|
|
96
|
+
* then compares it to the stored encrypted password.
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} options
|
|
99
|
+
* @param {string} options.salt - The salt used during encryption.
|
|
100
|
+
* @param {string} options.password - The plain-text password to validate.
|
|
101
|
+
* @param {string} options.encryptedPassword - The previously encrypted password (hex string).
|
|
102
|
+
* @param {string} [options.passwordPrivateKey] - Optional private key used in encryption.
|
|
103
|
+
* @returns {Promise<boolean>} A Promise that resolves to `true` if passwords match, otherwise `false`.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* const isValid = await isPasswordMatch({
|
|
107
|
+
* password: 'my-password',
|
|
108
|
+
* salt: 'abc123',
|
|
109
|
+
* encryptedPassword: '9af0a1b23c...',
|
|
110
|
+
* passwordPrivateKey: 'my-secret-key'
|
|
111
|
+
* })
|
|
112
|
+
* console.log(isValid) // true or false
|
|
113
|
+
*/
|
|
114
|
+
export const isPasswordMatch = async ({
|
|
115
|
+
salt,
|
|
116
|
+
password,
|
|
117
|
+
encryptedPassword,
|
|
118
|
+
passwordPrivateKey,
|
|
119
|
+
}) => {
|
|
120
|
+
const encryptedCurrentPassword = await getEncryptedBuffer({
|
|
121
|
+
expression: password,
|
|
122
|
+
salt: `${salt}${passwordPrivateKey ? passwordPrivateKey : ''}`,
|
|
123
|
+
})
|
|
124
|
+
const isMatch = encryptedCurrentPassword.equals(
|
|
125
|
+
Buffer.from(encryptedPassword, baseFormat),
|
|
126
|
+
)
|
|
127
|
+
return isMatch
|
|
128
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { encrypt, getSaltHex } from './crypto.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Encrypts a plain-text password using scrypt and a salt.
|
|
5
|
+
*
|
|
6
|
+
* This function generates a random salt (in hex) of the specified length,
|
|
7
|
+
* appends an optional passwordPrivateKey (if provided),
|
|
8
|
+
* and uses the Node.js `crypto.scrypt` algorithm to encrypt the password.
|
|
9
|
+
*
|
|
10
|
+
* @async
|
|
11
|
+
* @function encryptPassword
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} input - The input object containing the password.
|
|
14
|
+
* @param {string} input.password - The plain-text password to encrypt.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} options - Configuration options for encryption.
|
|
17
|
+
* @param {number} options.saltLength - The desired length of the generated salt (in bytes).
|
|
18
|
+
* @param {string} [options.passwordPrivateKey] - An optional private key to strengthen the salt.
|
|
19
|
+
*
|
|
20
|
+
* @returns {Promise<Object>} An object containing:
|
|
21
|
+
* - `salt` {string} The generated salt in hex format.
|
|
22
|
+
* - `password` {string} The encrypted password in hex format.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const result = await encryptPassword(
|
|
26
|
+
* { password: 'mySecretPassword' },
|
|
27
|
+
* { saltLength: 16, passwordPrivateKey: 'abc123' }
|
|
28
|
+
* );
|
|
29
|
+
* console.log(result); // { salt: 'f3ab...', password: '8e1f...' }
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export const encryptPassword = async (
|
|
33
|
+
{ password },
|
|
34
|
+
{ saltLength, passwordPrivateKey },
|
|
35
|
+
) => {
|
|
36
|
+
const salt = getSaltHex(saltLength)
|
|
37
|
+
|
|
38
|
+
const encrypted = await encrypt({
|
|
39
|
+
salt,
|
|
40
|
+
passwordPrivateKey,
|
|
41
|
+
expression: password,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
salt,
|
|
46
|
+
password: encrypted,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import httpStatus from 'http-status'
|
|
2
2
|
|
|
3
|
-
import { HttpError } from '../../http/HttpError.js'
|
|
4
3
|
import { GENERAL_ERROR } from '../error-codes.js'
|
|
4
|
+
import { HttpError } from '../../http/HttpError.js'
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('fastify').FastifyReply} Reply */
|
|
7
|
+
/** @typedef {import('fastify').FastifyRequest} Request */
|
|
8
|
+
/** @typedef {import('pino').Logger} Logger */
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Generic error-handling wrapper that logs and throws a safe error.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
2
|
+
import { ID_PREFIXES } from './prefixes.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates a new UUID v4 string.
|
|
6
|
+
*
|
|
7
|
+
* @returns {string} A new UUID (version 4).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* generateId() // '550e8400-e29b-41d4-a716-446655440000'
|
|
11
|
+
*/
|
|
12
|
+
export const generateId = () => {
|
|
13
|
+
return uuidv4()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generates a unique ID with the given prefix.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} prefix - A prefix string to prepend to the UUID.
|
|
20
|
+
* @returns {string} A unique ID in the format `${prefix}_${uuid}`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* generatePrefixedId('usr') // 'usr_550e8400-e29b-41d4-a716-446655440000'
|
|
24
|
+
*/
|
|
25
|
+
export const generatePrefixedId = (prefix) => {
|
|
26
|
+
return `${prefix}_${generateId()}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generates a user ID with a `usr_` prefix.
|
|
31
|
+
*
|
|
32
|
+
* @returns {string} A user ID.
|
|
33
|
+
*/
|
|
34
|
+
export const generateUserId = () => generatePrefixedId(ID_PREFIXES.USER)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates a tenant ID with a `tnt_` prefix.
|
|
38
|
+
*
|
|
39
|
+
* @returns {string} A tenant ID.
|
|
40
|
+
*/
|
|
41
|
+
export const generateTenantId = () => generatePrefixedId(ID_PREFIXES.TENANT)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generates a permission ID with a `prm_` prefix.
|
|
45
|
+
*
|
|
46
|
+
* @returns {string} A permission ID.
|
|
47
|
+
*/
|
|
48
|
+
export const generatePermissionId = () =>
|
|
49
|
+
generatePrefixedId(ID_PREFIXES.PERMISSION)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generates a correlation ID with a `crln_` prefix.
|
|
53
|
+
*
|
|
54
|
+
* @returns {string} A correlation ID.
|
|
55
|
+
*/
|
|
56
|
+
export const generateCorrelationId = () =>
|
|
57
|
+
generatePrefixedId(ID_PREFIXES.CORRELATION)
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generates a verification ID with a `vrf_` prefix.
|
|
61
|
+
*
|
|
62
|
+
* @returns {string} A verification ID.
|
|
63
|
+
*/
|
|
64
|
+
export const generateVerificationId = () =>
|
|
65
|
+
generatePrefixedId(ID_PREFIXES.VERIFICATION)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generates a role permissions ID with a `role_` prefix.
|
|
69
|
+
*
|
|
70
|
+
* @returns {string} A role permissions ID.
|
|
71
|
+
*/
|
|
72
|
+
export const generateRolePermissionsId = () =>
|
|
73
|
+
generatePrefixedId(ID_PREFIXES.ROLE_PERMISSIONS)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapping of entity types to their unique ID prefixes.
|
|
3
|
+
*
|
|
4
|
+
* These prefixes are prepended to UUIDs to create consistent and identifiable IDs across the system.
|
|
5
|
+
* For example: 'usr_550e8400-e29b-41d4-a716-446655440000'
|
|
6
|
+
*
|
|
7
|
+
* @readonly
|
|
8
|
+
* @enum {string}
|
|
9
|
+
*/
|
|
10
|
+
export const ID_PREFIXES = Object.freeze({
|
|
11
|
+
/** User entity ID prefix */
|
|
12
|
+
USER: 'usr',
|
|
13
|
+
|
|
14
|
+
/** Tenant entity ID prefix */
|
|
15
|
+
TENANT: 'tnt',
|
|
16
|
+
|
|
17
|
+
/** Permission entity ID prefix */
|
|
18
|
+
PERMISSION: 'prm',
|
|
19
|
+
|
|
20
|
+
/** Correlation ID prefix (e.g., for tracing requests) */
|
|
21
|
+
CORRELATION: 'crln',
|
|
22
|
+
|
|
23
|
+
/** Verification entity ID prefix (e.g., email/phone code) */
|
|
24
|
+
VERIFICATION: 'vrf',
|
|
25
|
+
|
|
26
|
+
/** Role-permissions mapping ID prefix */
|
|
27
|
+
ROLE_PERMISSIONS: 'role',
|
|
28
|
+
})
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export * from './core/index.js'
|
|
2
|
+
export * from './crypto/index.js'
|
|
3
|
+
export * from './logger/index.js'
|
|
1
4
|
export * from './fastify/index.js'
|
|
2
5
|
export * from './mongodb/index.js'
|
|
3
|
-
export * from './mongodb/connect.js'
|
|
4
6
|
export * from './rabbit-mq/index.js'
|
|
5
7
|
export * as http from './http/http.js'
|
|
6
8
|
export * from './http/responseType.js'
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import pino from 'pino'
|
|
2
|
+
|
|
3
|
+
const requiredMethods = ['info', 'warn', 'error', 'debug']
|
|
4
|
+
|
|
5
|
+
const dummyLogger = {
|
|
6
|
+
info: () => {},
|
|
7
|
+
warn: () => {},
|
|
8
|
+
error: () => {},
|
|
9
|
+
debug: () => {},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {any} logger
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
const isValidLogger = (logger) =>
|
|
17
|
+
typeof logger === 'object' &&
|
|
18
|
+
requiredMethods.every((method) => typeof logger[method] === 'function')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates or returns a logger instance based on user-provided options.
|
|
22
|
+
*
|
|
23
|
+
* Supported usages:
|
|
24
|
+
* - `undefined` → pino with level 'info'
|
|
25
|
+
* - `true` → pino with level 'info'
|
|
26
|
+
* - `{ logger: true, level?: string }` → pino with given level
|
|
27
|
+
* - `{ logger: LoggerObject }` → use provided logger
|
|
28
|
+
* - fallback → dummy logger
|
|
29
|
+
*
|
|
30
|
+
* @param {true | Object | undefined} option
|
|
31
|
+
* @returns {import('pino').Logger | typeof dummyLogger}
|
|
32
|
+
*/
|
|
33
|
+
export const getLogger = (option) => {
|
|
34
|
+
// No option provided or just true: use pino default
|
|
35
|
+
if (option === undefined || option === true) {
|
|
36
|
+
const logger = pino({ level: 'info' })
|
|
37
|
+
logger.info(
|
|
38
|
+
'[getLogger] No logger config provided. Using default Pino logger (level: info).',
|
|
39
|
+
)
|
|
40
|
+
return logger
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Option object provided
|
|
44
|
+
if (typeof option === 'object') {
|
|
45
|
+
const { logger, level } = option
|
|
46
|
+
|
|
47
|
+
// User asked for internal pino with level
|
|
48
|
+
if (logger === true) {
|
|
49
|
+
const pinoLogger = pino({ level: level || 'info' })
|
|
50
|
+
pinoLogger.info(
|
|
51
|
+
`[getLogger] Using internal Pino logger (level: ${level || 'info'}).`,
|
|
52
|
+
)
|
|
53
|
+
return pinoLogger
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// User supplied a custom logger
|
|
57
|
+
if (isValidLogger(logger)) {
|
|
58
|
+
return logger
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Invalid or no-op fallback
|
|
63
|
+
return dummyLogger
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './get-logger.js'
|
|
@@ -2,9 +2,33 @@ import aws from 'aws-sdk'
|
|
|
2
2
|
import nodemailer from 'nodemailer'
|
|
3
3
|
import sgTransport from 'nodemailer-sendgrid-transport'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Factory for creating Nodemailer transporters based on configuration.
|
|
7
|
+
*/
|
|
5
8
|
export class TransportFactory {
|
|
6
9
|
/**
|
|
7
|
-
*
|
|
10
|
+
* Create a Nodemailer transporter
|
|
11
|
+
*
|
|
12
|
+
* @param {object} config - Transport configuration object
|
|
13
|
+
* @param {'smtp' | 'gmail' | 'sendgrid' | 'ses'} config.type - Type of email transport
|
|
14
|
+
*
|
|
15
|
+
* For type 'smtp':
|
|
16
|
+
* @param {string} config.host - SMTP server host
|
|
17
|
+
* @param {number} config.port - SMTP server port
|
|
18
|
+
* @param {boolean} config.secure - Use TLS
|
|
19
|
+
* @param {{ user: string, pass: string }} config.auth - SMTP auth credentials
|
|
20
|
+
*
|
|
21
|
+
* For type 'gmail':
|
|
22
|
+
* @param {{ user: string, pass: string }} config.auth - Gmail credentials
|
|
23
|
+
*
|
|
24
|
+
* For type 'sendgrid':
|
|
25
|
+
* @param {string} config.apiKey - SendGrid API key
|
|
26
|
+
*
|
|
27
|
+
* For type 'ses':
|
|
28
|
+
* @param {string} config.accessKeyId - AWS access key
|
|
29
|
+
* @param {string} config.secretAccessKey - AWS secret
|
|
30
|
+
* @param {string} config.region - AWS region
|
|
31
|
+
*
|
|
8
32
|
* @returns {import('nodemailer').Transporter}
|
|
9
33
|
*/
|
|
10
34
|
static create(config) {
|