baileys-antiban 1.5.0 → 1.6.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.
- package/CHANGELOG.md +24 -0
- package/README.md +52 -0
- package/dist/antiban.d.ts +12 -0
- package/dist/antiban.js +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/jidCanonicalizer.d.ts +78 -0
- package/dist/jidCanonicalizer.js +172 -0
- package/dist/lidResolver.d.ts +96 -0
- package/dist/lidResolver.js +292 -0
- package/dist/wrapper.js +30 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.6.0] - 2026-04-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **LID/PN Race Condition Mitigation** — New modules to address the #1 reported Baileys bug: "Bad MAC / No Session / Invalid PreKey" errors caused by WhatsApp's Linked Identity (LID) migration
|
|
12
|
+
- `LidResolver` — Standalone utility for maintaining bidirectional LID↔PN mappings learned from message events
|
|
13
|
+
- `JidCanonicalizer` — Opt-in middleware that auto-learns from incoming events and canonicalizes outbound send targets to a single form (phone number by default)
|
|
14
|
+
- Both modules default to **disabled** — backward compatible, zero behavior change for existing users
|
|
15
|
+
- Middleware-layer mitigation only — root fix still requires [PR #2372](https://github.com/WhiskeySockets/Baileys/pull/2372) merged upstream
|
|
16
|
+
- Comprehensive test coverage: 56 new tests (29 LidResolver + 18 JidCanonicalizer + 9 integration)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `AntiBan` class now exposes `lidResolver` and `jidCanonicalizer` getters for direct access
|
|
20
|
+
- `AntiBanConfig` extended with `lidResolver` and `jidCanonicalizer` config options
|
|
21
|
+
- `AntiBanStats` includes `lidResolver` and `jidCanonicalizer` stats when enabled
|
|
22
|
+
- Wrapper's `sendMessage` now canonicalizes JID before all rate-limit/timelock/graph checks
|
|
23
|
+
- `messages.upsert` and `messages.update` handlers now auto-learn LID mappings when canonicalizer enabled
|
|
24
|
+
|
|
25
|
+
### Technical Details
|
|
26
|
+
- LRU eviction at configurable `maxEntries` (default 10,000)
|
|
27
|
+
- Optional persistence hooks for cross-restart state survival
|
|
28
|
+
- Device suffix stripping (`:N` in JIDs) for robust matching
|
|
29
|
+
- Supports both `canonical: 'pn'` (phone number) and `canonical: 'lid'` modes
|
|
30
|
+
- Shared resolver mode allows multiple canonicalizers to reference same mapping state
|
|
31
|
+
|
|
8
32
|
## [1.5.0] - 2026-04-18
|
|
9
33
|
|
|
10
34
|
### Added
|
package/README.md
CHANGED
|
@@ -61,6 +61,58 @@ console.log(stats.throttledSendCount); // Sends gated since reconnect
|
|
|
61
61
|
|
|
62
62
|
**Why?** When WhatsApp reconnects after a disconnection, sending messages at full rate immediately can trigger rate limit alarms. The reconnect throttle gradually ramps up sending rate over 60 seconds, mimicking how a human would resume messaging after their internet came back.
|
|
63
63
|
|
|
64
|
+
## LID / Phone Number Canonicalization
|
|
65
|
+
|
|
66
|
+
WhatsApp migrated to **Linked Identity (LID)** in 2024. A contact now has two JID forms:
|
|
67
|
+
- Phone number: `27825651069@s.whatsapp.net`
|
|
68
|
+
- LID: `123456789@lid`
|
|
69
|
+
|
|
70
|
+
Messages can arrive under either form. If an encryption session was established under one form and a message arrives under the other, decryption fails → **"Bad MAC / No Session / Invalid PreKey"** errors (the #1 reported Baileys bug).
|
|
71
|
+
|
|
72
|
+
baileys-antiban v1.6+ provides **middleware-layer mitigation** via two new modules:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { wrapSocket } from 'baileys-antiban';
|
|
76
|
+
|
|
77
|
+
const sock = makeWASocket({ ... });
|
|
78
|
+
const safeSock = wrapSocket(sock, {
|
|
79
|
+
jidCanonicalizer: {
|
|
80
|
+
enabled: true, // Enable LID/PN canonicalization
|
|
81
|
+
canonical: 'pn', // Normalize to phone-number form (default)
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// That's it! Incoming events auto-learn LID↔PN mappings.
|
|
86
|
+
// Outbound sends are auto-canonicalized to phone-number form.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Advanced: Standalone Resolver**
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { LidResolver } from 'baileys-antiban';
|
|
93
|
+
|
|
94
|
+
const resolver = new LidResolver({
|
|
95
|
+
canonical: 'pn',
|
|
96
|
+
maxEntries: 10_000, // LRU cache size
|
|
97
|
+
persistence: {
|
|
98
|
+
load: async () => JSON.parse(await fs.readFile('lid-map.json', 'utf8')),
|
|
99
|
+
save: async (map) => fs.writeFile('lid-map.json', JSON.stringify(map)),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Learn from message events
|
|
104
|
+
resolver.learn({
|
|
105
|
+
lid: '123456789@lid',
|
|
106
|
+
pn: '27825651069@s.whatsapp.net',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Resolve canonical form
|
|
110
|
+
const canonical = resolver.resolveCanonical('123456789@lid');
|
|
111
|
+
// → '27825651069@s.whatsapp.net'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Note:** This is a middleware-layer workaround. The root fix lives inside Baileys' crypto pipeline ([PR #2372](https://github.com/WhiskeySockets/Baileys/pull/2372)).
|
|
115
|
+
|
|
64
116
|
## v1.3 Features
|
|
65
117
|
|
|
66
118
|
### ReplyRatioGuard
|
package/dist/antiban.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { ContactGraphWarmer, type ContactGraphConfig, type ContactGraphStats } f
|
|
|
22
22
|
import { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats } from './presenceChoreographer.js';
|
|
23
23
|
import { RetryReasonTracker, type RetryTrackerConfig, type RetryStats } from './retryTracker.js';
|
|
24
24
|
import { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThrottleStats } from './reconnectThrottle.js';
|
|
25
|
+
import { LidResolver, type LidResolverConfig, type LidResolverStats } from './lidResolver.js';
|
|
26
|
+
import { JidCanonicalizer, type JidCanonicalizerConfig, type JidCanonicalizerStats } from './jidCanonicalizer.js';
|
|
25
27
|
export interface AntiBanConfig {
|
|
26
28
|
rateLimiter?: Partial<RateLimiterConfig>;
|
|
27
29
|
warmUp?: Partial<WarmUpConfig>;
|
|
@@ -32,6 +34,8 @@ export interface AntiBanConfig {
|
|
|
32
34
|
presence?: Partial<PresenceChoreographerConfig>;
|
|
33
35
|
retryTracker?: Partial<RetryTrackerConfig>;
|
|
34
36
|
reconnectThrottle?: Partial<ReconnectThrottleConfig>;
|
|
37
|
+
lidResolver?: LidResolverConfig;
|
|
38
|
+
jidCanonicalizer?: JidCanonicalizerConfig;
|
|
35
39
|
/** Log warnings and blocks to console (default: true) */
|
|
36
40
|
logging?: boolean;
|
|
37
41
|
}
|
|
@@ -54,6 +58,8 @@ export interface AntiBanStats {
|
|
|
54
58
|
presence?: PresenceChoreographerStats;
|
|
55
59
|
retryTracker?: RetryStats | null;
|
|
56
60
|
reconnectThrottle?: ReconnectThrottleStats | null;
|
|
61
|
+
lidResolver?: LidResolverStats | null;
|
|
62
|
+
jidCanonicalizer?: JidCanonicalizerStats | null;
|
|
57
63
|
}
|
|
58
64
|
export declare class AntiBan {
|
|
59
65
|
private rateLimiter;
|
|
@@ -65,6 +71,8 @@ export declare class AntiBan {
|
|
|
65
71
|
private presenceChoreographer;
|
|
66
72
|
private retryTrackerModule;
|
|
67
73
|
private reconnectThrottleModule;
|
|
74
|
+
private lidResolverModule;
|
|
75
|
+
private jidCanonicalizerModule;
|
|
68
76
|
private logging;
|
|
69
77
|
private stats;
|
|
70
78
|
constructor(config?: AntiBanConfig, warmUpState?: WarmUpState);
|
|
@@ -114,6 +122,10 @@ export declare class AntiBan {
|
|
|
114
122
|
get retryTracker(): RetryReasonTracker;
|
|
115
123
|
/** Get the reconnect throttle for direct access */
|
|
116
124
|
get reconnectThrottle(): PostReconnectThrottle;
|
|
125
|
+
/** Get the LID resolver for direct access */
|
|
126
|
+
get lidResolver(): LidResolver | null;
|
|
127
|
+
/** Get the JID canonicalizer for direct access */
|
|
128
|
+
get jidCanonicalizer(): JidCanonicalizer | null;
|
|
117
129
|
/**
|
|
118
130
|
* Export warm-up state for persistence between restarts
|
|
119
131
|
*/
|
package/dist/antiban.js
CHANGED
|
@@ -22,6 +22,8 @@ import { ContactGraphWarmer } from './contactGraph.js';
|
|
|
22
22
|
import { PresenceChoreographer } from './presenceChoreographer.js';
|
|
23
23
|
import { RetryReasonTracker } from './retryTracker.js';
|
|
24
24
|
import { PostReconnectThrottle } from './reconnectThrottle.js';
|
|
25
|
+
import { LidResolver } from './lidResolver.js';
|
|
26
|
+
import { JidCanonicalizer } from './jidCanonicalizer.js';
|
|
25
27
|
export class AntiBan {
|
|
26
28
|
rateLimiter;
|
|
27
29
|
warmUp;
|
|
@@ -32,6 +34,8 @@ export class AntiBan {
|
|
|
32
34
|
presenceChoreographer;
|
|
33
35
|
retryTrackerModule;
|
|
34
36
|
reconnectThrottleModule;
|
|
37
|
+
lidResolverModule = null;
|
|
38
|
+
jidCanonicalizerModule = null;
|
|
35
39
|
logging;
|
|
36
40
|
stats = {
|
|
37
41
|
messagesAllowed: 0,
|
|
@@ -86,6 +90,30 @@ export class AntiBan {
|
|
|
86
90
|
...config.reconnectThrottle,
|
|
87
91
|
baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute,
|
|
88
92
|
});
|
|
93
|
+
// Initialize LID resolver and canonicalizer if configured
|
|
94
|
+
// If jidCanonicalizer is enabled but no resolver provided, create standalone resolver
|
|
95
|
+
if (config.jidCanonicalizer?.enabled) {
|
|
96
|
+
// Create or use provided resolver
|
|
97
|
+
if (config.jidCanonicalizer.resolver) {
|
|
98
|
+
// User provided their own resolver
|
|
99
|
+
this.jidCanonicalizerModule = new JidCanonicalizer(config.jidCanonicalizer);
|
|
100
|
+
this.lidResolverModule = config.jidCanonicalizer.resolver;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Create new resolver using lidResolver config if provided
|
|
104
|
+
const resolverConfig = config.lidResolver || config.jidCanonicalizer.resolverConfig;
|
|
105
|
+
const resolver = new LidResolver(resolverConfig);
|
|
106
|
+
this.lidResolverModule = resolver;
|
|
107
|
+
this.jidCanonicalizerModule = new JidCanonicalizer({
|
|
108
|
+
...config.jidCanonicalizer,
|
|
109
|
+
resolver,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else if (config.lidResolver) {
|
|
114
|
+
// Standalone resolver without canonicalizer
|
|
115
|
+
this.lidResolverModule = new LidResolver(config.lidResolver);
|
|
116
|
+
}
|
|
89
117
|
}
|
|
90
118
|
/**
|
|
91
119
|
* Check if a message can be sent and get required delay.
|
|
@@ -286,6 +314,12 @@ export class AntiBan {
|
|
|
286
314
|
if (this.reconnectThrottleModule['config']?.enabled) {
|
|
287
315
|
stats.reconnectThrottle = this.reconnectThrottleModule.getStats();
|
|
288
316
|
}
|
|
317
|
+
if (this.lidResolverModule) {
|
|
318
|
+
stats.lidResolver = this.lidResolverModule.getStats();
|
|
319
|
+
}
|
|
320
|
+
if (this.jidCanonicalizerModule) {
|
|
321
|
+
stats.jidCanonicalizer = this.jidCanonicalizerModule.getStats();
|
|
322
|
+
}
|
|
289
323
|
return stats;
|
|
290
324
|
}
|
|
291
325
|
/** Get the timelock guard for direct access */
|
|
@@ -312,6 +346,14 @@ export class AntiBan {
|
|
|
312
346
|
get reconnectThrottle() {
|
|
313
347
|
return this.reconnectThrottleModule;
|
|
314
348
|
}
|
|
349
|
+
/** Get the LID resolver for direct access */
|
|
350
|
+
get lidResolver() {
|
|
351
|
+
return this.lidResolverModule;
|
|
352
|
+
}
|
|
353
|
+
/** Get the JID canonicalizer for direct access */
|
|
354
|
+
get jidCanonicalizer() {
|
|
355
|
+
return this.jidCanonicalizerModule;
|
|
356
|
+
}
|
|
315
357
|
/**
|
|
316
358
|
* Export warm-up state for persistence between restarts
|
|
317
359
|
*/
|
|
@@ -364,6 +406,8 @@ export class AntiBan {
|
|
|
364
406
|
this.presenceChoreographer.reset();
|
|
365
407
|
this.retryTrackerModule.destroy();
|
|
366
408
|
this.reconnectThrottleModule.destroy();
|
|
409
|
+
this.jidCanonicalizerModule?.destroy();
|
|
410
|
+
this.lidResolverModule?.destroy();
|
|
367
411
|
if (this.logging) {
|
|
368
412
|
console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
|
|
369
413
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export { ContactGraphWarmer, type ContactGraphConfig, type ContactGraphStats, ty
|
|
|
17
17
|
export { PresenceChoreographer, type PresenceChoreographerConfig, type PresenceChoreographerStats } from './presenceChoreographer.js';
|
|
18
18
|
export { RetryReasonTracker, type RetryTrackerConfig, type RetryStats, type RetryReason } from './retryTracker.js';
|
|
19
19
|
export { PostReconnectThrottle, type ReconnectThrottleConfig, type ReconnectThrottleStats } from './reconnectThrottle.js';
|
|
20
|
+
export { LidResolver, type LidResolverConfig, type LidResolverStats, type LidMapping } from './lidResolver.js';
|
|
21
|
+
export { JidCanonicalizer, type JidCanonicalizerConfig, type JidCanonicalizerStats } from './jidCanonicalizer.js';
|
|
20
22
|
export { wrapSocket, type WrappedSocket, type WrapSocketOptions } from './wrapper.js';
|
|
21
23
|
export { MessageQueue, type QueuedMessage, type MessageQueueConfig } from './messageQueue.js';
|
|
22
24
|
export { ContentVariator, type VariatorConfig } from './contentVariator.js';
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,9 @@ export { PresenceChoreographer } from './presenceChoreographer.js';
|
|
|
20
20
|
// v1.5 new modules
|
|
21
21
|
export { RetryReasonTracker } from './retryTracker.js';
|
|
22
22
|
export { PostReconnectThrottle } from './reconnectThrottle.js';
|
|
23
|
+
// v1.6 new modules
|
|
24
|
+
export { LidResolver } from './lidResolver.js';
|
|
25
|
+
export { JidCanonicalizer } from './jidCanonicalizer.js';
|
|
23
26
|
// Socket wrapper
|
|
24
27
|
export { wrapSocket } from './wrapper.js';
|
|
25
28
|
// Optional features
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JID Canonicalizer — Opt-in middleware for LID/PN normalization
|
|
3
|
+
*
|
|
4
|
+
* Wraps LidResolver to provide automatic:
|
|
5
|
+
* 1. Learning from incoming message events
|
|
6
|
+
* 2. Canonicalization of outbound send targets
|
|
7
|
+
*
|
|
8
|
+
* This mitigates the LID/PN race condition that causes "Bad MAC / No Session /
|
|
9
|
+
* Invalid PreKey" errors (Baileys issue #1769, our PR #2372).
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const canonicalizer = new JidCanonicalizer({ enabled: true });
|
|
13
|
+
*
|
|
14
|
+
* // On incoming event
|
|
15
|
+
* canonicalizer.onIncomingEvent({ messages: [...] });
|
|
16
|
+
*
|
|
17
|
+
* // On outbound send
|
|
18
|
+
* const canonicalJid = canonicalizer.canonicalizeTarget(jid);
|
|
19
|
+
* await sock.sendMessage(canonicalJid, content);
|
|
20
|
+
*
|
|
21
|
+
* Note: This is a middleware-layer mitigation. The root fix requires merging
|
|
22
|
+
* PR #2372 into Baileys' crypto pipeline.
|
|
23
|
+
*/
|
|
24
|
+
import { LidResolver, type LidResolverConfig, type LidResolverStats } from './lidResolver.js';
|
|
25
|
+
export interface JidCanonicalizerConfig {
|
|
26
|
+
/** Enable canonicalization (default: false — opt-in) */
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/** Provide your own resolver to share across modules. Otherwise one is created. */
|
|
29
|
+
resolver?: LidResolver;
|
|
30
|
+
/** Config for creating a new resolver (ignored if resolver provided) */
|
|
31
|
+
resolverConfig?: LidResolverConfig;
|
|
32
|
+
/** Canonicalize outbound sendMessage targets. Default true. */
|
|
33
|
+
canonicalizeOutbound?: boolean;
|
|
34
|
+
/** Learn from inbound events. Default true. */
|
|
35
|
+
learnFromEvents?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface JidCanonicalizerStats {
|
|
38
|
+
resolver: LidResolverStats;
|
|
39
|
+
outboundCanonicalized: number;
|
|
40
|
+
outboundPassthrough: number;
|
|
41
|
+
inboundLearned: number;
|
|
42
|
+
}
|
|
43
|
+
export declare class JidCanonicalizer {
|
|
44
|
+
private config;
|
|
45
|
+
private lidResolver;
|
|
46
|
+
private ownsResolver;
|
|
47
|
+
private stats;
|
|
48
|
+
constructor(config?: JidCanonicalizerConfig);
|
|
49
|
+
/**
|
|
50
|
+
* Access the underlying resolver (for cross-module sharing)
|
|
51
|
+
*/
|
|
52
|
+
get resolver(): LidResolver;
|
|
53
|
+
/**
|
|
54
|
+
* Called by wrapper on every outbound send. Returns canonical JID.
|
|
55
|
+
*/
|
|
56
|
+
canonicalizeTarget(jid: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Called by wrapper on messages.upsert event. Learns mappings.
|
|
59
|
+
*/
|
|
60
|
+
onIncomingEvent(upsert: {
|
|
61
|
+
messages: Array<any>;
|
|
62
|
+
type?: string;
|
|
63
|
+
}): void;
|
|
64
|
+
/**
|
|
65
|
+
* Called by wrapper on messages.update event. Learns from sent-message refs.
|
|
66
|
+
*/
|
|
67
|
+
onMessageUpdate(updates: Array<any>): void;
|
|
68
|
+
getStats(): JidCanonicalizerStats;
|
|
69
|
+
destroy(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Extract LID↔PN mappings from a message object
|
|
72
|
+
*/
|
|
73
|
+
private learnFromMessage;
|
|
74
|
+
/**
|
|
75
|
+
* Extract mappings from message.key
|
|
76
|
+
*/
|
|
77
|
+
private learnFromMessageKey;
|
|
78
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JID Canonicalizer — Opt-in middleware for LID/PN normalization
|
|
3
|
+
*
|
|
4
|
+
* Wraps LidResolver to provide automatic:
|
|
5
|
+
* 1. Learning from incoming message events
|
|
6
|
+
* 2. Canonicalization of outbound send targets
|
|
7
|
+
*
|
|
8
|
+
* This mitigates the LID/PN race condition that causes "Bad MAC / No Session /
|
|
9
|
+
* Invalid PreKey" errors (Baileys issue #1769, our PR #2372).
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const canonicalizer = new JidCanonicalizer({ enabled: true });
|
|
13
|
+
*
|
|
14
|
+
* // On incoming event
|
|
15
|
+
* canonicalizer.onIncomingEvent({ messages: [...] });
|
|
16
|
+
*
|
|
17
|
+
* // On outbound send
|
|
18
|
+
* const canonicalJid = canonicalizer.canonicalizeTarget(jid);
|
|
19
|
+
* await sock.sendMessage(canonicalJid, content);
|
|
20
|
+
*
|
|
21
|
+
* Note: This is a middleware-layer mitigation. The root fix requires merging
|
|
22
|
+
* PR #2372 into Baileys' crypto pipeline.
|
|
23
|
+
*/
|
|
24
|
+
import { LidResolver } from './lidResolver.js';
|
|
25
|
+
const DEFAULT_CONFIG = {
|
|
26
|
+
enabled: false,
|
|
27
|
+
canonicalizeOutbound: true,
|
|
28
|
+
learnFromEvents: true,
|
|
29
|
+
};
|
|
30
|
+
export class JidCanonicalizer {
|
|
31
|
+
config;
|
|
32
|
+
lidResolver;
|
|
33
|
+
ownsResolver; // Track if we created the resolver (for destroy)
|
|
34
|
+
stats = {
|
|
35
|
+
outboundCanonicalized: 0,
|
|
36
|
+
outboundPassthrough: 0,
|
|
37
|
+
inboundLearned: 0,
|
|
38
|
+
};
|
|
39
|
+
constructor(config = {}) {
|
|
40
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
41
|
+
// Use provided resolver or create new one
|
|
42
|
+
if (config.resolver) {
|
|
43
|
+
this.lidResolver = config.resolver;
|
|
44
|
+
this.ownsResolver = false;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.lidResolver = new LidResolver(config.resolverConfig);
|
|
48
|
+
this.ownsResolver = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Access the underlying resolver (for cross-module sharing)
|
|
53
|
+
*/
|
|
54
|
+
get resolver() {
|
|
55
|
+
return this.lidResolver;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Called by wrapper on every outbound send. Returns canonical JID.
|
|
59
|
+
*/
|
|
60
|
+
canonicalizeTarget(jid) {
|
|
61
|
+
if (!this.config.enabled || !this.config.canonicalizeOutbound) {
|
|
62
|
+
return jid;
|
|
63
|
+
}
|
|
64
|
+
const canonical = this.lidResolver.resolveCanonical(jid);
|
|
65
|
+
if (canonical !== jid) {
|
|
66
|
+
this.stats.outboundCanonicalized++;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this.stats.outboundPassthrough++;
|
|
70
|
+
}
|
|
71
|
+
return canonical;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Called by wrapper on messages.upsert event. Learns mappings.
|
|
75
|
+
*/
|
|
76
|
+
onIncomingEvent(upsert) {
|
|
77
|
+
if (!this.config.enabled || !this.config.learnFromEvents) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const msg of upsert.messages || []) {
|
|
81
|
+
this.learnFromMessage(msg);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Called by wrapper on messages.update event. Learns from sent-message refs.
|
|
86
|
+
*/
|
|
87
|
+
onMessageUpdate(updates) {
|
|
88
|
+
if (!this.config.enabled || !this.config.learnFromEvents) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const update of updates) {
|
|
92
|
+
// messages.update doesn't typically carry LID info — mostly for retry tracking
|
|
93
|
+
// But handle edge case where update.key has participant/participantPn
|
|
94
|
+
if (update.key) {
|
|
95
|
+
this.learnFromMessageKey(update.key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
getStats() {
|
|
100
|
+
return {
|
|
101
|
+
resolver: this.lidResolver.getStats(),
|
|
102
|
+
outboundCanonicalized: this.stats.outboundCanonicalized,
|
|
103
|
+
outboundPassthrough: this.stats.outboundPassthrough,
|
|
104
|
+
inboundLearned: this.stats.inboundLearned,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
destroy() {
|
|
108
|
+
// Only destroy resolver if we created it
|
|
109
|
+
if (this.ownsResolver) {
|
|
110
|
+
this.lidResolver.destroy();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Private helpers
|
|
114
|
+
/**
|
|
115
|
+
* Extract LID↔PN mappings from a message object
|
|
116
|
+
*/
|
|
117
|
+
learnFromMessage(msg) {
|
|
118
|
+
if (!msg.key)
|
|
119
|
+
return;
|
|
120
|
+
// Extract from message key
|
|
121
|
+
this.learnFromMessageKey(msg.key);
|
|
122
|
+
// Additional extraction from message envelope (if present)
|
|
123
|
+
// Some Baileys forks/versions expose participantPn at the message level
|
|
124
|
+
if (msg.participantPn && msg.key.participant) {
|
|
125
|
+
this.lidResolver.learn({
|
|
126
|
+
lid: msg.key.participant.endsWith('@lid') ? msg.key.participant : undefined,
|
|
127
|
+
pn: msg.participantPn,
|
|
128
|
+
});
|
|
129
|
+
this.stats.inboundLearned++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Extract mappings from message.key
|
|
134
|
+
*/
|
|
135
|
+
learnFromMessageKey(key) {
|
|
136
|
+
if (!key)
|
|
137
|
+
return;
|
|
138
|
+
// Case 1: participant (LID) + participantPn (PN)
|
|
139
|
+
// This appears in group messages where sender uses LID
|
|
140
|
+
if (key.participant && key.participantPn) {
|
|
141
|
+
if (key.participant.endsWith('@lid')) {
|
|
142
|
+
this.lidResolver.learn({
|
|
143
|
+
lid: key.participant,
|
|
144
|
+
pn: key.participantPn,
|
|
145
|
+
});
|
|
146
|
+
this.stats.inboundLearned++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Case 2: remoteJid (LID) + senderPn (PN)
|
|
150
|
+
// This appears in 1:1 messages from LID senders
|
|
151
|
+
if (key.remoteJid && key.senderPn) {
|
|
152
|
+
if (key.remoteJid.endsWith('@lid')) {
|
|
153
|
+
this.lidResolver.learn({
|
|
154
|
+
lid: key.remoteJid,
|
|
155
|
+
pn: key.senderPn,
|
|
156
|
+
});
|
|
157
|
+
this.stats.inboundLearned++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Case 3: participant (PN) exists but we have remoteJid (LID)
|
|
161
|
+
// Inverse case where participant is PN form
|
|
162
|
+
if (key.participant && key.remoteJid) {
|
|
163
|
+
if (key.participant.endsWith('@s.whatsapp.net') && key.remoteJid.endsWith('@lid')) {
|
|
164
|
+
this.lidResolver.learn({
|
|
165
|
+
lid: key.remoteJid,
|
|
166
|
+
pn: key.participant,
|
|
167
|
+
});
|
|
168
|
+
this.stats.inboundLearned++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LID Resolver — Maintains bidirectional LID↔PN mapping for contacts
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp migrated to LID (Linked Identity) in 2024. A contact now has two JIDs:
|
|
5
|
+
* - Phone number form: "27825651069@s.whatsapp.net"
|
|
6
|
+
* - LID form: "123456789@lid"
|
|
7
|
+
*
|
|
8
|
+
* Messages can arrive under either form. If an encryption session was established
|
|
9
|
+
* under one form and a message arrives under the other, decryption fails → "Bad MAC".
|
|
10
|
+
*
|
|
11
|
+
* This utility:
|
|
12
|
+
* - Learns LID↔PN mappings from message events
|
|
13
|
+
* - Normalizes JIDs to a canonical form (phone number by default)
|
|
14
|
+
* - Provides lookup for cross-form resolution
|
|
15
|
+
* - Optionally persists state across restarts
|
|
16
|
+
*
|
|
17
|
+
* This is a standalone utility — can be used independently or via JidCanonicalizer.
|
|
18
|
+
*/
|
|
19
|
+
export interface LidResolverConfig {
|
|
20
|
+
/** Canonical form to normalize to. Default: 'pn' (phone-number) */
|
|
21
|
+
canonical?: 'pn' | 'lid';
|
|
22
|
+
/** Optional persistence hooks — if provided, map survives restarts */
|
|
23
|
+
persistence?: {
|
|
24
|
+
load?: () => Promise<Record<string, any>> | Record<string, any>;
|
|
25
|
+
save?: (map: Record<string, any>) => Promise<void> | void;
|
|
26
|
+
};
|
|
27
|
+
/** Max entries to hold in memory (LRU). Default 10_000 */
|
|
28
|
+
maxEntries?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface LidMapping {
|
|
31
|
+
lid: string;
|
|
32
|
+
pn: string;
|
|
33
|
+
phone?: string;
|
|
34
|
+
learnedAt: number;
|
|
35
|
+
seenCount: number;
|
|
36
|
+
}
|
|
37
|
+
export interface LidResolverStats {
|
|
38
|
+
totalMappings: number;
|
|
39
|
+
learnedFromEvents: number;
|
|
40
|
+
lookupsServed: number;
|
|
41
|
+
lookupMisses: number;
|
|
42
|
+
canonicalForm: 'pn' | 'lid';
|
|
43
|
+
}
|
|
44
|
+
export declare class LidResolver {
|
|
45
|
+
private config;
|
|
46
|
+
private persistence?;
|
|
47
|
+
private lidToPn;
|
|
48
|
+
private pnToLid;
|
|
49
|
+
private stats;
|
|
50
|
+
constructor(config?: LidResolverConfig);
|
|
51
|
+
/**
|
|
52
|
+
* Learn from a message event. Idempotent.
|
|
53
|
+
* Accepts partial mappings — will use whatever fields are available.
|
|
54
|
+
*/
|
|
55
|
+
learn(mapping: {
|
|
56
|
+
lid?: string;
|
|
57
|
+
pn?: string;
|
|
58
|
+
phone?: string;
|
|
59
|
+
}): void;
|
|
60
|
+
/**
|
|
61
|
+
* Given any form (LID or PN), return the canonical form.
|
|
62
|
+
* Falls back to input if unknown (no throw).
|
|
63
|
+
*/
|
|
64
|
+
resolveCanonical(jid: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Lookup partner form. Returns null if unknown.
|
|
67
|
+
*/
|
|
68
|
+
getLid(pn: string): string | null;
|
|
69
|
+
getPn(lid: string): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Full mapping for inspection
|
|
72
|
+
*/
|
|
73
|
+
getMapping(jid: string): LidMapping | null;
|
|
74
|
+
/**
|
|
75
|
+
* Seed from persistence (called automatically in constructor if persistence provided)
|
|
76
|
+
*/
|
|
77
|
+
hydrate(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Flush current map to persistence
|
|
80
|
+
*/
|
|
81
|
+
flush(): Promise<void>;
|
|
82
|
+
getStats(): LidResolverStats;
|
|
83
|
+
/**
|
|
84
|
+
* Clear everything
|
|
85
|
+
*/
|
|
86
|
+
reset(): void;
|
|
87
|
+
destroy(): void;
|
|
88
|
+
/**
|
|
89
|
+
* Normalize JID: strip device suffix `:N`
|
|
90
|
+
*/
|
|
91
|
+
private normalizeJid;
|
|
92
|
+
/**
|
|
93
|
+
* Evict least recently accessed mapping (LRU)
|
|
94
|
+
*/
|
|
95
|
+
private evictLRU;
|
|
96
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LID Resolver — Maintains bidirectional LID↔PN mapping for contacts
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp migrated to LID (Linked Identity) in 2024. A contact now has two JIDs:
|
|
5
|
+
* - Phone number form: "27825651069@s.whatsapp.net"
|
|
6
|
+
* - LID form: "123456789@lid"
|
|
7
|
+
*
|
|
8
|
+
* Messages can arrive under either form. If an encryption session was established
|
|
9
|
+
* under one form and a message arrives under the other, decryption fails → "Bad MAC".
|
|
10
|
+
*
|
|
11
|
+
* This utility:
|
|
12
|
+
* - Learns LID↔PN mappings from message events
|
|
13
|
+
* - Normalizes JIDs to a canonical form (phone number by default)
|
|
14
|
+
* - Provides lookup for cross-form resolution
|
|
15
|
+
* - Optionally persists state across restarts
|
|
16
|
+
*
|
|
17
|
+
* This is a standalone utility — can be used independently or via JidCanonicalizer.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_CONFIG = {
|
|
20
|
+
canonical: 'pn',
|
|
21
|
+
maxEntries: 10_000,
|
|
22
|
+
};
|
|
23
|
+
export class LidResolver {
|
|
24
|
+
config;
|
|
25
|
+
persistence;
|
|
26
|
+
// Bidirectional maps: lid→pn and pn→lid
|
|
27
|
+
lidToPn = new Map();
|
|
28
|
+
pnToLid = new Map(); // pn → lid (for quick reverse lookup)
|
|
29
|
+
stats = {
|
|
30
|
+
learnedFromEvents: 0,
|
|
31
|
+
lookupsServed: 0,
|
|
32
|
+
lookupMisses: 0,
|
|
33
|
+
};
|
|
34
|
+
constructor(config = {}) {
|
|
35
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
36
|
+
this.persistence = config.persistence;
|
|
37
|
+
// Auto-hydrate if persistence provided
|
|
38
|
+
if (this.persistence?.load) {
|
|
39
|
+
void this.hydrate();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Learn from a message event. Idempotent.
|
|
44
|
+
* Accepts partial mappings — will use whatever fields are available.
|
|
45
|
+
*/
|
|
46
|
+
learn(mapping) {
|
|
47
|
+
let lid = mapping.lid ? this.normalizeJid(mapping.lid) : undefined;
|
|
48
|
+
let pn = mapping.pn ? this.normalizeJid(mapping.pn) : undefined;
|
|
49
|
+
const phone = mapping.phone;
|
|
50
|
+
// Validate we have both lid and pn (or can derive pn from phone)
|
|
51
|
+
if (!lid || (!pn && !phone)) {
|
|
52
|
+
return; // Insufficient data
|
|
53
|
+
}
|
|
54
|
+
// Derive pn from phone if not provided
|
|
55
|
+
if (!pn && phone) {
|
|
56
|
+
pn = `${phone}@s.whatsapp.net`;
|
|
57
|
+
}
|
|
58
|
+
// Validate forms
|
|
59
|
+
if (!lid || !pn)
|
|
60
|
+
return;
|
|
61
|
+
if (!lid.endsWith('@lid'))
|
|
62
|
+
return;
|
|
63
|
+
if (!pn.endsWith('@s.whatsapp.net'))
|
|
64
|
+
return;
|
|
65
|
+
// Check if we already know this mapping
|
|
66
|
+
const existing = this.lidToPn.get(lid);
|
|
67
|
+
if (existing) {
|
|
68
|
+
// Already learned — just increment seen count
|
|
69
|
+
existing.seenCount++;
|
|
70
|
+
existing.learnedAt = Date.now(); // Update access time for LRU
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Learn new mapping
|
|
74
|
+
const extractedPhone = phone || pn.split('@')[0];
|
|
75
|
+
const newMapping = {
|
|
76
|
+
lid,
|
|
77
|
+
pn,
|
|
78
|
+
phone: extractedPhone,
|
|
79
|
+
learnedAt: Date.now(),
|
|
80
|
+
seenCount: 1,
|
|
81
|
+
};
|
|
82
|
+
// Check if we need to evict (LRU)
|
|
83
|
+
if (this.lidToPn.size >= this.config.maxEntries) {
|
|
84
|
+
this.evictLRU();
|
|
85
|
+
}
|
|
86
|
+
this.lidToPn.set(lid, newMapping);
|
|
87
|
+
this.pnToLid.set(pn, lid);
|
|
88
|
+
this.stats.learnedFromEvents++;
|
|
89
|
+
// Async flush to persistence (don't await)
|
|
90
|
+
if (this.persistence?.save) {
|
|
91
|
+
void this.flush();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Given any form (LID or PN), return the canonical form.
|
|
96
|
+
* Falls back to input if unknown (no throw).
|
|
97
|
+
*/
|
|
98
|
+
resolveCanonical(jid) {
|
|
99
|
+
const normalized = this.normalizeJid(jid);
|
|
100
|
+
if (this.config.canonical === 'pn') {
|
|
101
|
+
// Want PN form
|
|
102
|
+
if (normalized.endsWith('@lid')) {
|
|
103
|
+
const mapping = this.lidToPn.get(normalized);
|
|
104
|
+
if (mapping) {
|
|
105
|
+
this.stats.lookupsServed++;
|
|
106
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
107
|
+
return mapping.pn;
|
|
108
|
+
}
|
|
109
|
+
this.stats.lookupMisses++;
|
|
110
|
+
return jid; // Fallback to original input
|
|
111
|
+
}
|
|
112
|
+
// Already PN form
|
|
113
|
+
this.stats.lookupsServed++;
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Want LID form
|
|
118
|
+
if (normalized.endsWith('@s.whatsapp.net')) {
|
|
119
|
+
const lid = this.pnToLid.get(normalized);
|
|
120
|
+
if (lid) {
|
|
121
|
+
this.stats.lookupsServed++;
|
|
122
|
+
const mapping = this.lidToPn.get(lid);
|
|
123
|
+
if (mapping) {
|
|
124
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
125
|
+
}
|
|
126
|
+
return lid;
|
|
127
|
+
}
|
|
128
|
+
this.stats.lookupMisses++;
|
|
129
|
+
return jid; // Fallback to original input
|
|
130
|
+
}
|
|
131
|
+
// Already LID form
|
|
132
|
+
this.stats.lookupsServed++;
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Lookup partner form. Returns null if unknown.
|
|
138
|
+
*/
|
|
139
|
+
getLid(pn) {
|
|
140
|
+
const normalized = this.normalizeJid(pn);
|
|
141
|
+
const lid = this.pnToLid.get(normalized);
|
|
142
|
+
if (lid) {
|
|
143
|
+
const mapping = this.lidToPn.get(lid);
|
|
144
|
+
if (mapping) {
|
|
145
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return lid || null;
|
|
149
|
+
}
|
|
150
|
+
getPn(lid) {
|
|
151
|
+
const normalized = this.normalizeJid(lid);
|
|
152
|
+
const mapping = this.lidToPn.get(normalized);
|
|
153
|
+
if (mapping) {
|
|
154
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
155
|
+
return mapping.pn;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Full mapping for inspection
|
|
161
|
+
*/
|
|
162
|
+
getMapping(jid) {
|
|
163
|
+
const normalized = this.normalizeJid(jid);
|
|
164
|
+
// Try as LID first
|
|
165
|
+
const byLid = this.lidToPn.get(normalized);
|
|
166
|
+
if (byLid) {
|
|
167
|
+
byLid.learnedAt = Date.now(); // Update LRU access time
|
|
168
|
+
return byLid;
|
|
169
|
+
}
|
|
170
|
+
// Try as PN
|
|
171
|
+
const lid = this.pnToLid.get(normalized);
|
|
172
|
+
if (lid) {
|
|
173
|
+
const mapping = this.lidToPn.get(lid);
|
|
174
|
+
if (mapping) {
|
|
175
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
176
|
+
return mapping;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Seed from persistence (called automatically in constructor if persistence provided)
|
|
183
|
+
*/
|
|
184
|
+
async hydrate() {
|
|
185
|
+
if (!this.persistence?.load)
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
const stored = await this.persistence.load();
|
|
189
|
+
if (!stored || typeof stored !== 'object')
|
|
190
|
+
return;
|
|
191
|
+
// Restore mappings
|
|
192
|
+
for (const [lid, serialized] of Object.entries(stored)) {
|
|
193
|
+
if (typeof serialized === 'string') {
|
|
194
|
+
// Old format: lid → pn string
|
|
195
|
+
const pn = serialized;
|
|
196
|
+
const phone = pn.split('@')[0];
|
|
197
|
+
const mapping = {
|
|
198
|
+
lid,
|
|
199
|
+
pn,
|
|
200
|
+
phone,
|
|
201
|
+
learnedAt: Date.now(),
|
|
202
|
+
seenCount: 1,
|
|
203
|
+
};
|
|
204
|
+
this.lidToPn.set(lid, mapping);
|
|
205
|
+
this.pnToLid.set(pn, lid);
|
|
206
|
+
}
|
|
207
|
+
else if (typeof serialized === 'object' && serialized !== null) {
|
|
208
|
+
// New format: lid → LidMapping object
|
|
209
|
+
const mapping = serialized;
|
|
210
|
+
this.lidToPn.set(lid, mapping);
|
|
211
|
+
this.pnToLid.set(mapping.pn, lid);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
// Silently fail hydration — don't crash on corrupt persistence
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Flush current map to persistence
|
|
221
|
+
*/
|
|
222
|
+
async flush() {
|
|
223
|
+
if (!this.persistence?.save)
|
|
224
|
+
return;
|
|
225
|
+
try {
|
|
226
|
+
const toStore = {};
|
|
227
|
+
for (const [lid, mapping] of this.lidToPn.entries()) {
|
|
228
|
+
toStore[lid] = mapping;
|
|
229
|
+
}
|
|
230
|
+
await this.persistence.save(toStore);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
// Silently fail flush — don't crash
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
getStats() {
|
|
237
|
+
return {
|
|
238
|
+
totalMappings: this.lidToPn.size,
|
|
239
|
+
learnedFromEvents: this.stats.learnedFromEvents,
|
|
240
|
+
lookupsServed: this.stats.lookupsServed,
|
|
241
|
+
lookupMisses: this.stats.lookupMisses,
|
|
242
|
+
canonicalForm: this.config.canonical,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Clear everything
|
|
247
|
+
*/
|
|
248
|
+
reset() {
|
|
249
|
+
this.lidToPn.clear();
|
|
250
|
+
this.pnToLid.clear();
|
|
251
|
+
this.stats = {
|
|
252
|
+
learnedFromEvents: 0,
|
|
253
|
+
lookupsServed: 0,
|
|
254
|
+
lookupMisses: 0,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
destroy() {
|
|
258
|
+
this.reset();
|
|
259
|
+
// Flush one final time
|
|
260
|
+
if (this.persistence?.save) {
|
|
261
|
+
void this.flush();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Private helpers
|
|
265
|
+
/**
|
|
266
|
+
* Normalize JID: strip device suffix `:N`
|
|
267
|
+
*/
|
|
268
|
+
normalizeJid(jid) {
|
|
269
|
+
// Strip device suffix e.g. "123:45@s.whatsapp.net" → "123@s.whatsapp.net"
|
|
270
|
+
return jid.replace(/:\d+@/, '@');
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Evict least recently accessed mapping (LRU)
|
|
274
|
+
*/
|
|
275
|
+
evictLRU() {
|
|
276
|
+
let oldestLid = null;
|
|
277
|
+
let oldestTime = Infinity;
|
|
278
|
+
for (const [lid, mapping] of this.lidToPn.entries()) {
|
|
279
|
+
if (mapping.learnedAt < oldestTime) {
|
|
280
|
+
oldestTime = mapping.learnedAt;
|
|
281
|
+
oldestLid = lid;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (oldestLid) {
|
|
285
|
+
const mapping = this.lidToPn.get(oldestLid);
|
|
286
|
+
if (mapping) {
|
|
287
|
+
this.pnToLid.delete(mapping.pn);
|
|
288
|
+
}
|
|
289
|
+
this.lidToPn.delete(oldestLid);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
package/dist/wrapper.js
CHANGED
|
@@ -63,7 +63,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
// Catch 463 errors from message updates + track retries
|
|
66
|
+
// Catch 463 errors from message updates + track retries + learn LID mappings
|
|
67
67
|
if (events['messages.update']) {
|
|
68
68
|
const updates = events['messages.update'];
|
|
69
69
|
for (const update of updates) {
|
|
@@ -77,10 +77,14 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
77
77
|
// Retry tracking
|
|
78
78
|
antiban.retryTracker.onMessageUpdate(update);
|
|
79
79
|
}
|
|
80
|
+
// LID canonicalizer learning
|
|
81
|
+
antiban.jidCanonicalizer?.onMessageUpdate(updates);
|
|
80
82
|
}
|
|
81
|
-
// Register known chats from incoming messages + handle reply suggestions
|
|
83
|
+
// Register known chats from incoming messages + handle reply suggestions + learn LID mappings
|
|
82
84
|
if (events['messages.upsert']) {
|
|
83
85
|
const { messages } = events['messages.upsert'];
|
|
86
|
+
// Learn LID mappings FIRST (before any other processing)
|
|
87
|
+
antiban.jidCanonicalizer?.onIncomingEvent(events['messages.upsert']);
|
|
84
88
|
for (const msg of messages || []) {
|
|
85
89
|
const jid = msg.key?.remoteJid;
|
|
86
90
|
if (!jid)
|
|
@@ -136,7 +140,7 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
136
140
|
});
|
|
137
141
|
}
|
|
138
142
|
});
|
|
139
|
-
// Catch 463 errors from message updates + track retries
|
|
143
|
+
// Catch 463 errors from message updates + track retries + learn LID mappings
|
|
140
144
|
sock.ev.on('messages.update', (updates) => {
|
|
141
145
|
for (const update of updates) {
|
|
142
146
|
// 463 error detection
|
|
@@ -149,9 +153,14 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
149
153
|
// Retry tracking
|
|
150
154
|
antiban.retryTracker.onMessageUpdate(update);
|
|
151
155
|
}
|
|
156
|
+
// LID canonicalizer learning
|
|
157
|
+
antiban.jidCanonicalizer?.onMessageUpdate(updates);
|
|
152
158
|
});
|
|
153
|
-
// Register known chats from incoming messages + handle reply suggestions
|
|
154
|
-
sock.ev.on('messages.upsert', (
|
|
159
|
+
// Register known chats from incoming messages + handle reply suggestions + learn LID mappings
|
|
160
|
+
sock.ev.on('messages.upsert', (upsert) => {
|
|
161
|
+
const { messages } = upsert;
|
|
162
|
+
// Learn LID mappings FIRST (before any other processing)
|
|
163
|
+
antiban.jidCanonicalizer?.onIncomingEvent(upsert);
|
|
155
164
|
for (const msg of messages || []) {
|
|
156
165
|
const jid = msg.key?.remoteJid;
|
|
157
166
|
if (!jid)
|
|
@@ -189,9 +198,20 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
189
198
|
// Create proxy that intercepts sendMessage
|
|
190
199
|
const originalSendMessage = sock.sendMessage.bind(sock);
|
|
191
200
|
const wrappedSendMessage = async (jid, content, options) => {
|
|
201
|
+
/**
|
|
202
|
+
* LID/PN Canonicalization — Normalize JID to canonical form
|
|
203
|
+
*
|
|
204
|
+
* This mitigates the LID/PN race that causes "Bad MAC / No Session / Invalid PreKey"
|
|
205
|
+
* errors (Baileys #1769, our PR #2372). When a message event arrives under one form
|
|
206
|
+
* (e.g. LID) but the crypto session was established under another (e.g. PN), decryption
|
|
207
|
+
* fails. By normalizing all outbound targets to a single form, we reduce this race.
|
|
208
|
+
*
|
|
209
|
+
* This is middleware-layer mitigation only. Root fix requires PR #2372 merged upstream.
|
|
210
|
+
*/
|
|
211
|
+
const canonicalJid = antiban.jidCanonicalizer?.canonicalizeTarget(jid) || jid;
|
|
192
212
|
// Extract text content for rate limiter analysis
|
|
193
213
|
const text = content?.text || content?.caption || content?.image?.caption || '';
|
|
194
|
-
const decision = await antiban.beforeSend(
|
|
214
|
+
const decision = await antiban.beforeSend(canonicalJid, text);
|
|
195
215
|
if (!decision.allowed) {
|
|
196
216
|
throw new Error(`[baileys-antiban] Message blocked: ${decision.reason}`);
|
|
197
217
|
}
|
|
@@ -199,11 +219,11 @@ export function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
|
199
219
|
if (decision.delayMs > 0) {
|
|
200
220
|
await new Promise(resolve => setTimeout(resolve, decision.delayMs));
|
|
201
221
|
}
|
|
202
|
-
// Send message
|
|
222
|
+
// Send message (using canonical JID)
|
|
203
223
|
try {
|
|
204
|
-
const result = await originalSendMessage(
|
|
205
|
-
antiban.afterSend(
|
|
206
|
-
antiban.timelock.registerKnownChat(
|
|
224
|
+
const result = await originalSendMessage(canonicalJid, content, options);
|
|
225
|
+
antiban.afterSend(canonicalJid, text);
|
|
226
|
+
antiban.timelock.registerKnownChat(canonicalJid);
|
|
207
227
|
// Clear retry tracking on successful send
|
|
208
228
|
if (result?.key?.id) {
|
|
209
229
|
antiban.retryTracker.clear(result.key.id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baileys-antiban",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Transport-agnostic anti-ban middleware for Baileys and baileyrs — human-like messaging patterns to protect your WhatsApp number",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|