<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rajat Ghate</title>
    <description>The latest articles on DEV Community by Rajat Ghate (@rajat_ghate).</description>
    <link>https://dev.to/rajat_ghate</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4124094%2Fc3384231-4097-47a2-8bd3-92424dfcaef0.png</url>
      <title>DEV Community: Rajat Ghate</title>
      <link>https://dev.to/rajat_ghate</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rajat_ghate"/>
    <language>en</language>
    <item>
      <title>WhatsApp's LID migration broke my bot's @mention detection</title>
      <dc:creator>Rajat Ghate</dc:creator>
      <pubDate>Sat, 17 Oct 2026 10:29:00 +0000</pubDate>
      <link>https://dev.to/rajat_ghate/whatsapps-lid-migration-broke-my-bots-mention-detection-3n04</link>
      <guid>https://dev.to/rajat_ghate/whatsapps-lid-migration-broke-my-bots-mention-detection-3n04</guid>
      <description>&lt;p&gt;I built a WhatsApp group bot that only answers when it is &lt;code&gt;@mentioned&lt;/code&gt;. It worked&lt;br&gt;
perfectly in my test group. Then I added it to a second group and it went&lt;br&gt;
completely silent — no error, no warning, no failed assertion. It received every&lt;br&gt;
message and replied to none of them.&lt;/p&gt;

