@nexustechpro/baileys 2.0.5 → 2.1.0

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.
@@ -0,0 +1,162 @@
1
+ import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidMetaAI, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js';
2
+ // Same phone-number pattern as WABinary's isJidBot, applied against the user
3
+ // part so the check is invariant to @c.us ↔ @s.whatsapp.net normalization.
4
+ const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
5
+ /**
6
+ * Mirrors WA Web's `Wid.isRegularUser()` (user ∧ ¬PSA ∧ ¬Bot). Used to gate tctoken
7
+ * storage against malformed notifications — WA Web filters server-side but we
8
+ * defend here for parity with `WAWebSetTcTokenChatAction.handleIncomingTcToken`.
9
+ * Works for both pre- and post-normalized JIDs (`@c.us` vs `@s.whatsapp.net`).
10
+ */
11
+ function isRegularUser(jid) {
12
+ if (!jid)
13
+ return false;
14
+ const user = jid.split('@')[0] ?? '';
15
+ if (user === '0')
16
+ return false; // PSA
17
+ if (BOT_PHONE_REGEX.test(user))
18
+ return false; // Bot by phone pattern
19
+ if (isJidMetaAI(jid))
20
+ return false; // MetaAI (@bot server)
21
+ return !!(isPnUser(jid) || isLidUser(jid) || isHostedPnUser(jid) || isHostedLidUser(jid) || jid.endsWith('@c.us'));
22
+ }
23
+ const TC_TOKEN_BUCKET_DURATION = 604800; // 7 days
24
+ const TC_TOKEN_NUM_BUCKETS = 4; // ~28-day rolling window
25
+ /** Sentinel key under `tctoken` store holding a JSON array of tracked storage JIDs for cross-session pruning. */
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). */
28
+ export async function readTcTokenIndex(keys) {
29
+ const data = await keys.get('tctoken', [TC_TOKEN_INDEX_KEY]);
30
+ const entry = data[TC_TOKEN_INDEX_KEY];
31
+ if (!entry?.token?.length)
32
+ return [];
33
+ try {
34
+ const parsed = JSON.parse(Buffer.from(entry.token).toString());
35
+ if (!Array.isArray(parsed))
36
+ return [];
37
+ return parsed.filter((j) => typeof j === 'string' && j.length > 0 && j !== TC_TOKEN_INDEX_KEY);
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ }
43
+ /** Build a SignalDataSet fragment that writes the merged index (persisted ∪ added) under the sentinel key. */
44
+ export async function buildMergedTcTokenIndexWrite(keys, addedJids) {
45
+ const persisted = await readTcTokenIndex(keys);
46
+ const merged = new Set(persisted);
47
+ for (const jid of addedJids) {
48
+ if (jid && jid !== TC_TOKEN_INDEX_KEY)
49
+ merged.add(jid);
50
+ }
51
+ return {
52
+ [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...merged])) }
53
+ };
54
+ }
55
+ // WA Web has separate sender/receiver AB props for these but they're identical today
56
+ export function isTcTokenExpired(timestamp) {
57
+ if (timestamp === null || timestamp === undefined)
58
+ return true;
59
+ const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
60
+ if (isNaN(ts))
61
+ return true;
62
+ const now = Math.floor(Date.now() / 1000);
63
+ const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
64
+ const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1);
65
+ const cutoffTimestamp = cutoffBucket * TC_TOKEN_BUCKET_DURATION;
66
+ return ts < cutoffTimestamp;
67
+ }
68
+ export function shouldSendNewTcToken(senderTimestamp) {
69
+ if (senderTimestamp === undefined)
70
+ return true;
71
+ const now = Math.floor(Date.now() / 1000);
72
+ const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
73
+ const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION);
74
+ return currentBucket > senderBucket;
75
+ }
76
+ /** Resolve JID to LID for tctoken storage (WA Web stores under LID) */
77
+ export async function resolveTcTokenJid(jid, getLIDForPN) {
78
+ if (isLidUser(jid))
79
+ return jid;
80
+ const lid = await getLIDForPN(jid);
81
+ return lid ?? jid;
82
+ }
83
+ /** Resolve target JID for issuing privacy token based on AB prop 14303 */
84
+ export async function resolveIssuanceJid(jid, issueToLid, getLIDForPN, getPNForLID) {
85
+ if (issueToLid) {
86
+ if (isLidUser(jid))
87
+ return jid;
88
+ const lid = await getLIDForPN(jid);
89
+ return lid ?? jid;
90
+ }
91
+ if (!isLidUser(jid))
92
+ return jid;
93
+ if (getPNForLID) {
94
+ const pn = await getPNForLID(jid);
95
+ return pn ?? jid;
96
+ }
97
+ return jid;
98
+ }
99
+ export async function buildTcTokenFromJid({ authState, jid, baseContent = [], getLIDForPN }) {
100
+ try {
101
+ const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
102
+ const tcTokenData = await authState.keys.get('tctoken', [storageJid]);
103
+ const entry = tcTokenData?.[storageJid];
104
+ const tcTokenBuffer = entry?.token;
105
+ if (!tcTokenBuffer?.length || isTcTokenExpired(entry?.timestamp)) {
106
+ if (tcTokenBuffer) {
107
+ // Preserve senderTimestamp so shouldSendNewTcToken() keeps its dedupe state
108
+ // after we drop the unusable peer token. Only wipe the record entirely when
109
+ // there's nothing worth keeping.
110
+ const cleared = entry?.senderTimestamp !== undefined
111
+ ? { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp }
112
+ : null;
113
+ await authState.keys.set({ tctoken: { [storageJid]: cleared } });
114
+ }
115
+ return baseContent.length > 0 ? baseContent : undefined;
116
+ }
117
+ baseContent.push({
118
+ tag: 'tctoken',
119
+ attrs: {},
120
+ content: tcTokenBuffer
121
+ });
122
+ return baseContent;
123
+ }
124
+ catch (error) {
125
+ return baseContent.length > 0 ? baseContent : undefined;
126
+ }
127
+ }
128
+ export async function storeTcTokensFromIqResult({ result, fallbackJid, keys, getLIDForPN, onNewJidStored }) {
129
+ const tokensNode = getBinaryNodeChild(result, 'tokens');
130
+ if (!tokensNode)
131
+ return;
132
+ const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
133
+ for (const tokenNode of tokenNodes) {
134
+ if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) {
135
+ continue;
136
+ }
137
+ // In notifications tokenNode.attrs.jid is your own device JID, not the sender's
138
+ const rawJid = jidNormalizedUser(fallbackJid || tokenNode.attrs.jid);
139
+ if (!isRegularUser(rawJid))
140
+ continue;
141
+ const storageJid = await resolveTcTokenJid(rawJid, getLIDForPN);
142
+ const existingTcData = await keys.get('tctoken', [storageJid]);
143
+ const existingEntry = existingTcData[storageJid];
144
+ const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
145
+ const incomingTs = tokenNode.attrs.t ? Number(tokenNode.attrs.t) : 0;
146
+ // timestamp-less tokens would be immediately expired
147
+ if (!incomingTs)
148
+ continue;
149
+ if (existingTs > 0 && existingTs > incomingTs)
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
+ });
160
+ onNewJidStored?.(storageJid);
161
+ }
162
+ }
@@ -1,121 +1,204 @@
1
- import { Mutex } from 'async-mutex';
2
- import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
3
- import { join } from 'path';
4
- import { proto } from '../../WAProto/index.js';
5
- import { initAuthCreds } from './auth-utils.js';
6
- import { BufferJSON } from './generics.js';
7
- // We need to lock files due to the fact that we are using async functions to read and write files
8
- // https://github.com/WhiskeySockets/Baileys/issues/794
9
- // https://github.com/nodejs/node/issues/26338
10
- // Use a Map to store mutexes for each file path
11
- const fileLocks = new Map();
12
- // Get or create a mutex for a specific file path
1
+ import { Mutex } from 'async-mutex'
2
+ import { mkdir, readFile, rename, stat, unlink, writeFile, readdir } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { createHash } from 'crypto'
5
+ import { proto } from '../../WAProto/index.js'
6
+ import { initAuthCreds } from './auth-utils.js'
7
+ import { BufferJSON } from './generics.js'
8
+
9
+ // ─── Constants ────────────────────────────────────────────────────────────────
10
+ const CURRENT_VERSION = 1
11
+ const DEFAULT_PREKEY_RETENTION = 150
12
+ const DEFAULT_CLEANUP_THRESHOLD = 50
13
+ const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
14
+
15
+ // ─── File Lock Registry ───────────────────────────────────────────────────────
16
+ const fileLocks = new Map()
13
17
  const getFileLock = (path) => {
14
- let mutex = fileLocks.get(path);
15
- if (!mutex) {
16
- mutex = new Mutex();
17
- fileLocks.set(path, mutex);
18
- }
19
- return mutex;
20
- };
18
+ if (!fileLocks.has(path)) fileLocks.set(path, new Mutex())
19
+ return fileLocks.get(path)
20
+ }
21
+ const releaseFileLock = (path) => {
22
+ if (fileLocks.has(path) && !fileLocks.get(path).isLocked()) fileLocks.delete(path)
23
+ }
24
+
25
+ // ─── Checksum ─────────────────────────────────────────────────────────────────
26
+ const computeChecksum = (data) => createHash('sha256').update(data).digest('hex')
27
+
21
28
  /**
22
- * stores the full authentication state in a single folder.
23
- * Far more efficient than singlefileauthstate
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.
24
32
  *
25
- * Again, I wouldn't endorse this for any production level use other than perhaps a bot.
26
- * Would recommend writing an auth state for use with a proper SQL or No-SQL DB
27
- * */
28
- export const useMultiFileAuthState = async (folder) => {
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
+ export const useMultiFileAuthState = async (folder, options = {}) => {
40
+ const {
41
+ preKeyRetention = DEFAULT_PREKEY_RETENTION,
42
+ cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD,
43
+ logger,
44
+ } = options
45
+ const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-')
46
+ const filePath = (file) => join(folder, fixFileName(file))
47
+ const tmpPath = (file) => filePath(file) + '.tmp'
48
+
49
+ // ─── Folder Bootstrap ───────────────────────────────────────────────────────
50
+ const folderInfo = await stat(folder).catch(() => null)
51
+ if (folderInfo) {
52
+ if (!folderInfo.isDirectory()) throw new Error(`Path exists but is not a directory: ${folder}`)
53
+ } else {
54
+ await mkdir(folder, { recursive: true })
55
+ }
56
+
57
+ // ─── Atomic Write ────────────────────────────────────────────────────────────
30
58
  const writeData = async (data, file) => {
31
- const filePath = join(folder, fixFileName(file));
32
- const mutex = getFileLock(filePath);
33
- return mutex.acquire().then(async (release) => {
34
- try {
35
- await writeFile(filePath, JSON.stringify(data, BufferJSON.replacer));
36
- }
37
- finally {
38
- release();
39
- }
40
- });
41
- };
59
+ const fp = filePath(file)
60
+ const tp = tmpPath(file)
61
+ const mutex = getFileLock(fp)
62
+ const release = await mutex.acquire()
63
+ try {
64
+ const serialized = JSON.stringify(data, BufferJSON.replacer)
65
+ const checksum = computeChecksum(serialized)
66
+ const payload = JSON.stringify({ data: JSON.parse(serialized), __checksum: checksum })
67
+ await writeFile(tp, payload)
68
+ await rename(tp, fp)
69
+ } finally {
70
+ release()
71
+ releaseFileLock(fp)
72
+ }
73
+ }
74
+
75
+ // ─── Read with Checksum Verification ─────────────────────────────────────────
42
76
  const readData = async (file) => {
77
+ const fp = filePath(file)
78
+ const mutex = getFileLock(fp)
79
+ const release = await mutex.acquire()
43
80
  try {
44
- const filePath = join(folder, fixFileName(file));
45
- const mutex = getFileLock(filePath);
46
- return await mutex.acquire().then(async (release) => {
47
- try {
48
- const data = await readFile(filePath, { encoding: 'utf-8' });
49
- return JSON.parse(data, BufferJSON.reviver);
50
- }
51
- finally {
52
- release();
81
+ const raw = await readFile(fp, { encoding: 'utf-8' }).catch(() => null)
82
+ if (!raw) return null
83
+ try {
84
+ const parsed = JSON.parse(raw)
85
+ if (parsed.__checksum) {
86
+ const expected = computeChecksum(JSON.stringify(parsed.data))
87
+ if (expected !== parsed.__checksum) throw new Error('checksum mismatch')
88
+ return JSON.parse(JSON.stringify(parsed.data), BufferJSON.reviver)
53
89
  }
54
- });
55
- }
56
- catch (error) {
57
- return null;
90
+ // legacy file without checksum — read as-is for compatibility
91
+ return JSON.parse(raw, BufferJSON.reviver)
92
+ } catch (err) {
93
+ logger?.warn({ file, err: err.message }, 'failed to read auth file')
94
+ return null
95
+ }
96
+ } finally {
97
+ release()
98
+ releaseFileLock(fp)
58
99
  }
59
- };
100
+ }
101
+
102
+ // ─── Remove File ────────────────────────────────────────────────────────────
60
103
  const removeData = async (file) => {
104
+ const fp = filePath(file)
105
+ const mutex = getFileLock(fp)
106
+ const release = await mutex.acquire()
61
107
  try {
62
- const filePath = join(folder, fixFileName(file));
63
- const mutex = getFileLock(filePath);
64
- return mutex.acquire().then(async (release) => {
65
- try {
66
- await unlink(filePath);
67
- }
68
- catch {
69
- }
70
- finally {
71
- release();
72
- }
73
- });
108
+ await unlink(fp).catch(() => { })
109
+ } finally {
110
+ release()
111
+ releaseFileLock(fp)
74
112
  }
75
- catch { }
76
- };
77
- const folderInfo = await stat(folder).catch(() => { });
78
- if (folderInfo) {
79
- if (!folderInfo.isDirectory()) {
80
- throw new Error(`found something that is not a directory at ${folder}, either delete it or specify a different location`);
113
+ }
114
+
115
+ // ─── Credentials Bootstrap ──────────────────────────────────────────────────
116
+ let creds = (await readData('creds.json')) || initAuthCreds()
117
+ if (!creds.__version) {
118
+ creds.__version = CURRENT_VERSION
119
+ } else if (creds.__version < CURRENT_VERSION) {
120
+ creds.__version = CURRENT_VERSION
121
+ }
122
+
123
+ // ─── Prekey Cleanup ─────────────────────────────────────────────────────────
124
+ let cleanupRunning = false
125
+ let lastCleanupAt = 0
126
+ let lastCleanedPreKeyId = creds.nextPreKeyId
127
+
128
+ const cleanOldPreKeys = async () => {
129
+ const now = Date.now()
130
+ if (cleanupRunning) return
131
+ if (now - lastCleanupAt < CLEANUP_INTERVAL_MS) return
132
+ cleanupRunning = true
133
+ try {
134
+ const minId = creds.nextPreKeyId - preKeyRetention
135
+ if (minId <= 0) return
136
+ const files = await readdir(folder)
137
+ const targets = []
138
+ for (const file of files) {
139
+ const match = file.match(/^pre-key-(\d+)\.json(\.tmp)?$/)
140
+ if (!match) continue
141
+ if (parseInt(match[1], 10) < minId) targets.push(join(folder, file))
142
+ }
143
+ if (!targets.length) return
144
+ await Promise.all(targets.map(f => unlink(f).catch(() => { })))
145
+ lastCleanupAt = Date.now()
146
+ lastCleanedPreKeyId = creds.nextPreKeyId
147
+ logger?.info({ deleted: targets.length, minId }, 'prekey cleanup complete')
148
+ } catch (err) {
149
+ logger?.warn({ err }, 'prekey cleanup failed')
150
+ } finally {
151
+ cleanupRunning = false
81
152
  }
82
153
  }
83
- else {
84
- await mkdir(folder, { recursive: true });
154
+
155
+ cleanOldPreKeys().catch(() => { })
156
+
157
+ // ─── Stats ──────────────────────────────────────────────────────────────────
158
+ const getStats = async () => {
159
+ const files = await readdir(folder).catch(() => [])
160
+ const preKeyFiles = files.filter(f => /^pre-key-\d+\.json$/.test(f))
161
+ return {
162
+ totalFiles: files.length,
163
+ preKeyCount: preKeyFiles.length,
164
+ nextPreKeyId: creds.nextPreKeyId,
165
+ lastCleanupAt: lastCleanupAt ? new Date(lastCleanupAt).toISOString() : null,
166
+ }
85
167
  }
86
- const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-');
87
- const creds = (await readData('creds.json')) || initAuthCreds();
168
+
88
169
  return {
89
170
  state: {
90
171
  creds,
91
172
  keys: {
92
173
  get: async (type, ids) => {
93
- const data = {};
174
+ const data = {}
94
175
  await Promise.all(ids.map(async (id) => {
95
- let value = await readData(`${type}-${id}.json`);
176
+ let value = await readData(`${type}-${id}.json`)
96
177
  if (type === 'app-state-sync-key' && value) {
97
- value = proto.Message.AppStateSyncKeyData.fromObject(value);
178
+ value = proto.Message.AppStateSyncKeyData.fromObject(value)
98
179
  }
99
- data[id] = value;
100
- }));
101
- return data;
180
+ data[id] = value
181
+ }))
182
+ return data
102
183
  },
103
184
  set: async (data) => {
104
- const tasks = [];
185
+ const tasks = []
105
186
  for (const category in data) {
106
187
  for (const id in data[category]) {
107
- const value = data[category][id];
108
- const file = `${category}-${id}.json`;
109
- tasks.push(value ? writeData(value, file) : removeData(file));
188
+ const value = data[category][id]
189
+ tasks.push(value ? writeData(value, `${category}-${id}.json`) : removeData(`${category}-${id}.json`))
110
190
  }
111
191
  }
112
- await Promise.all(tasks);
113
- }
114
- }
192
+ await Promise.all(tasks)
193
+ },
194
+ },
115
195
  },
