b2b-platform-utils 1.1.64 → 1.1.68

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/errorsMap.js CHANGED
@@ -941,6 +941,60 @@ const STATIC_ERRORS = [
941
941
  httpCode: 503,
942
942
  description: "Provider request is temporarily throttled, try again later.",
943
943
  },
944
+ {
945
+ errorCode: 1154,
946
+ errorKey: "twoFaAlreadyEnabled",
947
+ httpCode: 422,
948
+ description: "Two factor authentication is already enabled.",
949
+ },
950
+ {
951
+ errorCode: 1155,
952
+ errorKey: "twoFaNotEnabled",
953
+ httpCode: 422,
954
+ description: "Two factor authentication is not enabled.",
955
+ },
956
+ {
957
+ errorCode: 1156,
958
+ errorKey: "twoFaSetupRequired",
959
+ httpCode: 422,
960
+ description: "Two factor authentication setup is required.",
961
+ },
962
+ {
963
+ errorCode: 1157,
964
+ errorKey: "twoFaChallengeNotFound",
965
+ httpCode: 404,
966
+ description: "Two factor authentication challenge was not found.",
967
+ },
968
+ {
969
+ errorCode: 1158,
970
+ errorKey: "twoFaChallengeExpired",
971
+ httpCode: 422,
972
+ description: "Two factor authentication challenge is expired.",
973
+ },
974
+ {
975
+ errorCode: 1159,
976
+ errorKey: "twoFaTooManyAttempts",
977
+ httpCode: 429,
978
+ description: "Too many two factor authentication attempts.",
979
+ },
980
+ {
981
+ errorCode: 1160,
982
+ errorKey: "twoFaInvalidPurpose",
983
+ httpCode: 422,
984
+ description: "Two factor authentication purpose is missing or invalid.",
985
+ },
986
+ {
987
+ errorCode: 1161,
988
+ errorKey: "twoFaPendingSecretMissing",
989
+ httpCode: 422,
990
+ description: "Two factor authentication pending secret is missing.",
991
+ },
992
+ {
993
+ errorCode: 1162,
994
+ errorKey: "twoFaSecretMissing",
995
+ httpCode: 422,
996
+ description: "Two factor authentication secret is missing.",
997
+ },
944
998
  ];
945
999
 
