@zervonex/baileys 3.0.3 → 3.0.4

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.
@@ -458,7 +458,7 @@ export const makeMessagesRecvSocket = (config) => {
458
458
  break
459
459
  case 'membership_approval_mode':
460
460
  const approvalMode = getBinaryNodeChild(child, 'group_join')
461
- if (approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE; msg.messageStubParameters = [approvalMode.attrs.state] }
461
+ if (approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE; msg.messageStubParameters = [approvalMode?.attrs?.state] }
462
462
  break
463
463
  case 'created_membership_requests':
464
464
  msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
@@ -674,7 +674,7 @@ export const makeMessagesSocket = (config) => {
674
674
  const isPrivate = isJidUser(normalizedId)
675
675
  const type = isPrivate ? 'statusMentionMessage' : 'groupStatusMentionMessage'
676
676
  const protocolMessage = { [type]: { message: { protocolMessage: { key: msg.key, type: 25 } } }, messageContextInfo: { messageSecret: randomBytes(32) } }
677
- const statusMsg = await generateWAMessageFromContent(normalizedId, protocolMessage, {})
677
+ const statusMsg = await await generateWAMessageFromContent(normalizedId, protocolMessage, {})
678
678
  await relayMessage(normalizedId, statusMsg.message, { additionalNodes: [{ tag: 'meta', attrs: isPrivate ? { is_status_mention: 'true' } : { is_group_status_mention: 'true' } }] })
679
679
  await delay(2000)
680
680
  } catch (error) { logger.error({ id, error }, 'failed to send status mention') }
@@ -16,7 +16,7 @@ import {
16
16
  bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode,
17
17
  generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getCompanionPlatformId,
18
18
  getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler,
19
- promiseTimeout, signedKeyPair, xmppSignedPreKey, getPlatformId, isAndroidBrowser
19
+ promiseTimeout, signedKeyPair, xmppSignedPreKey
20
20
  } from '../Utils/index.js'
21
21
  import {
22
22
  assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren,
@@ -530,6 +530,7 @@ export const makeSocket = (config) => {
530
530
  // ─── Pairing ─────────────────────────────────────────────────────────────────
531
531
 
532
532
  const requestPairingCode = async (phoneNumber, customPairingCode) => {
533
+ if (authState.creds.pairingCode && !customPairingCode) return authState.creds.pairingCode
533
534
  await waitForSocketOpen()
534
535
  await new Promise((resolve) => setTimeout(resolve, 500))
535
536
  const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5))
@@ -547,8 +548,8 @@ export const makeSocket = (config) => {
547
548
  content: [
548
549
  { tag: 'link_code_pairing_wrapped_companion_ephemeral_pub', attrs: {}, content: await generatePairingKey() },
549
550
  { tag: 'companion_server_auth_key_pub', attrs: {}, content: authState.creds.noiseKey.public },
550
- { tag: 'companion_platform_id', attrs: {}, content: isAndroidBrowser(browser) ? getPlatformId('Chrome') : getCompanionPlatformId(browser) },
551
- { tag: 'companion_platform_display', attrs: {}, content: isAndroidBrowser(browser) ? 'Chrome (Mac OS)' : `${browser[1]} (${browser[0]})` },
551
+ { tag: 'companion_platform_id', attrs: {}, content: getCompanionPlatformId(browser) },
552
+ { tag: 'companion_platform_display', attrs: {}, content: `${browser[1]} (${browser[0]})` },
552
553
  { tag: 'link_code_pairing_nonce', attrs: {}, content: '0' }
553
554
  ]
554
555
  }]
@@ -35,7 +35,7 @@ export const makeMessageBuilderSocket = (config) => {
35
35
  if (content.nativeFlow && !content.interactiveMessage) {
36
36
  const buttons = Array.isArray(content.nativeFlow) ? content.nativeFlow : [content.nativeFlow]
37
37
  const { generateWAMessageFromContent } = await import('../Utils/index.js')
38
- const msg = await generateWAMessageFromContent(jid, {
38
+ const msg = await await generateWAMessageFromContent(jid, {
39
39
  interactiveMessage: { body: { text: content.text || '' }, footer: { text: content.footer || '' }, header: { hasMediaAttachment: false }, nativeFlowMessage: { buttons } }
40
40
  }, { userJid: sock.user?.id })
41
41
  await result.relayMessage(jid, msg.message, { messageId: msg.key.id, aiLabel: !!(content.ai ?? content.aiLabel ?? options.aiLabel ?? options.ai ?? config.aiLabel ?? config.ai) })
@@ -13,14 +13,18 @@ export const Browsers = {
13
13
  baileys: browser => ['Baileys', browser, '6.5.0'],
14
14
  windows: browser => ['Windows', browser, '10.0.26100'],
15
15
  android: browser => [browser, 'Android', ''],
16
+ ios: browser => ['Mac OS', browser, '18.4'],
17
+ ipad: browser => ['iPadOS', browser, '18.4'],
16
18
  appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
17
19
  }
18
20
 
19
- export const getPlatformId = browser => {
21
+ export const getPlatformType = (browser) => {
20
22
  const upper = browser.toUpperCase()
21
- if (upper === 'ANDROID') return proto.DeviceProps.PlatformType.ANDROID_PHONE.toString()
22
- const type = proto.DeviceProps.PlatformType[upper]
23
- return (type ?? proto.DeviceProps.PlatformType.CHROME).toString()
23
+ if (upper === 'ANDROID') return proto.DeviceProps.PlatformType.ANDROID_PHONE
24
+ if (upper === 'IPAD') return proto.DeviceProps.PlatformType.IPAD
25
+ if (upper === 'IOS') return proto.DeviceProps.PlatformType.IOS_PHONE
26
+ if (upper === 'DESKTOP') return proto.DeviceProps.PlatformType.DESKTOP
27
+ return proto.DeviceProps.PlatformType[upper] ?? proto.DeviceProps.PlatformType.CHROME
24
28
  }
25
29
 
26
30
  export const isAndroidBrowser = browser => browser[1]?.toUpperCase() === 'ANDROID'
@@ -117,7 +117,7 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
117
117
  const hmac = hmacSign(syncAction.index, key.indexKey)
118
118
  if (Buffer.compare(hmac, record.index.blob) !== 0) throw new Boom('HMAC index verification failed')
119
119
  }
120
- onMutation({ syncAction, index: JSON.parse(Buffer.from(syncAction.index).toString()) })
120
+ if (syncAction.index) onMutation({ syncAction, index: JSON.parse(Buffer.from(syncAction.index).toString()) })
121
121
  ltGenerator.mix({ indexMac: record.index.blob, valueMac: ogValueMac, operation })
122
122
  }
123
123
  return ltGenerator.finish()
@@ -30,11 +30,20 @@ if (ffmpegStatic) process.env.FFMPEG_PATH = ffmpegStatic
30
30
  const MIMETYPE_DEFAULTS = { image: 'image/jpeg', video: 'video/mp4', document: 'application/pdf', audio: 'audio/ogg; codecs=opus', sticker: 'image/webp', 'product-catalog-image': 'image/jpeg' }
31
31
 
32
32
  // High-level user-facing send keys that are NOT proto field names — used only to detect pass-through
33
- const HIGH_LEVEL_KEYS = new Set(['text', 'image', 'video', 'audio', 'document', 'sticker', 'contacts', 'location', 'react', 'delete', 'forward', 'disappearingMessagesInChat', 'groupInvite', 'stickerPack', 'pin', 'buttonReply', 'ptv', 'product', 'listReply', 'event', 'poll', 'inviteAdmin', 'requestPayment', 'sharePhoneNumber', 'requestPhoneNumber', 'limitSharing', 'viewOnce', 'mentions', 'edit', 'buttons', 'templateButtons', 'sections', 'interactiveButtons', 'album', 'call', 'paymentInvite', 'order', 'keep', 'shop', 'payment', 'collection'])
33
+ const HIGH_LEVEL_KEYS = new Set(['text', 'image', 'video', 'audio', 'document', 'sticker', 'contacts', 'location',
34
+ 'react', 'delete', 'forward', 'disappearingMessagesInChat', 'groupInvite', 'stickerPack', 'pin', 'buttonReply',
35
+ 'ptv', 'product', 'listReply', 'event', 'poll', 'inviteAdmin', 'requestPayment', 'sharePhoneNumber',
36
+ 'requestPhoneNumber', 'limitSharing', 'viewOnce', 'mentions', 'edit', 'buttons', 'templateButtons',
37
+ 'sections', 'interactiveButtons', 'album', 'call', 'paymentInvite', 'order', 'keep', 'shop', 'payment', 'collection'])
34
38
 
35
39
  // Wrapper field names for normalizeMessageContent — derived from what WA wraps messages in
36
- const WRAPPER_KEYS = ['ephemeralMessage', 'viewOnceMessage', 'documentWithCaptionMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension', 'editedMessage', 'groupMentionedMessage', 'botInvokeMessage', 'lottieStickerMessage', 'eventCoverImage', 'statusMentionMessage', 'pollCreationOptionImageMessage', 'associatedChildMessage', 'groupStatusMentionMessage', 'pollCreationMessageV4', 'pollCreationMessageV5', 'statusAddYours', 'groupStatusMessage', 'limitSharingMessage', 'botTaskMessage', 'questionMessage', 'botForwardedMessage']
37
-
40
+ const WRAPPER_KEYS = ['ephemeralMessage', 'viewOnceMessage', 'documentWithCaptionMessage', 'viewOnceMessageV2', 'editedMessage',
41
+ 'groupMentionedMessage', 'botInvokeMessage', 'lottieStickerMessage', 'eventCoverImage', 'viewOnceMessageV2Extension',
42
+ 'statusMentionMessage', 'pollCreationOptionImageMessage', 'associatedChildMessage', 'groupStatusMentionMessage',
43
+ 'pollCreationMessageV4', 'pollCreationMessageV5', 'statusAddYours', 'groupStatusMessage', 'limitSharingMessage',
44
+ 'botTaskMessage', 'questionMessage', 'botForwardedMessage', 'commentMessage', 'groupStatusMessageV2', 'questionReplyMessage',
45
+ 'newsletterAdminProfileMessage', 'spoilerMessage', 'newsletterAdminProfileStatusMessage', 'botPlatformRegistrationSuccessMessage',
46
+ 'newsletterScheduledMessage', 'acp2SettingMessage', 'audioStickerMessage', 'botGroupParticipantMessage']
38
47
  const REUPLOAD_REQUIRED_STATUS = [410, 404]
39
48
  const STICKER_MAX_BYTES = 1_000_000
40
49
  const CIRCLE_GEQ = `clip(256-hypot(X-W/2\\,Y-H/2)\\,0\\,1)*255`
@@ -156,6 +165,32 @@ export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) =>
156
165
  try { return await getUrlInfo(url) } catch (e) { logger?.warn({ trace: e.stack }, 'url generation failed') }
157
166
  }
158
167
 
168
+ // ─── MEDIA RESOLUTION ─────────────────────────────────────────────────────────
169
+ const MEDIA_MESSAGE_TYPES = ['Image', 'Video', 'Audio', 'Document', 'Sticker']
170
+ const RESOLVABLE_MEDIA = [
171
+ ...MEDIA_MESSAGE_TYPES.map(t => ({ key: `${t.toLowerCase()}Message`, type: t.toLowerCase() })),
172
+ // raw field names used in card headers
173
+ { key: 'image', type: 'image' },
174
+ { key: 'video', type: 'video' },
175
+ ]
176
+ const resolveMedia = async (obj, upload) => {
177
+ if (!obj || typeof obj !== 'object' || !upload || typeof upload !== 'function') return
178
+ for (const { key, type } of RESOLVABLE_MEDIA) {
179
+ if (!obj[key]) continue
180
+ const isRaw = key === 'image' || key === 'video'
181
+ if (!isRaw && obj[key].directPath) continue
182
+ const src = isRaw ? obj[key] : (obj[key].url ?? obj[key])
183
+ const result = await prepareWAMessageMedia({ [type]: typeof src === 'string' ? { url: src } : src }, { upload })
184
+ if (isRaw) { obj[`${type}Message`] = result[`${type}Message`]; delete obj[key] }
185
+ else obj[key] = result[`${type}Message`]
186
+ if ('hasMediaAttachment' in obj) obj.hasMediaAttachment = true
187
+ }
188
+ for (const val of Object.values(obj)) {
189
+ if (Array.isArray(val)) for (const item of val) await resolveMedia(item, upload)
190
+ else if (val && typeof val === 'object') await resolveMedia(val, upload)
191
+ }
192
+ }
193
+
159
194
  // ─── EXPORTS: MESSAGE HELPERS ─────────────────────────────────────────────────
160
195
 
161
196
  export const getContentType = (content) => {
@@ -235,6 +270,7 @@ export const prepareWAMessageMedia = async (message, options) => {
235
270
  const { buffer } = await compressWebp(rawBuf, { packName, authorName, stickerType: message.stickerType || options?.stickerType || StickerTypes.ROUNDED, quality: message.quality || options?.quality || 80 })
236
271
  uploadData.media = buffer
237
272
  uploadData.stickerSentTs = Date.now()
273
+ if (message.premium != null) uploadData.premium = message.premium
238
274
  } catch (e) { options.logger?.warn({ err: e }, 'sticker formatting failed, sending raw') }
239
275
  }
240
276
 
@@ -351,7 +387,7 @@ export const prepareStickerPackMessage = async (stickerPack, options) => {
351
387
  const hash = sha256(finalBuf).toString('base64').replace(/\//g, '-').replace(/=/g, '')
352
388
  const fileSize = finalBuf.length
353
389
  writeFileSync(tmpOut, finalBuf); finalBuf = null
354
- return { fileName: `${hash}.webp`, filePath: tmpOut, fileSize, mimetype: 'image/webp', isAnimated: s.isAnimated || false, isLottie: s.isLottie || false, emojis, accessibilityLabel: s.accessibilityLabel || '' }
390
+ return { fileName: `${hash}.webp`, filePath: tmpOut, fileSize, mimetype: 'image/webp', isAnimated: s.isAnimated || false, isLottie: s.isLottie || false, emojis, accessibilityLabel: s.accessibilityLabel || '', ...((s.premium ?? stickerPack.premium) != null && { premium: s.premium ?? stickerPack.premium }) }
355
391
  } catch (e) {
356
392
  options.logger?.warn({ err: e }, `failed processing sticker at index ${i}`)
357
393
  skippedStickers.push({ index: i, reason: e.message }); fs.unlink(tmpOut).catch(() => { }); return null
@@ -381,7 +417,7 @@ export const prepareStickerPackMessage = async (stickerPack, options) => {
381
417
  name: totalBatches > 1 ? `${packName} (${batchIdx + 1}/${totalBatches})` : packName, publisher: authorName,
382
418
  stickerPackId: totalBatches > 1 ? `${stickerPackIdValue}_${batchIdx}` : stickerPackIdValue,
383
419
  stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, stickerPackSize: zipBuf.length,
384
- stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, isLottie: s.isLottie, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
420
+ stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, isLottie: s.isLottie, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel, ...(s.premium != null && { premium: s.premium }) })),
385
421
  fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
386
422
  directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
387
423
  ...(thumbRes && { thumbnailDirectPath: thumbRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252, thumbnailSha256: thumbRes._enc?.fileSha256, thumbnailEncSha256: thumbRes._enc?.fileEncSha256, imageDataHash: thumbRes._thumbBuf ? sha256(thumbRes._thumbBuf).toString('base64') : undefined }),
@@ -612,46 +648,61 @@ export const generateWAMessageContent = async (message, options = {}) => {
612
648
  // ─── ASSEMBLE FULL WA MESSAGE ─────────────────────────────────────────────────
613
649
 
614
650
  export const generateWAMessageFromContent = (jid, message, options) => {
615
- if (!options.timestamp) options.timestamp = new Date()
616
- const innerMessage = normalizeMessageContent(message)
617
- const key = getContentType(innerMessage)
618
- const { quoted, userJid } = options
619
- if (quoted && !isJidNewsletter(jid)) {
620
- const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
621
- const normalizedQuoted = normalizeMessageContent(quoted.message)
622
- if (normalizedQuoted) {
623
- const quotedType = getContentType(normalizedQuoted)
624
- const quotedMsg = proto.Message.fromObject({ [quotedType]: normalizedQuoted[quotedType] })
625
- const quotedContent = quotedMsg[quotedType]
626
- if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) delete quotedContent.contextInfo
627
- const contextInfo = innerMessage[key]?.contextInfo || {}
628
- contextInfo.participant = jidNormalizedUser(participant)
629
- contextInfo.stanzaId = quoted.key.id
630
- contextInfo.quotedMessage = quotedMsg
631
- if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid
632
- if (innerMessage[key]) innerMessage[key].contextInfo = contextInfo
651
+ const upload = options.upload ?? options.waUploadToServer
652
+ const assemble = () => {
653
+ if (!options.timestamp) options.timestamp = new Date()
654
+ const innerMessage = normalizeMessageContent(message)
655
+ const key = getContentType(innerMessage)
656
+ const { quoted, userJid } = options
657
+ if (quoted && !isJidNewsletter(jid)) {
658
+ const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
659
+ const normalizedQuoted = normalizeMessageContent(quoted.message)
660
+ if (normalizedQuoted) {
661
+ const quotedType = getContentType(normalizedQuoted)
662
+ const quotedMsg = proto.Message.fromObject({ [quotedType]: normalizedQuoted[quotedType] })
663
+ const quotedContent = quotedMsg[quotedType]
664
+ if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) delete quotedContent.contextInfo
665
+ const contextInfo = innerMessage[key]?.contextInfo || {}
666
+ contextInfo.participant = jidNormalizedUser(participant)
667
+ contextInfo.stanzaId = quoted.key.id
668
+ contextInfo.quotedMessage = quotedMsg
669
+ if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid
670
+ if (innerMessage[key]) innerMessage[key].contextInfo = contextInfo
671
+ }
633
672
  }
673
+ if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
674
+ innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL }
675
+ }
676
+ return WAProto.WebMessageInfo.fromObject({
677
+ key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
678
+ message: WAProto.Message.create(message),
679
+ messageTimestamp: unixTimestampSeconds(options.timestamp),
680
+ messageStubParameters: [],
681
+ participant: (isJidGroup(jid) || isJidStatusBroadcast(jid)) ? userJid : undefined,
682
+ status: WAMessageStatus.PENDING,
683
+ })
634
684
  }
635
- if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
636
- innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL }
637
- }
638
- return WAProto.WebMessageInfo.fromObject({
639
- key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
640
- message: WAProto.Message.create(message),
641
- messageTimestamp: unixTimestampSeconds(options.timestamp),
642
- messageStubParameters: [],
643
- participant: (isJidGroup(jid) || isJidStatusBroadcast(jid)) ? userJid : undefined,
644
- status: WAMessageStatus.PENDING,
645
- })
685
+ const result = assemble()
686
+ if (!upload) return result
687
+ return Object.assign(result, { then(resolve, reject) { resolveMedia(message, upload).then(() => resolve(assemble())).catch(reject) } })
646
688
  }
