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 +54 -0
- package/fileSystem.js +130 -30
- package/optimizeMedia.js +53 -6
- package/package.json +1 -1
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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
|
|
389
|
+
return downloadHttpWithMetadata(trimmed, {
|
|
390
|
+
timeoutMs,
|
|
391
|
+
maxBytes,
|
|
392
|
+
maxRedirects,
|
|
393
|
+
});
|
|
345
394
|
}
|
|
346
395
|
|
|
347
396
|
const absPath = resolvePath(trimmed);
|
|
348
|
-
|
|
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
|
|
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
|
|
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
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
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
|
-
|
|
474
|
+
const onData = (chunk) => {
|
|
398
475
|
received += chunk.length;
|
|
399
476
|
if (received > options.maxBytes) {
|
|
400
|
-
|
|
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("
|
|
411
|
-
|
|
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
|
-
|
|
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",
|
|
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).
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|