@zervonex/baileys 0.0.0-stage → 3.0.1

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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3203 -2
  3. package/WAProto/WAProtoCompile.js +291 -0
  4. package/WAProto/fetcher.js +69 -0
  5. package/WAProto/index.js +88 -0
  6. package/WAProto/parser.js +313 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Builders/AIRich.js +590 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +448 -0
  11. package/lib/Builders/index.js +3 -0
  12. package/lib/Defaults/index.js +225 -0
  13. package/lib/Defaults/phonenumber-mcc.json +223 -0
  14. package/lib/Signal/index.js +2 -0
  15. package/lib/Signal/libsignal.js +523 -0
  16. package/lib/Signal/lid-mapping.js +283 -0
  17. package/lib/Socket/Client/index.js +2 -0
  18. package/lib/Socket/Client/types.js +10 -0
  19. package/lib/Socket/Client/websocket.js +84 -0
  20. package/lib/Socket/business.js +152 -0
  21. package/lib/Socket/chats.js +855 -0
  22. package/lib/Socket/communities.js +147 -0
  23. package/lib/Socket/groups.js +294 -0
  24. package/lib/Socket/index.js +15 -0
  25. package/lib/Socket/messages-recv.js +904 -0
  26. package/lib/Socket/messages-send.js +693 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +752 -0
  29. package/lib/Socket/nexus-handler.js +55 -0
  30. package/lib/Socket/registration.js +155 -0
  31. package/lib/Socket/socket.js +798 -0
  32. package/lib/Socket/spam-report.js +65 -0
  33. package/lib/Socket/username.js +82 -0
  34. package/lib/Store/index.js +6 -0
  35. package/lib/Store/make-cache-manager-store.js +81 -0
  36. package/lib/Store/make-in-memory-store.js +425 -0
  37. package/lib/Store/make-ordered-dictionary.js +82 -0
  38. package/lib/Store/object-repository.js +31 -0
  39. package/lib/Store/reporting-info-store.js +38 -0
  40. package/lib/Types/Auth.js +2 -0
  41. package/lib/Types/Bussines.js +2 -0
  42. package/lib/Types/Call.js +2 -0
  43. package/lib/Types/Chat.js +8 -0
  44. package/lib/Types/Contact.js +2 -0
  45. package/lib/Types/Events.js +2 -0
  46. package/lib/Types/GroupMetadata.js +2 -0
  47. package/lib/Types/Label.js +25 -0
  48. package/lib/Types/LabelAssociation.js +7 -0
  49. package/lib/Types/Message.js +15 -0
  50. package/lib/Types/Newsletter.js +100 -0
  51. package/lib/Types/Product.js +2 -0
  52. package/lib/Types/Signal.js +2 -0
  53. package/lib/Types/Socket.js +14 -0
  54. package/lib/Types/State.js +26 -0
  55. package/lib/Types/USync.js +2 -0
  56. package/lib/Types/index.js +30 -0
  57. package/lib/Utils/auth-utils.js +142 -0
  58. package/lib/Utils/baileys-event-stream.js +33 -0
  59. package/lib/Utils/bot-capabilities.js +40 -0
  60. package/lib/Utils/bot-planning-replay.js +44 -0
  61. package/lib/Utils/browser-utils.js +26 -0
  62. package/lib/Utils/business.js +75 -0
  63. package/lib/Utils/chat-utils.js +316 -0
  64. package/lib/Utils/companion-reg-client-utils.js +20 -0
  65. package/lib/Utils/consumer-application.js +38 -0
  66. package/lib/Utils/crypto.js +52 -0
  67. package/lib/Utils/decode-wa-message.js +197 -0
  68. package/lib/Utils/event-buffer.js +273 -0
  69. package/lib/Utils/extras.js +186 -0
  70. package/lib/Utils/generics.js +195 -0
  71. package/lib/Utils/history.js +51 -0
  72. package/lib/Utils/identity-change-handler.js +19 -0
  73. package/lib/Utils/index.js +39 -0
  74. package/lib/Utils/key-store.js +3 -0
  75. package/lib/Utils/link-preview.js +51 -0
  76. package/lib/Utils/logger.js +2 -0
  77. package/lib/Utils/lt-hash.js +3 -0
  78. package/lib/Utils/make-mutex.js +16 -0
  79. package/lib/Utils/message-composer.js +59 -0
  80. package/lib/Utils/message-retry-manager.js +48 -0
  81. package/lib/Utils/messages-media.js +520 -0
  82. package/lib/Utils/messages.js +742 -0
  83. package/lib/Utils/meta-ai-msmsg.js +122 -0
  84. package/lib/Utils/meta-compositing.js +75 -0
  85. package/lib/Utils/modern-messages.js +235 -0
  86. package/lib/Utils/native-flow.js +55 -0
  87. package/lib/Utils/noise-handler.js +161 -0
  88. package/lib/Utils/pre-key-manager.js +76 -0
  89. package/lib/Utils/process-message.js +399 -0
  90. package/lib/Utils/reporting-utils.js +171 -0
  91. package/lib/Utils/signal.js +167 -0
  92. package/lib/Utils/spam-report-utils.js +142 -0
  93. package/lib/Utils/sync-action-utils.js +33 -0
  94. package/lib/Utils/tc-token-utils.js +102 -0
  95. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  96. package/lib/Utils/validate-connection.js +181 -0
  97. package/lib/WABinary/constants.js +1321 -0
  98. package/lib/WABinary/decode.js +152 -0
  99. package/lib/WABinary/encode.js +140 -0
  100. package/lib/WABinary/generic-utils.js +179 -0
  101. package/lib/WABinary/index.js +6 -0
  102. package/lib/WABinary/jid-utils.js +235 -0
  103. package/lib/WABinary/types.js +4 -0
  104. package/lib/WAM/BinaryInfo.js +10 -0
  105. package/lib/WAM/constants.js +22853 -0
  106. package/lib/WAM/encode.js +150 -0
  107. package/lib/WAM/index.js +3 -0
  108. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  109. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  110. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  111. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  112. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  113. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  114. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  115. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  116. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  118. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  119. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  120. package/lib/WAUSync/Protocols/index.js +12 -0
  121. package/lib/WAUSync/USyncQuery.js +100 -0
  122. package/lib/WAUSync/USyncUser.js +20 -0
  123. package/lib/WAUSync/index.js +3 -0
  124. package/lib/index.js +68 -0
  125. package/package.json +216 -5