647
689
 
648
- // Primary entry point. generateWAMessageContent returns a proto Message object.
649
- // generateWAMessageFromContent wraps existing content into a WebMessageInfo.
650
- // Call the wrong one by accident and it self-corrects rather than failing silently.
651
- export const generateWAMessage = async (jid, content, options = {}) => {
690
+ // Primary entry point. Builds a WebMessageInfo from raw content.
691
+ // Returns a thenable — key.id is available synchronously, full message resolves on await.
692
+ // If called with an existing WebMessageInfo by accident, passes it through unchanged.
693
+ export const generateWAMessage = (jid, content, options = {}) => {
652
694
  if (content?.key?.remoteJid && content?.message) return content
653
- options.logger = options?.logger?.child({ msgId: options.messageId })
654
- return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options)
695
+ const msgId = options.messageId || generateMessageIDV2()
696
+ const result = { key: { remoteJid: jid, fromMe: true, id: msgId }, messageId: msgId }
697
+ const promise = (async () => {
698
+ options.logger = options?.logger?.child({ msgId })
699
+ return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid, messageId: msgId }), { ...options, messageId: msgId })
700
+ })()
701
+ return Object.assign(result, {
702
+ then: (resolve, reject) => promise.then(resolve, reject),
703
+ catch: (reject) => promise.catch(reject),
704
+ finally: (fn) => promise.finally(fn),
705
+ })
655
706
  }