946
1000
  const STATIC_BY_KEY = Object.fromEntries(
package/fileSystem.js CHANGED
@@ -311,6 +311,42 @@ function getAbsoluteMediaResourceUrl(service, instance, fileName) {
311
311
  return `${baseUrl}/file/${service}/${instance}/${fileName}`;
312
312
  }
313
313
 
314
+ /**
315
+ * Normalize an HTTP Content-Type header value.
316
+ *
317
+ * @param {string|undefined|null} contentType
318
+ * @returns {string}
319
+ */
320
+ function normalizeContentType(contentType) {
321
+ if (contentType == null || contentType === "") {
322
+ return "";
323
+ }
324
+
325
+ const normalized = String(contentType).trim().toLowerCase();
326
+ const semicolonIndex = normalized.indexOf(";");
327
+ return semicolonIndex >= 0
328
+ ? normalized.slice(0, semicolonIndex).trim()
329
+ : normalized;
330
+ }
331
+
332
+ /**
333
+ * Resolve download options with existing defaults.
334
+ *
335
+ * @param {Object} [options]
336
+ * @returns {{ timeoutMs: number, maxBytes: number, maxRedirects: number }}
337
+ */
338
+ function resolveDownloadOptions(options = {}) {
339
+ return {
340
+ timeoutMs: Number.isInteger(options.timeoutMs) ? options.timeoutMs : 15000,
341
+ maxBytes: Number.isInteger(options.maxBytes)
342
+ ? options.maxBytes
343
+ : 25 * 1024 * 1024,
344
+ maxRedirects: Number.isInteger(options.maxRedirects)
345
+ ? options.maxRedirects
346
+ : 3,
347
+ };
348
+ }
349
+
314
350
  /**
315
351
  * Download remote resource to Buffer or read local file to Buffer.
316
352
  *
@@ -322,50 +358,85 @@ function getAbsoluteMediaResourceUrl(service, instance, fileName) {
322
358
  * @returns {Promise<Buffer>}
323
359
  */
324
360
  function downloadToBuffer(source, options = {}) {
325
- const timeoutMs = Number.isInteger(options.timeoutMs)
326
- ? options.timeoutMs
327
- : 15000;
328
- const maxBytes = Number.isInteger(options.maxBytes)
329
- ? options.maxBytes
330
- : 25 * 1024 * 1024;
331
- const maxRedirects = Number.isInteger(options.maxRedirects)
332
- ? options.maxRedirects
333
- : 3;
361
+ return downloadToBufferWithMetadata(source, options).then(
362
+ (result) => result.buffer
363
+ );
364
+ }
365
+
366
+ /**
367
+ * Download remote resource or read local file with response metadata.
368
+ *
369
+ * @param {string} source - URL (http/https) or local file path.
370
+ * @param {Object} [options]
371
+ * @param {number} [options.timeoutMs=15000] - Request timeout.
372
+ * @param {number} [options.maxBytes=26214400] - Max allowed size in bytes (default 25MB).
373
+ * @param {number} [options.maxRedirects=3] - Max redirects to follow.
374
+ * @returns {Promise<{ buffer: Buffer, contentType: string, finalUrl: string }>}
375
+ */
376
+ async function downloadToBufferWithMetadata(source, options = {}) {
377
+ const { timeoutMs, maxBytes, maxRedirects } =
378
+ resolveDownloadOptions(options);
334
379
 
335
380
  if (!source || typeof source !== "string") {
336
- return Promise.reject(
337
- new TypeError("downloadToBuffer: source must be a non-empty string")
381
+ throw new TypeError(
382
+ "downloadToBuffer: source must be a non-empty string"
338
383
  );
339
384
  }
340
385
 
341
386
  const trimmed = source.trim();
342
387
 
343
388
  if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
344
- return downloadHttpToBuffer(trimmed, { timeoutMs, maxBytes, maxRedirects });
389
+ return downloadHttpWithMetadata(trimmed, {
390
+ timeoutMs,
391
+ maxBytes,
392
+ maxRedirects,
393
+ });
345
394
  }
346
395
 
347
396
  const absPath = resolvePath(trimmed);
348
- return fsPromises.readFile(absPath);
397
+ const buffer = await fsPromises.readFile(absPath);
398
+
399
+ return {
400
+ buffer,
401
+ contentType: getMimeTypeByFileExtension(absPath),
402
+ finalUrl: absPath,
403
+ };
349
404
  }
350
405
 
351
406
  /**
352
- * Download an HTTP/HTTPS resource to Buffer with basic redirect support.
407
+ * Download an HTTP/HTTPS resource with metadata and basic redirect support.
353
408
  *
354
409
  * @param {string} url
355
410
  * @param {Object} options
356
411
  * @param {number} options.timeoutMs
357
412
  * @param {number} options.maxBytes
358
413
  * @param {number} options.maxRedirects
359
- * @returns {Promise<Buffer>}
414
+ * @returns {Promise<{ buffer: Buffer, contentType: string, finalUrl: string }>}
360
415
  */
361
- function downloadHttpToBuffer(url, options) {
416
+ function downloadHttpWithMetadata(url, options) {
362
417
  return new Promise((resolve, reject) => {
363
418
  const client = url.startsWith("https://") ? https : http;
419
+ let settled = false;
420
+ let resRef = null;
421
+
422
+ const finalize = (err, result) => {
423
+ if (settled) {
424
+ return;
425
+ }
426
+ settled = true;
427
+
428
+ if (err) {
429
+ reject(err instanceof Error ? err : new Error(String(err)));
430
+ return;
431
+ }
432
+
433
+ resolve(result);
434
+ };
364
435
 
365
436
  const req = client.get(url, (res) => {
437
+ resRef = res;
366
438
  const statusCode = res.statusCode || 0;
367
439
 
368
- // Follow redirects
369
440
  if (
370
441
  statusCode >= 300 &&
371
442
  statusCode < 400 &&
@@ -374,17 +445,23 @@ function downloadHttpToBuffer(url, options) {
374
445
  ) {
375
446
  res.resume();
376
447
  const nextUrl = new URL(res.headers.location, url).toString();
377
- return resolve(
378
- downloadHttpToBuffer(nextUrl, {
379
- ...options,
380
- maxRedirects: options.maxRedirects - 1,
381
- })
382
- );
448
+ return downloadHttpWithMetadata(nextUrl, {
449
+ ...options,
450
+ maxRedirects: options.maxRedirects - 1,
451
+ })
452
+ .then((result) => finalize(null, result))
453
+ .catch((err) =>
454
+ finalize(
455
+ new Error(
456
+ err?.details || err?.message || err
457
+ )
458
+ )
459
+ );
383
460
  }
384
461
 
385
462
  if (statusCode < 200 || statusCode >= 300) {
386
463
  res.resume();
387
- return reject(
464
+ return finalize(
388
465
  new Error(
389
466
  `downloadToBuffer: request failed with status ${statusCode}`
390
467
  )
@@ -394,10 +471,13 @@ function downloadHttpToBuffer(url, options) {
394
471
  const chunks = [];
395
472
  let received = 0;
396
473
 
397
- res.on("data", (chunk) => {
474
+ const onData = (chunk) => {
398
475
  received += chunk.length;
399
476
  if (received > options.maxBytes) {
400
- req.destroy(
477
+ res.removeListener("data", onData);
478
+ res.destroy();
479
+ req.destroy();
480
+ finalize(
401
481
  new Error(
402
482
  `downloadToBuffer: maxBytes exceeded (${options.maxBytes})`
403
483
  )
@@ -405,19 +485,38 @@ function downloadHttpToBuffer(url, options) {
405
485
  return;
406
486
  }
407
487
  chunks.push(chunk);
488
+ };
489
+
490
+ res.on("data", onData);
491
+
492
+ res.on("end", () => {
493
+ if (settled) {
494
+ return;
495
+ }
496
+
497
+ finalize(null, {
498
+ buffer: Buffer.concat(chunks),
499
+ contentType: normalizeContentType(res.headers["content-type"]),
500
+ finalUrl: url,
501
+ });
408
502
  });
409
503
 
410
- res.on("end", () => resolve(Buffer.concat(chunks)));
411
- res.on("error", reject);
504
+ res.on("error", (err) =>
505
+ finalize(new Error(err?.details || err?.message || err))
506
+ );
412
507
  });
413
508
 
414
509
  req.setTimeout(options.timeoutMs, () => {
415
- req.destroy(
510
+ resRef?.destroy();
511
+ req.destroy();
512
+ finalize(
416
513
  new Error(`downloadToBuffer: timeout after ${options.timeoutMs}ms`)
417
514
  );
418
515
  });
419
516
 
420
- req.on("error", reject);
517
+ req.on("error", (err) =>
518
+ finalize(new Error(err?.details || err?.message || err))
519
+ );
421
520
  });
422
521
  }
423
522
 
@@ -434,6 +533,7 @@ module.exports = {
434
533
  removeFile,
435
534
  unlinkIfExists,
436
535
  downloadToBuffer,
536
+ downloadToBufferWithMetadata,
437
537
 
438
538
  // MIME helpers
439
539
  getImageExtension,
package/optimizeMedia.js CHANGED
@@ -83,13 +83,40 @@ async function saveGameImageBufferWEBPWithOptions(fileDataBuffer, options = {})
83
83
  }
84
84
 
85
85
  const WEBP_CONVERSION_FORMATS = new Set(['jpeg', 'png', 'webp']);
86
+ const MANDATORY_PASSTHROUGH_FORMATS = new Set(['gif', 'avif', 'svg']);
87
+
88
+ const MIME_TO_EXTENSION = {
89
+ 'image/jpeg': 'jpeg',
90
+ 'image/jpg': 'jpeg',
91
+ 'image/png': 'png',
92
+ 'image/webp': 'webp',
93
+ 'image/gif': 'gif',
94
+ 'image/avif': 'avif',
95
+ 'image/svg+xml': 'svg'
96
+ };
86
97
 
87
98
  function normalizeImageFileType(originalFileType) {
88
99
  if (!originalFileType) {
89
100
  return null;
90
101
  }
91
102
 
92
- let type = String(originalFileType).toLowerCase().replace(/^\./, '');
103
+ let type = String(originalFileType).trim().toLowerCase();
104
+
105
+ if (!type) {
106
+ return null;
107
+ }
108
+
109
+ const semicolonIndex = type.indexOf(';');
110
+
111
+ if (semicolonIndex !== -1) {
112
+ type = type.slice(0, semicolonIndex).trim();
113
+ }
114
+
115
+ if (MIME_TO_EXTENSION[type]) {
116
+ return MIME_TO_EXTENSION[type];
117
+ }
118
+
119
+ type = type.replace(/^\./, '');
93
120
 
94
121
  if (type === 'jpg') {
95
122
  type = 'jpeg';
@@ -98,8 +125,27 @@ function normalizeImageFileType(originalFileType) {
98
125
  return type;
99
126
  }
100
127
 
128
+ function buildPassthroughFormats(options) {
129
+ const passthroughSet = new Set(MANDATORY_PASSTHROUGH_FORMATS);
130
+ const configured = options.passthroughFormats;
131
+
132
+ if (!Array.isArray(configured)) {
133
+ return passthroughSet;
134
+ }
135
+
136
+ for (const value of configured) {
137
+ const normalized = normalizeImageFileType(value);
138
+
139
+ if (normalized) {
140
+ passthroughSet.add(normalized);
141
+ }
142
+ }
143
+
144
+ return passthroughSet;
145
+ }
146
+
101
147
  function isKnownFormat(fileType) {
102
- return fileType === 'gif' || fileType === 'avif' || WEBP_CONVERSION_FORMATS.has(fileType);
148
+ return MANDATORY_PASSTHROUGH_FORMATS.has(fileType) || WEBP_CONVERSION_FORMATS.has(fileType);
103
149
  }
104
150
 
105
151
  function hasResizeOptions(options) {
@@ -151,7 +197,7 @@ async function resizePipelineIfNeeded(pipeline, fileDataBuffer, options) {
151
197
  }
152
198
 
153
199
  async function encodeWebpBuffer(fileDataBuffer, options) {
154
- let pipeline = sharp(fileDataBuffer);
200
+ let pipeline = sharp(fileDataBuffer).rotate();
155
201
  pipeline = await resizePipelineIfNeeded(pipeline, fileDataBuffer, options);
156
202
 
157
203
  return pipeline
@@ -166,7 +212,7 @@ async function encodeWebpBuffer(fileDataBuffer, options) {
166
212
  }
167
213
 
168
214
  /**
169
- * Format-aware image optimization: preserves GIF/AVIF, converts JPEG/PNG/WebP to WebP, safe fallback on failure.
215
+ * Format-aware image optimization: preserves GIF/AVIF/SVG, converts JPEG/PNG/WebP to WebP, safe fallback on failure.
170
216
  * @param {Buffer} fileDataBuffer
171
217
  * @param {string} originalFileType
172
218
  * @param {object} [options]
@@ -187,10 +233,11 @@ async function optimizeImageBufferByFormat(fileDataBuffer, originalFileType, opt
187
233
  ...options
188
234
  };
189
235
 
236
+ const passthroughFormats = buildPassthroughFormats(opts);
190
237
  let normalizedType = normalizeImageFileType(originalFileType);
191
238
 
192
239
  try {
193
- if (normalizedType === 'gif' || normalizedType === 'avif') {
240
+ if (normalizedType && passthroughFormats.has(normalizedType)) {
194
241
  return { buffer: fileDataBuffer, fileType: normalizedType, optimized: false };
195
242
  }
196
243
 
@@ -203,7 +250,7 @@ async function optimizeImageBufferByFormat(fileDataBuffer, originalFileType, opt
203
250
  }
204
251
  }
205
252
 
206
- if (normalizedType === 'gif' || normalizedType === 'avif') {
253
+ if (normalizedType && passthroughFormats.has(normalizedType)) {
207
254
  return { buffer: fileDataBuffer, fileType: normalizedType, optimized: false };
208
255
  }
209
256
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "b2b-platform-utils",
3
- "version": "1.1.64",
3
+ "version": "1.1.68",
4
4
  "description": "Shared utilities for Node.js microservices: errors map, local cache, logger, numbers, dates, filesystem, media optimization, paginator, slugger, crypto wrapper, sanitize HTML, sorting.",
5
5
  "type": "commonjs",
6
6
  "license": "KingSizer",