@@ -0,0 +1,44 @@
1
+ import { buildPlainPlaceholder, PlanningStepStatus } from './meta-compositing.js'
2
+ import { delay } from './generics.js'
3
+
4
+ // Live planning animation: shows pending steps, edits each to DONE one-by-one,
5
+ // deletes placeholder, then sends the final message. Works on all WA clients.
6
+ export const replayPlanning = async (sock, jid, steps, finalContent, { description = 'Thinking…', placeholderText = '', stepDelayMs = 900, finalPauseMs = 600, abortOnDisconnect = true, sendOptions = {} } = {}) => {
7
+ if (!steps?.length) throw new Error('replayPlanning: steps must have at least one entry')
8
+
9
+ let aborted = false
10
+ if (abortOnDisconnect) sock.ev?.once?.('connection.update', ({ connection }) => { if (connection === 'close') aborted = true })
11
+
12
+ await sock.sendPresenceUpdate('composing', jid)
13
+
14
+ const initial = steps.map(s => ({ ...s, status: PlanningStepStatus.IN_PROGRESS }))
15
+ const placeholder = await sock.sendMessage(jid, buildPlainPlaceholder(description, initial, placeholderText))
16
+ const key = placeholder?.key
17
+
18
+ try {
19
+ const current = [...initial]
20
+ for (let i = 0; i < current.length; i++) {
21
+ if (aborted) break
22
+ await delay(stepDelayMs)
23
+ if (aborted) break
24
+ current[i] = { ...current[i], status: PlanningStepStatus.DONE }
25
+ if (key) await sock.sendMessage(jid, { edit: key, ...buildPlainPlaceholder(description, current, placeholderText) })
26
+ }
27
+ if (!aborted && finalPauseMs > 0) await delay(finalPauseMs)
28
+ if (key && !aborted) await sock.sendMessage(jid, { delete: key })
29
+ } catch (_) {
30
+ try { if (key) await sock.sendMessage(jid, { delete: key }) } catch (__) { }
31
+ }
32
+
33
+ await sock.sendPresenceUpdate('paused', jid)
34
+ if (sendOptions._skipFinalSend) return placeholder
35
+ return sock.sendMessage(jid, finalContent, sendOptions)
36
+ }
37
+
38
+ // Animation without a final message
39
+ export const replayPlanningOnly = (sock, jid, steps, options = {}) =>
40
+ replayPlanning(sock, jid, steps, null, { ...options, sendOptions: { ...options.sendOptions, _skipFinalSend: true } })
41
+
42
+ export const buildReasoningSteps = titles => titles.map(title => ({ title, isReasoning: true }))
43
+ export const buildSearchSteps = titles => titles.map(title => ({ title, isEnhancedSearch: true }))
44
+ export const mixedSteps = defs => defs.map(({ title, body, type }) => ({ title, ...(body ? { body } : {}), ...(type === 'reasoning' ? { isReasoning: true } : {}), ...(type === 'search' ? { isEnhancedSearch: true } : {}) }))
@@ -0,0 +1,26 @@
1
+ import { platform, release } from 'os'
2
+ import { proto } from '../../WAProto/index.js'
3
+
4
+ const PLATFORM_MAP = {
5
+ aix: 'AIX', darwin: 'Mac OS', win32: 'Windows', android: 'Android',
6
+ freebsd: 'FreeBSD', openbsd: 'OpenBSD', sunos: 'Solaris',
7
+ linux: undefined, haiku: undefined, cygwin: undefined, netbsd: undefined
8
+ }
9
+
10
+ export const Browsers = {
11
+ ubuntu: browser => ['Ubuntu', browser, '24.04.2'],
12
+ macOS: browser => ['Mac OS', browser, '15.4.1'],
13
+ baileys: browser => ['Baileys', browser, '6.5.0'],
14
+ windows: browser => ['Windows', browser, '10.0.26100'],
15
+ android: browser => [browser, 'Android', ''],
16
+ appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
17
+ }
18
+
19
+ export const getPlatformId = browser => {
20
+ 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()
24
+ }
25
+
26
+ export const isAndroidBrowser = browser => browser[1]?.toUpperCase() === 'ANDROID'
@@ -0,0 +1,75 @@
1
+ import { Boom } from '@hapi/boom'
2
+ import { createHash } from 'crypto'
3
+ import { getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString } from '../WABinary/index.js'
4
+ import { getStream, getUrlFromDirectPath, toReadable } from './messages-media.js'
5
+
6
+ export const parseCatalogNode = node => {
7
+ const catalogNode = getBinaryNodeChild(node, 'product_catalog')
8
+ const paging = getBinaryNodeChild(catalogNode, 'paging')
9
+ return { products: getBinaryNodeChildren(catalogNode, 'product').map(parseProductNode), nextPageCursor: paging ? getBinaryNodeChildString(paging, 'after') : undefined }
10
+ }
11
+
12
+ export const parseCollectionsNode = node => {
13
+ const collectionsNode = getBinaryNodeChild(node, 'collections')
14
+ return { collections: getBinaryNodeChildren(collectionsNode, 'collection').map(collectionNode => ({ id: getBinaryNodeChildString(collectionNode, 'id'), name: getBinaryNodeChildString(collectionNode, 'name'), products: getBinaryNodeChildren(collectionNode, 'product').map(parseProductNode), status: parseStatusInfo(collectionNode) })) }
15
+ }
16
+
17
+ export const parseOrderDetailsNode = node => {
18
+ const orderNode = getBinaryNodeChild(node, 'order')
19
+ const priceNode = getBinaryNodeChild(orderNode, 'price')
20
+ const products = getBinaryNodeChildren(orderNode, 'product').map(productNode => {
21
+ const imageNode = getBinaryNodeChild(productNode, 'image')
22
+ return { id: getBinaryNodeChildString(productNode, 'id'), name: getBinaryNodeChildString(productNode, 'name'), imageUrl: getBinaryNodeChildString(imageNode, 'url'), price: +getBinaryNodeChildString(productNode, 'price'), currency: getBinaryNodeChildString(productNode, 'currency'), quantity: +getBinaryNodeChildString(productNode, 'quantity') }
23
+ })
24
+ return { price: { total: +getBinaryNodeChildString(priceNode, 'total'), currency: getBinaryNodeChildString(priceNode, 'currency') }, products }
25
+ }
26
+
27
+ export const toProductNode = (productId, product) => {
28
+ const attrs = {}
29
+ const content = []
30
+ const push = (tag, value) => content.push({ tag, attrs: {}, content: Buffer.from(value) })
31
+ if (productId !== undefined) push('id', productId)
32
+ if (product.name !== undefined) push('name', product.name)
33
+ if (product.description !== undefined) push('description', product.description)
34
+ if (product.retailerId !== undefined) push('retailer_id', product.retailerId)
35
+ if (product.images?.length) {
36
+ content.push({
37
+ tag: 'media', attrs: {}, content: product.images.map(img => {
38
+ if (!('url' in img)) throw new Boom('Expected img for product to already be uploaded', { statusCode: 400 })
39
+ return { tag: 'image', attrs: {}, content: [{ tag: 'url', attrs: {}, content: Buffer.from(img.url.toString()) }] }
40
+ })
41
+ })
42
+ }
43
+ if (product.price !== undefined) push('price', product.price.toString())
44
+ if (product.currency !== undefined) push('currency', product.currency)
45
+ if ('originCountryCode' in product) {
46
+ if (product.originCountryCode === undefined) attrs['compliance_category'] = 'COUNTRY_ORIGIN_EXEMPT'
47
+ else content.push({ tag: 'compliance_info', attrs: {}, content: [{ tag: 'country_code_origin', attrs: {}, content: Buffer.from(product.originCountryCode) }] })
48
+ }
49
+ if (product.isHidden !== undefined) attrs['is_hidden'] = product.isHidden.toString()
50
+ return { tag: 'product', attrs, content }
51
+ }
52
+
53
+ export const parseProductNode = productNode => {
54
+ const mediaNode = getBinaryNodeChild(productNode, 'media')
55
+ const statusInfoNode = getBinaryNodeChild(productNode, 'status_info')
56
+ return { id: getBinaryNodeChildString(productNode, 'id'), imageUrls: parseImageUrls(mediaNode), reviewStatus: { whatsapp: getBinaryNodeChildString(statusInfoNode, 'status') }, availability: 'in stock', name: getBinaryNodeChildString(productNode, 'name'), retailerId: getBinaryNodeChildString(productNode, 'retailer_id'), url: getBinaryNodeChildString(productNode, 'url'), description: getBinaryNodeChildString(productNode, 'description'), price: +getBinaryNodeChildString(productNode, 'price'), currency: getBinaryNodeChildString(productNode, 'currency'), isHidden: productNode.attrs.is_hidden === 'true' }
57
+ }
58
+
59
+ export const uploadingNecessaryImagesOfProduct = async (product, waUploadToServer, timeoutMs = 30000) =>
60
+ ({ ...product, images: product.images ? await uploadingNecessaryImages(product.images, waUploadToServer, timeoutMs) : product.images })
61
+
62
+ export const uploadingNecessaryImages = (images, waUploadToServer, timeoutMs = 30000) =>
63
+ Promise.all(images.map(async img => {
64
+ if ('url' in img && img.url.toString().includes('.whatsapp.net')) return { url: img.url.toString() }
65
+ const { stream } = await getStream(img)
66
+ const hasher = createHash('sha256')
67
+ const blocks = []
68
+ for await (const block of stream) { hasher.update(block); blocks.push(block) }
69
+ const sha = hasher.digest('base64')
70
+ const { directPath } = await waUploadToServer(toReadable(Buffer.concat(blocks)), { mediaType: 'product-catalog-image', fileEncSha256B64: sha, timeoutMs })
71
+ return { url: getUrlFromDirectPath(directPath) }
72
+ }))
73
+
74
+ const parseImageUrls = mediaNode => { const imgNode = getBinaryNodeChild(mediaNode, 'image'); return { requested: getBinaryNodeChildString(imgNode, 'request_image_url'), original: getBinaryNodeChildString(imgNode, 'original_image_url') } }
75
+ const parseStatusInfo = mediaNode => { const node = getBinaryNodeChild(mediaNode, 'status_info'); return { status: getBinaryNodeChildString(node, 'status'), canAppeal: getBinaryNodeChildString(node, 'can_appeal') === 'true' } }
@@ -0,0 +1,316 @@
1
+ import { Boom } from '@hapi/boom'
2
+ import { expandAppStateKeys } from 'whatsapp-rust-bridge'
3
+ import { proto } from '../../WAProto/index.js'
4
+ import { LabelAssociationType } from '../Types/LabelAssociation.js'
5
+ import { processContactAction, emitSyncActionResults } from './sync-action-utils.js'
6
+ import { getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary/index.js'
7
+ import { aesDecrypt, aesEncrypt, hmacSign } from './crypto.js'
8
+ import { toNumber } from './generics.js'
9
+ import { LT_HASH_ANTI_TAMPERING } from './lt-hash.js'
10
+ import { downloadContentFromMessage } from './messages-media.js'
11
+
12
+ const OP = proto.SyncdMutation.SyncdOperation
13
+
14
+ const mutationKeys = keydata => {
15
+ const keys = expandAppStateKeys(keydata)
16
+ return { indexKey: keys.indexKey, valueEncryptionKey: keys.valueEncryptionKey, valueMacKey: keys.valueMacKey, snapshotMacKey: keys.snapshotMacKey, patchMacKey: keys.patchMacKey }
17
+ }
18
+
19
+ const generateMac = (operation, data, keyId, key) => {
20
+ const opByte = operation === OP.SET ? 0x01 : 0x02
21
+ const keyIdBuffer = typeof keyId === 'string' ? Buffer.from(keyId, 'base64') : keyId
22
+ const keyData = new Uint8Array(1 + keyIdBuffer.length)
23
+ keyData[0] = opByte
24
+ keyData.set(keyIdBuffer, 1)
25
+ const last = new Uint8Array(8)
26
+ last[7] = keyData.length
27
+ const total = new Uint8Array(keyData.length + data.length + last.length)
28
+ total.set(keyData, 0); total.set(data, keyData.length); total.set(last, keyData.length + data.length)
29
+ return hmacSign(total, key, 'sha512').subarray(0, 32)
30
+ }
31
+
32
+ const to64BitNetworkOrder = e => { const buff = Buffer.alloc(8); buff.writeUint32BE(e, 4); return buff }
33
+
34
+ export const makeLtHashGenerator = ({ indexValueMap, hash }) => {
35
+ indexValueMap = { ...indexValueMap }
36
+ const addBuffs = [], subBuffs = []
37
+ return {
38
+ mix: ({ indexMac, valueMac, operation }) => {
39
+ const key = Buffer.from(indexMac).toString('base64')
40
+ const prev = indexValueMap[key]
41
+ if (operation === OP.REMOVE) { if (!prev) return; delete indexValueMap[key] }
42
+ else { addBuffs.push(valueMac); indexValueMap[key] = { valueMac } }
43
+ if (prev) subBuffs.push(prev.valueMac)
44
+ },
45
+ finish: async () => ({ hash: Buffer.from(LT_HASH_ANTI_TAMPERING.subtractThenAdd(hash, subBuffs, addBuffs)), indexValueMap })
46
+ }
47
+ }
48
+
49
+ const generateSnapshotMac = (lthash, version, name, key) => hmacSign(Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, 'utf-8')]), key, 'sha256')
50
+ const generatePatchMac = (snapshotMac, valueMacs, version, type, key) => hmacSign(Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')]), key)
51
+
52
+ export const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} })
53
+ export const ensureLTHashStateVersion = state => { if (typeof state.version !== 'number' || isNaN(state.version)) state.version = 0; return state }
54
+
55
+ export const MAX_SYNC_ATTEMPTS = 2
56
+ export const isMissingKeyError = error => error?.data?.isMissingKey === true
57
+ export const isAppStateSyncIrrecoverable = (error, attempts) => attempts >= MAX_SYNC_ATTEMPTS || error?.name === 'TypeError'
58
+
59
+ export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, operation }, myAppStateKeyId, state, getAppStateSyncKey) => {
60
+ const key = myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
61
+ if (!key) throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { data: { isMissingKey: true } })
62
+ const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
63
+ state = { ...state, indexValueMap: { ...state.indexValueMap } }
64
+ const indexBuffer = Buffer.from(JSON.stringify(index))
65
+ const dataProto = proto.SyncActionData.fromObject({ index: indexBuffer, value: syncAction, padding: new Uint8Array(0), version: apiVersion })
66
+ const encoded = proto.SyncActionData.encode(dataProto).finish()
67
+ const keyValue = mutationKeys(key.keyData)
68
+ const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
69
+ const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
70
+ const indexMac = hmacSign(indexBuffer, keyValue.indexKey)
71
+ const generator = makeLtHashGenerator(state)
72
+ generator.mix({ indexMac, valueMac, operation })
73
+ Object.assign(state, await generator.finish())
74
+ state.version += 1
75
+ const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey)
76
+ const patch = {
77
+ patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),
78
+ snapshotMac,
79
+ keyId: { id: encKeyId },
80
+ mutations: [{ operation, record: { index: { blob: indexMac }, value: { blob: Buffer.concat([encValue, valueMac]) }, keyId: { id: encKeyId } } }]
81
+ }
82
+ state.indexValueMap[indexMac.toString('base64')] = { valueMac }
83
+ return { patch, state }
84
+ }
85
+
86
+ export const decodeSyncdMutations = async (msgMutations, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
87
+ const ltGenerator = makeLtHashGenerator(initialState)
88
+ const derivedKeyCache = new Map()
89
+
90
+ async function getKey(keyId) {
91
+ const base64Key = Buffer.from(keyId).toString('base64')
92
+ if (derivedKeyCache.has(base64Key)) return derivedKeyCache.get(base64Key)
93
+ const keyEnc = await getAppStateSyncKey(base64Key)
94
+ if (!keyEnc) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true, msgMutations } })
95
+ const keys = mutationKeys(keyEnc.keyData)
96
+ derivedKeyCache.set(base64Key, keys)
97
+ return keys
98
+ }
99
+
100
+ for (const msgMutation of msgMutations) {
101
+ const operation = 'operation' in msgMutation ? msgMutation.operation : OP.SET
102
+ const record = 'record' in msgMutation && msgMutation.record ? msgMutation.record : msgMutation
103
+ let key
104
+ try { key = await getKey(record.keyId.id) } catch (err) { if (isMissingKeyError(err)) throw err; continue }
105
+ const content = record.value.blob
106
+ const encContent = content.subarray(0, -32)
107
+ const ogValueMac = content.subarray(-32)
108
+ if (validateMacs) {
109
+ const contentHmac = generateMac(operation, encContent, record.keyId.id, key.valueMacKey)
110
+ if (Buffer.compare(contentHmac, ogValueMac) !== 0) continue
111
+ }
112
+ let result
113
+ try { result = aesDecrypt(encContent, key.valueEncryptionKey) } catch { continue }
114
+ const syncAction = proto.SyncActionData.decode(result)
115
+ if (validateMacs) {
116
+ const hmac = hmacSign(syncAction.index, key.indexKey)
117
+ if (Buffer.compare(hmac, record.index.blob) !== 0) throw new Boom('HMAC index verification failed')
118
+ }
119
+ onMutation({ syncAction, index: JSON.parse(Buffer.from(syncAction.index).toString()) })
120
+ ltGenerator.mix({ indexMac: record.index.blob, valueMac: ogValueMac, operation })
121
+ }
122
+ return ltGenerator.finish()
123
+ }
124
+
125
+ export const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
126
+ if (validateMacs) {
127
+ const base64Key = Buffer.from(msg.keyId.id).toString('base64')
128
+ const mainKeyObj = await getAppStateSyncKey(base64Key)
129
+ if (!mainKeyObj) throw new Boom(`failed to find key "${base64Key}" to decode patch`, { data: { isMissingKey: true, msg } })
130
+ const mainKey = mutationKeys(mainKeyObj.keyData)
131
+ const mutationmacs = msg.mutations.map(m => m.record.value.blob.slice(-32))
132
+ const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, toNumber(msg.version.version), name, mainKey.patchMacKey)
133
+ if (Buffer.compare(patchMac, msg.patchMac) !== 0) throw new Boom('Invalid patch mac')
134
+ }
135
+ return decodeSyncdMutations(msg.mutations, initialState, getAppStateSyncKey, onMutation, validateMacs)
136
+ }
137
+
138
+ export const extractSyncdPatches = async (result, options) => {
139
+ const syncNode = getBinaryNodeChild(result, 'sync')
140
+ const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
141
+ const final = {}
142
+ await Promise.all(collectionNodes.map(async collectionNode => {
143
+ const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
144
+ const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
145
+ const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot')
146
+ const syncds = []
147
+ const name = collectionNode.attrs.name
148
+ const hasMorePatches = collectionNode.attrs.has_more_patches === 'true'
149
+ let snapshot
150
+ if (snapshotNode?.content) {
151
+ if (!Buffer.isBuffer(snapshotNode)) snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
152
+ const blobRef = proto.ExternalBlobReference.decode(snapshotNode.content)
153
+ snapshot = proto.SyncdSnapshot.decode(await downloadExternalBlob(blobRef, options))
154
+ }
155
+ for (let { content } of patches) {
156
+ if (!content) continue
157
+ if (!Buffer.isBuffer(content)) content = Buffer.from(Object.values(content))
158
+ const syncd = proto.SyncdPatch.decode(content)
159
+ if (!syncd.version) syncd.version = { version: +collectionNode.attrs.version + 1 }
160
+ syncds.push(syncd)
161
+ }
162
+ final[name] = { patches: syncds, hasMorePatches, snapshot }
163
+ }))
164
+ return final
165
+ }
166
+
167
+ export const downloadExternalBlob = async (blob, options) => {
168
+ const stream = await downloadContentFromMessage(blob, 'md-app-state', { options })
169
+ const bufferArray = []
170
+ for await (const chunk of stream) bufferArray.push(chunk)
171
+ return Buffer.concat(bufferArray)
172
+ }
173
+
174
+ export const downloadExternalPatch = async (blob, options) => {
175
+ const buffer = await downloadExternalBlob(blob, options)
176
+ return proto.SyncdMutations.decode(buffer)
177
+ }
178
+
179
+ export const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, minimumVersionNumber, validateMacs = true, logger) => {
180
+ const newState = newLTHashState()
181
+ newState.version = toNumber(snapshot.version.version)
182
+ const mutationMap = {}
183
+ const areMutationsRequired = minimumVersionNumber === undefined || newState.version > minimumVersionNumber
184
+ const onMutation = areMutationsRequired ? mutation => { const index = mutation.syncAction.index?.toString(); mutationMap[index] = mutation } : () => { }
185
+ const { hash, indexValueMap } = await decodeSyncdMutations(snapshot.records, newState, getAppStateSyncKey, onMutation, validateMacs)
186
+ newState.hash = hash
187
+ newState.indexValueMap = indexValueMap
188
+ if (validateMacs) {
189
+ const base64Key = Buffer.from(snapshot.keyId.id).toString('base64')
190
+ const keyEnc = await getAppStateSyncKey(base64Key)
191
+ if (!keyEnc) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } })
192
+ const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, mutationKeys(keyEnc.keyData).snapshotMacKey)
193
+ // soft failure — MAC mismatch on snapshot doesn't abort; state is still usable
194
+ if (Buffer.compare(snapshot.mac, computedSnapshotMac) !== 0) logger?.warn({ name, version: newState.version }, 'LTHash verification failed on snapshot, continuing with partial state')
195
+ }
196
+ return { state: newState, mutationMap }
197
+ }
198
+
199
+ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, options, minimumVersionNumber, logger, validateMacs = true) => {
200
+ const newState = { ...initial, indexValueMap: { ...initial.indexValueMap } }
201
+ const mutationMap = {}
202
+ for (const syncd of syncds) {
203
+ const { version, keyId, snapshotMac } = syncd
204
+ if (syncd.externalMutations) {
205
+ logger?.trace({ name, version }, 'downloading external patch')
206
+ const ref = await downloadExternalPatch(syncd.externalMutations, options)
207
+ logger?.debug({ name, version, mutations: ref.mutations.length }, 'downloaded external patch')
208
+ syncd.mutations?.push(...ref.mutations)
209
+ }
210
+ const patchVersion = toNumber(version.version)
211
+ newState.version = patchVersion
212
+ const shouldMutate = minimumVersionNumber === undefined || patchVersion > minimumVersionNumber
213
+ const onMutation = shouldMutate ? mutation => { const index = mutation.syncAction.index?.toString(); mutationMap[index] = mutation } : () => { }
214
+ let decodeResult
215
+ try { decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, onMutation, validateMacs) }
216
+ catch (err) { if (isMissingKeyError(err)) throw err; logger?.warn({ name, version: patchVersion, error: err.message }, 'failed to decode patch, skipping'); continue }
217
+ newState.hash = decodeResult.hash
218
+ newState.indexValueMap = decodeResult.indexValueMap
219
+ if (validateMacs) {
220
+ const base64Key = Buffer.from(keyId.id).toString('base64')
221
+ const keyEnc = await getAppStateSyncKey(base64Key)
222
+ if (!keyEnc) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } })
223
+ const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, mutationKeys(keyEnc.keyData).snapshotMacKey)
224
+ // MAC mismatch mid-patch-stream means state is corrupted from this point; stop processing
225
+ if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) { logger?.warn({ name, version: newState.version }, 'LTHash verification failed, skipping remaining patches'); break }
226
+ }
227
+ syncd.mutations = []
228
+ }
229
+ return { state: newState, mutationMap }
230
+ }
231
+
232
+ export const chatModificationToAppPatch = (mod, jid) => {
233
+ const getMessageRange = lastMessages => {
234
+ if (!Array.isArray(lastMessages)) return lastMessages
235
+ const lastMsg = lastMessages[lastMessages.length - 1]
236
+ return {
237
+ lastMessageTimestamp: lastMsg?.messageTimestamp,
238
+ messages: lastMessages?.length ? lastMessages.map(m => {
239
+ if (!m.key?.id || !m.key?.remoteJid) throw new Boom('Incomplete key', { statusCode: 400, data: m })
240
+ if (isJidGroup(m.key.remoteJid) && !m.key.fromMe && !m.key.participant) throw new Boom('Expected not from me message to have participant', { statusCode: 400, data: m })
241
+ if (!m.messageTimestamp || !toNumber(m.messageTimestamp)) throw new Boom('Missing timestamp in last message list', { statusCode: 400, data: m })
242
+ if (m.key.participant) m.key.participant = jidNormalizedUser(m.key.participant)
243
+ return m
244
+ }) : undefined
245
+ }
246
+ }
247
+
248
+ let patch
249
+ if ('mute' in mod) patch = { syncAction: { muteAction: { muted: !!mod.mute, muteEndTimestamp: mod.mute || undefined } }, index: ['mute', jid], type: 'regular_high', apiVersion: 2, operation: OP.SET }
250
+ else if ('archive' in mod) patch = { syncAction: { archiveChatAction: { archived: !!mod.archive, messageRange: getMessageRange(mod.lastMessages) } }, index: ['archive', jid], type: 'regular_low', apiVersion: 3, operation: OP.SET }
251
+ else if ('markRead' in mod) patch = { syncAction: { markChatAsReadAction: { read: mod.markRead, messageRange: getMessageRange(mod.lastMessages) } }, index: ['markChatAsRead', jid], type: 'regular_low', apiVersion: 3, operation: OP.SET }
252
+ else if ('deleteForMe' in mod) { const { timestamp, key, deleteMedia } = mod.deleteForMe; patch = { syncAction: { deleteMessageForMeAction: { deleteMedia, messageTimestamp: timestamp } }, index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'], type: 'regular_high', apiVersion: 3, operation: OP.SET } }
253
+ else if ('clear' in mod) patch = { syncAction: { clearChatAction: { messageRange: getMessageRange(mod.lastMessages) } }, index: ['clearChat', jid, '1', '0'], type: 'regular_high', apiVersion: 6, operation: OP.SET }
254
+ else if ('pin' in mod) patch = { syncAction: { pinAction: { pinned: !!mod.pin } }, index: ['pin_v1', jid], type: 'regular_low', apiVersion: 5, operation: OP.SET }
255
+ else if ('contact' in mod) patch = { syncAction: { contactAction: mod.contact || {} }, index: ['contact', jid], type: 'critical_unblock_low', apiVersion: 2, operation: mod.contact ? OP.SET : OP.REMOVE }
256
+ else if ('disableLinkPreviews' in mod) patch = { syncAction: { privacySettingDisableLinkPreviewsAction: mod.disableLinkPreviews || {} }, index: ['setting_disableLinkPreviews'], type: 'regular', apiVersion: 8, operation: OP.SET }
257
+ else if ('star' in mod) { const key = mod.star.messages[0]; patch = { syncAction: { starAction: { starred: !!mod.star.star } }, index: ['star', jid, key.id, key.fromMe ? '1' : '0', '0'], type: 'regular_low', apiVersion: 2, operation: OP.SET } }
258
+ else if ('delete' in mod) patch = { syncAction: { deleteChatAction: { messageRange: getMessageRange(mod.lastMessages) } }, index: ['deleteChat', jid, '1'], type: 'regular_high', apiVersion: 6, operation: OP.SET }
259
+ else if ('pushNameSetting' in mod) patch = { syncAction: { pushNameSetting: { name: mod.pushNameSetting } }, index: ['setting_pushName'], type: 'critical_block', apiVersion: 1, operation: OP.SET }
260
+ else if ('quickReply' in mod) patch = { syncAction: { quickReplyAction: { count: 0, deleted: mod.quickReply.deleted || false, keywords: [], message: mod.quickReply.message || '', shortcut: mod.quickReply.shortcut || '' } }, index: ['quick_reply', mod.quickReply.timestamp || String(Math.floor(Date.now() / 1000))], type: 'regular', apiVersion: 2, operation: OP.SET }
261
+ else if ('addLabel' in mod) patch = { syncAction: { labelEditAction: { name: mod.addLabel.name, color: mod.addLabel.color, predefinedId: mod.addLabel.predefinedId, deleted: mod.addLabel.deleted } }, index: ['label_edit', mod.addLabel.id], type: 'regular', apiVersion: 3, operation: OP.SET }
262
+ else if ('addChatLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: true } }, index: [LabelAssociationType.Chat, mod.addChatLabel.labelId, jid], type: 'regular', apiVersion: 3, operation: OP.SET }
263
+ else if ('removeChatLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: false } }, index: [LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid], type: 'regular', apiVersion: 3, operation: OP.SET }
264
+ else if ('addMessageLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: true } }, index: [LabelAssociationType.Message, mod.addMessageLabel.labelId, jid, mod.addMessageLabel.messageId, '0', '0'], type: 'regular', apiVersion: 3, operation: OP.SET }
265
+ else if ('removeMessageLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: false } }, index: [LabelAssociationType.Message, mod.removeMessageLabel.labelId, jid, mod.removeMessageLabel.messageId, '0', '0'], type: 'regular', apiVersion: 3, operation: OP.SET }
266
+ else throw new Boom('not supported')
267
+
268
+ patch.syncAction.timestamp = Date.now()
269
+ return patch
270
+ }
271
+
272
+ export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) => {
273
+ const isInitialSync = !!initialSyncOpts
274
+ const accountSettings = initialSyncOpts?.accountSettings
275
+ logger?.trace({ syncAction, initialSync: isInitialSync }, 'processing sync action')
276
+ const { syncAction: { value: action }, index: [type, id, msgId, fromMe] } = syncAction
277
+
278
+ const getChatUpdateConditional = (id, msgRange) => !isInitialSync ? undefined : data => {
279
+ const chat = data.historySets.chats[id] || data.chatUpserts[id]
280
+ if (!chat) return false
281
+ if (!msgRange) return true
282
+ const lastMsgTimestamp = Number(msgRange?.lastMessageTimestamp || msgRange?.lastSystemMessageTimestamp || 0)
283
+ return lastMsgTimestamp >= Number(chat?.lastMessageRecvTimestamp || 0)
284
+ }
285
+
286
+ if (action?.muteAction) ev.emit('chats.update', [{ id, muteEndTime: action.muteAction?.muted ? toNumber(action.muteAction.muteEndTimestamp) : null, conditional: getChatUpdateConditional(id, undefined) }])
287
+ else if (action?.archiveChatAction || type === 'archive' || type === 'unarchive') { const archiveAction = action?.archiveChatAction; ev.emit('chats.update', [{ id, archived: archiveAction ? archiveAction.archived : type === 'archive', conditional: getChatUpdateConditional(id, !accountSettings?.unarchiveChats ? undefined : archiveAction?.messageRange) }]) }
288
+ else if (action?.markChatAsReadAction) ev.emit('chats.update', [{ id, unreadCount: isInitialSync && action.markChatAsReadAction.read ? null : action.markChatAsReadAction?.read ? 0 : -1, conditional: getChatUpdateConditional(id, action.markChatAsReadAction?.messageRange) }])
289
+ else if (action?.deleteMessageForMeAction || type === 'deleteMessageForMe') ev.emit('messages.delete', { keys: [{ remoteJid: id, id: msgId, fromMe: fromMe === '1' }] })
290
+ else if (action?.contactAction) emitSyncActionResults(ev, processContactAction(action.contactAction, id, logger))
291
+ else if (action?.pushNameSetting) { const name = action?.pushNameSetting?.name; if (name && me?.name !== name) ev.emit('creds.update', { me: { ...me, name } }) }
292
+ else if (action?.pinAction) ev.emit('chats.update', [{ id, pinned: action.pinAction?.pinned ? toNumber(action.timestamp) : null, conditional: getChatUpdateConditional(id, undefined) }])
293
+ else if (action?.unarchiveChatsSetting) { const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats; ev.emit('creds.update', { accountSettings: { unarchiveChats } }); if (accountSettings) accountSettings.unarchiveChats = unarchiveChats }
294
+ else if (action?.starAction || type === 'star') { let starred = action?.starAction?.starred; if (typeof starred !== 'boolean') starred = syncAction.index[syncAction.index.length - 1] === '1'; ev.emit('messages.update', [{ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' }, update: { starred } }]) }
295
+ else if (action?.deleteChatAction || type === 'deleteChat') { if (!isInitialSync) ev.emit('chats.delete', [id]) }
296
+ else if (action?.labelEditAction) { const { name, color, deleted, predefinedId } = action.labelEditAction; ev.emit('labels.edit', { id, name, color, deleted, predefinedId: predefinedId ? String(predefinedId) : undefined }) }
297
+ else if (action?.labelAssociationAction) ev.emit('labels.association', { type: action.labelAssociationAction.labeled ? 'add' : 'remove', association: type === LabelAssociationType.Chat ? { type: LabelAssociationType.Chat, chatId: syncAction.index[2], labelId: syncAction.index[1] } : { type: LabelAssociationType.Message, chatId: syncAction.index[2], messageId: syncAction.index[3], labelId: syncAction.index[1] } })
298
+ else if (action?.localeSetting?.locale) ev.emit('settings.update', { setting: 'locale', value: action.localeSetting.locale })
299
+ else if (action?.timeFormatAction) ev.emit('settings.update', { setting: 'timeFormat', value: action.timeFormatAction })
300
+ else if (action?.pnForLidChatAction?.pnJid) ev.emit('lid-mapping.update', { lid: id, pn: action.pnForLidChatAction.pnJid })
301
+ else if (action?.privacySettingRelayAllCalls) ev.emit('settings.update', { setting: 'privacySettingRelayAllCalls', value: action.privacySettingRelayAllCalls })
302
+ else if (action?.statusPrivacy) ev.emit('settings.update', { setting: 'statusPrivacy', value: action.statusPrivacy })
303
+ else if (action?.lockChatAction) ev.emit('chats.lock', { id, locked: !!action.lockChatAction.locked })
304
+ else if (action?.privacySettingDisableLinkPreviewsAction) ev.emit('settings.update', { setting: 'disableLinkPreviews', value: action.privacySettingDisableLinkPreviewsAction })
305
+ else if (action?.notificationActivitySettingAction?.notificationActivitySetting) ev.emit('settings.update', { setting: 'notificationActivitySetting', value: action.notificationActivitySettingAction.notificationActivitySetting })
306
+ else if (action?.lidContactAction) ev.emit('contacts.upsert', [{ id, name: action.lidContactAction.fullName || action.lidContactAction.firstName || action.lidContactAction.username || undefined, username: action.lidContactAction.username || undefined, lid: id, phoneNumber: undefined }])
307
+ else if (action?.privacySettingChannelsPersonalisedRecommendationAction) ev.emit('settings.update', { setting: 'channelsPersonalisedRecommendation', value: action.privacySettingChannelsPersonalisedRecommendationAction })
308
+ else if (action?.aiThreadRenameAction) ev.emit('chats.update', [{ id, name: action.aiThreadRenameAction.newTitle || undefined, conditional: getChatUpdateConditional(id, undefined) }])
309
+ else if (action?.threadPinAction) ev.emit('chats.update', [{ id, pinned: action.threadPinAction.pinned ? toNumber(action.timestamp) : null, conditional: getChatUpdateConditional(id, undefined) }])
310
+ else if (action?.newsletterSavedInterestsAction) ev.emit('settings.update', { setting: 'newsletterSavedInterests', value: action.newsletterSavedInterestsAction.newsletterSavedInterests })
311
+ else if (action?.interactiveMessageAction) ev.emit('messages.update', [{ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' }, update: { interactiveMessageAction: action.interactiveMessageAction } }])
312
+ else if (action?.deviceCapabilities || action?.deviceCapabilitiesV2) ev.emit('creds.update', { deviceCapabilities: action.deviceCapabilities || action.deviceCapabilitiesV2 })
313
+ else if (action?.nctSaltSyncAction) ev.emit('creds.update', { nctSalt: action.nctSaltSyncAction.salt })
314
+ else if (action?.bubbleLockMessageAction) ev.emit('messages.update', [{ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' }, update: { bubbleLocked: !!action.bubbleLockMessageAction.locked } }])
315
+ else logger?.debug({ syncAction, id }, 'unprocessable update')
316
+ }
@@ -0,0 +1,20 @@
1
+ export const CompanionWebClientType = Object.freeze({ UNKNOWN: 0, CHROME: 1, EDGE: 2, FIREFOX: 3, IE: 4, OPERA: 5, SAFARI: 6, ELECTRON: 7, UWP: 8, OTHER_WEB_CLIENT: 9 })
2
+
3
+ const BROWSER_TO_COMPANION = {
4
+ Chrome: CompanionWebClientType.CHROME,
5
+ Edge: CompanionWebClientType.EDGE,
6
+ Firefox: CompanionWebClientType.FIREFOX,
7
+ IE: CompanionWebClientType.IE,
8
+ Opera: CompanionWebClientType.OPERA,
9
+ Safari: CompanionWebClientType.SAFARI
10
+ }
11
+
12
+ export const getCompanionWebClientType = ([os, browserName]) => {
13
+ if (browserName === 'Desktop') return os === 'Windows' ? CompanionWebClientType.UWP : CompanionWebClientType.ELECTRON
14
+ return BROWSER_TO_COMPANION[browserName] ?? CompanionWebClientType.OTHER_WEB_CLIENT
15
+ }
16
+
17
+ export const getCompanionPlatformId = browser => getCompanionWebClientType(browser).toString()
18
+
19
+ export const buildPairingQRData = (ref, noiseKeyB64, identityKeyB64, advB64, browser) =>
20
+ 'https://wa.me/settings/linked_devices#' + [ref, noiseKeyB64, identityKeyB64, advB64, getCompanionPlatformId(browser)].join(',')
@@ -0,0 +1,38 @@
1
+ import { proto } from '../../WAProto/index.js'
2
+
3
+ export const decodeConsumerApplication = buffer => {
4
+ if (!Buffer.isBuffer(buffer) && !(buffer instanceof Uint8Array)) throw new TypeError('decodeConsumerApplication: buffer must be Buffer or Uint8Array')
5
+ return proto.ConsumerApplication.decode(buffer)
6
+ }
7
+
8
+ const textOf = mt => (mt && typeof mt.text === 'string' ? mt.text : undefined)
9
+
10
+ const mapContent = content => {
11
+ if (!content) return null
12
+ switch (content.content) {
13
+ case 'messageText': return { conversation: textOf(content.messageText) ?? '' }
14
+ case 'extendedTextMessage': { const e = content.extendedTextMessage; return { extendedTextMessage: { text: textOf(e.text), matchedText: e.matchedText, canonicalUrl: e.canonicalUrl, description: e.description, title: e.title } } }
15
+ case 'imageMessage': return { imageMessage: { mediaPayload: content.imageMessage.image, caption: textOf(content.imageMessage.caption) } }
16
+ case 'videoMessage': return { videoMessage: { mediaPayload: content.videoMessage.video, caption: textOf(content.videoMessage.caption) } }
17
+ case 'audioMessage': return { audioMessage: { mediaPayload: content.audioMessage.audio, ptt: content.audioMessage.ptt } }
18
+ case 'documentMessage': return { documentMessage: { mediaPayload: content.documentMessage.document, fileName: content.documentMessage.fileName } }
19
+ case 'stickerMessage': return { stickerMessage: { mediaPayload: content.stickerMessage.sticker } }
20
+ case 'contactMessage': return { contactMessage: { mediaPayload: content.contactMessage.contact } }
21
+ case 'contactsArrayMessage': return { contactsArrayMessage: { displayName: content.contactsArrayMessage.displayName, contacts: content.contactsArrayMessage.contacts || [] } }
22
+ case 'locationMessage': { const loc = content.locationMessage.location || {}; return { locationMessage: { degreesLatitude: loc.degreesLatitude, degreesLongitude: loc.degreesLongitude, name: loc.name } } }
23
+ case 'liveLocationMessage': { const loc = content.liveLocationMessage.location || {}; return { liveLocationMessage: { degreesLatitude: loc.degreesLatitude, degreesLongitude: loc.degreesLongitude, caption: textOf(content.liveLocationMessage.caption) } } }
24
+ case 'reactionMessage': return { reactionMessage: content.reactionMessage }
25
+ default: return null
26
+ }
27
+ }
28
+
29
+ // primary use: newsletter/channel message decoding pipeline
30
+ export const consumerApplicationToMessage = app => {
31
+ const payload = app?.payload
32
+ if (!payload) return null
33
+ switch (payload.payload) {
34
+ case 'content': return mapContent(payload.content)
35
+ case 'applicationData': { const ad = payload.applicationData; if (ad?.applicationContent === 'revoke' && ad.revoke?.key) return { protocolMessage: { key: ad.revoke.key, type: proto.Message.ProtocolMessage.Type.REVOKE } }; return null }
36
+ default: return null
37
+ }
38
+ }
@@ -0,0 +1,52 @@
1
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
2
+ import { calculateAgreement, calculateSignature, verifySignature as rustVerifySignature, generateKeyPair as rustGenerateKeyPair, hkdf, md5 } from 'whatsapp-rust-bridge'
3
+ import { KEY_BUNDLE_TYPE } from '../Defaults/index.js'
4
+
5
+ export { hkdf, md5 }
6
+
7
+ const GCM_TAG_LENGTH = 16 // 128 >> 3
8
+
9
+ export const generateSignalPubKey = pubKey => pubKey.length === 33 ? pubKey : Buffer.concat([KEY_BUNDLE_TYPE, pubKey])
10
+
11
+ export const Curve = {
12
+ generateKeyPair: () => { const { pubKey, privKey } = rustGenerateKeyPair(); return { private: Buffer.from(privKey), public: Buffer.from(pubKey.slice(1)) } },
13
+ sharedKey: (privateKey, publicKey) => Buffer.from(calculateAgreement(generateSignalPubKey(publicKey), privateKey)),
14
+ sign: (privateKey, buf) => calculateSignature(privateKey, buf),
15
+ verify: (pubKey, message, signature) => { try { return rustVerifySignature(generateSignalPubKey(pubKey), message, signature) } catch { return false } }
16
+ }
17
+
18
+ export const signedKeyPair = (identityKeyPair, keyId) => {
19
+ const preKey = Curve.generateKeyPair()
20
+ const pubKey = generateSignalPubKey(preKey.public)
21
+ return { keyPair: preKey, signature: Curve.sign(identityKeyPair.private, pubKey), keyId }
22
+ }
23
+
24
+ export const aesEncryptGCM = (plaintext, key, iv, additionalData) => {
25
+ const cipher = createCipheriv('aes-256-gcm', key, iv)
26
+ cipher.setAAD(additionalData)
27
+ return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()])
28
+ }
29
+
30
+ export const aesDecryptGCM = (ciphertext, key, iv, additionalData) => {
31
+ const decipher = createDecipheriv('aes-256-gcm', key, iv)
32
+ decipher.setAAD(additionalData)
33
+ decipher.setAuthTag(ciphertext.subarray(-GCM_TAG_LENGTH))
34
+ return Buffer.concat([decipher.update(ciphertext.subarray(0, -GCM_TAG_LENGTH)), decipher.final()])
35
+ }
36
+
37
+ export const aesEncryptCTR = (plaintext, key, iv) => { const c = createCipheriv('aes-256-ctr', key, iv); return Buffer.concat([c.update(plaintext), c.final()]) }
38
+ export const aesDecryptCTR = (ciphertext, key, iv) => { const d = createDecipheriv('aes-256-ctr', key, iv); return Buffer.concat([d.update(ciphertext), d.final()]) }
39
+
40
+ export const aesDecryptWithIV = (buffer, key, IV) => { const d = createDecipheriv('aes-256-cbc', key, IV); return Buffer.concat([d.update(buffer), d.final()]) }
41
+ export const aesDecrypt = (buffer, key) => aesDecryptWithIV(buffer.subarray(16), key, buffer.subarray(0, 16))
42
+
43
+ export const aesEncryptWithIV = (buffer, key, IV) => { const c = createCipheriv('aes-256-cbc', key, IV); return Buffer.concat([c.update(buffer), c.final()]) }
44
+ export const aesEncrypt = (buffer, key) => { const IV = randomBytes(16); return Buffer.concat([IV, ...(() => { const c = createCipheriv('aes-256-cbc', key, IV); return [c.update(buffer), c.final()] })()]) }
45
+
46
+ export const hmacSign = (buffer, key, variant = 'sha256') => createHmac(variant, key).update(buffer).digest()
47
+ export const sha256 = buffer => createHash('sha256').update(buffer).digest()
48
+
49
+ export const derivePairingCodeKey = async (pairingCode, salt) => {
50
+ const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(pairingCode), { name: 'PBKDF2' }, false, ['deriveBits'])
51
+ return Buffer.from(await crypto.subtle.deriveBits({ name: 'PBKDF2', salt: salt instanceof Uint8Array ? salt : new Uint8Array(salt), iterations: 2 << 16, hash: 'SHA-256' }, keyMaterial, 256))
52
+ }