656
707
 
657
708
  // ─── UPDATE UTILITIES ─────────────────────────────────────────────────────────
@@ -11,64 +11,38 @@ const DEFAULT_PREKEY_RETENTION = 150
11
11
  const DEFAULT_CLEANUP_THRESHOLD = 50
12
12
  const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
13
13
 
14
- const bootstrapCreds = (raw) => {
15
- const c = raw ?? initAuthCreds()
16
- if (!c.__version || c.__version < CURRENT_VERSION) c.__version = CURRENT_VERSION
17
- return c
18
- }
19
-
20
- const patchAppStateKey = (type, v) =>
21
- type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
14
+ const bootstrapCreds = (raw) => { const c = raw ?? initAuthCreds(); if (!c.__version || c.__version < CURRENT_VERSION) c.__version = CURRENT_VERSION; return c }
15
+ const patchAppStateKey = (type, v) => type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
22
16
 
23
17
  // ── File-based ────────────────────────────────────────────────────────────────
24
18
 
25
19
  export const useMultiFileAuthState = async (folder, options = {}) => {
26
20
  const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, logger } = options
27
-
28
21
  const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-')
29
22
  const filePath = (file) => join(folder, fixFileName(file))
30
23
  const tmpPath = (file) => filePath(file) + '.tmp'
