@aunucore/baileys 1.0.84 → 1.0.86

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.
@@ -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
- ffmpeg.stderr.on('data', (chunk) => stderrChunks.push(chunk));
114
- ffmpeg.on('error', reject);
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
- const { stream } = await getStream(mediaUpload);
191
- // Convert the resulting stream to a buffer
192
- buffer = await toBuffer(stream);
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
- head = Buffer.concat([head, data]);
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: timeoutMs,
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...'}`);
@@ -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 { sharp } = await loadLib();
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 { sharp } = await loadLib();
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');
@@ -1301,6 +1334,15 @@ export const generateWAMessageFromContent = (jid, message, options) => {
1301
1334
  }
1302
1335
  const innerMessage = normalizeMessageContent(message);
1303
1336
  const key = getContentType(innerMessage);
1337
+ // AunuBaileys (1.0.86, HB-122): on the raw path the content
1338
+ // value can be a bare string ('conversation') or absent
1339
+ // entirely (key undefined). Quote and ephemeral contextInfo
1340
+ // can only be attached to an object-shaped value — the `in`
1341
+ // check and the property assignment below both threw raw
1342
+ // TypeErrors otherwise (Cannot use 'in' operator... /
1343
+ // Cannot create property 'contextInfo' on string...).
1344
+ const contextTarget = key ? innerMessage[key] : undefined;
1345
+ const canAttachContext = !!contextTarget && typeof contextTarget === 'object';
1304
1346
  const timestamp = unixTimestampSeconds(options.timestamp);
1305
1347
  const { quoted, userJid } = options;
1306
1348
  if (quoted) {
@@ -1315,7 +1357,7 @@ export const generateWAMessageFromContent = (jid, message, options) => {
1315
1357
  if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {
1316
1358
  delete quotedContent.contextInfo;
1317
1359
  }
1318
- const contextInfo = ('contextInfo' in innerMessage[key] && innerMessage[key]?.contextInfo) || {};
1360
+ const contextInfo = (canAttachContext && 'contextInfo' in contextTarget && contextTarget.contextInfo) || {};
1319
1361
  contextInfo.participant = jidNormalizedUser(participant);
1320
1362
  contextInfo.stanzaId = quoted.key.id;
1321
1363
  contextInfo.quotedMessage = quotedMsg;
@@ -1327,9 +1369,9 @@ export const generateWAMessageFromContent = (jid, message, options) => {
1327
1369
  if (!isJidNewsletter(jid) && jid !== quoted.key.remoteJid) {
1328
1370
  contextInfo.remoteJid = quoted.key.remoteJid;
1329
1371
  }
1330
- if (contextInfo && innerMessage[key]) {
1372
+ if (contextInfo && canAttachContext) {
1331
1373
  /* @ts-ignore */
1332
- innerMessage[key].contextInfo = contextInfo;
1374
+ contextTarget.contextInfo = contextInfo;
1333
1375
  }
1334
1376
  }
1335
1377
  if (
@@ -1340,7 +1382,9 @@ export const generateWAMessageFromContent = (jid, message, options) => {
1340
1382
  // already not converted to disappearing message
1341
1383
  key !== 'ephemeralMessage' &&
1342
1384
  // newsletters don't support ephemeral messages
1343
- !isJidNewsletter(jid)) {
1385
+ !isJidNewsletter(jid) &&
1386
+ // and a string/absent content value cannot carry one (HB-122)
1387
+ canAttachContext) {
1344
1388
  /* @ts-ignore */
1345
1389
  innerMessage[key].contextInfo = {
1346
1390
  ...(innerMessage[key].contextInfo || {}),
@@ -1603,6 +1647,15 @@ export const downloadMediaMessage = async (message, type, options, ctx) => {
1603
1647
  }
1604
1648
  const contentType = getContentType(mContent);
1605
1649
  let mediaType = contentType?.replace('Message', '');
1650
+ // AunuBaileys (1.0.85, HB-252): the content type derives
1651
+ // 'stickerPack' from stickerPackMessage, but the HKDF info
1652
+ // mapping only knows the upload-side name 'sticker-pack' —
1653
+ // downloads derived their keys from 'WhatsApp undefined
1654
+ // Keys' and ALWAYS failed with a misleading bad-decrypt
1655
+ // error. Normalise to the name the pack was encrypted with.
1656
+ if (mediaType === 'stickerPack') {
1657
+ mediaType = 'sticker-pack';
1658
+ }
1606
1659
  const media = mContent[contentType];
1607
1660
  if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media))) {
1608
1661
  throw new Boom(`"${contentType}" message is not a media message`);
@@ -284,12 +284,24 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
284
284
  ...content.reactionMessage,
285
285
  key: message.key
286
286
  };
287
- ev.emit('messages.reaction', [
288
- {
289
- reaction,
290
- key: content.reactionMessage?.key
291
- }
292
- ]);
287
+ // AunuBaileys (1.0.86, HB-187): reactionMessage.key is an
288
+ // optional proto field — a reaction without it used to be
289
+ // emitted with key === undefined and detonated downstream
290
+ // (the event-buffer append and the in-memory store both
291
+ // deref key.remoteJid) with a raw TypeError, AFTER the
292
+ // delivery receipt had already been sent: the stanza
293
+ // earned a NACK 500 on top of its receipt and the reaction
294
+ // was lost on every redelivery. The reaction has no
295
+ // target message, so there is nothing to attach it to —
296
+ // do not emit it at all.
297
+ if (content.reactionMessage.key) {
298
+ ev.emit('messages.reaction', [
299
+ {
300
+ reaction,
301
+ key: content.reactionMessage.key
302
+ }
303
+ ]);
304
+ }
293
305
  }
294
306
  else if (message.messageStubType) {
295
307
  const jid = message.key?.remoteJid;
@@ -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