@prototypevip/baileys 0.0.8 → 0.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.
- package/lib/AntiBan/antiban.js +922 -0
- package/lib/AntiBan/banRecoveryOrchestrator.js +284 -0
- package/lib/AntiBan/cli.js +177 -0
- package/lib/AntiBan/contactGraph.js +236 -0
- package/lib/AntiBan/contentVariator.js +150 -0
- package/lib/AntiBan/credsSnapshot.js +121 -0
- package/lib/AntiBan/deliveryTracker.js +107 -0
- package/lib/AntiBan/deviceFingerprint.js +106 -0
- package/lib/AntiBan/fleetEventStore.js +157 -0
- package/lib/AntiBan/groupOperationGuard.js +116 -0
- package/lib/AntiBan/health.js +207 -0
- package/lib/AntiBan/humanEntropy.js +337 -0
- package/lib/AntiBan/index.js +77 -0
- package/lib/AntiBan/instanceCoordinator.js +175 -0
- package/lib/AntiBan/jidCanonicalizer.js +256 -0
- package/lib/AntiBan/jidCircuitBreaker.js +186 -0
- package/lib/AntiBan/legitimacySignalInjector.js +231 -0
- package/lib/AntiBan/lidFirstResolver.js +174 -0
- package/lib/AntiBan/lidResolver.js +324 -0
- package/lib/AntiBan/messageQueue.js +187 -0
- package/lib/AntiBan/messageRecovery.js +283 -0
- package/lib/AntiBan/messageTypeRegistry.js +369 -0
- package/lib/AntiBan/observability.js +145 -0
- package/lib/AntiBan/patch.js +302 -0
- package/lib/AntiBan/persist.js +87 -0
- package/lib/AntiBan/presenceChoreographer.js +430 -0
- package/lib/AntiBan/presets.js +105 -0
- package/lib/AntiBan/profiles.js +31 -0
- package/lib/AntiBan/proxyRotator.js +306 -0
- package/lib/AntiBan/rateLimiter.js +261 -0
- package/lib/AntiBan/readReceiptVariance.js +88 -0
- package/lib/AntiBan/reconnectThrottle.js +180 -0
- package/lib/AntiBan/replyRatio.js +161 -0
- package/lib/AntiBan/reputationVoucher.js +373 -0
- package/lib/AntiBan/retryReason.js +91 -0
- package/lib/AntiBan/retryTracker.js +172 -0
- package/lib/AntiBan/scheduler.js +111 -0
- package/lib/AntiBan/sessionFingerprint.js +248 -0
- package/lib/AntiBan/sessionStability.js +330 -0
- package/lib/AntiBan/stateAdapter.js +73 -0
- package/lib/AntiBan/stateExport.js +152 -0
- package/lib/AntiBan/stealthConnect.js +130 -0
- package/lib/AntiBan/timelockGuard.js +181 -0
- package/lib/AntiBan/topologyThrottler.js +339 -0
- package/lib/AntiBan/warmup.js +140 -0
- package/lib/AntiBan/webhooks.js +80 -0
- package/lib/AntiBan/wrapper.js +456 -0
- package/lib/Socket/groups.js +26 -21
- package/lib/Socket/index.js +132 -7
- package/lib/Socket/messages-send.js +68 -5
- package/lib/Utils/ai-rich-formatter.js +256 -0
- package/lib/Utils/index.js +1 -0
- package/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LegitimacySignalInjector — Human imperfection signals
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp's detection looks for accounts that are TOO perfect:
|
|
5
|
+
* - Never make typos
|
|
6
|
+
* - Always reply at consistent speeds
|
|
7
|
+
* - Typing duration linearly correlates with message length
|
|
8
|
+
*
|
|
9
|
+
* This module injects realistic imperfections:
|
|
10
|
+
* - Typos followed by corrections (2-3% of messages)
|
|
11
|
+
* - Read-receipt without immediate reply (read gap)
|
|
12
|
+
* - Mid-typing pauses for longer messages
|
|
13
|
+
*
|
|
14
|
+
* Note: Complements PresenceChoreographer, doesn't replace it.
|
|
15
|
+
* PC handles timing/circadian rhythm; this handles content imperfections.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_CONFIG = {
|
|
18
|
+
enableTypos: true,
|
|
19
|
+
typoProbability: 0.025,
|
|
20
|
+
typoCorrectMinMs: 500,
|
|
21
|
+
typoCorrectMaxMs: 2000,
|
|
22
|
+
enableReadGaps: true,
|
|
23
|
+
readGapProbability: 0.15,
|
|
24
|
+
readGapMinMs: 300_000,
|
|
25
|
+
readGapMaxMs: 3_600_000,
|
|
26
|
+
enableTypingPauses: true,
|
|
27
|
+
typingPauseLengthThreshold: 50,
|
|
28
|
+
typingPauseProbability: 0.4,
|
|
29
|
+
typingPauseMinMs: 1_500,
|
|
30
|
+
typingPauseMaxMs: 6_000,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* QWERTY keyboard adjacency map for realistic typos.
|
|
34
|
+
* Maps common keys to their adjacent neighbors.
|
|
35
|
+
*/
|
|
36
|
+
const QWERTY_ADJACENT = {
|
|
37
|
+
'a': ['q', 's', 'w', 'z'],
|
|
38
|
+
'b': ['v', 'g', 'h', 'n'],
|
|
39
|
+
'c': ['x', 'd', 'f', 'v'],
|
|
40
|
+
'd': ['s', 'e', 'r', 'f', 'c', 'x'],
|
|
41
|
+
'e': ['w', 'r', 'd', 's'],
|
|
42
|
+
'f': ['d', 'r', 't', 'g', 'v', 'c'],
|
|
43
|
+
'g': ['f', 't', 'y', 'h', 'b', 'v'],
|
|
44
|
+
'h': ['g', 'y', 'u', 'j', 'n', 'b'],
|
|
45
|
+
'i': ['u', 'o', 'k', 'j'],
|
|
46
|
+
'j': ['h', 'u', 'i', 'k', 'n', 'm'],
|
|
47
|
+
'k': ['j', 'i', 'o', 'l', 'm'],
|
|
48
|
+
'l': ['k', 'o', 'p'],
|
|
49
|
+
'm': ['n', 'j', 'k'],
|
|
50
|
+
'n': ['b', 'h', 'j', 'm'],
|
|
51
|
+
'o': ['i', 'p', 'l', 'k'],
|
|
52
|
+
'p': ['o', 'l'],
|
|
53
|
+
'q': ['w', 'a'],
|
|
54
|
+
'r': ['e', 't', 'f', 'd'],
|
|
55
|
+
's': ['a', 'w', 'e', 'd', 'x', 'z'],
|
|
56
|
+
't': ['r', 'y', 'g', 'f'],
|
|
57
|
+
'u': ['y', 'i', 'j', 'h'],
|
|
58
|
+
'v': ['c', 'f', 'g', 'b'],
|
|
59
|
+
'w': ['q', 'e', 's', 'a'],
|
|
60
|
+
'x': ['z', 's', 'd', 'c'],
|
|
61
|
+
'y': ['t', 'u', 'h', 'g'],
|
|
62
|
+
'z': ['a', 's', 'x'],
|
|
63
|
+
};
|
|
64
|
+
export class LegitimacySignalInjector {
|
|
65
|
+
config;
|
|
66
|
+
stats = {
|
|
67
|
+
typosInjected: 0,
|
|
68
|
+
correctionsGenerated: 0,
|
|
69
|
+
readGapsInjected: 0,
|
|
70
|
+
typingPausesInjected: 0,
|
|
71
|
+
};
|
|
72
|
+
constructor(config = {}) {
|
|
73
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Determine if a typo should be injected for this message.
|
|
77
|
+
* Returns typo version of text + correction delay, or null if no typo.
|
|
78
|
+
*/
|
|
79
|
+
shouldInjectTypo(text) {
|
|
80
|
+
if (!this.config.enableTypos) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
// Only inject typos in messages > 10 chars
|
|
84
|
+
if (text.length <= 10) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// Probability check
|
|
88
|
+
if (Math.random() >= this.config.typoProbability) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Don't inject typos in URLs
|
|
92
|
+
if (this.containsUrl(text)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// Extract words (excluding @mentions and numbers)
|
|
96
|
+
const words = text.split(/\s+/);
|
|
97
|
+
const eligibleWords = words.filter(word => word.length >= 3 && !word.startsWith('@') && !/^\d+$/.test(word));
|
|
98
|
+
if (eligibleWords.length === 0) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
// Pick a random eligible word
|
|
102
|
+
const targetWord = eligibleWords[Math.floor(Math.random() * eligibleWords.length)];
|
|
103
|
+
const typoWord = this.injectTypoInWord(targetWord);
|
|
104
|
+
if (!typoWord || typoWord === targetWord) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
// Generate typo text
|
|
108
|
+
const typoText = text.replace(targetWord, typoWord);
|
|
109
|
+
// Generate correction
|
|
110
|
+
const correctionDelay = this.randomBetween(this.config.typoCorrectMinMs, this.config.typoCorrectMaxMs);
|
|
111
|
+
// Correction format: "*correctedword" for single word, or full message for short texts
|
|
112
|
+
let correctionText;
|
|
113
|
+
if (text.length < 30) {
|
|
114
|
+
// Short message: resend the whole thing
|
|
115
|
+
correctionText = text;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Long message: use WA correction convention
|
|
119
|
+
correctionText = `*${targetWord}`;
|
|
120
|
+
}
|
|
121
|
+
this.stats.typosInjected++;
|
|
122
|
+
this.stats.correctionsGenerated++;
|
|
123
|
+
return {
|
|
124
|
+
typoText,
|
|
125
|
+
correctionDelay,
|
|
126
|
+
correctionText,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Determine if a read gap should be injected before sending a reply.
|
|
131
|
+
* Returns gap duration in ms, or null if no gap.
|
|
132
|
+
*/
|
|
133
|
+
shouldInjectReadGap() {
|
|
134
|
+
if (!this.config.enableReadGaps) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (Math.random() >= this.config.readGapProbability) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const gapMs = this.randomBetween(this.config.readGapMinMs, this.config.readGapMaxMs);
|
|
141
|
+
this.stats.readGapsInjected++;
|
|
142
|
+
return gapMs;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* For a message of given length, calculate mid-typing pause positions.
|
|
146
|
+
* Returns array of { afterChars, pauseDurationMs } or empty array.
|
|
147
|
+
*/
|
|
148
|
+
getTypingPauses(messageLength) {
|
|
149
|
+
if (!this.config.enableTypingPauses) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
// Only for messages above threshold
|
|
153
|
+
if (messageLength < this.config.typingPauseLengthThreshold) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
// Probability check
|
|
157
|
+
if (Math.random() >= this.config.typingPauseProbability) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const pauses = [];
|
|
161
|
+
// Inject 1-2 pauses at natural break points
|
|
162
|
+
const numPauses = Math.random() < 0.6 ? 1 : 2;
|
|
163
|
+
for (let i = 0; i < numPauses; i++) {
|
|
164
|
+
let position;
|
|
165
|
+
if (i === 0) {
|
|
166
|
+
// First pause around 40% mark
|
|
167
|
+
position = Math.floor(messageLength * (0.35 + Math.random() * 0.15));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Second pause around 70% mark
|
|
171
|
+
position = Math.floor(messageLength * (0.65 + Math.random() * 0.15));
|
|
172
|
+
}
|
|
173
|
+
const pauseDurationMs = this.randomBetween(this.config.typingPauseMinMs, this.config.typingPauseMaxMs);
|
|
174
|
+
pauses.push({
|
|
175
|
+
afterChars: position,
|
|
176
|
+
pauseDurationMs,
|
|
177
|
+
});
|
|
178
|
+
this.stats.typingPausesInjected++;
|
|
179
|
+
}
|
|
180
|
+
// Sort by position
|
|
181
|
+
pauses.sort((a, b) => a.afterChars - b.afterChars);
|
|
182
|
+
return pauses;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get statistics.
|
|
186
|
+
*/
|
|
187
|
+
getStats() {
|
|
188
|
+
return { ...this.stats };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Reset statistics.
|
|
192
|
+
*/
|
|
193
|
+
reset() {
|
|
194
|
+
this.stats = {
|
|
195
|
+
typosInjected: 0,
|
|
196
|
+
correctionsGenerated: 0,
|
|
197
|
+
readGapsInjected: 0,
|
|
198
|
+
typingPausesInjected: 0,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Private helpers
|
|
202
|
+
injectTypoInWord(word) {
|
|
203
|
+
// Find a char that has QWERTY neighbors
|
|
204
|
+
const chars = word.toLowerCase().split('');
|
|
205
|
+
const eligibleIndices = chars
|
|
206
|
+
.map((char, idx) => ({ char, idx }))
|
|
207
|
+
.filter(({ char }) => QWERTY_ADJACENT[char] !== undefined);
|
|
208
|
+
if (eligibleIndices.length === 0) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
// Pick random position
|
|
212
|
+
const target = eligibleIndices[Math.floor(Math.random() * eligibleIndices.length)];
|
|
213
|
+
const neighbors = QWERTY_ADJACENT[target.char];
|
|
214
|
+
const replacement = neighbors[Math.floor(Math.random() * neighbors.length)];
|
|
215
|
+
// Preserve original casing
|
|
216
|
+
const originalChar = word[target.idx];
|
|
217
|
+
const replacementChar = originalChar === originalChar.toUpperCase()
|
|
218
|
+
? replacement.toUpperCase()
|
|
219
|
+
: replacement;
|
|
220
|
+
// Build typo word
|
|
221
|
+
const typoChars = word.split('');
|
|
222
|
+
typoChars[target.idx] = replacementChar;
|
|
223
|
+
return typoChars.join('');
|
|
224
|
+
}
|
|
225
|
+
containsUrl(text) {
|
|
226
|
+
return /https?:\/\/|www\./i.test(text);
|
|
227
|
+
}
|
|
228
|
+
randomBetween(min, max) {
|
|
229
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LidFirstResolver — Standalone LID↔Phone mapper for Baileys auth state
|
|
3
|
+
*
|
|
4
|
+
* Lightweight drop-in utility that:
|
|
5
|
+
* - Loads LID↔phone mappings from Baileys' auth state directory
|
|
6
|
+
* - Resolves phone numbers to LID JIDs and vice versa
|
|
7
|
+
* - Learns new mappings from Baileys events
|
|
8
|
+
* - Works independently of the full AntiBan system
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { LidFirstResolver } from 'baileys-antiban';
|
|
13
|
+
* const resolver = new LidFirstResolver();
|
|
14
|
+
* resolver.loadFromAuthDir('./whatsapp-auth/my-session');
|
|
15
|
+
* const jid = resolver.resolveToLID('27825651069'); // → "210543692497008@lid" or null
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
19
|
+
* @license MIT
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
export class LidFirstResolver {
|
|
24
|
+
lidToPhone = new Map();
|
|
25
|
+
phoneToLid = new Map(); // phone → lid (quick reverse lookup)
|
|
26
|
+
/**
|
|
27
|
+
* Load mappings from Baileys auth state directory.
|
|
28
|
+
* Looks for lid-mapping-*_reverse.json files.
|
|
29
|
+
*/
|
|
30
|
+
loadFromAuthDir(authDir) {
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(authDir)) {
|
|
33
|
+
return; // Directory doesn't exist, nothing to load
|
|
34
|
+
}
|
|
35
|
+
const files = fs.readdirSync(authDir);
|
|
36
|
+
const reverseMappingFiles = files.filter(f => f.startsWith('lid-mapping-') && f.endsWith('_reverse.json'));
|
|
37
|
+
for (const file of reverseMappingFiles) {
|
|
38
|
+
const filePath = path.join(authDir, file);
|
|
39
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
40
|
+
const data = JSON.parse(content);
|
|
41
|
+
// Baileys reverse mapping format: { "lid@lid": "phone@s.whatsapp.net" }
|
|
42
|
+
for (const [lid, pnJid] of Object.entries(data)) {
|
|
43
|
+
if (typeof pnJid === 'string') {
|
|
44
|
+
const phone = this.extractPhone(pnJid);
|
|
45
|
+
if (phone && lid.endsWith('@lid')) {
|
|
46
|
+
const mapping = {
|
|
47
|
+
lid: this.normalizeLid(lid),
|
|
48
|
+
phone,
|
|
49
|
+
learnedAt: Date.now(),
|
|
50
|
+
source: 'auth-dir',
|
|
51
|
+
};
|
|
52
|
+
this.lidToPhone.set(mapping.lid, mapping);
|
|
53
|
+
this.phoneToLid.set(phone, mapping.lid);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Silently fail — don't crash if auth dir is malformed
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Learn a new mapping from a Baileys event (messages, contacts, etc.).
|
|
65
|
+
* Accepts partial data — will extract what it can.
|
|
66
|
+
*/
|
|
67
|
+
learnFromEvent(event) {
|
|
68
|
+
try {
|
|
69
|
+
// Extract from message event structure
|
|
70
|
+
if (event.key?.remoteJid) {
|
|
71
|
+
const jid = event.key.remoteJid;
|
|
72
|
+
this.learnJid(jid, 'event');
|
|
73
|
+
}
|
|
74
|
+
// Extract from participant field (group messages)
|
|
75
|
+
if (event.key?.participant) {
|
|
76
|
+
const jid = event.key.participant;
|
|
77
|
+
this.learnJid(jid, 'event');
|
|
78
|
+
}
|
|
79
|
+
// Extract from contact event
|
|
80
|
+
if (event.id) {
|
|
81
|
+
this.learnJid(event.id, 'event');
|
|
82
|
+
}
|
|
83
|
+
// Extract from pushName field (has phone)
|
|
84
|
+
if (event.pushName && event.key?.remoteJid) {
|
|
85
|
+
this.learnJid(event.key.remoteJid, 'event');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// Silently fail — don't crash on malformed events
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve phone number or phone JID to LID JID.
|
|
94
|
+
* Returns null if not known.
|
|
95
|
+
*/
|
|
96
|
+
resolveToLID(phoneOrJid) {
|
|
97
|
+
const phone = this.extractPhone(phoneOrJid);
|
|
98
|
+
if (!phone)
|
|
99
|
+
return null;
|
|
100
|
+
return this.phoneToLid.get(phone) || null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve LID JID to phone number.
|
|
104
|
+
* Returns null if not known.
|
|
105
|
+
*/
|
|
106
|
+
resolveToPhone(lid) {
|
|
107
|
+
const normalized = this.normalizeLid(lid);
|
|
108
|
+
const mapping = this.lidToPhone.get(normalized);
|
|
109
|
+
return mapping ? mapping.phone : null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get full mapping for a given JID (either LID or phone).
|
|
113
|
+
* Returns null if not known.
|
|
114
|
+
*/
|
|
115
|
+
getMapping(jid) {
|
|
116
|
+
const normalized = this.normalizeLid(jid);
|
|
117
|
+
// Try as LID
|
|
118
|
+
const byLid = this.lidToPhone.get(normalized);
|
|
119
|
+
if (byLid)
|
|
120
|
+
return byLid;
|
|
121
|
+
// Try as phone
|
|
122
|
+
const phone = this.extractPhone(jid);
|
|
123
|
+
if (phone) {
|
|
124
|
+
const lid = this.phoneToLid.get(phone);
|
|
125
|
+
if (lid)
|
|
126
|
+
return this.lidToPhone.get(lid) || null;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get total number of known mappings.
|
|
132
|
+
*/
|
|
133
|
+
size() {
|
|
134
|
+
return this.lidToPhone.size;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Clear all mappings.
|
|
138
|
+
*/
|
|
139
|
+
clear() {
|
|
140
|
+
this.lidToPhone.clear();
|
|
141
|
+
this.phoneToLid.clear();
|
|
142
|
+
}
|
|
143
|
+
// Private helpers
|
|
144
|
+
learnJid(_jid, _source) {
|
|
145
|
+
// Check if this is a LID JID with a phone equivalent we can learn
|
|
146
|
+
// For now, we can only learn from auth dir or from paired data
|
|
147
|
+
// Single JID without context can't create a mapping
|
|
148
|
+
// This is intentionally limited — real learning happens in loadFromAuthDir
|
|
149
|
+
}
|
|
150
|
+
extractPhone(jid) {
|
|
151
|
+
if (!jid)
|
|
152
|
+
return null;
|
|
153
|
+
// Remove @s.whatsapp.net suffix if present
|
|
154
|
+
let cleaned = jid.replace('@s.whatsapp.net', '');
|
|
155
|
+
// Remove device suffix :N if present
|
|
156
|
+
cleaned = cleaned.replace(/:\d+$/, '');
|
|
157
|
+
// Check if it's a phone number (digits only)
|
|
158
|
+
if (/^\d+$/.test(cleaned)) {
|
|
159
|
+
return cleaned;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
normalizeLid(lid) {
|
|
164
|
+
// Remove device suffix :N if present
|
|
165
|
+
return lid.replace(/:\d+@/, '@');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Factory function for creating a singleton resolver instance.
|
|
170
|
+
* Useful for shared state across modules.
|
|
171
|
+
*/
|
|
172
|
+
export function createLidFirstResolver() {
|
|
173
|
+
return new LidFirstResolver();
|
|
174
|
+
}
|