31
24
 
32
25
  const folderInfo = await stat(folder).catch(() => null)
33
- if (folderInfo) {
34
- if (!folderInfo.isDirectory()) throw new Error(`Path exists but is not a directory: ${folder}`)
35
- } else {
36
- await mkdir(folder, { recursive: true })
37
- }
26
+ if (folderInfo) { if (!folderInfo.isDirectory()) throw new Error(`Path exists but is not a directory: ${folder}`) }
27
+ else { await mkdir(folder, { recursive: true }) }
38
28
 
39
29
  const writeData = async (data, file) => {
40
- const fp = filePath(file)
41
- const tp = tmpPath(file)
42
- const payload = JSON.stringify(data, BufferJSON.replacer)
30
+ const fp = filePath(file), tp = tmpPath(file), payload = JSON.stringify(data, BufferJSON.replacer)
43
31
  await writeFile(tp, payload)
44
- try {
45
- await rename(tp, fp)
46
- } catch {
47
- await writeFile(fp, payload)
48
- await unlink(tp).catch(() => { })
49
- }
32
+ try { await rename(tp, fp) } catch { await writeFile(fp, payload); await unlink(tp).catch(() => { }) }
50
33
  }
51
34
 
52
35
  const readData = async (file) => {
53
36
  const raw = await readFile(filePath(file), { encoding: 'utf-8' }).catch(() => null)
54
37
  if (!raw) return null
55
- try {
56
- const parsed = JSON.parse(raw, BufferJSON.reviver)
57
- if (parsed && typeof parsed === 'object' && '__checksum' in parsed && 'data' in parsed) return parsed.data
58
- return parsed
59
- } catch (err) {
60
- logger?.warn({ file, err: err.message }, 'failed to read auth file — reinitializing')
61
- await unlink(filePath(file)).catch(() => { })
62
- return null
63
- }
38
+ try { const p = JSON.parse(raw, BufferJSON.reviver); return (p && typeof p === 'object' && '__checksum' in p && 'data' in p) ? p.data : p }
39
+ catch (err) { logger?.warn({ file, err: err.message }, 'failed to read auth file — reinitializing'); await unlink(filePath(file)).catch(() => { }); return null }
64
40
  }
