@aunucore/baileys 1.0.83 → 1.0.85
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/lib/Socket/chats.js +11 -5
- package/lib/Store/make-in-memory-store.js +87 -11
- package/lib/Utils/auth-utils.js +27 -3
- package/lib/Utils/baileys-event-stream.js +39 -4
- package/lib/Utils/event-buffer.js +38 -6
- package/lib/Utils/generics.js +20 -1
- package/lib/Utils/index.d.ts +1 -0
- package/lib/Utils/index.js +1 -0
- package/lib/Utils/lid-store.js +4 -1
- package/lib/Utils/link-preview.js +87 -35
- package/lib/Utils/messages-media.js +123 -20
- package/lib/Utils/messages.js +50 -8
- package/lib/Utils/safe-fetch.d.ts +57 -0
- package/lib/Utils/safe-fetch.js +466 -0
- package/package.json +1 -1
|
@@ -97,6 +97,15 @@ export async function getMediaKeys(buffer, mediaType) {
|
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
99
|
/** Extracts video thumb using FFMPEG */
|
|
100
|
+
// AunuBaileys (1.0.85, HB-128): ffmpeg used to run with NO timeout
|
|
101
|
+
// and NO kill, accumulating stderr without bound — a wedged ffmpeg
|
|
102
|
+
// hung generateThumbnail, and with it the Promise.all inside
|
|
103
|
+
// prepareWAMessageMedia, so the video send never settled; a chatty
|
|
104
|
+
// ffmpeg could also grow stderr in memory without limit. Bound
|
|
105
|
+
// both: SIGKILL after the timeout, and keep only the first 64 KiB
|
|
106
|
+
// of stderr (plenty for the error message).
|
|
107
|
+
const FFMPEG_THUMB_TIMEOUT_MS = 30000;
|
|
108
|
+
const FFMPEG_STDERR_CAP_BYTES = 64 * 1024;
|
|
100
109
|
const extractVideoThumb = async (path, destPath, time, size) => new Promise((resolve, reject) => {
|
|
101
110
|
// AunuBaileys: spawn dengan array argumen, tanpa shell (dari @itsliaaa/baileys) —
|
|
102
111
|
// path/waktu/ukuran tidak lagi diinterpolasi ke string perintah shell
|
|
@@ -110,12 +119,33 @@ const extractVideoThumb = async (path, destPath, time, size) => new Promise((res
|
|
|
110
119
|
destPath
|
|
111
120
|
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
112
121
|
const stderrChunks = [];
|
|
113
|
-
|
|
114
|
-
ffmpeg.on('
|
|
122
|
+
let stderrBytes = 0;
|
|
123
|
+
ffmpeg.stderr.on('data', (chunk) => {
|
|
124
|
+
if (stderrBytes >= FFMPEG_STDERR_CAP_BYTES) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const keep = chunk.subarray(0, FFMPEG_STDERR_CAP_BYTES - stderrBytes);
|
|
128
|
+
stderrChunks.push(keep);
|
|
129
|
+
stderrBytes += keep.length;
|
|
130
|
+
});
|
|
131
|
+
let timedOut = false;
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
timedOut = true;
|
|
134
|
+
ffmpeg.kill('SIGKILL');
|
|
135
|
+
}, FFMPEG_THUMB_TIMEOUT_MS);
|
|
136
|
+
timer.unref?.();
|
|
137
|
+
ffmpeg.on('error', (err) => {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
reject(err);
|
|
140
|
+
});
|
|
115
141
|
ffmpeg.on('close', (code) => {
|
|
142
|
+
clearTimeout(timer);
|
|
116
143
|
if (code === 0) {
|
|
117
144
|
resolve();
|
|
118
145
|
}
|
|
146
|
+
else if (timedOut) {
|
|
147
|
+
reject(new Error(`ffmpeg timed out after ${FFMPEG_THUMB_TIMEOUT_MS}ms while extracting a video thumbnail`));
|
|
148
|
+
}
|
|
119
149
|
else {
|
|
120
150
|
reject(new Error(`ffmpeg exited with code ${code}: ${Buffer.concat(stderrChunks).toString('utf8').trim()}`));
|
|
121
151
|
}
|
|
@@ -179,6 +209,10 @@ export const extractImageThumb = async (bufferOrFilePath, width = 32, maxBytes)
|
|
|
179
209
|
}
|
|
180
210
|
};
|
|
181
211
|
export const encodeBase64EncodedStringForUpload = (b64) => encodeURIComponent(b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''));
|
|
212
|
+
// AunuBaileys (1.0.85, HB-219): bounds for the remote fetch in
|
|
213
|
+
// generateProfilePicture (16 MiB matches the product-image cap).
|
|
214
|
+
const MAX_PROFILE_PICTURE_SOURCE_BYTES = 16 * 1024 * 1024;
|
|
215
|
+
const PROFILE_PICTURE_FETCH_TIMEOUT_MS = 30000;
|
|
182
216
|
export const generateProfilePicture = async (mediaUpload, dimensions) => {
|
|
183
217
|
let buffer;
|
|
184
218
|
const { width: w = 640, height: h = 640 } = dimensions || {};
|
|
@@ -187,9 +221,16 @@ export const generateProfilePicture = async (mediaUpload, dimensions) => {
|
|
|
187
221
|
}
|
|
188
222
|
else {
|
|
189
223
|
// Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
224
|
+
// AunuBaileys (1.0.85, HB-219): a remote URL used to be
|
|
225
|
+
// buffered with NO size cap and NO timeout — a huge or
|
|
226
|
+
// never-ending response was accumulated in full before the
|
|
227
|
+
// image decode finally rejected it (verified: 12 MB of junk
|
|
228
|
+
// buffered whole, RSS +53 MB), and a stalled server hung
|
|
229
|
+
// updateProfilePicture forever. Bound both, like the
|
|
230
|
+
// sticker-pack and product-image paths already do.
|
|
231
|
+
const { stream } = await getStream(mediaUpload, { timeout: PROFILE_PICTURE_FETCH_TIMEOUT_MS });
|
|
232
|
+
// Convert the resulting stream to a buffer, with a hard cap
|
|
233
|
+
buffer = await toBufferWithLimit(stream, MAX_PROFILE_PICTURE_SOURCE_BYTES);
|
|
193
234
|
}
|
|
194
235
|
const lib = await getImageProcessingLibrary();
|
|
195
236
|
let img;
|
|
@@ -323,9 +364,20 @@ export const getStream = async (item, opts) => {
|
|
|
323
364
|
if (Buffer.isBuffer(item)) {
|
|
324
365
|
return { stream: toReadable(item), type: 'buffer' };
|
|
325
366
|
}
|
|
367
|
+
// AunuBaileys (1.0.85, HB-208): unusable media used to explode
|
|
368
|
+
// here as raw TypeErrors — 'stream' in undefined/null for a
|
|
369
|
+
// missing media value, a toString deref for an object with no
|
|
370
|
+
// usable url, a createReadStream type error for a numeric url.
|
|
371
|
+
// Reject all of them with a clear Boom 400 instead.
|
|
372
|
+
if (!item || typeof item !== 'object') {
|
|
373
|
+
throw new Boom('Invalid media: expected a Buffer, a stream, or an object with a url', { statusCode: 400 });
|
|
374
|
+
}
|
|
326
375
|
if ('stream' in item) {
|
|
327
376
|
return { stream: item.stream, type: 'readable' };
|
|
328
377
|
}
|
|
378
|
+
if (typeof item.url !== 'string' && !(item.url instanceof URL)) {
|
|
379
|
+
throw new Boom('Invalid media: expected a Buffer, a stream, or an object with a url', { statusCode: 400 });
|
|
380
|
+
}
|
|
329
381
|
const urlStr = item.url.toString();
|
|
330
382
|
if (urlStr.startsWith('data:')) {
|
|
331
383
|
const comma = urlStr.indexOf(',');
|
|
@@ -533,7 +585,17 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
|
|
|
533
585
|
}
|
|
534
586
|
}
|
|
535
587
|
if (streamableVerdict === null) {
|
|
536
|
-
|
|
588
|
+
// AunuBaileys (1.0.85, HB-220): the scan only ever
|
|
589
|
+
// reads the leading ~1 KiB (it decides from box
|
|
590
|
+
// headers/sizes in that prefix and forces a verdict
|
|
591
|
+
// past AUNU_SIDECAR_SCAN_MAX_BYTES), but the WHOLE
|
|
592
|
+
// chunk used to be concatenated into `head` and held
|
|
593
|
+
// for the entire call — and for Buffer input the
|
|
594
|
+
// whole media arrives as ONE chunk, so `head` was a
|
|
595
|
+
// full second copy of the media (measured: ~+88 MB
|
|
596
|
+
// peak RSS on a 48 MB video). Retain only the prefix
|
|
597
|
+
// the scan can consume; its verdicts are unchanged.
|
|
598
|
+
head = Buffer.concat([head, data]).subarray(0, AUNU_SIDECAR_SCAN_MAX_BYTES + 32);
|
|
537
599
|
streamableVerdict = aunuScanStreamable(head);
|
|
538
600
|
if (streamableVerdict === null && head.length > AUNU_SIDECAR_SCAN_MAX_BYTES) {
|
|
539
601
|
streamableVerdict = true;
|
|
@@ -735,20 +797,37 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv, mac
|
|
|
735
797
|
const decryptLength = toSmallestChunkSize(data.length);
|
|
736
798
|
remainingBytes = data.slice(decryptLength);
|
|
737
799
|
data = data.slice(0, decryptLength);
|
|
738
|
-
if (!aes) {
|
|
739
|
-
let ivValue = iv;
|
|
740
|
-
if (firstBlockIsIV) {
|
|
741
|
-
ivValue = data.slice(0, AES_CHUNK_SIZE);
|
|
742
|
-
data = data.slice(AES_CHUNK_SIZE);
|
|
743
|
-
}
|
|
744
|
-
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue);
|
|
745
|
-
// if an end byte that is not EOF is specified
|
|
746
|
-
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
|
747
|
-
if (endByte) {
|
|
748
|
-
aes.setAutoPadding(false);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
800
|
try {
|
|
801
|
+
if (!aes) {
|
|
802
|
+
// AunuBaileys (1.0.85, HB-114/HB-158): on the
|
|
803
|
+
// ranged path the IV is carved out of the first
|
|
804
|
+
// 16 bytes of the body — when the first chunk(s)
|
|
805
|
+
// total less than that, there is nothing to build
|
|
806
|
+
// a decipher from yet. createDecipheriv used to
|
|
807
|
+
// sit OUTSIDE this try and threw a bare
|
|
808
|
+
// "Invalid initialization vector" TypeError for
|
|
809
|
+
// exactly that download. Wait for more bytes
|
|
810
|
+
// instead (a <16-byte slice means decryptLength
|
|
811
|
+
// was 0, so everything is still in
|
|
812
|
+
// remainingBytes), and keep the construction
|
|
813
|
+
// inside the try so any failure surfaces as a
|
|
814
|
+
// proper stream error with context.
|
|
815
|
+
if (firstBlockIsIV && data.length < AES_CHUNK_SIZE) {
|
|
816
|
+
callback();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
let ivValue = iv;
|
|
820
|
+
if (firstBlockIsIV) {
|
|
821
|
+
ivValue = data.slice(0, AES_CHUNK_SIZE);
|
|
822
|
+
data = data.slice(AES_CHUNK_SIZE);
|
|
823
|
+
}
|
|
824
|
+
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue);
|
|
825
|
+
// if an end byte that is not EOF is specified
|
|
826
|
+
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
|
827
|
+
if (endByte) {
|
|
828
|
+
aes.setAutoPadding(false);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
752
831
|
pushBytes(aes.update(data), b => this.push(b));
|
|
753
832
|
callback();
|
|
754
833
|
}
|
|
@@ -807,8 +886,18 @@ export function extensionForMediaMessage(message) {
|
|
|
807
886
|
}
|
|
808
887
|
return extension;
|
|
809
888
|
}
|
|
889
|
+
// AunuBaileys (1.0.85, HB-127): default ceiling for one media-upload POST.
|
|
890
|
+
const MEDIA_UPLOAD_DEFAULT_TIMEOUT_MS = 120000;
|
|
810
891
|
export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
|
|
811
892
|
return async (filePath, { mediaType, fileEncSha256B64, timeoutMs, newsletter }) => {
|
|
893
|
+
// AunuBaileys (1.0.85, HB-127): timeoutMs used to be a pure
|
|
894
|
+
// pass-through of the host's mediaUploadTimeoutMs, with no
|
|
895
|
+
// default anywhere — an upload host that black-holes the
|
|
896
|
+
// connection (accepts TCP, never speaks) left the POST, and
|
|
897
|
+
// with it prepareWAMessageMedia -> sendMessage, pending
|
|
898
|
+
// FOREVER. Sanitise at the boundary: a finite positive
|
|
899
|
+
// value is honored, anything else gets the default.
|
|
900
|
+
const effectiveTimeoutMs = typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : MEDIA_UPLOAD_DEFAULT_TIMEOUT_MS;
|
|
812
901
|
// send a query JSON to obtain the url & auth token to upload our media
|
|
813
902
|
let uploadInfo = await refreshMediaConn(false);
|
|
814
903
|
let urls;
|
|
@@ -837,7 +926,7 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
|
|
|
837
926
|
Origin: DEFAULT_ORIGIN
|
|
838
927
|
},
|
|
839
928
|
httpsAgent: fetchAgent,
|
|
840
|
-
timeout:
|
|
929
|
+
timeout: effectiveTimeoutMs,
|
|
841
930
|
responseType: 'json',
|
|
842
931
|
maxBodyLength: Infinity,
|
|
843
932
|
maxContentLength: Infinity
|
|
@@ -858,6 +947,20 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
|
|
|
858
947
|
catch (error) {
|
|
859
948
|
if (axios.isAxiosError(error)) {
|
|
860
949
|
result = error.response?.data;
|
|
950
|
+
// AunuBaileys (1.0.85, HB-137): a 401/403 means
|
|
951
|
+
// the media auth itself is stale. The no-URL
|
|
952
|
+
// branch above already force-refreshes in that
|
|
953
|
+
// situation, but this error branch kept retrying
|
|
954
|
+
// the remaining hosts with the SAME dead auth —
|
|
955
|
+
// and every later send reused the cached conn
|
|
956
|
+
// until its TTL expired by the local clock, so
|
|
957
|
+
// all media sends kept failing. Refresh
|
|
958
|
+
// (best-effort: a failed refresh keeps the old
|
|
959
|
+
// info) so the next host — and the next send —
|
|
960
|
+
// uses fresh auth.
|
|
961
|
+
if (error.response?.status === 401 || error.response?.status === 403) {
|
|
962
|
+
uploadInfo = await refreshMediaConn(true).catch(() => uploadInfo);
|
|
963
|
+
}
|
|
861
964
|
}
|
|
862
965
|
const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname;
|
|
863
966
|
logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);
|
package/lib/Utils/messages.js
CHANGED
|
@@ -94,6 +94,17 @@ export const prepareWAMessageMedia = async (message, options) => {
|
|
|
94
94
|
media: message[mediaType]
|
|
95
95
|
};
|
|
96
96
|
delete uploadData[mediaType];
|
|
97
|
+
// AunuBaileys (1.0.85, HB-208 + addendum): reject an unusable
|
|
98
|
+
// media value HERE, before the cacheable-key computation below
|
|
99
|
+
// derefs it — a null media used to explode in that computation
|
|
100
|
+
// itself ('url' in null), and undefined / non-object media as a
|
|
101
|
+
// raw TypeError further down in getStream. Hosts get a clear
|
|
102
|
+
// Boom 400 at the boundary instead. ({} and { url: undefined }
|
|
103
|
+
// pass this check and are rejected by the matching getStream
|
|
104
|
+
// guard.)
|
|
105
|
+
if (typeof uploadData.media !== 'object' || uploadData.media === null) {
|
|
106
|
+
throw new Boom(`Invalid media for "${mediaType}": expected a Buffer, a stream, or an object with a url`, { statusCode: 400 });
|
|
107
|
+
}
|
|
97
108
|
// check if cacheable + generate cache key
|
|
98
109
|
const cacheableKey = typeof uploadData.media === 'object' &&
|
|
99
110
|
'url' in uploadData.media &&
|
|
@@ -421,15 +432,25 @@ async function prepareStickerPackMessage(stickerPack, options) {
|
|
|
421
432
|
// AunuBaileys (1.0.71): only load the image library when
|
|
422
433
|
// a non-WebP sticker actually needs converting — pure
|
|
423
434
|
// WebP packs used to pay the import cost for nothing.
|
|
424
|
-
const
|
|
425
|
-
if (sharp) {
|
|
435
|
+
const imgLib = await loadLib();
|
|
436
|
+
if ('sharp' in imgLib && imgLib.sharp) {
|
|
426
437
|
// Convert to WebP, preserving metadata
|
|
427
|
-
webpBuffer = await sharp.default(buffer).webp().toBuffer();
|
|
438
|
+
webpBuffer = await imgLib.sharp.default(buffer).webp().toBuffer();
|
|
428
439
|
// Non-WebP inputs converted to WebP are not animated
|
|
429
440
|
isAnimated = false;
|
|
430
441
|
}
|
|
442
|
+
else if ('image' in imgLib && imgLib.image?.Transformer) {
|
|
443
|
+
// AunuBaileys (1.0.85, HB-164): the @napi-rs/image
|
|
444
|
+
// backend converts to WebP fine (extractImageThumb
|
|
445
|
+
// already uses it) — the pack path used to ignore
|
|
446
|
+
// it, so a host with @napi-rs/image but no sharp
|
|
447
|
+
// failed EVERY non-WebP sticker (and an all-PNG
|
|
448
|
+
// pack failed outright via HB-97).
|
|
449
|
+
webpBuffer = await new imgLib.image.Transformer(buffer).webp();
|
|
450
|
+
isAnimated = false;
|
|
451
|
+
}
|
|
431
452
|
else {
|
|
432
|
-
throw new Boom('No image processing library (sharp) available for converting sticker to WebP. Either install sharp or provide stickers in WebP format.');
|
|
453
|
+
throw new Boom('No image processing library (sharp or @napi-rs/image) available for converting sticker to WebP. Either install sharp or @napi-rs/image, or provide stickers in WebP format.');
|
|
433
454
|
}
|
|
434
455
|
}
|
|
435
456
|
if (webpBuffer.length > 1024 * 1024) {
|
|
@@ -474,12 +495,17 @@ async function prepareStickerPackMessage(stickerPack, options) {
|
|
|
474
495
|
coverWebpBuffer = coverBuffer;
|
|
475
496
|
}
|
|
476
497
|
else {
|
|
477
|
-
const
|
|
478
|
-
if (sharp) {
|
|
479
|
-
coverWebpBuffer = await sharp.default(coverBuffer).webp().toBuffer();
|
|
498
|
+
const coverLib = await loadLib();
|
|
499
|
+
if ('sharp' in coverLib && coverLib.sharp) {
|
|
500
|
+
coverWebpBuffer = await coverLib.sharp.default(coverBuffer).webp().toBuffer();
|
|
501
|
+
}
|
|
502
|
+
else if ('image' in coverLib && coverLib.image?.Transformer) {
|
|
503
|
+
// AunuBaileys (1.0.85, HB-164): @napi-rs/image backend,
|
|
504
|
+
// same gap as the sticker conversion above.
|
|
505
|
+
coverWebpBuffer = await new coverLib.image.Transformer(coverBuffer).webp();
|
|
480
506
|
}
|
|
481
507
|
else {
|
|
482
|
-
throw new Boom('No image processing library (sharp) available for converting cover to WebP. Either install sharp or provide cover in WebP format.');
|
|
508
|
+
throw new Boom('No image processing library (sharp or @napi-rs/image) available for converting cover to WebP. Either install sharp or @napi-rs/image, or provide cover in WebP format.');
|
|
483
509
|
}
|
|
484
510
|
}
|
|
485
511
|
// Add cover to ZIP data
|
|
@@ -539,6 +565,13 @@ async function prepareStickerPackMessage(stickerPack, options) {
|
|
|
539
565
|
if ('sharp' in lib && lib.sharp) {
|
|
540
566
|
thumbnailBuffer = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
|
|
541
567
|
}
|
|
568
|
+
else if ('image' in lib && lib.image?.Transformer) {
|
|
569
|
+
// AunuBaileys (1.0.85, HB-164): @napi-rs/image backend —
|
|
570
|
+
// the thumbnail used to fall through to a throw (and a
|
|
571
|
+
// silently dropped pack thumbnail) on hosts whose only
|
|
572
|
+
// image library is @napi-rs/image.
|
|
573
|
+
thumbnailBuffer = await new lib.image.Transformer(coverBuffer).resize(252, 252).jpeg(50);
|
|
574
|
+
}
|
|
542
575
|
else if ('jimp' in lib && lib.jimp) {
|
|
543
576
|
const jimpImage = await lib.jimp.Jimp.read(coverBuffer);
|
|
544
577
|
thumbnailBuffer = await jimpImage.resize({ w: 252, h: 252 }).getBuffer('image/jpeg');
|
|
@@ -1603,6 +1636,15 @@ export const downloadMediaMessage = async (message, type, options, ctx) => {
|
|
|
1603
1636
|
}
|
|
1604
1637
|
const contentType = getContentType(mContent);
|
|
1605
1638
|
let mediaType = contentType?.replace('Message', '');
|
|
1639
|
+
// AunuBaileys (1.0.85, HB-252): the content type derives
|
|
1640
|
+
// 'stickerPack' from stickerPackMessage, but the HKDF info
|
|
1641
|
+
// mapping only knows the upload-side name 'sticker-pack' —
|
|
1642
|
+
// downloads derived their keys from 'WhatsApp undefined
|
|
1643
|
+
// Keys' and ALWAYS failed with a misleading bad-decrypt
|
|
1644
|
+
// error. Normalise to the name the pack was encrypted with.
|
|
1645
|
+
if (mediaType === 'stickerPack') {
|
|
1646
|
+
mediaType = 'sticker-pack';
|
|
1647
|
+
}
|
|
1606
1648
|
const media = mContent[contentType];
|
|
1607
1649
|
if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media))) {
|
|
1608
1650
|
throw new Boom(`"${contentType}" message is not a media message`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { LookupFunction } from 'net';
|
|
2
|
+
export declare class SafeFetchError extends Error {
|
|
3
|
+
code: string;
|
|
4
|
+
host?: string;
|
|
5
|
+
address?: string;
|
|
6
|
+
statusCode?: number;
|
|
7
|
+
limit?: number;
|
|
8
|
+
constructor(code: string, message: string, extra?: Record<string, unknown>);
|
|
9
|
+
}
|
|
10
|
+
export declare const SAFE_FETCH_DEFAULTS: Readonly<{
|
|
11
|
+
timeout: 3000;
|
|
12
|
+
maxRedirects: 5;
|
|
13
|
+
maxBytes: number;
|
|
14
|
+
}>;
|
|
15
|
+
/** true when `address` must never be contacted. Unparseable input is blocked (fail closed). */
|
|
16
|
+
export declare const isBlockedAddress: (address: string) => boolean;
|
|
17
|
+
export declare const createGuardedLookup: (opts?: {
|
|
18
|
+
lookup?: LookupFunction;
|
|
19
|
+
isBlocked?: (address: string) => boolean;
|
|
20
|
+
}) => LookupFunction;
|
|
21
|
+
export type SafeHttpGetOptions = {
|
|
22
|
+
/** idle/connect timeout per hop in ms (default 3000) */
|
|
23
|
+
timeout?: number;
|
|
24
|
+
/** hard deadline for the whole call incl. redirects (default 3 x timeout) */
|
|
25
|
+
totalTimeout?: number;
|
|
26
|
+
/** response cap in bytes, applied after decompression (default 1 MiB) */
|
|
27
|
+
maxBytes?: number;
|
|
28
|
+
/** 'throw' (default) or 'truncate' when the cap is hit */
|
|
29
|
+
onLimit?: 'throw' | 'truncate';
|
|
30
|
+
/** default 5 */
|
|
31
|
+
maxRedirects?: number;
|
|
32
|
+
/** 'any' (default) | 'same-host' | 'none'. Every hop is address-checked either way. */
|
|
33
|
+
redirects?: 'any' | 'same-host' | 'none';
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
/** return false to skip downloading the body */
|
|
36
|
+
shouldReadBody?: (status: number, headers: Record<string, string>) => boolean;
|
|
37
|
+
/** explicit opt-out of the address policy (default false) */
|
|
38
|
+
allowPrivateNetworks?: boolean;
|
|
39
|
+
/** injection points (host hooks, tests) */
|
|
40
|
+
lookup?: LookupFunction;
|
|
41
|
+
isBlocked?: (address: string) => boolean;
|
|
42
|
+
};
|
|
43
|
+
export type SafeHttpGetResult = {
|
|
44
|
+
url: string;
|
|
45
|
+
status: number;
|
|
46
|
+
headers: Record<string, string>;
|
|
47
|
+
body: Buffer;
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
redirects: number;
|
|
50
|
+
};
|
|
51
|
+
export declare function safeHttpGet(input: string | URL, options?: SafeHttpGetOptions): Promise<SafeHttpGetResult>;
|
|
52
|
+
/** Reads width/height from PNG, GIF, JPEG and WebP headers without decoding. null if unknown. */
|
|
53
|
+
export declare const readImageDimensions: (buf: Buffer) => {
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
} | null;
|
|
57
|
+
//# sourceMappingURL=safe-fetch.d.ts.map
|