116
196
  saveCreds: async () => {
117
- return writeData(creds, 'creds.json');
118
- }
119
- };
120
- };
121
- //# sourceMappingURL=use-multi-file-auth-state.js.map
197
+ if (creds.nextPreKeyId - lastCleanedPreKeyId >= cleanupThreshold) {
198
+ cleanOldPreKeys().catch(() => { })
199
+ }
200
+ return writeData(creds, 'creds.json')
201
+ },
202
+ getStats,
203
+ }
204
+ }
@@ -17,6 +17,12 @@ export const TAGS = {
17
17
  NIBBLE_8: 255,
18
18
  PACKED_MAX: 127
19
19
  };
20
+ export const VIOLATION_TYPES = {
21
+ '15': 'Spam/Bulk Messaging',
22
+ '16': 'Automation/Bot Usage',
23
+ '18': 'Terms of Service Violation',
24
+ '20': 'Illegal Content'
25
+ };
20
26
  export const DOUBLE_BYTE_TOKENS = [
21
27
  [
22
28
  'read-self',
@@ -3,4 +3,5 @@ export * from './decode.js';
3
3
  export * from './generic-utils.js';
4
4
  export * from './jid-utils.js';
5
5
  export * from './types.js';
6
+ export * from './constants.js';
6
7
  //# sourceMappingURL=index.js.map
package/lib/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  import gradient from 'gradient-string';
3
3
  import makeWASocket from './Socket/index.js';
4
- import NexusHandler from './Socket/nexus-handler.js';
5
4
  const banner = `
6
5
  ╔══════════════════════════════════════════════════════════════════╗
7
6
  ║ ║
@@ -32,7 +31,7 @@ const banner = `
32
31
  const info = `
33
32
  ┌───────────────────────────────────────────────────────────────────────┐
34
33
  │ 📦 Package: @nexustechpro/baileys │
35
- │ 🔖 Version: 2.0.5
34
+ │ 🔖 Version: 2.1.0
36
35
  │ ⚡ Status: Production Ready │
37
36
  ├───────────────────────────────────────────────────────────────────────┤
38
37
  │ 🚀 Advanced WhatsApp Web API Client │
@@ -63,5 +62,5 @@ export * from './Defaults/index.js';
63
62
  export * from './WABinary/index.js';
64
63
  export * from './WAM/index.js';
65
64
  export * from './WAUSync/index.js';
66
- export { NexusHandler, makeWASocket };
65
+ export * from './Socket/index.js';
67
66
  export default makeWASocket;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@nexustechpro/baileys",
4
4
  "type": "module",
5
- "version": "2.0.5",
5
+ "version": "2.1.0",
6
6
  "description": "Advanced WhatsApp Web API built on Baileys — interactive messages, buttons, carousels, products, events, payments, polls, albums, sticker packs and more.",
7
7
  "keywords": [
8
8
  "whatsapp",
@@ -49,7 +49,7 @@
49
49
  "events",
50
50
  "payments",
51
51
  "polls",
52
- "album",
52
+ "albums",
53
53
  "sticker-pack",
54
54
  "nodejs",
55
55
  "typescript",
@@ -98,7 +98,9 @@
98
98
  "engine-requirements.js"
99
99
  ],
100
100
  "dependencies": {
101
+ "@adiwajshing/keyed-db": "^0.2.4",
101
102
  "@cacheable/node-cache": "^1.4.0",
103
+ "@microlink/mql": "^0.16.1",
102
104
  "@hapi/boom": "^9.1.3",
103
105
  "async-mutex": "^0.5.0",
104
106
  "axios": "^1.6.0",
@@ -106,17 +108,19 @@
106
108
  "chalk": "^4.1.2",
107
109
  "fflate": "^0.8.2",
108
110
  "gradient-string": "^2.0.2",
109
- "link-preview-js": "^3.0.4",
111
+ "libphonenumber-js": "^1.13.3",
112
+ "link-preview-js": "^4.0.3",
110
113
  "lru-cache": "^11.1.0",
111
114
  "music-metadata": "^11.7.0",
112
115
  "p-queue": "^9.0.0",
113
116
  "pino": "^9.6",
114
- "uuid": "latest",
115
117
  "protobufjs": "latest",
118
+ "qrcode-terminal": "^0.12.0",
116
119
  "sharp": "^0.32.0",
120
+ "url-regex-safe": "^4.0.0",
121
+ "uuid": "latest",
117
122
  "whatsapp-rust-bridge": "latest",
118
- "ws": "latest",
119
- "yarn": "^1.22.22"
123
+ "ws": "latest"
120
124
  },
121
125
  "devDependencies": {
122
126
  "@eslint/eslintrc": "^3.3.1",
@@ -137,7 +141,7 @@
137
141
  "jimp": "^1.6.0",
138
142
  "jiti": "^2.4.2",
139
143
  "json": "^11.0.0",
140
- "link-preview-js": "^3.0.4",
144
+ "link-preview-js": "^4.0.3",
141
145
  "lru-cache": "^11.1.0",
142
146
  "open": "^8.4.2",
143
147
  "pino-pretty": "^13.1.1",
@@ -154,7 +158,7 @@
154
158
  "peerDependencies": {
155
159
  "audio-decode": "^2.1.3",
156
160
  "jimp": "^1.6.0",
157
- "link-preview-js": "^3.0.4"
161
+ "link-preview-js": "^4.0.3"
158
162
  },
159
163
  "peerDependenciesMeta": {
160
164
  "audio-decode": {
@@ -168,4 +172,4 @@
168
172
  "engines": {
169
173
  "node": ">=20.0.0"
170
174
  }
171
- }
175
+ }