65
41
 
66
42
  const removeData = (file) => unlink(filePath(file)).catch(() => { })
67
43
 
68
44
  let creds = bootstrapCreds(await readData('creds.json'))
69
- let cleanupRunning = false
70
- let lastCleanupAt = 0
71
- let lastCleanedPreKeyId = creds.nextPreKeyId
45
+ let cleanupRunning = false, lastCleanupAt = 0, lastCleanedPreKeyId = creds.nextPreKeyId
72
46
 
73
47
  const cleanOldPreKeys = async () => {
74
48
  const now = Date.now()
@@ -77,63 +51,30 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
77
51
  try {
78
52
  const minId = creds.nextPreKeyId - preKeyRetention
79
53
  if (minId <= 0) return
80
- const targets = (await readdir(folder))
81
- .map(f => f.match(/^pre-key-(\d+)\.json(\.tmp)?$/))
82
- .filter(m => m && parseInt(m[1], 10) < minId)
83
- .map(m => join(folder, m[0]))
54
+ const targets = (await readdir(folder)).map(f => f.match(/^pre-key-(\d+)\.json(\.tmp)?$/)).filter(m => m && parseInt(m[1], 10) < minId).map(m => join(folder, m[0]))
84
55
  if (!targets.length) return
85
56
  await Promise.all(targets.map(f => unlink(f).catch(() => { })))
86
- lastCleanupAt = Date.now()
87
- lastCleanedPreKeyId = creds.nextPreKeyId
88
- } catch (err) {
89
- logger?.warn({ err }, 'prekey cleanup failed')
90
- } finally {
91
- cleanupRunning = false
92
- }
57
+ lastCleanupAt = Date.now(); lastCleanedPreKeyId = creds.nextPreKeyId
58
+ } catch (err) { logger?.warn({ err }, 'prekey cleanup failed') }
59
+ finally { cleanupRunning = false }
93
60
  }
