@nexustechpro/baileys 2.1.2 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +471 -10
- package/lib/Defaults/index.js +0 -4
- package/lib/Signal/libsignal.js +138 -249
- package/lib/Signal/lid-mapping.js +23 -4
- package/lib/Socket/chats.js +1 -0
- package/lib/Socket/messages-recv.js +30 -18
- package/lib/Socket/messages-send.js +57 -37
- package/lib/Socket/newsletter.js +22 -17
- package/lib/Socket/nexus-handler.js +222 -328
- package/lib/Socket/socket.js +27 -18
- package/lib/Utils/browser-utils.js +12 -3
- package/lib/Utils/chat-utils.js +2 -2
- package/lib/Utils/key-store.js +1 -15
- package/lib/Utils/lt-hash.js +2 -43
- package/lib/Utils/messages-media.js +21 -16
- package/lib/Utils/messages.js +4 -2
- package/lib/Utils/process-message.js +234 -215
- package/lib/Utils/tc-token-utils.js +71 -71
- package/lib/Utils/use-multi-file-auth-state.js +211 -63
- package/lib/Utils/validate-connection.js +15 -3
- package/lib/WABinary/constants.js +14 -0
- package/lib/index.js +1 -1
- package/package.json +27 -12
|
@@ -1,101 +1,119 @@
|
|
|
1
1
|
import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidMetaAI, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js';
|
|
2
|
+
import { TC_TOKEN_INDEX_KEY, unixTimestampSeconds } from '../Utils/index.js';
|
|
3
|
+
import { randomBytes } from 'crypto'
|
|
2
4
|
// Same phone-number pattern as WABinary's isJidBot, applied against the user
|
|
3
5
|
// part so the check is invariant to @c.us ↔ @s.whatsapp.net normalization.
|
|
4
6
|
const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
|
|
7
|
+
|
|
8
|
+
const generateTcToken = () => {
|
|
9
|
+
const bucket = Math.floor(unixTimestampSeconds() / 604800)
|
|
10
|
+
const bucketByte = (bucket - 2900) & 0xff
|
|
11
|
+
return Buffer.concat([
|
|
12
|
+
Buffer.from([0x04, 0x01, bucketByte]),
|
|
13
|
+
randomBytes(8)
|
|
14
|
+
])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const preSeedTcToken = async ({ authState, jid, getLIDForPN, logger }) => {
|
|
18
|
+
const tcTokenJid = await resolveTcTokenJid(jid, getLIDForPN)
|
|
19
|
+
const existing = await authState.keys.get('tctoken', [tcTokenJid])
|
|
20
|
+
if (existing[tcTokenJid]?.token?.length) return { token: existing[tcTokenJid].token, storageJid: tcTokenJid }
|
|
21
|
+
const token = generateTcToken()
|
|
22
|
+
const timestamp = String(unixTimestampSeconds())
|
|
23
|
+
const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
|
|
24
|
+
const existingEntry = existing[tcTokenJid] || {}
|
|
25
|
+
await authState.keys.set({ tctoken: { [tcTokenJid]: { ...existingEntry, token, timestamp }, ...indexWrite } })
|
|
26
|
+
logger?.debug({ jid, tcTokenJid }, 'pre-seeded locally generated tctoken')
|
|
27
|
+
return { token, storageJid: tcTokenJid }
|
|
28
|
+
}
|
|
5
29
|
/**
|
|
6
30
|
* Mirrors WA Web's `Wid.isRegularUser()` (user ∧ ¬PSA ∧ ¬Bot). Used to gate tctoken
|
|
7
31
|
* storage against malformed notifications — WA Web filters server-side but we
|
|
8
32
|
* defend here for parity with `WAWebSetTcTokenChatAction.handleIncomingTcToken`.
|
|
9
33
|
* Works for both pre- and post-normalized JIDs (`@c.us` vs `@s.whatsapp.net`).
|
|
10
34
|
*/
|
|
11
|
-
function isRegularUser(jid) {
|
|
12
|
-
if (!jid)
|
|
13
|
-
return false;
|
|
35
|
+
export function isRegularUser(jid) {
|
|
36
|
+
if (!jid) return false;
|
|
14
37
|
const user = jid.split('@')[0] ?? '';
|
|
15
|
-
if (user === '0')
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
return false; // Bot by phone pattern
|
|
19
|
-
if (isJidMetaAI(jid))
|
|
20
|
-
return false; // MetaAI (@bot server)
|
|
38
|
+
if (user === '0') return false; // PSA
|
|
39
|
+
if (BOT_PHONE_REGEX.test(user)) return false; // Bot by phone pattern
|
|
40
|
+
if (isJidMetaAI(jid)) return false; // MetaAI (@bot server)
|
|
21
41
|
return !!(isPnUser(jid) || isLidUser(jid) || isHostedPnUser(jid) || isHostedLidUser(jid) || jid.endsWith('@c.us'));
|
|
22
42
|
}
|
|
43
|
+
|
|
23
44
|
const TC_TOKEN_BUCKET_DURATION = 604800; // 7 days
|
|
24
45
|
const TC_TOKEN_NUM_BUCKETS = 4; // ~28-day rolling window
|
|
25
|
-
|
|
26
|
-
export const TC_TOKEN_INDEX_KEY = '__index';
|
|
27
|
-
/** Read the persisted tctoken JID index and return its entries (never contains the sentinel key itself). */
|
|
46
|
+
|
|
28
47
|
export async function readTcTokenIndex(keys) {
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
// Try new sentinel key first, fall back to old 'index' key
|
|
49
|
+
for (const key of [TC_TOKEN_INDEX_KEY, 'index']) {
|
|
50
|
+
const data = await keys.get('tctoken', [key])
|
|
51
|
+
const entry = data[key]
|
|
52
|
+
if (!entry?.token?.length) continue
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(Buffer.from(entry.token).toString())
|
|
55
|
+
if (!Array.isArray(parsed)) continue
|
|
56
|
+
return parsed.filter(j => typeof j === 'string' && j.length > 0 && j !== TC_TOKEN_INDEX_KEY && j !== 'index')
|
|
57
|
+
} catch {
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
41
60
|
}
|
|
61
|
+
return []
|
|
42
62
|
}
|
|
43
|
-
|
|
63
|
+
|
|
44
64
|
export async function buildMergedTcTokenIndexWrite(keys, addedJids) {
|
|
45
|
-
const persisted = await readTcTokenIndex(keys)
|
|
46
|
-
const merged = new Set(persisted)
|
|
65
|
+
const persisted = await readTcTokenIndex(keys)
|
|
66
|
+
const merged = new Set(persisted)
|
|
47
67
|
for (const jid of addedJids) {
|
|
48
|
-
if (jid && jid !== TC_TOKEN_INDEX_KEY)
|
|
49
|
-
merged.add(jid);
|
|
68
|
+
if (jid && jid !== TC_TOKEN_INDEX_KEY && jid !== 'index') merged.add(jid)
|
|
50
69
|
}
|
|
51
70
|
return {
|
|
52
71
|
[TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...merged])) }
|
|
53
|
-
}
|
|
72
|
+
}
|
|
54
73
|
}
|
|
55
74
|
// WA Web has separate sender/receiver AB props for these but they're identical today
|
|
56
75
|
export function isTcTokenExpired(timestamp) {
|
|
57
|
-
if (timestamp === null || timestamp === undefined)
|
|
58
|
-
return true;
|
|
76
|
+
if (timestamp === null || timestamp === undefined) return true;
|
|
59
77
|
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
|
|
60
|
-
if (isNaN(ts))
|
|
61
|
-
return true;
|
|
78
|
+
if (isNaN(ts)) return true;
|
|
62
79
|
const now = Math.floor(Date.now() / 1000);
|
|
63
80
|
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
|
|
64
81
|
const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1);
|
|
65
82
|
const cutoffTimestamp = cutoffBucket * TC_TOKEN_BUCKET_DURATION;
|
|
66
83
|
return ts < cutoffTimestamp;
|
|
67
84
|
}
|
|
85
|
+
|
|
68
86
|
export function shouldSendNewTcToken(senderTimestamp) {
|
|
69
|
-
if (senderTimestamp === undefined)
|
|
70
|
-
return true;
|
|
87
|
+
if (senderTimestamp === undefined) return true;
|
|
71
88
|
const now = Math.floor(Date.now() / 1000);
|
|
72
89
|
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
|
|
73
90
|
const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION);
|
|
74
91
|
return currentBucket > senderBucket;
|
|
75
92
|
}
|
|
93
|
+
|
|
76
94
|
/** Resolve JID to LID for tctoken storage (WA Web stores under LID) */
|
|
77
95
|
export async function resolveTcTokenJid(jid, getLIDForPN) {
|
|
78
|
-
if (isLidUser(jid))
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return lid
|
|
96
|
+
if (isLidUser(jid)) return jid
|
|
97
|
+
const lid = await getLIDForPN(jid)
|
|
98
|
+
if (!lid) return null
|
|
99
|
+
return lid
|
|
82
100
|
}
|
|
101
|
+
|
|
83
102
|
/** Resolve target JID for issuing privacy token based on AB prop 14303 */
|
|
84
103
|
export async function resolveIssuanceJid(jid, issueToLid, getLIDForPN, getPNForLID) {
|
|
85
104
|
if (issueToLid) {
|
|
86
|
-
if (isLidUser(jid))
|
|
87
|
-
return jid;
|
|
105
|
+
if (isLidUser(jid)) return jid;
|
|
88
106
|
const lid = await getLIDForPN(jid);
|
|
89
107
|
return lid ?? jid;
|
|
90
108
|
}
|
|
91
|
-
if (!isLidUser(jid))
|
|
92
|
-
return jid;
|
|
109
|
+
if (!isLidUser(jid)) return jid;
|
|
93
110
|
if (getPNForLID) {
|
|
94
111
|
const pn = await getPNForLID(jid);
|
|
95
112
|
return pn ?? jid;
|
|
96
113
|
}
|
|
97
114
|
return jid;
|
|
98
115
|
}
|
|
116
|
+
|
|
99
117
|
export async function buildTcTokenFromJid({ authState, jid, baseContent = [], getLIDForPN }) {
|
|
100
118
|
try {
|
|
101
119
|
const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
|
|
@@ -114,49 +132,31 @@ export async function buildTcTokenFromJid({ authState, jid, baseContent = [], ge
|
|
|
114
132
|
}
|
|
115
133
|
return baseContent.length > 0 ? baseContent : undefined;
|
|
116
134
|
}
|
|
117
|
-
baseContent.push({
|
|
118
|
-
tag: 'tctoken',
|
|
119
|
-
attrs: {},
|
|
120
|
-
content: tcTokenBuffer
|
|
121
|
-
});
|
|
135
|
+
baseContent.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer });
|
|
122
136
|
return baseContent;
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
137
|
+
} catch (error) {
|
|
125
138
|
return baseContent.length > 0 ? baseContent : undefined;
|
|
126
139
|
}
|
|
127
140
|
}
|
|
141
|
+
|
|
128
142
|
export async function storeTcTokensFromIqResult({ result, fallbackJid, keys, getLIDForPN, onNewJidStored }) {
|
|
129
143
|
const tokensNode = getBinaryNodeChild(result, 'tokens');
|
|
130
|
-
if (!tokensNode)
|
|
131
|
-
return;
|
|
144
|
+
if (!tokensNode) return;
|
|
132
145
|
const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
|
|
133
146
|
for (const tokenNode of tokenNodes) {
|
|
134
|
-
if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array))
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
147
|
+
if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) continue;
|
|
137
148
|
// In notifications tokenNode.attrs.jid is your own device JID, not the sender's
|
|
138
149
|
const rawJid = jidNormalizedUser(fallbackJid || tokenNode.attrs.jid);
|
|
139
|
-
if (!isRegularUser(rawJid))
|
|
140
|
-
continue;
|
|
150
|
+
if (!isRegularUser(rawJid)) continue;
|
|
141
151
|
const storageJid = await resolveTcTokenJid(rawJid, getLIDForPN);
|
|
142
152
|
const existingTcData = await keys.get('tctoken', [storageJid]);
|
|
143
153
|
const existingEntry = existingTcData[storageJid];
|
|
144
154
|
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
|
|
145
155
|
const incomingTs = tokenNode.attrs.t ? Number(tokenNode.attrs.t) : 0;
|
|
146
156
|
// timestamp-less tokens would be immediately expired
|
|
147
|
-
if (!incomingTs)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
continue;
|
|
151
|
-
await keys.set({
|
|
152
|
-
tctoken: {
|
|
153
|
-
[storageJid]: {
|
|
154
|
-
...existingEntry,
|
|
155
|
-
token: Buffer.from(tokenNode.content),
|
|
156
|
-
timestamp: tokenNode.attrs.t
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
});
|
|
157
|
+
if (!incomingTs) continue;
|
|
158
|
+
if (existingTs > 0 && existingTs > incomingTs) continue;
|
|
159
|
+
await keys.set({ tctoken: { [storageJid]: { ...existingEntry, token: Buffer.from(tokenNode.content), timestamp: tokenNode.attrs.t } } });
|
|
160
160
|
onNewJidStored?.(storageJid);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
@@ -2,17 +2,30 @@ import { Mutex } from 'async-mutex'
|
|
|
2
2
|
import { mkdir, readFile, rename, stat, unlink, writeFile, readdir } from 'fs/promises'
|
|
3
3
|
import { join } from 'path'
|
|
4
4
|
import { createHash } from 'crypto'
|
|
5
|
+
import { Keyv } from 'keyv'
|
|
5
6
|
import { proto } from '../../WAProto/index.js'
|
|
6
7
|
import { initAuthCreds } from './auth-utils.js'
|
|
7
8
|
import { BufferJSON } from './generics.js'
|
|
9
|
+
import { PROTOCOL_ADAPTERS } from '../WABinary/index.js'
|
|
8
10
|
|
|
9
|
-
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
10
11
|
const CURRENT_VERSION = 1
|
|
11
12
|
const DEFAULT_PREKEY_RETENTION = 150
|
|
12
13
|
const DEFAULT_CLEANUP_THRESHOLD = 50
|
|
13
14
|
const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
const bootstrapCreds = (raw) => {
|
|
17
|
+
const c = raw ?? initAuthCreds()
|
|
18
|
+
if (!c.__version || c.__version < CURRENT_VERSION) c.__version = CURRENT_VERSION
|
|
19
|
+
return c
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const patchAppStateKey = (type, v) =>
|
|
23
|
+
type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
|
|
24
|
+
|
|
25
|
+
const computeChecksum = (data) => createHash('sha256').update(data).digest('hex')
|
|
26
|
+
|
|
27
|
+
// ─── File-based auth state ────────────────────────────────────────────────────
|
|
28
|
+
|
|
16
29
|
const fileLocks = new Map()
|
|
17
30
|
const getFileLock = (path) => {
|
|
18
31
|
if (!fileLocks.has(path)) fileLocks.set(path, new Mutex())
|
|
@@ -22,31 +35,13 @@ const releaseFileLock = (path) => {
|
|
|
22
35
|
if (fileLocks.has(path) && !fileLocks.get(path).isLocked()) fileLocks.delete(path)
|
|
23
36
|
}
|
|
24
37
|
|
|
25
|
-
// ─── Checksum ─────────────────────────────────────────────────────────────────
|
|
26
|
-
const computeChecksum = (data) => createHash('sha256').update(data).digest('hex')
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Production-grade multi-file auth state for Baileys.
|
|
30
|
-
* Atomic writes, checksum integrity, and smart prekey cleanup.
|
|
31
|
-
* Compatible with standard Baileys auth folder format.
|
|
32
|
-
*
|
|
33
|
-
* @param {string} folder - Directory to store auth files
|
|
34
|
-
* @param {object} [options] - Configuration options
|
|
35
|
-
* @param {number} [options.preKeyRetention=150] - How many recent prekeys to keep
|
|
36
|
-
* @param {number} [options.cleanupThreshold=50] - How many new prekeys trigger a cleanup
|
|
37
|
-
* @param {object} [options.logger] - Optional logger with .info/.warn methods
|
|
38
|
-
*/
|
|
39
38
|
export const useMultiFileAuthState = async (folder, options = {}) => {
|
|
40
|
-
const {
|
|
41
|
-
|
|
42
|
-
cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD,
|
|
43
|
-
logger,
|
|
44
|
-
} = options
|
|
39
|
+
const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, logger } = options
|
|
40
|
+
|
|
45
41
|
const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-')
|
|
46
42
|
const filePath = (file) => join(folder, fixFileName(file))
|
|
47
43
|
const tmpPath = (file) => filePath(file) + '.tmp'
|
|
48
44
|
|
|
49
|
-
// ─── Folder Bootstrap ───────────────────────────────────────────────────────
|
|
50
45
|
const folderInfo = await stat(folder).catch(() => null)
|
|
51
46
|
if (folderInfo) {
|
|
52
47
|
if (!folderInfo.isDirectory()) throw new Error(`Path exists but is not a directory: ${folder}`)
|
|
@@ -54,16 +49,13 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
|
|
|
54
49
|
await mkdir(folder, { recursive: true })
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
// ─── Atomic Write ────────────────────────────────────────────────────────────
|
|
58
52
|
const writeData = async (data, file) => {
|
|
59
53
|
const fp = filePath(file)
|
|
60
54
|
const tp = tmpPath(file)
|
|
61
|
-
const
|
|
62
|
-
const release = await mutex.acquire()
|
|
55
|
+
const release = await getFileLock(fp).acquire()
|
|
63
56
|
try {
|
|
64
57
|
const serialized = JSON.stringify(data, BufferJSON.replacer)
|
|
65
|
-
const
|
|
66
|
-
const payload = JSON.stringify({ data: JSON.parse(serialized), __checksum: checksum })
|
|
58
|
+
const payload = JSON.stringify({ data: JSON.parse(serialized), __checksum: computeChecksum(serialized) })
|
|
67
59
|
await writeFile(tp, payload)
|
|
68
60
|
try {
|
|
69
61
|
await rename(tp, fp)
|
|
@@ -77,11 +69,9 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
|
|
|
77
69
|
}
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
// ─── Read with Checksum Verification ─────────────────────────────────────────
|
|
81
72
|
const readData = async (file) => {
|
|
82
73
|
const fp = filePath(file)
|
|
83
|
-
const
|
|
84
|
-
const release = await mutex.acquire()
|
|
74
|
+
const release = await getFileLock(fp).acquire()
|
|
85
75
|
try {
|
|
86
76
|
const raw = await readFile(fp, { encoding: 'utf-8' }).catch(() => null)
|
|
87
77
|
if (!raw) return null
|
|
@@ -92,28 +82,26 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
|
|
|
92
82
|
if (expected !== parsed.__checksum) {
|
|
93
83
|
logger?.warn({ file }, 'checksum mismatch — reinitializing file')
|
|
94
84
|
await writeFile(fp, JSON.stringify({ data: parsed.data, __checksum: computeChecksum(JSON.stringify(parsed.data)) })).catch(() => { })
|
|
95
|
-
return JSON.parse(JSON.stringify(parsed.data), BufferJSON.reviver)
|
|
96
85
|
}
|
|
97
86
|
return JSON.parse(JSON.stringify(parsed.data), BufferJSON.reviver)
|
|
98
87
|
}
|
|
99
|
-
// legacy file — rewrite with checksum
|
|
100
88
|
const reserialized = JSON.stringify(JSON.parse(raw, BufferJSON.reviver), BufferJSON.replacer)
|
|
101
|
-
|
|
102
|
-
await writeFile(fp, JSON.stringify({ data: JSON.parse(reserialized), __checksum: checksum })).catch(() => { })
|
|
89
|
+
await writeFile(fp, JSON.stringify({ data: JSON.parse(reserialized), __checksum: computeChecksum(reserialized) })).catch(() => { })
|
|
103
90
|
return JSON.parse(raw, BufferJSON.reviver)
|
|
104
91
|
} catch (err) {
|
|
105
92
|
logger?.warn({ file, err: err.message }, 'failed to read auth file — reinitializing')
|
|
106
93
|
await unlink(fp).catch(() => { })
|
|
107
94
|
return null
|
|
108
95
|
}
|
|
109
|
-
} finally {
|
|
96
|
+
} finally {
|
|
97
|
+
release()
|
|
98
|
+
releaseFileLock(fp)
|
|
99
|
+
}
|
|
110
100
|
}
|
|
111
101
|
|
|
112
|
-
// ─── Remove File ────────────────────────────────────────────────────────────
|
|
113
102
|
const removeData = async (file) => {
|
|
114
103
|
const fp = filePath(file)
|
|
115
|
-
const
|
|
116
|
-
const release = await mutex.acquire()
|
|
104
|
+
const release = await getFileLock(fp).acquire()
|
|
117
105
|
try {
|
|
118
106
|
await unlink(fp).catch(() => { })
|
|
119
107
|
} finally {
|
|
@@ -122,34 +110,23 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
|
|
|
122
110
|
}
|
|
123
111
|
}
|
|
124
112
|
|
|
125
|
-
|
|
126
|
-
let creds = (await readData('creds.json')) || initAuthCreds()
|
|
127
|
-
if (!creds.__version) {
|
|
128
|
-
creds.__version = CURRENT_VERSION
|
|
129
|
-
} else if (creds.__version < CURRENT_VERSION) {
|
|
130
|
-
creds.__version = CURRENT_VERSION
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ─── Prekey Cleanup ─────────────────────────────────────────────────────────
|
|
113
|
+
let creds = bootstrapCreds(await readData('creds.json'))
|
|
134
114
|
let cleanupRunning = false
|
|
135
115
|
let lastCleanupAt = 0
|
|
136
116
|
let lastCleanedPreKeyId = creds.nextPreKeyId
|
|
137
117
|
|
|
138
118
|
const cleanOldPreKeys = async () => {
|
|
139
119
|
const now = Date.now()
|
|
140
|
-
if (cleanupRunning) return
|
|
141
|
-
if (now - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
120
|
+
if (cleanupRunning || now - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
142
121
|
cleanupRunning = true
|
|
143
122
|
try {
|
|
144
123
|
const minId = creds.nextPreKeyId - preKeyRetention
|
|
145
124
|
if (minId <= 0) return
|
|
146
125
|
const files = await readdir(folder)
|
|
147
|
-
const targets =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (parseInt(match[1], 10) < minId) targets.push(join(folder, file))
|
|
152
|
-
}
|
|
126
|
+
const targets = files
|
|
127
|
+
.map(f => f.match(/^pre-key-(\d+)\.json(\.tmp)?$/))
|
|
128
|
+
.filter(m => m && parseInt(m[1], 10) < minId)
|
|
129
|
+
.map(m => join(folder, m[0]))
|
|
153
130
|
if (!targets.length) return
|
|
154
131
|
await Promise.all(targets.map(f => unlink(f).catch(() => { })))
|
|
155
132
|
lastCleanupAt = Date.now()
|
|
@@ -164,7 +141,6 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
|
|
|
164
141
|
|
|
165
142
|
cleanOldPreKeys().catch(() => { })
|
|
166
143
|
|
|
167
|
-
// ─── Stats ──────────────────────────────────────────────────────────────────
|
|
168
144
|
const getStats = async () => {
|
|
169
145
|
const files = await readdir(folder).catch(() => [])
|
|
170
146
|
const preKeyFiles = files.filter(f => /^pre-key-\d+\.json$/.test(f))
|
|
@@ -184,9 +160,7 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
|
|
|
184
160
|
const data = {}
|
|
185
161
|
await Promise.all(ids.map(async (id) => {
|
|
186
162
|
let value = await readData(`${type}-${id}.json`)
|
|
187
|
-
if (type === 'app-state-sync-key' && value)
|
|
188
|
-
value = proto.Message.AppStateSyncKeyData.fromObject(value)
|
|
189
|
-
}
|
|
163
|
+
if (type === 'app-state-sync-key' && value) value = proto.Message.AppStateSyncKeyData.fromObject(value)
|
|
190
164
|
data[id] = value
|
|
191
165
|
}))
|
|
192
166
|
return data
|
|
@@ -204,11 +178,185 @@ export const useMultiFileAuthState = async (folder, options = {}) => {
|
|
|
204
178
|
},
|
|
205
179
|
},
|
|
206
180
|
saveCreds: async () => {
|
|
207
|
-
if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) {
|
|
208
|
-
cleanOldPreKeys().catch(() => { })
|
|
209
|
-
}
|
|
181
|
+
if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
|
|
210
182
|
return writeData(creds, 'creds.json')
|
|
211
183
|
},
|
|
212
184
|
getStats,
|
|
213
185
|
}
|
|
214
|
-
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ─── Keyv-backed auth state ───────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
const ser = (v) => {
|
|
191
|
+
if (v == null) return null
|
|
192
|
+
const serialized = JSON.stringify(v, BufferJSON.replacer)
|
|
193
|
+
return { data: JSON.parse(serialized), __checksum: computeChecksum(serialized) }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const unwrapNestedValue = (obj) => {
|
|
197
|
+
let cur = obj
|
|
198
|
+
while (cur && typeof cur === 'object' && !Array.isArray(cur) && Object.keys(cur).length === 1 && 'value' in cur) cur = cur.value
|
|
199
|
+
return cur
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const deser = (raw, logger, onRewrite) => {
|
|
203
|
+
if (raw == null) return null
|
|
204
|
+
let parsed
|
|
205
|
+
if (typeof raw === 'string') {
|
|
206
|
+
try { parsed = JSON.parse(raw) } catch (err) {
|
|
207
|
+
logger?.warn?.({ err: err.message }, '[Auth] failed to parse stored value — treating as missing')
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
parsed = raw
|
|
212
|
+
}
|
|
213
|
+
parsed = unwrapNestedValue(parsed)
|
|
214
|
+
if (parsed == null) return null
|
|
215
|
+
if (typeof parsed === 'object' && '__checksum' in parsed) {
|
|
216
|
+
const innerData = unwrapNestedValue(parsed.data)
|
|
217
|
+
const serializedInner = JSON.stringify(innerData)
|
|
218
|
+
const expected = computeChecksum(serializedInner)
|
|
219
|
+
if (expected !== parsed.__checksum) {
|
|
220
|
+
logger?.warn?.('[Auth] checksum mismatch or legacy-wrapped value — reinitializing')
|
|
221
|
+
onRewrite?.(JSON.stringify({ data: innerData, __checksum: expected }))
|
|
222
|
+
}
|
|
223
|
+
return JSON.parse(serializedInner, BufferJSON.reviver)
|
|
224
|
+
}
|
|
225
|
+
return JSON.parse(JSON.stringify(parsed), BufferJSON.reviver)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Maps our unified collectionName option to each adapter's actual constructor key
|
|
229
|
+
const COLLECTION_OPTION_KEY = {
|
|
230
|
+
'@keyv/mongo': 'collection',
|
|
231
|
+
'@keyv/postgres': 'table',
|
|
232
|
+
'@keyv/mysql': 'table',
|
|
233
|
+
'@keyv/sqlite': 'table',
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const _adapterCache = new Map()
|
|
237
|
+
export const clearAdapterCache = () => _adapterCache.clear()
|
|
238
|
+
|
|
239
|
+
const resolveAdapter = async (connectionString, collectionName) => {
|
|
240
|
+
const cacheKey = collectionName ? `${connectionString}::${collectionName}` : connectionString
|
|
241
|
+
if (_adapterCache.has(cacheKey)) return _adapterCache.get(cacheKey)
|
|
242
|
+
let protocol
|
|
243
|
+
try { protocol = new URL(connectionString).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${connectionString}"`) }
|
|
244
|
+
const pkg = PROTOCOL_ADAPTERS[protocol]
|
|
245
|
+
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.`)
|
|
246
|
+
let mod
|
|
247
|
+
try { mod = await import(pkg) } catch (e) {
|
|
248
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" is required for this backend — install it: npm i ${pkg}`)
|
|
249
|
+
throw e
|
|
250
|
+
}
|
|
251
|
+
const Adapter = mod.default ?? mod[Object.keys(mod)[0]]
|
|
252
|
+
const optionKey = COLLECTION_OPTION_KEY[pkg]
|
|
253
|
+
// Pass one merged options object — Postgres/MySQL silently ignore a second positional argument
|
|
254
|
+
const adapter = (collectionName && optionKey)
|
|
255
|
+
? new Adapter({ url: connectionString, uri: connectionString, [optionKey]: collectionName })
|
|
256
|
+
: new Adapter(connectionString)
|
|
257
|
+
adapter.setMaxListeners?.(0)
|
|
258
|
+
_adapterCache.set(cacheKey, adapter)
|
|
259
|
+
return adapter
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const isAdapterLike = (v) => v && typeof v === 'object' && typeof v.get === 'function' && typeof v.set === 'function' && typeof v.delete === 'function' && typeof v.clear === 'function'
|
|
263
|
+
|
|
264
|
+
const resolveStore = async (backend, collectionName) => {
|
|
265
|
+
if (backend == null) return null
|
|
266
|
+
if (typeof backend === 'string') return resolveAdapter(backend, collectionName)
|
|
267
|
+
if (backend instanceof Keyv) return backend.opts?.store ?? backend.store ?? null
|
|
268
|
+
if (isAdapterLike(backend)) return backend
|
|
269
|
+
throw new Error('[Auth] backend must be a connection string, Keyv instance, or KeyvStoreAdapter')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
|
|
273
|
+
const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, logger, collectionName } = opts
|
|
274
|
+
|
|
275
|
+
const store = await resolveStore(backend, collectionName)
|
|
276
|
+
const keyv = store ? new Keyv({ store, namespace: sessionId }) : new Keyv({ namespace: sessionId })
|
|
277
|
+
keyv.on('error', (err) => logger?.warn?.({ err: err?.message }, '[Auth] store error'))
|
|
278
|
+
|
|
279
|
+
const keyName = (type, id) => `${type}-${id}`
|
|
280
|
+
|
|
281
|
+
let creds = bootstrapCreds(deser(await keyv.get('creds'), logger, (fixed) => keyv.set('creds', fixed)))
|
|
282
|
+
let cleanupRunning = false
|
|
283
|
+
let lastCleanupAt = 0
|
|
284
|
+
let lastCleanedPreKeyId = creds.nextPreKeyId
|
|
285
|
+
|
|
286
|
+
const cleanOldPreKeys = async () => {
|
|
287
|
+
const now = Date.now()
|
|
288
|
+
if (cleanupRunning || now - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
289
|
+
cleanupRunning = true
|
|
290
|
+
try {
|
|
291
|
+
const minId = creds.nextPreKeyId - preKeyRetention
|
|
292
|
+
if (minId <= 0) return
|
|
293
|
+
const ids = []
|
|
294
|
+
for (let id = Math.max(0, lastCleanedPreKeyId - preKeyRetention); id < minId; id++) ids.push(keyName('pre-key', id))
|
|
295
|
+
if (!ids.length) return
|
|
296
|
+
await Promise.all(ids.map((k) => keyv.delete(k).catch(() => { })))
|
|
297
|
+
lastCleanupAt = Date.now()
|
|
298
|
+
lastCleanedPreKeyId = creds.nextPreKeyId
|
|
299
|
+
logger?.info?.({ deleted: ids.length, minId }, '[Auth] prekey cleanup done')
|
|
300
|
+
} catch (e) {
|
|
301
|
+
logger?.warn?.({ err: e.message }, '[Auth] prekey cleanup failed')
|
|
302
|
+
} finally {
|
|
303
|
+
cleanupRunning = false
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
cleanOldPreKeys().catch(() => { })
|
|
308
|
+
|
|
309
|
+
const keys = {
|
|
310
|
+
get: async (type, ids) => {
|
|
311
|
+
const data = {}
|
|
312
|
+
if (!ids.length) return data
|
|
313
|
+
const rawKeys = ids.map((id) => keyName(type, id))
|
|
314
|
+
const rawValues = await keyv.getMany(rawKeys)
|
|
315
|
+
const rewrites = []
|
|
316
|
+
ids.forEach((id, i) => {
|
|
317
|
+
data[id] = patchAppStateKey(type, deser(rawValues[i], logger, (fixed) => rewrites.push(keyv.set(rawKeys[i], fixed))))
|
|
318
|
+
})
|
|
319
|
+
if (rewrites.length) await Promise.all(rewrites).catch(() => { })
|
|
320
|
+
return data
|
|
321
|
+
},
|
|
322
|
+
set: async (data) => {
|
|
323
|
+
const setEntries = []
|
|
324
|
+
const delKeys = []
|
|
325
|
+
for (const type in data) {
|
|
326
|
+
for (const id in data[type]) {
|
|
327
|
+
const value = data[type][id]
|
|
328
|
+
const key = keyName(type, id)
|
|
329
|
+
if (value != null) setEntries.push({ key, value: ser(value) })
|
|
330
|
+
else delKeys.push(key)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
await Promise.all([
|
|
334
|
+
setEntries.length ? keyv.setMany(setEntries) : null,
|
|
335
|
+
delKeys.length ? keyv.deleteMany(delKeys) : null,
|
|
336
|
+
])
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const saveCreds = async () => {
|
|
341
|
+
if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
|
|
342
|
+
await keyv.set('creds', ser(creds))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const clearSession = async () => {
|
|
346
|
+
const store = keyv.opts?.store ?? keyv.store
|
|
347
|
+
if (store && typeof store.deleteMany === 'function' && typeof store.iterator === 'function') {
|
|
348
|
+
const toDelete = []
|
|
349
|
+
for await (const [key] of store.iterator(sessionId)) toDelete.push(key)
|
|
350
|
+
if (toDelete.length) await store.deleteMany(toDelete)
|
|
351
|
+
} else {
|
|
352
|
+
await keyv.clear()
|
|
353
|
+
}
|
|
354
|
+
await close()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const close = async () => { keyv.removeAllListeners?.() }
|
|
358
|
+
|
|
359
|
+
return { state: { creds, keys }, saveCreds, clearSession }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export { Keyv }
|
|
@@ -13,7 +13,9 @@ const getUserAgent = (config) => {
|
|
|
13
13
|
secondary: config.version[1],
|
|
14
14
|
tertiary: config.version[2]
|
|
15
15
|
},
|
|
16
|
-
platform:
|
|
16
|
+
platform: config.browser[1].toLocaleLowerCase().includes('android')
|
|
17
|
+
? proto.ClientPayload.UserAgent.Platform.MACOS
|
|
18
|
+
: proto.ClientPayload.UserAgent.Platform.WEB,
|
|
17
19
|
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
|
18
20
|
osVersion: '0.1',
|
|
19
21
|
device: 'Desktop',
|
|
@@ -43,7 +45,9 @@ const getClientPayload = (config) => {
|
|
|
43
45
|
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
|
|
44
46
|
userAgent: getUserAgent(config)
|
|
45
47
|
};
|
|
46
|
-
|
|
48
|
+
if (!config.browser[1].toLowerCase().includes('android')) {
|
|
49
|
+
payload.webInfo = getWebInfo(config)
|
|
50
|
+
}
|
|
47
51
|
return payload;
|
|
48
52
|
};
|
|
49
53
|
export const generateLoginNode = (userJid, config) => {
|
|
@@ -61,6 +65,9 @@ export const generateLoginNode = (userJid, config) => {
|
|
|
61
65
|
};
|
|
62
66
|
const getPlatformType = (platform) => {
|
|
63
67
|
const platformType = platform.toUpperCase();
|
|
68
|
+
if (platformType === 'ANDROID') {
|
|
69
|
+
return proto.DeviceProps.PlatformType.ANDROID_PHONE;
|
|
70
|
+
}
|
|
64
71
|
return (proto.DeviceProps.PlatformType[platformType] ||
|
|
65
72
|
proto.DeviceProps.PlatformType.CHROME);
|
|
66
73
|
};
|
|
@@ -77,6 +84,7 @@ export const generateRegistrationNode = ({ registrationId, signedPreKey, signedI
|
|
|
77
84
|
historySyncConfig: {
|
|
78
85
|
storageQuotaMb: 569150,
|
|
79
86
|
inlineInitialPayloadInE2EeMsg: true,
|
|
87
|
+
recentSyncDaysLimit: undefined,
|
|
80
88
|
supportCallLogHistory: false,
|
|
81
89
|
supportBotUserAgentChatHistory: true,
|
|
82
90
|
supportCagReactionsAndPolls: true,
|
|
@@ -84,7 +92,11 @@ export const generateRegistrationNode = ({ registrationId, signedPreKey, signedI
|
|
|
84
92
|
supportRecentSyncChunkMessageCountTuning: true,
|
|
85
93
|
supportHostedGroupMsg: true,
|
|
86
94
|
supportFbidBotChatHistory: true,
|
|
87
|
-
|
|
95
|
+
supportAddOnHistorySyncMigration: undefined,
|
|
96
|
+
supportMessageAssociation: true,
|
|
97
|
+
supportGroupHistory: false,
|
|
98
|
+
onDemandReady: undefined,
|
|
99
|
+
supportGuestChat: undefined
|
|
88
100
|
},
|
|
89
101
|
version: {
|
|
90
102
|
primary: 10,
|