@builderbot/provider-baileys 1.4.2-alpha.2 → 1.4.2-alpha.7

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/dist/index.mjs CHANGED
@@ -4,10 +4,10 @@ import require$$0$3 from 'constants';
4
4
  import require$$0$4 from 'stream';
5
5
  import require$$1 from 'util';
6
6
  import require$$5 from 'assert';
7
- import require$$1$1, { join, basename, resolve } from 'path';
7
+ import require$$1$1, { join, dirname, basename, resolve } from 'path';
8
8
  import require$$0$6 from 'zlib';
9
9
  import { Console } from 'console';
10
- import { readdir, unlink, writeFile } from 'fs/promises';
10
+ import { unlink, mkdir, writeFile, stat as stat$2, access, readFile, readdir } from 'fs/promises';
11
11
  import require$$1$2 from 'events';
12
12
  import require$$2$2, { tmpdir } from 'os';
13
13
  import require$$0$a from 'node:os';
@@ -33676,6 +33676,963 @@ function requireDist () {
33676
33676
 
33677
33677
  var distExports = requireDist();
33678
33678
 
33679
+ /**
33680
+ * @fileoverview LID (Local Identifier) Cache for WhatsApp Baileys Provider
33681
+ *
33682
+ * This module provides a caching layer for mapping WhatsApp Local Identifiers (LIDs)
33683
+ * to Phone Numbers (PNs). LIDs are privacy-preserving identifiers used by WhatsApp
33684
+ * that don't reveal the user's actual phone number.
33685
+ *
33686
+ * ## Architecture
33687
+ *
33688
+ * The cache uses a hybrid memory+file approach:
33689
+ * - **Hot path**: In-memory lookups via NodeCache (O(1), ~1μs)
33690
+ * - **Persistence**: JSON file for cross-restart durability
33691
+ * - **Strategy**: Write-through to memory, async flush to disk every 30s
33692
+ *
33693
+ * ## Key Features
33694
+ *
33695
+ * - **Zero-config**: Works out of the box with sensible defaults
33696
+ * - **Device suffix normalization**: `123:45@lid` and `123:99@lid` resolve to same entry
33697
+ * - **Phone number normalization**: Accepts `+123 456-7890`, `1234567890@c.us`, etc.
33698
+ * - **Security**: File permissions 0o600, PII masking in logs
33699
+ * - **Resilience**: Corrupted files auto-rebuild, flush failures logged
33700
+ *
33701
+ * ## Usage
33702
+ *
33703
+ * ```typescript
33704
+ * // Default: Hybrid (memory + file)
33705
+ * const cache = new HybridLidCache('my-bot', 86400 * 7) // 7 day TTL
33706
+ * await cache.ready()
33707
+ *
33708
+ * await cache.set('123456789:45@lid', '+34 691 015 468')
33709
+ * const pn = await cache.get('123456789:99@lid') // Returns '34691015468@s.whatsapp.net'
33710
+ *
33711
+ * await cache.close() // Persists to disk
33712
+ * ```
33713
+ *
33714
+ * @module lidCache
33715
+ * @author BuilderBot Team
33716
+ * @since 1.4.2
33717
+ */
33718
+ // =============================================================================
33719
+ // CONSTANTS
33720
+ // =============================================================================
33721
+ /** File format version for migrations */
33722
+ const CACHE_FILE_VERSION = 1;
33723
+ /** Default TTL: 7 days (seconds) */
33724
+ const DEFAULT_TTL_SECONDS = 86400 * 7;
33725
+ /** Auto-flush interval: 30 seconds (milliseconds) */
33726
+ const DEFAULT_FLUSH_INTERVAL_MS = 30000;
33727
+ /** Compact file when exceeds 10 MB */
33728
+ const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
33729
+ /** Compact when more than 10k entries */
33730
+ const COMPACT_AT_ENTRIES = 10000;
33731
+ /** Disable persistence after 10 consecutive flush failures */
33732
+ const MAX_FLUSH_FAILURES = 10;
33733
+ /** Unix file permissions: owner read/write only (0o600) */
33734
+ const FILE_PERMISSIONS = 0o600;
33735
+ /**
33736
+ * Helper to brand a string as LidJid (runtime check).
33737
+ * Returns null if the string is not a valid LID.
33738
+ */
33739
+ function asLidJid(value) {
33740
+ return isValidLid(value) ? normalizeLid(value) : null;
33741
+ }
33742
+ /**
33743
+ * Helper to brand a string as PnJid (runtime check).
33744
+ * Returns null if the string is not a valid phone number JID.
33745
+ */
33746
+ function asPnJid(value) {
33747
+ if (!isValidPn(value))
33748
+ return null;
33749
+ const normalized = normalizePn(value);
33750
+ return normalized ? normalized : null;
33751
+ }
33752
+ // =============================================================================
33753
+ // TYPE GUARDS
33754
+ // =============================================================================
33755
+ /**
33756
+ * Validates if a value is a valid LID string.
33757
+ *
33758
+ * A valid LID:
33759
+ * - Is a string
33760
+ * - Contains `@lid` suffix
33761
+ * - Has minimum length of 5 (e.g., `1@lid`)
33762
+ *
33763
+ * @param value - Value to validate
33764
+ * @returns Type predicate: true if value is a valid LID string
33765
+ *
33766
+ * @example
33767
+ * ```typescript
33768
+ * isValidLid('123456789@lid') // true
33769
+ * isValidLid('123456789:45@lid') // true
33770
+ * isValidLid('123@c.us') // false
33771
+ * isValidLid('') // false
33772
+ * isValidLid(null) // false
33773
+ * ```
33774
+ */
33775
+ function isValidLid(value) {
33776
+ return typeof value === 'string' && value.length >= 5 && value.includes('@lid');
33777
+ }
33778
+ /**
33779
+ * Validates if a value is a valid phone number string.
33780
+ *
33781
+ * Accepts:
33782
+ * - Bare digits: `1234567890`
33783
+ * - International: `+1234567890`
33784
+ * - Formatted: `+1 (555) 123-4567`
33785
+ * - WhatsApp JIDs: `123@s.whatsapp.net`, `123@c.us`
33786
+ *
33787
+ * @param value - Value to validate
33788
+ * @returns true if value contains at least one digit or is already a JID
33789
+ */
33790
+ function isValidPn(value) {
33791
+ if (typeof value !== 'string')
33792
+ return false;
33793
+ if (value.length === 0)
33794
+ return false;
33795
+ // Accept if already has WhatsApp JID format
33796
+ if (value.includes('@s.whatsapp.net') || value.includes('@c.us'))
33797
+ return true;
33798
+ // Accept if contains any digit (will be cleaned during normalization)
33799
+ const hasDigits = /\d/.test(value);
33800
+ return hasDigits;
33801
+ }
33802
+ /**
33803
+ * Validates if a value matches the CacheEntry interface.
33804
+ *
33805
+ * @internal
33806
+ */
33807
+ function isCacheEntry(value) {
33808
+ if (typeof value !== 'object' || value === null)
33809
+ return false;
33810
+ const entry = value;
33811
+ return typeof entry.pn === 'string' && typeof entry.ts === 'number' && !isNaN(entry.ts);
33812
+ }
33813
+ /**
33814
+ * Validates if a value matches the CacheFileData interface.
33815
+ *
33816
+ * @internal
33817
+ */
33818
+ function isCacheFileData(value) {
33819
+ if (typeof value !== 'object' || value === null)
33820
+ return false;
33821
+ const data = value;
33822
+ return typeof data.version === 'number' && typeof data.entries === 'object' && data.entries !== null;
33823
+ }
33824
+ // =============================================================================
33825
+ // UTILITY FUNCTIONS
33826
+ // =============================================================================
33827
+ /**
33828
+ * Normalizes a LID by removing the device suffix.
33829
+ *
33830
+ * WhatsApp sends LIDs with device identifiers (e.g., `:45`, `:99`) that vary
33831
+ * based on the user's device. The same contact will have different suffixes
33832
+ * on phone vs web vs tablet. This function strips the suffix for consistent
33833
+ * caching.
33834
+ *
33835
+ * @param lid - LID to normalize (e.g., '123456789:45@lid')
33836
+ * @returns Normalized LID (e.g., '123456789@lid'), or original if invalid
33837
+ *
33838
+ * @example
33839
+ * ```typescript
33840
+ * normalizeLid('123456789:45@lid') // '123456789@lid'
33841
+ * normalizeLid('123456789:99@lid') // '123456789@lid'
33842
+ * normalizeLid('123456789@lid') // '123456789@lid' (no change)
33843
+ * normalizeLid('123456789@c.us') // '123456789@c.us' (no change, not a LID)
33844
+ * normalizeLid('invalid') // 'invalid' (no change, invalid)
33845
+ * ```
33846
+ */
33847
+ function normalizeLid(lid) {
33848
+ if (!isValidLid(lid))
33849
+ return lid;
33850
+ // Remove :digits before @lid suffix only
33851
+ return lid.replace(/:\d+(?=@lid$)/, '');
33852
+ }
33853
+ /**
33854
+ * Sanitizes a session name for safe filesystem usage.
33855
+ *
33856
+ * Prevents path traversal and invalid filename characters.
33857
+ *
33858
+ * @internal
33859
+ * @param name - Raw session name
33860
+ * @returns Sanitized name safe for use in filenames
33861
+ */
33862
+ function sanitizeSessionName(name) {
33863
+ // Replace dangerous characters for filenames
33864
+ return name.replace(/[\\/:"*?<>|]/g, '_').replace(/\.{2,}/g, '_');
33865
+ }
33866
+ /**
33867
+ * Normalizes a phone number to consistent `@s.whatsapp.net` format.
33868
+ *
33869
+ * Handles multiple input formats commonly encountered from WhatsApp:
33870
+ * - Bare digits: `1234567890` → `1234567890@s.whatsapp.net`
33871
+ * - International: `+1234567890` → `1234567890@s.whatsapp.net`
33872
+ * - Spaced: `123 456 7890` → `1234567890@s.whatsapp.net`
33873
+ * - Formatted: `+1 (555) 123-4567` → `15551234567@s.whatsapp.net`
33874
+ * - Legacy: `123@c.us` → `123@s.whatsapp.net`
33875
+ * - Already normalized: `123@s.whatsapp.net` → `123@s.whatsapp.net` (no change)
33876
+ *
33877
+ * @param pn - Phone number in any format
33878
+ * @returns Normalized phone number, or original if cannot be normalized
33879
+ *
33880
+ * @example
33881
+ * ```typescript
33882
+ * normalizePn('34691015468') // '34691015468@s.whatsapp.net'
33883
+ * normalizePn('+34691015468') // '34691015468@s.whatsapp.net'
33884
+ * normalizePn('34 691 015 468') // '34691015468@s.whatsapp.net'
33885
+ * normalizePn('+1 (555) 123-4567') // '15551234567@s.whatsapp.net'
33886
+ * normalizePn('34691015468@c.us') // '34691015468@s.whatsapp.net'
33887
+ * normalizePn('34691015468@s.whatsapp.net') // '34691015468@s.whatsapp.net'
33888
+ * normalizePn('not-a-number') // 'not-a-number' (unchanged)
33889
+ * ```
33890
+ */
33891
+ function normalizePn(pn) {
33892
+ if (!isValidPn(pn))
33893
+ return pn;
33894
+ // Already in WhatsApp JID format
33895
+ if (pn.includes('@s.whatsapp.net'))
33896
+ return pn;
33897
+ // Legacy format conversion
33898
+ if (pn.includes('@c.us')) {
33899
+ const digits = pn.replace('@c.us', '');
33900
+ if (/^\d+$/.test(digits))
33901
+ return `${digits}@s.whatsapp.net`;
33902
+ }
33903
+ // Clean common formatting: +, spaces, dashes, dots, parentheses
33904
+ const cleaned = pn.replace(/^\+/, '').replace(/[\s\-\.\(\)]/g, '');
33905
+ // Validate we now have only digits
33906
+ if (/^\d+$/.test(cleaned)) {
33907
+ return `${cleaned}@s.whatsapp.net`;
33908
+ }
33909
+ // Cannot normalize, return original
33910
+ return pn;
33911
+ }
33912
+ /**
33913
+ * Masks a LID for safe logging (PII protection).
33914
+ *
33915
+ * Replaces middle characters with `***` to prevent logging full identifiers
33916
+ * while keeping enough for debugging.
33917
+ *
33918
+ * @internal
33919
+ * @param lid - LID to mask
33920
+ * @returns Masked LID (e.g., '123456789@lid' → '123***@lid')
33921
+ */
33922
+ function maskLid(lid) {
33923
+ if (lid.length < 8)
33924
+ return '***@lid';
33925
+ return lid.slice(0, 3) + '***' + lid.slice(lid.indexOf('@'));
33926
+ }
33927
+ // =============================================================================
33928
+ // HYBRID CACHE (Memory + File)
33929
+ // =============================================================================
33930
+ /**
33931
+ * Hybrid LID cache implementation with memory hot-path and file persistence.
33932
+ *
33933
+ * This is the recommended production implementation, providing:
33934
+ * - **Speed**: O(1) in-memory lookups via NodeCache (~1μs)
33935
+ * - **Durability**: Automatic persistence to JSON file every 30s
33936
+ * - **Resilience**: Survives process restarts, handles corrupted files gracefully
33937
+ * - **Security**: File permissions 0o600 (owner read/write only)
33938
+ *
33939
+ * ## File Location
33940
+ *
33941
+ * Files are stored at: `{cwd}/{sessionName}_sessions/lid-cache.json`
33942
+ *
33943
+ * The session name is sanitized to prevent path traversal attacks.
33944
+ *
33945
+ * ## Configuration
33946
+ *
33947
+ * | Option | Default | Description |
33948
+ * |--------|---------|-------------|
33949
+ * | `sessionName` | required | Unique name for this bot instance |
33950
+ * | `ttlSeconds` | 604800 (7 days) | Time-to-live for cache entries |
33951
+ * | `basePath` | `process.cwd()` | Directory for session files |
33952
+ * | `logger` | `console` | Logger for operational events |
33953
+ *
33954
+ * @example
33955
+ * ```typescript
33956
+ * // Basic usage
33957
+ * const cache = new HybridLidCache('my-bot')
33958
+ * await cache.ready()
33959
+ *
33960
+ * // Custom TTL and logger
33961
+ * const cache = new HybridLidCache('my-bot', 86400 * 30, undefined, winstonLogger)
33962
+ *
33963
+ * // Custom base path
33964
+ * const cache = new HybridLidCache('my-bot', 86400, '/var/lib/bot')
33965
+ * ```
33966
+ */
33967
+ class HybridLidCache {
33968
+ /**
33969
+ * Creates a new HybridLidCache instance.
33970
+ *
33971
+ * @param sessionName - Unique identifier for this cache instance (used in filename)
33972
+ * @param ttlSeconds - Time-to-live for cache entries (default: 7 days)
33973
+ * @param basePath - Base directory for session files (default: process.cwd())
33974
+ * @param logger - Logger instance for operational events (default: console)
33975
+ *
33976
+ * @throws Error if sessionName is empty or not a string
33977
+ * @throws Error if ttlSeconds is less than 60
33978
+ *
33979
+ * @remarks
33980
+ * The constructor starts async file loading and sets up periodic auto-flush.
33981
+ * Use {@link ready()} to wait for initial load completion before operations
33982
+ * that require data from previous runs.
33983
+ */
33984
+ constructor(sessionName, ttlSeconds = DEFAULT_TTL_SECONDS, basePath, logger) {
33985
+ /** True if memory has unwritten changes */
33986
+ this.dirty = false;
33987
+ /** True if flushToDisk() is currently running (prevents concurrent flushes) */
33988
+ this.flushing = false;
33989
+ /** Consecutive flush failure count (disables persistence after threshold) */
33990
+ this.consecutiveFlushFailures = 0;
33991
+ /** True after close() has been called */
33992
+ this.isClosed = false;
33993
+ // Validate required arguments
33994
+ if (!sessionName || typeof sessionName !== 'string') {
33995
+ throw new Error('sessionName is required and must be a string');
33996
+ }
33997
+ if (ttlSeconds < 60) {
33998
+ throw new Error('ttlSeconds must be at least 60 (1 minute)');
33999
+ }
34000
+ this.ttlSeconds = ttlSeconds;
34001
+ this.logger = logger || console;
34002
+ const sanitizedSession = sanitizeSessionName(sessionName);
34003
+ const cwd = basePath || process.cwd();
34004
+ this.filePath = join(cwd, `${sanitizedSession}_sessions`, 'lid-cache.json');
34005
+ // Initialize in-memory cache with TTL
34006
+ this.memory = new NodeCache({
34007
+ stdTTL: ttlSeconds,
34008
+ useClones: false, // Performance: don't clone objects
34009
+ deleteOnExpire: true, // Auto-cleanup expired entries
34010
+ });
34011
+ // Start async file load (tracked to prevent race conditions)
34012
+ this.loadPromise = this.loadFromDisk();
34013
+ // Setup periodic auto-flush (every 30s)
34014
+ this.flushInterval = setInterval(() => {
34015
+ if (!this.isClosed) {
34016
+ this.flushToDisk().catch((err) => {
34017
+ this.logger.error('[LID Cache] Periodic flush failed:', err);
34018
+ });
34019
+ }
34020
+ }, DEFAULT_FLUSH_INTERVAL_MS);
34021
+ // Unref interval so it doesn't block process exit (important for tests)
34022
+ if (this.flushInterval.unref) {
34023
+ this.flushInterval.unref();
34024
+ }
34025
+ }
34026
+ /**
34027
+ * Waits for the initial file load to complete.
34028
+ *
34029
+ * Use this method before operations that depend on data from previous runs:
34030
+ * - Before checking if a LID exists from a previous session
34031
+ * - In tests to ensure deterministic state
34032
+ * - During cache warming procedures
34033
+ *
34034
+ * @returns Promise that resolves when initial load completes
34035
+ *
34036
+ * @example
34037
+ * ```typescript
34038
+ * const cache = new HybridLidCache('my-bot')
34039
+ * await cache.ready() // Wait for any existing data to load
34040
+ * const pn = await cache.get('existing@lid') // Now safe to access
34041
+ * ```
34042
+ */
34043
+ async ready() {
34044
+ await this.loadPromise;
34045
+ }
34046
+ /**
34047
+ * Retrieves the phone number for a given LID.
34048
+ *
34049
+ * @param lid - WhatsApp Local Identifier (e.g., '123456789:45@lid')
34050
+ * @returns Phone number in format '1234567890@s.whatsapp.net', or null if:
34051
+ * - Cache is closed
34052
+ * - LID is invalid (doesn't match `*@lid` pattern)
34053
+ * - LID not found in cache
34054
+ * - Entry has expired
34055
+ */
34056
+ async get(lid) {
34057
+ if (this.isClosed)
34058
+ return null;
34059
+ if (!isValidLid(lid))
34060
+ return null;
34061
+ const normalized = normalizeLid(lid);
34062
+ const value = this.memory.get(normalized);
34063
+ if (!value)
34064
+ return null;
34065
+ // Refresh LRU timestamp (extends implicit TTL)
34066
+ this.memory.set(normalized, value);
34067
+ return value;
34068
+ }
34069
+ /**
34070
+ * Stores a LID → phone number mapping.
34071
+ *
34072
+ * Both the LID and phone number are normalized before storage:
34073
+ * - LID: Device suffix removed (`123:45@lid` → `123@lid`)
34074
+ * - PN: Formatted to `123@s.whatsapp.net`
34075
+ *
34076
+ * @param lid - WhatsApp Local Identifier
34077
+ * @param pn - Phone number in any accepted format
34078
+ * @returns Promise that resolves immediately (memory write is synchronous)
34079
+ *
34080
+ * @remarks
34081
+ * - Invalid inputs are silently rejected (no throw)
34082
+ * - The `dirty` flag is set, triggering async flush to disk within 30s
34083
+ * - If the cache is closed, this is a no-op
34084
+ */
34085
+ async set(lid, pn) {
34086
+ if (this.isClosed)
34087
+ return;
34088
+ if (!isValidLid(lid)) {
34089
+ this.logger.debug?.('[LID Cache] Rejected invalid LID:', maskLid(String(lid)));
34090
+ return;
34091
+ }
34092
+ if (!isValidPn(pn)) {
34093
+ this.logger.debug?.('[LID Cache] Rejected invalid PN:', pn);
34094
+ return;
34095
+ }
34096
+ const normalizedLid = normalizeLid(lid);
34097
+ const normalizedPn = normalizePn(pn);
34098
+ this.memory.set(normalizedLid, normalizedPn);
34099
+ this.dirty = true;
34100
+ }
34101
+ /**
34102
+ * Checks if a LID exists in the cache.
34103
+ *
34104
+ * @param lid - WhatsApp Local Identifier
34105
+ * @returns true if:
34106
+ * - LID is valid
34107
+ * - Cache is open
34108
+ * - Entry exists and hasn't expired
34109
+ *
34110
+ * @example
34111
+ * ```typescript
34112
+ * if (await cache.has('123@lid')) {
34113
+ * const pn = await cache.get('123@lid')
34114
+ * // ...
34115
+ * }
34116
+ * ```
34117
+ */
34118
+ async has(lid) {
34119
+ if (this.isClosed)
34120
+ return false;
34121
+ if (!isValidLid(lid))
34122
+ return false;
34123
+ const normalized = normalizeLid(lid);
34124
+ return this.memory.has(normalized);
34125
+ }
34126
+ /**
34127
+ * Clears all entries from the cache.
34128
+ *
34129
+ * @remarks
34130
+ * - Immediately clears memory
34131
+ * - Triggers synchronous flush to disk (empty file)
34132
+ * - Logs the operation at info level
34133
+ */
34134
+ async clear() {
34135
+ if (this.isClosed)
34136
+ return;
34137
+ this.memory.flushAll();
34138
+ this.dirty = true;
34139
+ this.logger.info?.('[LID Cache] Cache cleared');
34140
+ await this.flushToDisk();
34141
+ }
34142
+ /**
34143
+ * Closes the cache, releasing resources and triggering final persistence.
34144
+ *
34145
+ * This method:
34146
+ * 1. Stops the auto-flush interval
34147
+ * 2. Waits for any in-progress file load
34148
+ * 3. Performs a final flush to disk
34149
+ * 4. Closes the NodeCache instance
34150
+ * 5. Marks the cache as closed
34151
+ *
34152
+ * @returns Promise that resolves when cleanup is complete
34153
+ *
34154
+ * @remarks
34155
+ * - After close(), all operations return null/void
34156
+ * - Safe to call multiple times (subsequent calls are no-ops)
34157
+ * - Flush errors are logged but don't throw
34158
+ */
34159
+ async close() {
34160
+ if (this.isClosed)
34161
+ return;
34162
+ if (this.flushInterval) {
34163
+ clearInterval(this.flushInterval);
34164
+ this.flushInterval = undefined;
34165
+ }
34166
+ // Wait for initial load to complete if still in progress
34167
+ try {
34168
+ await this.loadPromise;
34169
+ }
34170
+ catch {
34171
+ // Ignore load errors during close
34172
+ }
34173
+ // Final flush attempt
34174
+ try {
34175
+ await this.flushToDisk();
34176
+ }
34177
+ catch (err) {
34178
+ this.logger.error('[LID Cache] Final flush failed on close:', err);
34179
+ }
34180
+ this.isClosed = true;
34181
+ this.memory.close();
34182
+ this.logger.info?.('[LID Cache] Closed');
34183
+ }
34184
+ /**
34185
+ * Forces compaction of the cache file by rewriting it with only valid entries.
34186
+ *
34187
+ * This removes any expired entries that might still be in the file
34188
+ * (since NodeCache auto-expiry only removes from memory, not disk).
34189
+ *
34190
+ * @returns Promise that resolves when compaction completes
34191
+ *
34192
+ * @remarks
34193
+ * - If the cache is empty, the file is deleted instead
34194
+ * - Automatically called when file exceeds 10MB or 10k entries
34195
+ */
34196
+ async compact() {
34197
+ if (this.isClosed)
34198
+ return;
34199
+ const keys = this.memory.keys();
34200
+ if (keys.length === 0) {
34201
+ // Empty cache - delete file
34202
+ try {
34203
+ await unlink(this.filePath);
34204
+ this.dirty = false;
34205
+ this.logger.info?.('[LID Cache] Empty cache file removed');
34206
+ }
34207
+ catch {
34208
+ // File may not exist
34209
+ }
34210
+ return;
34211
+ }
34212
+ // Force full rewrite
34213
+ this.dirty = true;
34214
+ await this.flushToDisk();
34215
+ this.logger.info?.('[LID Cache] Compacted', { entries: keys.length });
34216
+ }
34217
+ /**
34218
+ * Persists the current cache state to disk.
34219
+ *
34220
+ * @internal
34221
+ * @remarks
34222
+ * - Concurrent calls are deduplicated (only one flush runs at a time)
34223
+ * - No-op if nothing has changed since last flush (`dirty` flag check)
34224
+ * - File is written with 0o600 permissions (owner read/write only)
34225
+ * - After 10 consecutive failures, persistence is disabled
34226
+ */
34227
+ async flushToDisk() {
34228
+ // Prevent concurrent flushes
34229
+ if (this.flushing)
34230
+ return;
34231
+ if (!this.dirty)
34232
+ return;
34233
+ this.flushing = true;
34234
+ try {
34235
+ await mkdir(dirname(this.filePath), { recursive: true });
34236
+ const keys = this.memory.keys();
34237
+ // Compact if entry count exceeds threshold
34238
+ if (keys.length > COMPACT_AT_ENTRIES) {
34239
+ await this.compact();
34240
+ }
34241
+ const entries = {};
34242
+ const now = Date.now();
34243
+ for (const key of keys) {
34244
+ const value = this.memory.get(key);
34245
+ if (!value)
34246
+ continue;
34247
+ entries[key] = {
34248
+ pn: value,
34249
+ ts: now,
34250
+ };
34251
+ }
34252
+ const data = {
34253
+ version: CACHE_FILE_VERSION,
34254
+ entries,
34255
+ };
34256
+ // Write with secure permissions
34257
+ await writeFile(this.filePath, JSON.stringify(data, null, 2), {
34258
+ encoding: 'utf-8',
34259
+ mode: FILE_PERMISSIONS,
34260
+ });
34261
+ this.dirty = false;
34262
+ this.consecutiveFlushFailures = 0; // Reset on success
34263
+ // Check file size and compact if needed
34264
+ await this.checkAndCompactIfNeeded();
34265
+ }
34266
+ catch (err) {
34267
+ this.consecutiveFlushFailures++;
34268
+ if (this.consecutiveFlushFailures >= MAX_FLUSH_FAILURES) {
34269
+ this.logger.error(`[LID Cache] Flush failed ${MAX_FLUSH_FAILURES} times, disabling persistence. ` +
34270
+ 'Cache will work in-memory only until restart.', { error: err, filePath: this.filePath });
34271
+ this.dirty = false; // Stop trying
34272
+ }
34273
+ else {
34274
+ this.logger.warn(`[LID Cache] Flush failed (${this.consecutiveFlushFailures}/${MAX_FLUSH_FAILURES}):`, err);
34275
+ }
34276
+ throw err; // Re-throw for caller awareness
34277
+ }
34278
+ finally {
34279
+ this.flushing = false;
34280
+ }
34281
+ }
34282
+ /**
34283
+ * Checks file size and triggers compaction if exceeds threshold.
34284
+ *
34285
+ * @internal
34286
+ */
34287
+ async checkAndCompactIfNeeded() {
34288
+ try {
34289
+ const stats = await stat$2(this.filePath);
34290
+ if (stats.size > MAX_FILE_SIZE_BYTES) {
34291
+ this.logger.warn(`[LID Cache] File size ${stats.size} bytes exceeds threshold, compacting...`);
34292
+ await this.compact();
34293
+ }
34294
+ }
34295
+ catch {
34296
+ // File may not exist
34297
+ }
34298
+ }
34299
+ /**
34300
+ * Loads cache data from disk on startup.
34301
+ *
34302
+ * @internal
34303
+ * @remarks
34304
+ * - Silently handles missing file (first run)
34305
+ * - Automatically removes and recovers from corrupted files
34306
+ * - Validates TTL on each entry (entries older than TTL are skipped)
34307
+ */
34308
+ async loadFromDisk() {
34309
+ try {
34310
+ await access(this.filePath);
34311
+ }
34312
+ catch {
34313
+ // File doesn't exist - clean start
34314
+ return;
34315
+ }
34316
+ let data;
34317
+ try {
34318
+ const raw = await readFile(this.filePath, 'utf-8');
34319
+ data = JSON.parse(raw);
34320
+ }
34321
+ catch (err) {
34322
+ // Corrupted file - remove and start fresh
34323
+ try {
34324
+ await unlink(this.filePath);
34325
+ }
34326
+ catch {
34327
+ // ignore unlink errors
34328
+ }
34329
+ this.logger.error('[LID Cache] Corrupted cache file removed, starting fresh:', err);
34330
+ return;
34331
+ }
34332
+ // Validate file structure
34333
+ if (!isCacheFileData(data)) {
34334
+ this.logger.warn('[LID Cache] Invalid cache file format, starting fresh');
34335
+ return;
34336
+ }
34337
+ // Load valid, non-expired entries
34338
+ let loadedCount = 0;
34339
+ let expiredCount = 0;
34340
+ const now = Date.now();
34341
+ const ttlMs = this.ttlSeconds * 1000;
34342
+ for (const [key, entry] of Object.entries(data.entries)) {
34343
+ if (!isCacheEntry(entry))
34344
+ continue;
34345
+ // Check TTL from stored timestamp
34346
+ const age = now - entry.ts;
34347
+ if (age >= ttlMs) {
34348
+ expiredCount++;
34349
+ continue;
34350
+ }
34351
+ this.memory.set(key, entry.pn);
34352
+ loadedCount++;
34353
+ }
34354
+ if (loadedCount > 0 || expiredCount > 0) {
34355
+ this.logger.info('[LID Cache] Loaded entries from disk', {
34356
+ valid: loadedCount,
34357
+ expired: expiredCount,
34358
+ file: this.filePath,
34359
+ });
34360
+ }
34361
+ }
34362
+ /**
34363
+ * Returns cache statistics for monitoring.
34364
+ *
34365
+ * @returns Object with:
34366
+ * - `keys`: Number of entries currently in memory
34367
+ * - `hits`: Cache hit count (from NodeCache stats)
34368
+ * - `misses`: Cache miss count (from NodeCache stats)
34369
+ *
34370
+ * @example
34371
+ * ```typescript
34372
+ * const stats = cache.getStats()
34373
+ * console.log(`Cache: ${stats.keys} entries, ${stats.hits} hits, ${stats.misses} misses`)
34374
+ * // → Cache: 1523 entries, 4500 hits, 123 misses
34375
+ * ```
34376
+ */
34377
+ getStats() {
34378
+ return {
34379
+ keys: this.memory.keys().length,
34380
+ hits: this.memory.getStats()?.hits || 0,
34381
+ misses: this.memory.getStats()?.misses || 0,
34382
+ };
34383
+ }
34384
+ }
34385
+ // =============================================================================
34386
+ // MEMORY-ONLY CACHE (Testing)
34387
+ // =============================================================================
34388
+ /**
34389
+ * Memory-only LID cache implementation for testing.
34390
+ *
34391
+ * This implementation provides the same {@link LidCache} interface but without
34392
+ * file persistence. Useful for:
34393
+ * - Unit tests (no file I/O, no cleanup needed)
34394
+ * - Ephemeral caches that don't need durability
34395
+ * - Reducing disk wear in high-frequency test scenarios
34396
+ *
34397
+ * ## Differences from HybridLidCache
34398
+ *
34399
+ * | Feature | MemoryLidCache | HybridLidCache |
34400
+ * |---------|---------------|----------------|
34401
+ * | Persistence | ❌ None | ✅ JSON file |
34402
+ * | `ready()` | Optional | Recommended |
34403
+ * | `close()` | No-op | Flushes to disk |
34404
+ * | `compact()` | No-op | Rewrites file |
34405
+ * | Cross-restart | Data lost | Data preserved |
34406
+ *
34407
+ * @example
34408
+ * ```typescript
34409
+ * // Testing scenario
34410
+ * const cache = new MemoryLidCache(3600) // 1 hour TTL
34411
+ * await cache.set('123@lid', '456@s.whatsapp.net')
34412
+ * expect(await cache.get('123@lid')).toBe('456@s.whatsapp.net')
34413
+ * // No cleanup needed - data is ephemeral
34414
+ * ```
34415
+ */
34416
+ class MemoryLidCache {
34417
+ /**
34418
+ * Creates a new MemoryLidCache instance.
34419
+ *
34420
+ * @param ttlSeconds - Time-to-live for cache entries (default: 7 days)
34421
+ */
34422
+ constructor(ttlSeconds = DEFAULT_TTL_SECONDS) {
34423
+ this.memory = new NodeCache({
34424
+ stdTTL: ttlSeconds,
34425
+ useClones: false,
34426
+ });
34427
+ }
34428
+ /**
34429
+ * Retrieves the phone number for a given LID.
34430
+ *
34431
+ * @param lid - WhatsApp Local Identifier
34432
+ * @returns Phone number or null if not found/invalid
34433
+ */
34434
+ async get(lid) {
34435
+ if (!isValidLid(lid))
34436
+ return null;
34437
+ const normalized = normalizeLid(lid);
34438
+ return this.memory.get(normalized) || null;
34439
+ }
34440
+ /**
34441
+ * Stores a LID → phone number mapping.
34442
+ *
34443
+ * @param lid - WhatsApp Local Identifier
34444
+ * @param pn - Phone number in any format
34445
+ */
34446
+ async set(lid, pn) {
34447
+ if (!isValidLid(lid))
34448
+ return;
34449
+ if (!isValidPn(pn))
34450
+ return;
34451
+ const normalized = normalizeLid(lid);
34452
+ this.memory.set(normalized, pn);
34453
+ }
34454
+ /**
34455
+ * Checks if a LID exists in the cache.
34456
+ *
34457
+ * @param lid - WhatsApp Local Identifier
34458
+ * @returns true if entry exists and hasn't expired
34459
+ */
34460
+ async has(lid) {
34461
+ if (!isValidLid(lid))
34462
+ return false;
34463
+ const normalized = normalizeLid(lid);
34464
+ return this.memory.has(normalized);
34465
+ }
34466
+ /**
34467
+ * Clears all entries from the cache.
34468
+ */
34469
+ async clear() {
34470
+ this.memory.flushAll();
34471
+ }
34472
+ /**
34473
+ * Closes the cache. For MemoryLidCache, this is a no-op.
34474
+ *
34475
+ * @remarks The NodeCache instance is not closed to allow continued testing.
34476
+ * If you need to free memory, use `clear()` before `close()`.
34477
+ */
34478
+ async close() {
34479
+ // No-op for memory cache - data is already ephemeral
34480
+ }
34481
+ }
34482
+ /**
34483
+ * Factory function to create a LidCache instance based on configuration.
34484
+ *
34485
+ * This factory centralizes cache creation logic, making it reusable across
34486
+ * the codebase and easier to test/maintain.
34487
+ *
34488
+ * @param options - Factory configuration options
34489
+ * @returns Configured LidCache instance
34490
+ *
34491
+ * @example
34492
+ * ```typescript
34493
+ * // Default: HybridLidCache (file + memory)
34494
+ * const cache = createLidCache({ sessionName: 'my-bot' })
34495
+ *
34496
+ * // Memory-only (testing)
34497
+ * const cache = createLidCache({ strategy: 'memory', ttlSeconds: 3600 })
34498
+ *
34499
+ * // Custom implementation
34500
+ * const cache = createLidCache({ strategy: new RedisLidCache() })
34501
+ * ```
34502
+ */
34503
+ function createLidCache(options = {}) {
34504
+ const { strategy = 'file', sessionName = 'default', ttlSeconds = DEFAULT_TTL_SECONDS, basePath, logger } = options;
34505
+ // If custom instance passed, use it directly
34506
+ if (strategy && typeof strategy === 'object') {
34507
+ return strategy;
34508
+ }
34509
+ // Memory-only strategy
34510
+ if (strategy === 'memory') {
34511
+ return new MemoryLidCache(ttlSeconds);
34512
+ }
34513
+ // Default: Hybrid (file + memory)
34514
+ return new HybridLidCache(sessionName, ttlSeconds, basePath, logger);
34515
+ }
34516
+ /**
34517
+ * Type guard to check if a value is a valid MessageContext.
34518
+ *
34519
+ * @param value - Value to check
34520
+ * @returns true if the value matches MessageContext structure
34521
+ */
34522
+ function isMessageContext(value) {
34523
+ if (typeof value !== 'object' || value === null)
34524
+ return false;
34525
+ const ctx = value;
34526
+ // If key exists, it must be an object (MessageContextKey)
34527
+ if ('key' in ctx && ctx.key !== undefined) {
34528
+ if (typeof ctx.key !== 'object' || ctx.key === null)
34529
+ return false;
34530
+ }
34531
+ return true;
34532
+ }
34533
+ /**
34534
+ * Extracts and caches LID → PN mapping from an incoming message.
34535
+ *
34536
+ * This utility function inspects the message context to find LID/PN pairs
34537
+ * and stores them in the provided cache for future lookups.
34538
+ *
34539
+ * @param cache - LidCache instance to store the mapping
34540
+ * @param messageCtx - Baileys message context (WAMessage)
34541
+ * @returns Promise that resolves when caching is complete (or silently fails)
34542
+ *
34543
+ * @example
34544
+ * ```typescript
34545
+ * // In message handler:
34546
+ * for (const message of messages) {
34547
+ * await extractAndCacheLidFromMessage(lidCache, message)
34548
+ * }
34549
+ * ```
34550
+ */
34551
+ async function extractAndCacheLidFromMessage(cache, messageCtx) {
34552
+ try {
34553
+ const key = messageCtx?.key;
34554
+ if (!key)
34555
+ return;
34556
+ const isGroup = key.remoteJid?.includes('@g.us');
34557
+ if (isGroup) {
34558
+ // Groups: participant has the LID, participantAlt has the PN
34559
+ if (key.participant?.includes('@lid') && key.participantAlt) {
34560
+ await cache.set(key.participant, key.participantAlt);
34561
+ }
34562
+ }
34563
+ else {
34564
+ // DMs: remoteJid has the LID
34565
+ if (key.remoteJid?.includes('@lid')) {
34566
+ // Priority: remoteJidAlt > senderPn > participantPn
34567
+ const pn = key.remoteJidAlt || key.senderPn || key.participantPn;
34568
+ if (pn) {
34569
+ await cache.set(key.remoteJid, pn);
34570
+ }
34571
+ }
34572
+ }
34573
+ }
34574
+ catch {
34575
+ // Silent failure - don't block message processing
34576
+ }
34577
+ }
34578
+ /**
34579
+ * Resolves a LID to a phone number using cache-first strategy.
34580
+ *
34581
+ * This function implements the resolution chain:
34582
+ * 1. Check cache first (O(1), ~1μs)
34583
+ * 2. If miss, call fallback resolver (e.g., Baileys lidMapping)
34584
+ * 3. If resolved, store in cache for future lookups
34585
+ *
34586
+ * @param cache - LidCache instance for storing/retrieving mappings
34587
+ * @param fallbackResolver - Async function to resolve LID when not in cache
34588
+ * @param logger - Logger for operational events (optional)
34589
+ * @param lid - WhatsApp Local Identifier to resolve
34590
+ * @returns Phone number in format '1234567890@s.whatsapp.net', or null
34591
+ *
34592
+ * @example
34593
+ * ```typescript
34594
+ * const pn = await resolveLidToPn(
34595
+ * lidCache,
34596
+ * (lid) => baileysSignalRepo.lidMapping.getPNForLID(lid),
34597
+ * console,
34598
+ * '123456789@lid' as LidJid
34599
+ * )
34600
+ * ```
34601
+ */
34602
+ async function resolveLidToPn(cache, fallbackResolver, logger, lid) {
34603
+ try {
34604
+ // Validate/normalize the LID
34605
+ const normalizedLid = asLidJid(typeof lid === 'string' ? lid : lid);
34606
+ if (!normalizedLid) {
34607
+ logger?.error?.('[LID Cache] Invalid LID format:', lid);
34608
+ return null;
34609
+ }
34610
+ // 1. Check cache first (fast, O(1))
34611
+ const cached = await cache.get(normalizedLid);
34612
+ if (cached) {
34613
+ logger?.log?.(`[LID Cache] Hit: ${normalizedLid} -> ${cached}`);
34614
+ return cached;
34615
+ }
34616
+ // 2. Fallback to provided resolver
34617
+ const resolved = await fallbackResolver(normalizedLid);
34618
+ if (resolved) {
34619
+ // Validate the resolved value is a valid PN
34620
+ const normalizedPn = asPnJid(resolved);
34621
+ if (normalizedPn) {
34622
+ // Store in cache for next time
34623
+ await cache.set(normalizedLid, normalizedPn);
34624
+ logger?.log?.(`[LID Cache] Resolved: ${normalizedLid} -> ${normalizedPn}`);
34625
+ return normalizedPn;
34626
+ }
34627
+ }
34628
+ return null;
34629
+ }
34630
+ catch (e) {
34631
+ logger?.error?.('[LID Cache] Error resolving LID:', e);
34632
+ return null;
34633
+ }
34634
+ }
34635
+
33679
34636
  const keepFiles = ['creds.json', 'baileys_store.json', 'app-state-sync', 'session'];
33680
34637
  /**
33681
34638
  * @alpha
@@ -33948,6 +34905,8 @@ class BaileysProvider extends ProviderClass {
33948
34905
  if (messageCtx?.key?.id && messageCtx?.message) {
33949
34906
  this.messageCache?.set(`msg:${messageCtx.key.id}`, messageCtx.message);
33950
34907
  }
34908
+ // Aprender mapeo LID→PN desde mensaje entrante (async, no bloqueante)
34909
+ this.cacheLidFromMessage(messageCtx).catch(() => { });
33951
34910
  if (messageCtx?.messageStubParameters?.length &&
33952
34911
  messageCtx.messageStubParameters[0].includes('absent'))
33953
34912
  continue;
@@ -34197,17 +35156,18 @@ class BaileysProvider extends ProviderClass {
34197
35156
  }
34198
35157
  };
34199
35158
  /**
34200
- * Obtener número de teléfono (PN) para un LID (Local Identifier)
35159
+ * Obtener número de teléfono (PN) para un LID (Local Identifier).
35160
+ * Delegates to the standalone utility with Baileys lidMapping as fallback.
35161
+ *
34201
35162
  * @param lid - JID con formato '16424005304394@lid'
35163
+ * @returns Phone number en formato '1234567890@s.whatsapp.net', o null si no se resuelve
34202
35164
  */
34203
35165
  this.getPNForLID = async (lid) => {
34204
- try {
34205
- return (await this.lidMapping?.getPNForLID?.(lid)) ?? null;
34206
- }
34207
- catch (e) {
34208
- this.logger.log(`[${new Date().toISOString()}] Error getting PN for LID:`, e);
35166
+ // Normalize to branded type if valid
35167
+ const lidJid = typeof lid === 'string' ? asLidJid(lid) : lid;
35168
+ if (!lidJid)
34209
35169
  return null;
34210
- }
35170
+ return resolveLidToPn(this.lidCache, (id) => this.lidMapping?.getPNForLID?.(id) ?? Promise.resolve(null), this.logger, lidJid);
34211
35171
  };
34212
35172
  /**
34213
35173
  * Normaliza un número entrante a un JID válido para envío.
@@ -34492,6 +35452,8 @@ class BaileysProvider extends ProviderClass {
34492
35452
  errorOnMissing: false,
34493
35453
  });
34494
35454
  this.globalVendorArgs = { ...this.globalVendorArgs, ...args };
35455
+ // Initialize LID cache (hybrid file+memory or memory-only based on config)
35456
+ this.lidCache = this.initializeLidCache();
34495
35457
  this.setupCleanupHandlers();
34496
35458
  this.setupPeriodicCleanup();
34497
35459
  }
@@ -34559,6 +35521,12 @@ class BaileysProvider extends ProviderClass {
34559
35521
  this.messageCache.close();
34560
35522
  this.messageCache = undefined;
34561
35523
  }
35524
+ // Cerrar LID cache (flush final a disco)
35525
+ if (this.lidCache?.close) {
35526
+ this.lidCache.close().catch((err) => {
35527
+ this.logger.error(`[${new Date().toISOString()}] Error closing LID cache:`, err);
35528
+ });
35529
+ }
34562
35530
  this.mapSet.clear();
34563
35531
  this.idsDuplicates.length = 0;
34564
35532
  if (this.logStream && typeof this.logStream.end === 'function') {
@@ -34584,6 +35552,34 @@ class BaileysProvider extends ProviderClass {
34584
35552
  .get('/', this.indexHome);
34585
35553
  }
34586
35554
  afterHttpServerInit() { }
35555
+ // =============================================================================
35556
+ // LID CACHE INTEGRATION
35557
+ // =============================================================================
35558
+ /**
35559
+ * Inicializa el caché LID/PN usando el factory.
35560
+ * @returns Instancia de LidCache configurada según globalVendorArgs
35561
+ */
35562
+ initializeLidCache() {
35563
+ return createLidCache({
35564
+ strategy: this.globalVendorArgs.lidCache,
35565
+ sessionName: this.globalVendorArgs.name,
35566
+ ttlSeconds: this.globalVendorArgs.lidCacheTtl,
35567
+ logger: this.logger,
35568
+ });
35569
+ }
35570
+ /**
35571
+ * Delegates to the standalone utility for caching LID→PN from messages.
35572
+ * This wrapper maintains the method signature for internal use while
35573
+ * leveraging the exported function for reusability.
35574
+ */
35575
+ async cacheLidFromMessage(messageCtx) {
35576
+ // Type guard: ensure the message context has the expected structure
35577
+ if (!isMessageContext(messageCtx)) {
35578
+ this.logger.debug?.('Invalid message context for LID caching');
35579
+ return;
35580
+ }
35581
+ return extractAndCacheLidFromMessage(this.lidCache, messageCtx);
35582
+ }
34587
35583
  /**
34588
35584
  * Accede al lidMapping del signalRepository de Baileys.
34589
35585
  */
@@ -34642,4 +35638,4 @@ class BaileysProvider extends ProviderClass {
34642
35638
  }
34643
35639
  }
34644
35640
 
34645
- export { BaileysProvider, baileyCleanNumber };
35641
+ export { BaileysProvider, HybridLidCache, MemoryLidCache, asLidJid, asPnJid, baileyCleanNumber, createLidCache, extractAndCacheLidFromMessage, isMessageContext, normalizeLid, resolveLidToPn };