94
61
 
95
62
  cleanOldPreKeys().catch(() => { })
96
63
 
97
64
  const getStats = async () => {
98
65
  const files = await readdir(folder).catch(() => [])
99
- const preKeyFiles = files.filter(f => /^pre-key-\d+\.json$/.test(f))
100
- return {
101
- totalFiles: files.length,
102
- preKeyCount: preKeyFiles.length,
103
- nextPreKeyId: creds.nextPreKeyId,
104
- lastCleanupAt: lastCleanupAt ? new Date(lastCleanupAt).toISOString() : null,
105
- }
66
+ return { totalFiles: files.length, preKeyCount: files.filter(f => /^pre-key-\d+\.json$/.test(f)).length, nextPreKeyId: creds.nextPreKeyId, lastCleanupAt: lastCleanupAt ? new Date(lastCleanupAt).toISOString() : null }
106
67
  }
107
68
 
108
69
  return {
109
70
  state: {
110
71
  creds,
111
72
  keys: {
112
- get: async (type, ids) => {
113
- const data = {}
114
- await Promise.all(ids.map(async id => {
115
- let value = await readData(`${type}-${id}.json`)
116
- if (type === 'app-state-sync-key' && value) value = proto.Message.AppStateSyncKeyData.fromObject(value)
117
- data[id] = value
118
- }))
119
- return data
120
- },
121
- set: async (data) => {
122
- const tasks = []
123
- for (const category in data) {
124
- for (const id in data[category]) {
125
- const value = data[category][id]
126
- tasks.push(value ? writeData(value, `${category}-${id}.json`) : removeData(`${category}-${id}.json`))
127
- }
128
- }
129
- await Promise.all(tasks)
130
- },
73
+ get: async (type, ids) => { const data = {}; await Promise.all(ids.map(async id => { let v = await readData(`${type}-${id}.json`); data[id] = type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v })); return data },
74
+ set: async (data) => { const tasks = []; for (const cat in data) for (const id in data[cat]) { const v = data[cat][id]; tasks.push(v ? writeData(v, `${cat}-${id}.json`) : removeData(`${cat}-${id}.json`)) }; await Promise.all(tasks) },
131
75
  },
132
76
  },
133
- saveCreds: async () => {
134
- if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
135
- return writeData(creds, 'creds.json')
136
- },
77
+ saveCreds: async () => { if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { }); return writeData(creds, 'creds.json') },
137
78
  getStats,
138
79
  }
139
80
  }
@@ -142,72 +83,41 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
142
83
 
143
84
  const ser = (v) => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
144
85
 
145
- const unwrapNestedValue = (obj) => {
146
- let cur = obj
147
- while (cur && typeof cur === 'object' && !Array.isArray(cur) && Object.keys(cur).length === 1 && 'value' in cur) cur = cur.value
148
- return cur
149
- }
86
+ const unwrapNestedValue = (obj) => { let cur = obj; while (cur && typeof cur === 'object' && !Array.isArray(cur) && Object.keys(cur).length === 1 && 'value' in cur) cur = cur.value; return cur }
150
87
 
