@seip/blue-bird 1.1.3 → 1.1.5

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/core/hash.js CHANGED
@@ -1,201 +1,201 @@
1
- import crypto from "node:crypto";
2
-
3
- let bcryptModule = null;
4
- try {
5
- bcryptModule = (await import("bcrypt")).default || (await import("bcrypt"));
6
- } catch {
7
- // bcrypt not installed, scrypt native is used
8
- }
9
-
10
- /**
11
- * High-performance Password Hashing class.
12
- * Uses native node:crypto scrypt by default (zero npm dependencies, NIST recommended),
13
- * with seamless support for bcrypt when installed or verifying bcrypt hashes.
14
- */
15
- class Hash {
16
- /**
17
- * Hashes a plain text password using scrypt (default) or bcrypt.
18
- * @param {string} password - The plain text password.
19
- * @param {Object} [options={}] - Hashing options.
20
- * @param {string} [options.driver="scrypt"] - Hashing driver ('scrypt' or 'bcrypt').
21
- * @param {number} [options.rounds=10] - Salt rounds for bcrypt (if driver is 'bcrypt').
22
- * @param {number} [options.N=16384] - CPU/memory cost parameter for scrypt.
23
- * @param {number} [options.r=8] - Block size for scrypt.
24
- * @param {number} [options.p=1] - Parallelization parameter for scrypt.
25
- * @returns {Promise<string>} Formatted password hash string.
26
- * @example
27
- * const hash = await Hash.make("mySecretPassword");
28
- * // Using bcrypt:
29
- * const bcryptHash = await Hash.make("mySecretPassword", { driver: "bcrypt" });
30
- */
31
- static async make(password, options = {}) {
32
- if (typeof password !== "string" || !password) {
33
- throw new Error("[HASH ERROR] Password must be a non-empty string.");
34
- }
35
-
36
- const driver = (options.driver || "scrypt").toLowerCase();
37
-
38
- if (driver === "bcrypt") {
39
- if (!bcryptModule) {
40
- throw new Error(
41
- "[HASH ERROR] 'bcrypt' package is not installed. Run 'npm install bcrypt' or 'npx blue-bird add bcrypt', or use the default scrypt driver.",
42
- );
43
- }
44
- const rounds = options.rounds || 10;
45
- return bcryptModule.hash(password, rounds);
46
- }
47
-
48
- // Default: native scrypt with random salt
49
- const N = options.N || 16384;
50
- const r = options.r || 8;
51
- const p = options.p || 1;
52
- const keylen = 64;
53
- const salt = crypto.randomBytes(16);
54
-
55
- return new Promise((resolve, reject) => {
56
- crypto.scrypt(
57
- password,
58
- salt,
59
- keylen,
60
- { N, r, p, maxmem: 32 * 1024 * 1024 },
61
- (err, derivedKey) => {
62
- if (err) return reject(err);
63
- const hashString = `$scrypt$N=${N},r=${r},p=${p}$${salt.toString("hex")}$${derivedKey.toString("hex")}`;
64
- resolve(hashString);
65
- },
66
- );
67
- });
68
- }
69
-
70
- /**
71
- * Alias for make().
72
- * @param {string} password - The plain text password.
73
- * @param {Object} [options={}] - Options.
74
- * @returns {Promise<string>}
75
- */
76
- static async hash(password, options = {}) {
77
- return this.make(password, options);
78
- }
79
-
80
- /**
81
- * Verifies a plain text password against a hash string.
82
- * Automatically detects scrypt or bcrypt hash formats.
83
- * Uses timing-safe comparison to protect against side-channel attacks.
84
- *
85
- * @param {string} password - The plain text password to check.
86
- * @param {string} hash - The stored hash string.
87
- * @returns {Promise<boolean>} True if password matches, false otherwise.
88
- * @example
89
- * const isValid = await Hash.verify("mySecretPassword", storedHash);
90
- */
91
- static async verify(password, hash) {
92
- if (
93
- typeof password !== "string" ||
94
- !password ||
95
- typeof hash !== "string" ||
96
- !hash
97
- ) {
98
- return false;
99
- }
100
-
101
- // 1. Detect bcrypt hash format ($2a$, $2b$, $2y$)
102
- if (/^\$2[aby]\$\d{2}\$/.test(hash)) {
103
- if (!bcryptModule) {
104
- console.error(
105
- "[HASH ERROR] A bcrypt hash was detected, but the 'bcrypt' package is not installed. Run 'npm install bcrypt' or 'npx blue-bird add bcrypt'.",
106
- );
107
- return false;
108
- }
109
- try {
110
- return await bcryptModule.compare(password, hash);
111
- } catch {
112
- return false;
113
- }
114
- }
115
-
116
- // 2. Detect native scrypt format ($scrypt$N=...,r=...,p=...$salt$hash)
117
- if (hash.startsWith("$scrypt$")) {
118
- const parts = hash.split("$");
119
- // Format: ["", "scrypt", "N=16384,r=8,p=1", "saltHex", "hashHex"]
120
- if (parts.length !== 5) return false;
121
-
122
- const paramsStr = parts[2];
123
- const saltHex = parts[3];
124
- const originalHashHex = parts[4];
125
-
126
- if (!paramsStr || !saltHex || !originalHashHex) return false;
127
-
128
- let N = 16384,
129
- r = 8,
130
- p = 1;
131
- paramsStr.split(",").forEach((param) => {
132
- const [k, v] = param.split("=");
133
- if (k === "N") N = parseInt(v, 10) || N;
134
- if (k === "r") r = parseInt(v, 10) || r;
135
- if (k === "p") p = parseInt(v, 10) || p;
136
- });
137
-
138
- const salt = Buffer.from(saltHex, "hex");
139
- const originalHash = Buffer.from(originalHashHex, "hex");
140
-
141
- return new Promise((resolve) => {
142
- crypto.scrypt(
143
- password,
144
- salt,
145
- originalHash.length,
146
- { N, r, p, maxmem: 32 * 1024 * 1024 },
147
- (err, derivedKey) => {
148
- if (err) return resolve(false);
149
- try {
150
- const matches = crypto.timingSafeEqual(originalHash, derivedKey);
151
- resolve(matches);
152
- } catch {
153
- resolve(false);
154
- }
155
- },
156
- );
157
- });
158
- }
159
-
160
- return false;
161
- }
162
-
163
- /**
164
- * Alias for verify().
165
- * @param {string} password - The plain text password.
166
- * @param {string} hash - The stored hash string.
167
- * @returns {Promise<boolean>}
168
- */
169
- static async check(password, hash) {
170
- return this.verify(password, hash);
171
- }
172
-
173
- /**
174
- * Checks if a given hash needs to be rehashed to match updated security parameters.
175
- * @param {string} hash - The stored hash string.
176
- * @param {Object} [options={}] - Target options.
177
- * @returns {boolean} True if the hash should be regenerated.
178
- */
179
- static needsRehash(hash, options = {}) {
180
- if (!hash || typeof hash !== "string") return true;
181
- const targetDriver = (options.driver || "scrypt").toLowerCase();
182
-
183
- if (targetDriver === "bcrypt") {
184
- return !/^\$2[aby]\$\d{2}\$/.test(hash);
185
- }
186
-
187
- if (!hash.startsWith("$scrypt$")) return true;
188
-
189
- const parts = hash.split("$");
190
- if (parts.length !== 5) return true;
191
-
192
- const paramsStr = parts[2];
193
- const targetN = options.N || 16384;
194
- const targetR = options.r || 8;
195
- const targetP = options.p || 1;
196
-
197
- return !paramsStr.includes(`N=${targetN},r=${targetR},p=${targetP}`);
198
- }
199
- }
200
-
201
- export default Hash;
1
+ import crypto from "node:crypto";
2
+
3
+ let bcryptModule = null;
4
+ try {
5
+ bcryptModule = (await import("bcrypt")).default || (await import("bcrypt"));
6
+ } catch {
7
+ // bcrypt not installed, scrypt native is used
8
+ }
9
+
10
+ /**
11
+ * High-performance Password Hashing class.
12
+ * Uses native node:crypto scrypt by default (zero npm dependencies, NIST recommended),
13
+ * with seamless support for bcrypt when installed or verifying bcrypt hashes.
14
+ */
15
+ class Hash {
16
+ /**
17
+ * Hashes a plain text password using scrypt (default) or bcrypt.
18
+ * @param {string} password - The plain text password.
19
+ * @param {Object} [options={}] - Hashing options.
20
+ * @param {string} [options.driver="scrypt"] - Hashing driver ('scrypt' or 'bcrypt').
21
+ * @param {number} [options.rounds=10] - Salt rounds for bcrypt (if driver is 'bcrypt').
22
+ * @param {number} [options.N=16384] - CPU/memory cost parameter for scrypt.
23
+ * @param {number} [options.r=8] - Block size for scrypt.
24
+ * @param {number} [options.p=1] - Parallelization parameter for scrypt.
25
+ * @returns {Promise<string>} Formatted password hash string.
26
+ * @example
27
+ * const hash = await Hash.make("mySecretPassword");
28
+ * // Using bcrypt:
29
+ * const bcryptHash = await Hash.make("mySecretPassword", { driver: "bcrypt" });
30
+ */
31
+ static async make(password, options = {}) {
32
+ if (typeof password !== "string" || !password) {
33
+ throw new Error("[HASH ERROR] Password must be a non-empty string.");
34
+ }
35
+
36
+ const driver = (options.driver || "scrypt").toLowerCase();
37
+
38
+ if (driver === "bcrypt") {
39
+ if (!bcryptModule) {
40
+ throw new Error(
41
+ "[HASH ERROR] 'bcrypt' package is not installed. Run 'npm install bcrypt' or 'npx blue-bird add bcrypt', or use the default scrypt driver.",
42
+ );
43
+ }
44
+ const rounds = options.rounds || 10;
45
+ return bcryptModule.hash(password, rounds);
46
+ }
47
+
48
+ // Default: native scrypt with random salt
49
+ const N = options.N || 16384;
50
+ const r = options.r || 8;
51
+ const p = options.p || 1;
52
+ const keylen = 64;
53
+ const salt = crypto.randomBytes(16);
54
+
55
+ return new Promise((resolve, reject) => {
56
+ crypto.scrypt(
57
+ password,
58
+ salt,
59
+ keylen,
60
+ { N, r, p, maxmem: 32 * 1024 * 1024 },
61
+ (err, derivedKey) => {
62
+ if (err) return reject(err);
63
+ const hashString = `$scrypt$N=${N},r=${r},p=${p}$${salt.toString("hex")}$${derivedKey.toString("hex")}`;
64
+ resolve(hashString);
65
+ },
66
+ );
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Alias for make().
72
+ * @param {string} password - The plain text password.
73
+ * @param {Object} [options={}] - Options.
74
+ * @returns {Promise<string>}
75
+ */
76
+ static async hash(password, options = {}) {
77
+ return this.make(password, options);
78
+ }
79
+
80
+ /**
81
+ * Verifies a plain text password against a hash string.
82
+ * Automatically detects scrypt or bcrypt hash formats.
83
+ * Uses timing-safe comparison to protect against side-channel attacks.
84
+ *
85
+ * @param {string} password - The plain text password to check.
86
+ * @param {string} hash - The stored hash string.
87
+ * @returns {Promise<boolean>} True if password matches, false otherwise.
88
+ * @example
89
+ * const isValid = await Hash.verify("mySecretPassword", storedHash);
90
+ */
91
+ static async verify(password, hash) {
92
+ if (
93
+ typeof password !== "string" ||
94
+ !password ||
95
+ typeof hash !== "string" ||
96
+ !hash
97
+ ) {
98
+ return false;
99
+ }
100
+
101
+ // 1. Detect bcrypt hash format ($2a$, $2b$, $2y$)
102
+ if (/^\$2[aby]\$\d{2}\$/.test(hash)) {
103
+ if (!bcryptModule) {
104
+ console.error(
105
+ "[HASH ERROR] A bcrypt hash was detected, but the 'bcrypt' package is not installed. Run 'npm install bcrypt' or 'npx blue-bird add bcrypt'.",
106
+ );
107
+ return false;
108
+ }
109
+ try {
110
+ return await bcryptModule.compare(password, hash);
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ // 2. Detect native scrypt format ($scrypt$N=...,r=...,p=...$salt$hash)
117
+ if (hash.startsWith("$scrypt$")) {
118
+ const parts = hash.split("$");
119
+ // Format: ["", "scrypt", "N=16384,r=8,p=1", "saltHex", "hashHex"]
120
+ if (parts.length !== 5) return false;
121
+
122
+ const paramsStr = parts[2];
123
+ const saltHex = parts[3];
124
+ const originalHashHex = parts[4];
125
+
126
+ if (!paramsStr || !saltHex || !originalHashHex) return false;
127
+
128
+ let N = 16384,
129
+ r = 8,
130
+ p = 1;
131
+ paramsStr.split(",").forEach((param) => {
132
+ const [k, v] = param.split("=");
133
+ if (k === "N") N = parseInt(v, 10) || N;
134
+ if (k === "r") r = parseInt(v, 10) || r;
135
+ if (k === "p") p = parseInt(v, 10) || p;
136
+ });
137
+
138
+ const salt = Buffer.from(saltHex, "hex");
139
+ const originalHash = Buffer.from(originalHashHex, "hex");
140
+
141
+ return new Promise((resolve) => {
142
+ crypto.scrypt(
143
+ password,
144
+ salt,
145
+ originalHash.length,
146
+ { N, r, p, maxmem: 32 * 1024 * 1024 },
147
+ (err, derivedKey) => {
148
+ if (err) return resolve(false);
149
+ try {
150
+ const matches = crypto.timingSafeEqual(originalHash, derivedKey);
151
+ resolve(matches);
152
+ } catch {
153
+ resolve(false);
154
+ }
155
+ },
156
+ );
157
+ });
158
+ }
159
+
160
+ return false;
161
+ }
162
+
163
+ /**
164
+ * Alias for verify().
165
+ * @param {string} password - The plain text password.
166
+ * @param {string} hash - The stored hash string.
167
+ * @returns {Promise<boolean>}
168
+ */
169
+ static async check(password, hash) {
170
+ return this.verify(password, hash);
171
+ }
172
+
173
+ /**
174
+ * Checks if a given hash needs to be rehashed to match updated security parameters.
175
+ * @param {string} hash - The stored hash string.
176
+ * @param {Object} [options={}] - Target options.
177
+ * @returns {boolean} True if the hash should be regenerated.
178
+ */
179
+ static needsRehash(hash, options = {}) {
180
+ if (!hash || typeof hash !== "string") return true;
181
+ const targetDriver = (options.driver || "scrypt").toLowerCase();
182
+
183
+ if (targetDriver === "bcrypt") {
184
+ return !/^\$2[aby]\$\d{2}\$/.test(hash);
185
+ }
186
+
187
+ if (!hash.startsWith("$scrypt$")) return true;
188
+
189
+ const parts = hash.split("$");
190
+ if (parts.length !== 5) return true;
191
+
192
+ const paramsStr = parts[2];
193
+ const targetN = options.N || 16384;
194
+ const targetR = options.r || 8;
195
+ const targetP = options.p || 1;
196
+
197
+ return !paramsStr.includes(`N=${targetN},r=${targetR},p=${targetP}`);
198
+ }
199
+ }
200
+
201
+ export default Hash;
package/core/index.d.ts CHANGED
@@ -190,5 +190,12 @@ export class Database {
190
190
  close(): Promise<void>;
191
191
  }
192
192
 
193
+ export class Queue {
194
+ static process(jobName: string, handler: (payload: any) => Promise<any> | any): void;
195
+ static dispatch(jobName: string, payload?: any, options?: { delayMs?: number }): Promise<boolean>;
196
+ static loadJobs(jobsDir?: string): Promise<void>;
197
+ }
198
+
193
199
  export default App;
194
200
 
201
+
package/core/queue.js ADDED
@@ -0,0 +1,121 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getRedisClient } from "./cache.js";
4
+
5
+ /**
6
+ * Lightweight background queue worker module with Redis and in-memory fallback.
7
+ */
8
+ class QueueManager {
9
+ constructor() {
10
+ this.handlers = new Map();
11
+ this.memoryQueue = [];
12
+ this.isProcessing = false;
13
+ this.redisPrefix = "bluebird:queue:";
14
+ }
15
+
16
+ /**
17
+ * Registers a job handler function.
18
+ * @param {string} jobName - Name of the job.
19
+ * @param {Function} handler - Async function(payload).
20
+ */
21
+ process(jobName, handler) {
22
+ if (typeof handler !== "function") {
23
+ throw new Error(`Handler for job '${jobName}' must be a function.`);
24
+ }
25
+ this.handlers.set(jobName, handler);
26
+ }
27
+
28
+ /**
29
+ * Dispatches a new job to the queue.
30
+ * @param {string} jobName - Name of the job.
31
+ * @param {any} payload - Data payload to pass to the handler.
32
+ * @param {object} [options] - Options (e.g. delayMs).
33
+ * @returns {Promise<boolean>}
34
+ */
35
+ async dispatch(jobName, payload = {}, options = {}) {
36
+ const jobItem = {
37
+ id: `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
38
+ name: jobName,
39
+ payload,
40
+ createdAt: new Date().toISOString(),
41
+ };
42
+
43
+ const redis = getRedisClient();
44
+ if (redis && redis.isReady) {
45
+ try {
46
+ await redis.lPush(`${this.redisPrefix}jobs`, JSON.stringify(jobItem));
47
+ return true;
48
+ } catch (err) {
49
+ console.error("[QUEUE ERROR] Failed to dispatch job to Redis:", err.message);
50
+ }
51
+ }
52
+
53
+ // In-memory fallback
54
+ if (options.delayMs && options.delayMs > 0) {
55
+ setTimeout(() => {
56
+ this.memoryQueue.push(jobItem);
57
+ this.runMemoryWorker();
58
+ }, options.delayMs);
59
+ } else {
60
+ this.memoryQueue.push(jobItem);
61
+ setImmediate(() => this.runMemoryWorker());
62
+ }
63
+
64
+ return true;
65
+ }
66
+
67
+ /**
68
+ * Executes in-memory queue jobs sequentially.
69
+ * @private
70
+ */
71
+ async runMemoryWorker() {
72
+ if (this.isProcessing || this.memoryQueue.length === 0) return;
73
+ this.isProcessing = true;
74
+
75
+ while (this.memoryQueue.length > 0) {
76
+ const job = this.memoryQueue.shift();
77
+ if (!job) continue;
78
+
79
+ const handler = this.handlers.get(job.name);
80
+ if (!handler) {
81
+ console.warn(`[QUEUE WARN] No handler registered for job '${job.name}'.`);
82
+ continue;
83
+ }
84
+
85
+ try {
86
+ await handler(job.payload);
87
+ } catch (err) {
88
+ console.error(`[QUEUE ERROR] Error processing job '${job.name}' (${job.id}):`, err);
89
+ }
90
+ }
91
+
92
+ this.isProcessing = false;
93
+ }
94
+
95
+ /**
96
+ * Auto-loads all job definition files from backend/jobs/.
97
+ */
98
+ async loadJobs(jobsDir = path.resolve(process.cwd(), "backend/jobs")) {
99
+ if (!fs.existsSync(jobsDir)) return;
100
+
101
+ const files = fs
102
+ .readdirSync(jobsDir)
103
+ .filter((f) => f.endsWith(".js") || f.endsWith(".mjs"));
104
+
105
+ for (const file of files) {
106
+ const fullPath = path.join(jobsDir, file);
107
+ try {
108
+ const module = await import(`file://${fullPath}`);
109
+ if (typeof module.default === "function") {
110
+ const jobName = path.basename(file, path.extname(file));
111
+ this.process(jobName, module.default);
112
+ }
113
+ } catch (err) {
114
+ console.error(`[QUEUE ERROR] Failed to load job file '${file}':`, err.message);
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ export const Queue = new QueueManager();
121
+ export default Queue;
package/core/validate.js CHANGED
@@ -148,19 +148,20 @@ class Validator {
148
148
  * const result = await loginValidator.validate(req);
149
149
  */
150
150
  async validate(req) {
151
+ const isExpressReq = req && (req.body !== undefined || req.headers !== undefined);
151
152
  let lang =
152
153
  req?.body?.lang ||
153
154
  req?.query?.lang ||
154
155
  req?.params?.lang ||
155
156
  req?.cookies?.lang ||
156
- req?.headers["accept-language"]?.split(",")[0]?.split("-")[0] ||
157
+ req?.headers?.["accept-language"]?.split(",")[0]?.split("-")[0] ||
157
158
  req?.session?.lang ||
158
159
  this.lang_default ||
159
160
  "es";
160
161
  const msg = this.messages[lang] || this.messages.es;
161
162
  const errors = [];
162
163
  const messages = [];
163
- const body = req.body || {};
164
+ const body = isExpressReq ? (req.body || {}) : (req || {});
164
165
 
165
166
  for (const [field, config] of Object.entries(this.schema)) {
166
167
  let value = body[field];
@@ -54,6 +54,16 @@ services:
54
54
  - "${DB_PORT:-3306}:3306"
55
55
  volumes:
56
56
  - mysql_data:/var/lib/mysql
57
+ command: >
58
+ --default-authentication-plugin=mysql_native_password
59
+ --character-set-server=utf8mb4
60
+ --collation-server=utf8mb4_unicode_ci
61
+ --performance-schema=OFF
62
+ --innodb-buffer-pool-size=64M
63
+ --innodb-log-buffer-size=8M
64
+ --max-connections=50
65
+ --table-open-cache=200
66
+ --skip-log-bin
57
67
  healthcheck:
58
68
  test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD:-root}" ]
59
69
  interval: 10s