whalibmob 5.29.6 → 5.30.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/README.md CHANGED
@@ -1845,6 +1845,9 @@ Both are optional and both keep working when omitted — you get an error naming
1845
1845
  > [!NOTE]
1846
1846
  > Registration reports the screens it passes through to WhatsApp's `/client_log`, the way the phone clients do — a client that registers in total silence does something no real installation does. It is fire-and-forget and every failure is swallowed, so it can never take a registration down. Set `WA_FUNNEL_LOG=0` to send none of it.
1847
1847
 
1848
+ > [!NOTE]
1849
+ > Those events carry timestamps, and registration waits between them the way a person would: a few seconds to type the number in, a moment on the confirmation sheet, longer before asking again after a refusal. Without the waits the whole funnel leaves inside one millisecond, which no handset does. It adds a handful of seconds to a registration. Set `WA_REG_PACING=0` to remove them.
1850
+
1848
1851
  ### Registering as Android
1849
1852
 
1850
1853
  **There is nothing to do first.** Name the platform and register:
@@ -1339,10 +1339,17 @@ function getAppPid(store) {
1339
1339
  return store._appPid;
1340
1340
  }
1341
1341
 
1342
- function buildClientMetrics(attempt) {
1342
+ // is_sim_absent used to be the constant false while the same request could send
1343
+ // sim_mcc/sim_mnc as 000/000 — which is precisely what a handset reports when
1344
+ // there is no SIM in it. One request then said both "a SIM is present" and "no
1345
+ // operator", which no real handset ever says. Reading it off the operator the
1346
+ // request actually declares keeps the two halves telling one story.
1347
+ function buildClientMetrics(attempt, meta) {
1348
+ const mcc = meta && meta.mcc != null ? String(meta.mcc).trim() : '';
1349
+ const simAbsent = !/^\d+$/.test(mcc) || /^0+$/.test(mcc);
1343
1350
  const json = '{"attempts":' + (attempt || 1)
1344
1351
  + ',"app_campaign_download_source":"google-play|unknown"'
1345
- + ',"is_sim_absent":false}';
1352
+ + ',"is_sim_absent":' + (simAbsent ? 'true' : 'false') + '}';
1346
1353
  return encodeURIComponent(json);
1347
1354
  }
1348
1355
 
@@ -1383,7 +1390,7 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
1383
1390
  'prefer_sms_over_flash', wantsFlash ? 'false' : 'true',
1384
1391
  'simnum', '0',
1385
1392
  'airplane_mode_type', '0',
1386
- 'client_metrics', buildClientMetrics(attempt),
1393
+ 'client_metrics', buildClientMetrics(attempt, meta),
1387
1394
  'mistyped', '7',
1388
1395
  'advertising_id', store.advertisingId || '',
1389
1396
  'hasinrc', '1',
@@ -1641,6 +1648,45 @@ async function sendEncrypted(path, plaintext, store, waVersion) {
1641
1648
  return result;
1642
1649
  }
1643
1650
 
1651
+ // ---------- Pacing ----------
1652
+ //
1653
+ // The funnel events say a person walked through the screens. The timestamps on
1654
+ // them said otherwise: session_start, the number lookup and the code request
1655
+ // left within a few milliseconds of each other, because nothing in between was
1656
+ // waiting on a person. Nobody opens WhatsApp and has a phone number typed,
1657
+ // checked and submitted inside one millisecond, and that gap is visible to the
1658
+ // server on every event it receives.
1659
+ //
1660
+ // So the waits a person actually causes are put back. Each one is a range
1661
+ // rather than a number — a fixed delay is its own signature — and each is
1662
+ // named after the thing being waited for:
1663
+ //
1664
+ // enter_number typing the number in before the lookup fires
1665
+ // confirm_number the "is this your number?" sheet, and tapping through it
1666
+ // retry_code after a refused request, before asking again
1667
+ // switch_method picking a different delivery method out of the list
1668
+ //
1669
+ // Set WA_REG_PACING=0 to drop all of it, for anything that wants the request
1670
+ // shapes without the waiting.
1671
+ const PACING_RANGES_MS = {
1672
+ enter_number: [1800, 5200],
1673
+ confirm_number: [1200, 3600],
1674
+ retry_code: [2800, 7500],
1675
+ switch_method: [2000, 5000]
1676
+ };
1677
+
1678
+ function pacingEnabled() {
1679
+ return process.env.WA_REG_PACING !== '0';
1680
+ }
1681
+
1682
+ function humanPause(kind) {
1683
+ const range = PACING_RANGES_MS[kind];
1684
+ if (!range || !pacingEnabled()) return Promise.resolve();
1685
+ const ms = range[0] + Math.floor(Math.random() * (range[1] - range[0] + 1));
1686
+ _whaDbg('[DBG] REG pacing ' + kind + ' ' + ms + 'ms');
1687
+ return new Promise((resolve) => setTimeout(resolve, ms));
1688
+ }
1689
+
1644
1690
  // ---------- Funnel telemetry ----------
1645
1691
  //
1646
1692
  // The native client reports every screen it moves through — the number entry,
@@ -2042,10 +2088,13 @@ async function checkNumberStatus(phoneNumber) {
2042
2088
  async function assertRegistrationKeys(store, waVersion) {
2043
2089
  // The session-start event the native client fires before it knows the number.
2044
2090
  await sendPrePnFunnelLog(store, waVersion, 'session_start', 'registration_session_start');
2091
+ // The number gets typed between those two events.
2092
+ await humanPause('enter_number');
2045
2093
  await sendFunnelLog(store, waVersion, 'enter_number', 'exist_check', 'exist_attempt');
2046
2094
 
2047
2095
  for (let attempt = 0; attempt < 2; attempt++) {
2048
2096
  try {
2097
+ if (attempt > 0) await humanPause('retry_code');
2049
2098
  logDeviceIdentity('before /exist', store, waVersion);
2050
2099
  const result = await sendRequest('/exist', store, waVersion, false, null);
2051
2100
  // reason === 'incorrect' → keys not found → fresh
@@ -2140,6 +2189,8 @@ async function requestSmsCode(store, method, opts) {
2140
2189
  // 7. Unknown error first time → retry once
2141
2190
  let lastReason = null;
2142
2191
  let attemptNum = 1;
2192
+ // The confirmation sheet the app puts up before it will ask for a code.
2193
+ await humanPause('confirm_number');
2143
2194
  while (true) {
2144
2195
  // Rebuilt per attempt so client_metrics carries the current attempt count.
2145
2196
  const extra = getRequestVerificationCodeParameters(store, m, _regMeta, _device, attemptNum);
@@ -2204,6 +2255,9 @@ async function requestSmsCode(store, method, opts) {
2204
2255
  if (attemptNum > MAX_CODE_REQUEST_ATTEMPTS) {
2205
2256
  throw new Error(`Registration error (${m}): giving up after ${MAX_CODE_REQUEST_ATTEMPTS} attempts — raw: ${JSON.stringify(result)}`);
2206
2257
  }
2258
+
2259
+ // Nobody re-taps the button the instant the error lands.
2260
+ await humanPause('retry_code');
2207
2261
  }
2208
2262
  }
2209
2263
 
@@ -2214,6 +2268,8 @@ async function requestSmsCode(store, method, opts) {
2214
2268
  if (result && result._noRoutes && !autoFallbackDone && method !== 'email') {
2215
2269
  autoFallbackDone = true;
2216
2270
  process.stderr.write(`[REG] ${method} returned no_routes — auto-trying ${fallbackMethod}\n`);
2271
+ // Reading the refusal and picking another method off the list.
2272
+ await humanPause('switch_method');
2217
2273
  result = await _tryMethod(fallbackMethod);
2218
2274
  }
2219
2275
 
@@ -2373,3 +2429,6 @@ module.exports._verify = {
2373
2429
  flashCodeFromCallerId, flashCodeLength, codeForSubmission,
2374
2430
  getRequestVerificationCodeParameters
2375
2431
  };
2432
+
2433
+ // Pacing internals, exposed for tests. Not part of the public API.
2434
+ module.exports._pacing = { pacingEnabled, humanPause, buildClientMetrics, PACING_RANGES_MS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.29.6",
3
+ "version": "5.30.0",
4
4
  "description": "Node.js library for WhatsApp — register a number over SMS, or link as a companion by QR. Signal E2E encryption, media, groups, channels.",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",