151
88
  const deser = (raw, logger) => {
152
89
  if (raw == null) return null
153
90
  let parsed
154
- if (typeof raw === 'string') {
155
- try { parsed = JSON.parse(raw) } catch (err) {
156
- logger?.warn?.({ err: err.message }, '[Auth] failed to parse stored value — treating as missing')
157
- return null
158
- }
159
- } else {
160
- parsed = raw
161
- }
91
+ if (typeof raw === 'string') { try { parsed = JSON.parse(raw) } catch (err) { logger?.warn?.({ err: err.message }, '[Auth] failed to parse stored value — treating as missing'); return null } }
92
+ else { parsed = raw }
162
93
  parsed = unwrapNestedValue(parsed)
163
94
  if (parsed == null) return null
164
95
  if (typeof parsed === 'object' && '__checksum' in parsed && 'data' in parsed) parsed = parsed.data
165
96
  return JSON.parse(JSON.stringify(parsed), BufferJSON.reviver)
166
97
  }
167
98
 
168
- const COLLECTION_OPTION_KEY = {
169
- '@keyv/mongo': 'collection',
170
- '@keyv/postgres': 'table',
171
- '@keyv/mysql': 'table',
172
- '@keyv/sqlite': 'table',
173
- }
174
-
99
+ const COLLECTION_OPTION_KEY = { '@keyv/mongo': 'collection', '@keyv/postgres': 'table', '@keyv/mysql': 'table', '@keyv/sqlite': 'table' }
175
100
  const _adapterCache = new Map()
176
101
  export const clearAdapterCache = () => _adapterCache.clear()
177
102
 
178
103
  const resolveAdapter = async (connectionString, collectionName) => {
179
104
  const cacheKey = collectionName ? `${connectionString}::${collectionName}` : connectionString
180
105
  if (_adapterCache.has(cacheKey)) return _adapterCache.get(cacheKey)
181
-
182
106
  let protocol
183
107
  try { protocol = new URL(connectionString).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${connectionString}"`) }
184
-
185
108
  const pkg = PROTOCOL_ADAPTERS[protocol]
186
109
  if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: redis, mongodb, postgresql, mysql, sqlite, etcd, memcache. Pass a pre-built KeyvStoreAdapter instance for anything else.`)
187
-
188
110
  let mod
189
- try { mod = await import(pkg) } catch (e) {
190
- if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" is required for this backend — install it: npm i ${pkg}`)
191
- throw e
192
- }
193
-
111
+ try { mod = await import(pkg) } catch (e) { if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" is required for this backend — install it: npm i ${pkg}`); throw e }
194
112
  const Adapter = mod.default ?? mod[Object.keys(mod)[0]]
195
113
  const optionKey = COLLECTION_OPTION_KEY[pkg]
196
- const adapter = (collectionName && optionKey)
197
- ? new Adapter({ url: connectionString, uri: connectionString, [optionKey]: collectionName })
198
- : new Adapter(connectionString)
114
+ const adapter = (collectionName && optionKey) ? new Adapter({ url: connectionString, uri: connectionString, [optionKey]: collectionName }) : new Adapter(connectionString)
199
115
  adapter.setMaxListeners?.(0)
200
116
  _adapterCache.set(cacheKey, adapter)
201
117
  return adapter
202
118
  }
203
119
 
204
- const isAdapterLike = (v) =>
205
- v != null &&
206
- typeof v === 'object' &&
207
- typeof v.get === 'function' &&
208
- typeof v.set === 'function' &&
209
- typeof v.delete === 'function' &&
210
- typeof v.clear === 'function'
120
+ const isAdapterLike = (v) => v != null && typeof v === 'object' && typeof v.get === 'function' && typeof v.set === 'function' && typeof v.delete === 'function' && typeof v.clear === 'function'
211
121
 