&lt;p&gt;The cause is a migration WhatsApp has been rolling out through 2025 and 2026, and&lt;br&gt;
the thing that makes it painful is that &lt;strong&gt;the broken version works on your machine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqo81u77dxsqgx7853wpx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqo81u77dxsqgx7853wpx.png" alt="One person, two WhatsApp IDs: the same user identified by a phone-number JID and by a LID" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;Historically every WhatsApp participant was identified by a JID — a Jabber ID —&lt;br&gt;
which embedded their phone number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;919876543210@s.whatsapp.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WhatsApp is now also issuing a &lt;strong&gt;LID&lt;/strong&gt; (commonly expanded as "Link ID"; you will&lt;br&gt;
also see "linked identity" and "linked device identifier" — the naming is not&lt;br&gt;
consistent even in WhatsApp's own ecosystem). It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;275551234567890@lid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A LID is assigned per account, not per group, and it is stable: the same person&lt;br&gt;
has the same LID everywhere. It exists so that phone numbers can be hidden —&lt;br&gt;
in Communities, in groups where an admin has enabled &lt;strong&gt;Hide phone numbers&lt;/strong&gt;, and&lt;br&gt;
increasingly by default as WhatsApp moves away from phone numbers as the visible&lt;br&gt;
identifier.&lt;/p&gt;

&lt;p&gt;For a bot author, the consequence is small to describe and large to deal with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The same person, in the same group, can reach your code as either string — and you do not get to choose which one you receive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Why mention detection breaks silently
&lt;/h2&gt;

&lt;p&gt;Almost every WhatsApp bot tutorial contains some version of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mentions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;extendedTextMessage&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;contextInfo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;mentionedJid&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mentions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a string comparison between two strings that are both correct and are&lt;br&gt;
never equal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu00pl5wadi6z73amq1dt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu00pl5wadi6z73amq1dt.png" alt="Comparing a LID against a JID with === returns false, even though both belong to the same person" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is no exception to catch. The mention array is populated, your identity is&lt;br&gt;
populated, the comparison returns &lt;code&gt;false&lt;/code&gt;, and the bot concludes it was not&lt;br&gt;
addressed. In the logs this is indistinguishable from an ordinary message that&lt;br&gt;
nobody mentioned the bot in.&lt;/p&gt;

&lt;p&gt;It also has an unusually cruel failure profile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your test group probably shows phone numbers&lt;/strong&gt;, so everything arrives as a
JID and the bug never appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is intermittent across groups&lt;/strong&gt;, because the privacy setting is per group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can change under you&lt;/strong&gt;, when an admin enables number hiding on a group that
worked yesterday.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two open Baileys issues track the surrounding mess — &lt;a href="https://github.com/WhiskeySockets/Baileys/issues/1718" rel="noopener noreferrer"&gt;#1718&lt;/a&gt;&lt;br&gt;
on &lt;code&gt;@lid&lt;/code&gt; not resolving to a real number, and &lt;a href="https://github.com/openclaw/openclaw/issues/23029" rel="noopener noreferrer"&gt;openclaw #23029&lt;/a&gt;&lt;br&gt;
on exactly this mention-detection failure for reply-to-bot.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix that looks right and isn't
&lt;/h2&gt;

&lt;p&gt;The instinct is to normalise &lt;em&gt;inward&lt;/em&gt;: take the LID, resolve it to a phone number,&lt;br&gt;
compare phone numbers. Several forks and wrapper packages offer a &lt;code&gt;lidToJid()&lt;/code&gt; of&lt;br&gt;
some kind.&lt;/p&gt;

&lt;p&gt;I would avoid building on that, for two reasons.&lt;/p&gt;

&lt;p&gt;**You would be depending on a mapping you don't control. LID exists precisely so that the phone number need not be disclosed, so resolution is something WhatsApp may or may not give you — and it is least likely to in exactly the groups where LIDs show up. Building on it means building on the thing the feature was designed to prevent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You do not need it.&lt;/strong&gt; You are not trying to learn who someone is. You are&lt;br&gt;
answering a much smaller question: &lt;em&gt;is this me?&lt;/em&gt; And for that, you already hold&lt;br&gt;
both of your own identities.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix that works
&lt;/h2&gt;

&lt;p&gt;Baileys exposes both forms of your own identity on &lt;code&gt;sock.user&lt;/code&gt; — &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;lid&lt;/code&gt;.&lt;br&gt;
So rather than converting the incoming value, reduce &lt;strong&gt;both sides&lt;/strong&gt; to a canonical&lt;br&gt;
form and compare against &lt;strong&gt;both&lt;/strong&gt; of your keys.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx4n1vp9no93glfl9exmd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx4n1vp9no93glfl9exmd.png" alt="Four steps: read the message, normalise the ID, compare against both of the bot's identifiers, decide" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The normaliser is deliberately boring. Strip the domain, strip the device suffix,&lt;br&gt;
keep the digits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 *   "919876543210:12@s.whatsapp.net" -&amp;gt; "919876543210"
 *   "275551234567890@lid"            -&amp;gt; "275551234567890"
 *   "+91 98765 43210"                -&amp;gt; "919876543210"
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normaliseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localPart&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withoutDevice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localPart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;withoutDevice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="sr"&gt;/gu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;:12&lt;/code&gt; matters. WhatsApp appends a device suffix on multi-device accounts, so&lt;br&gt;
even two JIDs for the same person are not reliably equal as strings.&lt;/p&gt;

&lt;p&gt;Then collect every form of your own identity once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;identityKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;lid&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;normaliseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;normaliseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lid&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the check becomes a set membership test rather than an equality test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;identityKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;botIdentity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mention&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mentions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalised&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normaliseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mention&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalised&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalised&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;explicit_mention&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what this does &lt;strong&gt;not&lt;/strong&gt; do: it never tries to discover the phone number behind&lt;br&gt;
a LID. It compares two identifiers you were already given.&lt;/p&gt;
&lt;h2&gt;
  
  
  The second trap: replying to the bot
&lt;/h2&gt;

&lt;p&gt;Swipe-replying to a bot's message is, to a user, obviously addressing it. Many&lt;br&gt;
bots treat it as an implicit mention — and that path breaks the same way, because&lt;br&gt;
the quoted message's sender also arrives as a LID.&lt;/p&gt;

&lt;p&gt;The robust answer is to not use an identifier at all. You already know which&lt;br&gt;
messages you sent, because you stored them. Look the quoted message up by &lt;strong&gt;id&lt;/strong&gt;&lt;br&gt;
and check whether it was yours:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fasy4yrphfocq6e4zrozt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fasy4yrphfocq6e4zrozt.png" alt="Flowchart: is the bot mentioned? if not, is it a reply to the bot? otherwise ignore the message" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two checks, in order of confidence — and the second one is immune to the whole&lt;br&gt;
problem by construction, which is a good argument for keeping it even after the&lt;br&gt;
first is fixed. If you also support a text fallback such as &lt;code&gt;/^@?bot\b/i&lt;/code&gt;, put it&lt;br&gt;
last; it is the one most likely to fire by accident.&lt;/p&gt;

&lt;p&gt;Record &lt;em&gt;which&lt;/em&gt; check fired. When a bot answers something it shouldn't have, "it&lt;br&gt;
matched the text pattern" is a bug report; "it decided to reply" is a mystery.&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing something you cannot reproduce on demand
&lt;/h2&gt;

&lt;p&gt;You cannot flip WhatsApp's privacy setting from a test. What you can do is stop&lt;br&gt;
treating the payload shape as something only the network produces.&lt;/p&gt;

&lt;p&gt;Keep the identifier logic in its own module with no WhatsApp dependency — it takes&lt;br&gt;
strings and returns strings — and give it both shapes directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;matches a mention that arrives as a LID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;919000000001@s.whatsapp.net&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;275551234567890@lid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// The same bot, mentioned in each of the two forms.&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mention&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;919000000001@s.whatsapp.net&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;275551234567890@lid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;detectMention&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;messageWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mention&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;explicit_mention&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ignores a device suffix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Multi-device accounts append :&amp;lt;device&amp;gt;, so even two JIDs need normalising.&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normaliseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;919000000001:12@s.whatsapp.net&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;919000000001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part I would actually argue for. The LID problem is not hard once you&lt;br&gt;
understand it — perhaps thirty lines. It is hard to &lt;em&gt;find&lt;/em&gt;, and it is hard to be&lt;br&gt;
confident you have fixed it, because the environment that triggers it is not under&lt;br&gt;
your control. Pulling the decision into a pure function is what converts "I think&lt;br&gt;
this works now" into something you can assert.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else carries an identifier
&lt;/h2&gt;

&lt;p&gt;Mention arrays are where this is usually noticed first, but the same shape change&lt;br&gt;
reaches anything that names a participant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;participant&lt;/code&gt; on a group message — who sent it&lt;/li&gt;
&lt;li&gt;the quoted message's sender in &lt;code&gt;contextInfo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;group metadata participant lists, including admin checks&lt;/li&gt;
&lt;li&gt;any allowlist, blocklist or "is this the owner?" comparison you have written&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have an owner check that compares a configured phone number against an&lt;br&gt;
incoming identifier, it will fail the same way — and failing &lt;em&gt;open&lt;/em&gt; or &lt;em&gt;closed&lt;/em&gt;&lt;br&gt;
there matters a good deal more than a missed reply.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Treat a WhatsApp identifier the way you would treat a user-supplied email address:&lt;br&gt;
never compare it raw, normalise it at the boundary, and keep the normalisation in&lt;br&gt;
one tested place.&lt;/p&gt;

&lt;p&gt;The deeper point is that this was never really a string-comparison bug. It was an&lt;br&gt;
assumption — that a person has one address — that was true for years, quietly&lt;br&gt;
stopped being true, and was never written down anywhere in my code. The fix was&lt;br&gt;
small. Noticing was the expensive part.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/WhiskeySockets/Baileys/issues/1718" rel="noopener noreferrer"&gt;Baileys issue #1718 — &lt;code&gt;@lid&lt;/code&gt; remoteJid not returning real WhatsApp number&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openclaw/openclaw/issues/23029" rel="noopener noreferrer"&gt;openclaw issue #23029 — LID format breaks implicit mention detection on reply-to-bot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.whapi.cloud/help-desk/groups/what-is-lid-in-whatsapp-groups" rel="noopener noreferrer"&gt;What is &lt;code&gt;lid&lt;/code&gt; in WhatsApp groups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.whapi.cloud/help-desk/faq/whatsapp-lid-lid" rel="noopener noreferrer"&gt;WhatsApp LID (@lid) reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://whapi.cloud/blog/how-to-resolve-whatsapp-lid-to-phone-number" rel="noopener noreferrer"&gt;Resolving a WhatsApp LID to a phone number — and why you often cannot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>whatsapp</category>
      <category>baileys</category>
      <category>node</category>
      <category>bots</category>
    </item>
    <item>
      <title>Can you make a GitHub Pages site private?</title>
      <dc:creator>Rajat Ghate</dc:creator>
      <pubDate>Mon, 14 Sep 2026 08:21:04 +0000</pubDate>
      <link>https://dev.to/rajat_ghate/can-you-make-a-github-pages-site-private-2bj1</link>
      <guid>https://dev.to/rajat_ghate/can-you-make-a-github-pages-site-private-2bj1</guid>
      <description>&lt;p&gt;I ran into something recently while setting up a GitHub Pages site that I thought would be pretty straightforward.&lt;/p&gt;

&lt;p&gt;I had a private repository and wanted the website generated from it to be private too. Basically, I didn't want anyone else to be able to open the Pages URL.&lt;/p&gt;

&lt;p&gt;So I went looking for the setting that would let me do that.&lt;/p&gt;

&lt;p&gt;I couldn't find it.&lt;/p&gt;

&lt;p&gt;At first, I thought I was just missing something in the repository settings. But after digging through the GitHub Pages documentation, I realized the problem wasn't that I couldn't find the setting.&lt;/p&gt;

&lt;p&gt;For most GitHub plans, that setting simply doesn't exist.&lt;/p&gt;

&lt;p&gt;And the reason this is confusing is that there are actually two different things you can mean by “private.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Private repository ≠ private website
&lt;/h2&gt;

&lt;p&gt;A private repository controls who can see your source code.&lt;/p&gt;

&lt;p&gt;A private website controls who can actually open the published website.&lt;/p&gt;

&lt;p&gt;Those aren't the same thing.&lt;/p&gt;

&lt;p&gt;This is probably the part that's easiest to get wrong. If your repository is private, it's natural to assume that anything you publish from it will also be private.&lt;/p&gt;

&lt;p&gt;Unfortunately, that's not how GitHub Pages works.&lt;/p&gt;

&lt;p&gt;Two different kinds of private: a private repository still produces a public site&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fitwyw1jveo8glacepq0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fitwyw1jveo8glacepq0f.png" alt=" " width="799" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens with different GitHub plans?
&lt;/h2&gt;

&lt;p&gt;The exact options depend on the type of GitHub account you're using, but the basic situation looks like&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Pages from a private repo?&lt;/th&gt;
&lt;th&gt;Can the site itself be private?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free (personal)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro (personal)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team (organization)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise Cloud (organization)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, with some restrictions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the Free plan, there's an extra catch: if you're using GitHub Free, the repository has to be public to use Pages.&lt;/p&gt;

&lt;p&gt;The Pro plan is where this can get a little misleading.&lt;/p&gt;

&lt;p&gt;You can keep your repository private and publish the site from it, which sounds like it solves the problem.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;Your code is private, but the website is still public.&lt;/p&gt;

&lt;p&gt;Anyone who has the URL can open it. They don't need access to your repository or your GitHub account.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when can a GitHub Pages site actually be private?
&lt;/h2&gt;

&lt;p&gt;GitHub does support access-controlled Pages sites, but there are some fairly specific requirements.&lt;/p&gt;

&lt;p&gt;The private-site option is available with GitHub Enterprise Cloud, and it applies to certain project sites published from private or internal repositories owned by an organization.&lt;/p&gt;

&lt;p&gt;It isn't something you can enable on a personal GitHub account just by upgrading from Free to Pro.&lt;/p&gt;

&lt;p&gt;So if you're using GitHub as an individual developer and you're thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“I'll just make the repository private, and that should protect the website.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't rely on that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why doesn't GitHub just give us a private/public switch?
&lt;/h2&gt;

&lt;p&gt;This is where it helps to separate GitHub repositories from GitHub Pages.&lt;/p&gt;

&lt;p&gt;Pages is primarily a static-site hosting service. You give it your generated files, and GitHub serves those files as a website.&lt;/p&gt;

&lt;p&gt;A private website needs something else as well: access control.&lt;/p&gt;

&lt;p&gt;When somebody requests a page, something needs to figure out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who is this person?&lt;/li&gt;
&lt;li&gt;Are they allowed to see this site?&lt;/li&gt;
&lt;li&gt;Should they be asked to log in?&lt;/li&gt;
&lt;li&gt;What should happen if they aren't authorized?&lt;/li&gt;
&lt;li&gt;That's a different problem from simply serving static files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub does provide this kind of access control in its Enterprise offering, but regular GitHub Pages sites aren't designed to work as a private, authenticated website.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn69ut0a9n9i65ysfpm7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn69ut0a9n9i65ysfpm7h.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'd be careful about
&lt;/h2&gt;

&lt;p&gt;The distinction doesn't really matter if you're hosting a personal portfolio or a public project.&lt;/p&gt;

&lt;p&gt;It matters a lot if you're using Pages for something that shouldn't be public.&lt;/p&gt;

&lt;p&gt;For example, imagine you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal documentation&lt;/li&gt;
&lt;li&gt;a staging version of a website&lt;/li&gt;
&lt;li&gt;a client project that hasn't been released yet&lt;/li&gt;
&lt;li&gt;test data&lt;/li&gt;
&lt;li&gt;internal tools&lt;/li&gt;
&lt;li&gt;configuration files that you accidentally include in the build&lt;/li&gt;
&lt;li&gt;You put all of that in a private repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then you enable GitHub Pages.&lt;/p&gt;

&lt;p&gt;The repository can remain private while the resulting website is publicly accessible.&lt;/p&gt;

&lt;p&gt;That's the part I think is worth remembering.&lt;/p&gt;

&lt;p&gt;Don't treat GitHub Pages as private just because the repository is private.&lt;/p&gt;

&lt;p&gt;And don't assume that an obscure URL is enough protection either. If something is genuinely sensitive, it shouldn't depend on nobody discovering the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you use instead?
&lt;/h2&gt;

&lt;p&gt;If you actually need a website where only certain people can access it, there are a few better options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Pages + Cloudflare Access
&lt;/h2&gt;

&lt;p&gt;This is probably the first option I'd look at for a small project.&lt;/p&gt;

&lt;p&gt;You can deploy the static site normally and put Cloudflare Access in front of it. Access can require users to authenticate before they reach the site.&lt;/p&gt;

&lt;p&gt;For example, you can allow specific email addresses or an entire email domain.&lt;/p&gt;

&lt;p&gt;That gives you the thing GitHub Pages is missing here: an actual access-control layer in front of the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vercel or Netlify
&lt;/h2&gt;

&lt;p&gt;If you're already using Vercel or Netlify, their protection and preview features can also make sense.&lt;/p&gt;

&lt;p&gt;The exact features available depend on the plan, so I'd check the current pricing and feature list before choosing one specifically for authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Host it yourself
&lt;/h2&gt;

&lt;p&gt;You can also keep it simple and host the site yourself.&lt;/p&gt;

&lt;p&gt;A small VPS with nginx and basic authentication is enough for many internal sites. It requires more setup and maintenance, but at least you have direct control over how access is handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or just don't deploy it
&lt;/h2&gt;

&lt;p&gt;This is an option that's easy to overlook.&lt;/p&gt;

&lt;p&gt;If the site is something you're only using yourself, you might not need to put it on the internet at all.&lt;/p&gt;

&lt;p&gt;Most static-site generators can run a local development server, so you can just run the site on your machine when you need it.&lt;/p&gt;

&lt;p&gt;Sometimes that's the simplest solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I wish I'd known earlier
&lt;/h2&gt;

&lt;p&gt;The main takeaway for me was pretty simple:&lt;/p&gt;

&lt;p&gt;A private GitHub repository and a private GitHub Pages website are two separate things.&lt;/p&gt;

&lt;p&gt;Making the repository private protects your source code.&lt;/p&gt;

&lt;p&gt;It doesn't automatically protect the website generated from that repository.&lt;/p&gt;

&lt;p&gt;So before putting something on GitHub Pages, ask yourself which one you're actually trying to protect.&lt;/p&gt;

&lt;p&gt;If you only need to hide the source code, a private repository may be enough.&lt;/p&gt;

&lt;p&gt;If you need to restrict who can access the website itself, you'll need a service that provides authentication/access control or GitHub Enterprise Cloud under the specific Pages setup that supports it.&lt;/p&gt;

&lt;p&gt;That distinction is easy to miss, and it's much better to find it out before you've accidentally published something you meant to keep private.&lt;/p&gt;

</description>
      <category>github</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