212
122
  const resolveStore = async (backend, collectionName) => {
213
123
  if (backend == null) return null
@@ -219,17 +129,13 @@ const resolveStore = async (backend, collectionName) => {
219
129
 
220
130
  export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
221
131
  const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, logger, collectionName } = opts
222
-
223
132
  const store = await resolveStore(backend, collectionName)
224
133
  const keyv = store ? new Keyv({ store, namespace: sessionId }) : new Keyv({ namespace: sessionId })
225
134
  keyv.on('error', (err) => logger?.warn?.({ err: err?.message }, '[Auth] store error'))
226
135
 
227
136
  const keyName = (type, id) => `${type}-${id}`
228
-
229
137
  let creds = bootstrapCreds(deser(await keyv.get('creds'), logger))
230
- let cleanupRunning = false
231
- let lastCleanupAt = 0
232
- let lastCleanedPreKeyId = creds.nextPreKeyId
138
+ let cleanupRunning = false, lastCleanupAt = 0, lastCleanedPreKeyId = creds.nextPreKeyId
233
139
 
234
140
  const cleanOldPreKeys = async () => {
235
141
  const now = Date.now()
@@ -242,59 +148,35 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
242
148
  for (let id = Math.max(0, lastCleanedPreKeyId - preKeyRetention); id < minId; id++) ids.push(keyName('pre-key', id))
243
149
  if (!ids.length) return
244
150
  await Promise.all(ids.map(k => keyv.delete(k).catch(() => { })))
245
- lastCleanupAt = Date.now()
246
- lastCleanedPreKeyId = creds.nextPreKeyId
247
- } catch (e) {
248
- logger?.warn?.({ err: e.message }, '[Auth] prekey cleanup failed')
249
- } finally {
250
- cleanupRunning = false
251
- }
151
+ lastCleanupAt = Date.now(); lastCleanedPreKeyId = creds.nextPreKeyId
152
+ } catch (e) { logger?.warn?.({ err: e.message }, '[Auth] prekey cleanup failed') }
153
+ finally { cleanupRunning = false }
252
154
  }
253
155
 
254
156
  cleanOldPreKeys().catch(() => { })
255
157
 
256
158
  const keys = {
257
159
  get: async (type, ids) => {
258
- const data = {}
259
- if (!ids.length) return data
260
- const rawKeys = ids.map(id => keyName(type, id))
261
- const rawValues = await keyv.getMany(rawKeys)
262
- const rewrites = []
263
- ids.forEach((id, i) => {
264
- data[id] = patchAppStateKey(type, deser(rawValues[i], logger))
265
- })
266
- if (rewrites.length) await Promise.all(rewrites).catch(() => { })
267
- return data
160
+ if (!ids.length) return {}
161
+ const rawValues = await keyv.getMany(ids.map(id => keyName(type, id)))
162
+ return Object.fromEntries(ids.map((id, i) => [id, patchAppStateKey(type, deser(rawValues[i], logger))]))
268
163
  },
269
164
  set: async (data) => {
270
- const setEntries = []
271
- const delKeys = []
272
- for (const type in data) {
273
- for (const id in data[type]) {
274
- const value = data[type][id]
275
- const key = keyName(type, id)
276
- if (value != null) setEntries.push({ key, value: ser(value) })
277
- else delKeys.push(key)
278
- }
279
- }
280
- await Promise.all([
281
- setEntries.length ? keyv.setMany(setEntries) : null,
282
- delKeys.length ? keyv.deleteMany(delKeys) : null,
283
- ])
165
+ const setEntries = [], delKeys = []
166
+ for (const type in data) for (const id in data[type]) { const v = data[type][id], k = keyName(type, id); v != null ? setEntries.push({ key: k, value: ser(v) }) : delKeys.push(k) }
167
+ await Promise.all([setEntries.length ? keyv.setMany(setEntries) : null, delKeys.length ? keyv.deleteMany(delKeys) : null])
284
168
  },
285
169
  }
286
170
 
287
- const saveCreds = async () => {
288
- if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
289
- await keyv.set('creds', ser(creds))
290
- }
171
+ const saveCreds = async () => { if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { }); await keyv.set('creds', ser(creds)) }
291
172
 
292
173
  const clearSession = async () => {
293
174
  const s = keyv.opts?.store ?? keyv.store
294
- if (s && typeof s.deleteMany === 'function' && typeof s.iterator === 'function') {
175
+ if (s && typeof s.iterator === 'function') {
176
+ // v6: iterator() takes no namespace arg — uses this._namespace set via new Keyv({ namespace: sessionId })
295
177
  const toDelete = []
296
- for await (const [key] of s.iterator(sessionId)) toDelete.push(key)
297
- if (toDelete.length) await s.deleteMany(toDelete)
178
+ for await (const [key] of s.iterator()) toDelete.push(key)
179
+ if (toDelete.length) typeof s.deleteMany === 'function' ? await s.deleteMany(toDelete) : await Promise.all(toDelete.map(k => s.delete(k).catch(() => { })))
298
180
  } else {
299
181
  await keyv.clear()
300
182
  }
@@ -304,4 +186,15 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
304
186
  return { state: { creds, keys }, saveCreds, clearSession }
305
187
  }
306
188
 
189
+ export const hasKeyvAuth = async (sessionId, backend, opts = {}) => {
190
+ try {
191
+ const store = await resolveStore(backend, opts.collectionName)
192
+ const keyv = store ? new Keyv({ store, namespace: sessionId }) : new Keyv({ namespace: sessionId })
193
+ const raw = await keyv.get('creds')
194
+ if (!raw) return false
195
+ const parsed = deser(raw)
196
+ return !!(parsed?.noiseKey && parsed?.signedIdentityKey)
197
+ } catch { return false }
198
+ }
199
+
307
200
  export { Keyv }