Compare commits

..

3 Commits

Author SHA1 Message Date
Renato Alcara 6c09b14776 fix(signal): bot-review round 2 — injectE2ESession lock, doc/error fixes
Addresses bot review on commit 4bdcd5769d:

1) [Major — CodeRabbit + Copilot] `injectE2ESession` was the last
   session-mutating path still locking on raw `jid` while writing through
   `storeSession()` (which canonicalizes via `resolveLIDSignalAddress`). Same
   PN/LID lock-vs-storage drift as the original bug — fixed by computing
   `wireAddr` once and locking on it (same pattern as encrypt/decrypt).

2) [Minor — Copilot] `resolveLIDSignalAddress` docstring described the input
   as `userDomain.device` but the actual format from
   `jidToSignalProtocolAddress` is `signalUser.device`, where `signalUser` is
   either the bare user (WhatsApp domain) or `user_domainType` for LID/hosted.
   Updated to match.

3) [Minor — Copilot] `Missing device ID` error fired when the user portion
   (before the `.`) was empty, which was misleading. Split into two specific
   errors and added validation for the empty-device-portion case (e.g.
   trailing `.`). Each now points at the actual missing part.

4) [Doc — CodeRabbit residual race concern] Added a paragraph explaining the
   theoretical race between our `wireAddr` resolution and storage's internal
   re-resolution if `lidMapping` mutates between the two awaits. In production
   this is mitigated by the `migrateSession()` call upstream in
   `messages-recv.ts` running synchronously BEFORE `decryptMessage`, so the
   mapping is populated by the time the lock is acquired. Eliminating the
   residual entirely requires storage to skip re-resolution — bigger refactor
   reserved for a follow-up if the residual ever bites in prod.

5) [Out of scope — Copilot PR-metadata staleness] PR title still says "raw
   JID" from the original commit. Will be retitled on the GitHub side after
   this lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:36:20 -03:00
Renato Alcara 4bdcd5769d fix(signal): lock on wire address (not raw JID) — addresses bot review
Bots (Codex P1, Copilot, CodeRabbit) flagged a real defect in the previous
commit (79844a9d29): locking on the raw `jid` doesn't actually serialize
concurrent ops on the same session, because `signalStorage.loadSession`/
`storeSession` internally call `resolveLIDSignalAddress(id)` which canonicalizes
PN→LID before hitting `keys.get/set('session', ...)`. So:

  - Op A arrives as PN  → lock(PN_addr)  → storage rewrites to LID_addr
  - Op B arrives as LID → lock(LID_addr) → storage stays at LID_addr
  - Two locks, SAME session record → ratchet interleaving → Bad MAC.

This was the same drift the original `canonicalJid` was trying to prevent —
just expressed at a different layer. Pedro's snapshot doesn't trip it because
in Feb/6 there were no PN→LID mappings populated yet (WhatsApp hadn't fully
rolled out LID-addressed DSM), so storage canonicalization was a no-op and lock
key == storage key by accident.

Fix: extract `resolveLIDSignalAddress` to a top-level helper (parameterized by
`lidMapping`) and use it in BOTH places —
  1. `signalStorage.loadSession`/`storeSession` (unchanged behavior, just no
     longer a closure).
  2. `decryptMessage`/`encryptMessage` to compute the *wire address* and lock
     on it. Lock key now matches the storage key exactly.

This eliminates both directions of the PN/LID drift. PN/LID mapping latency on
the hot path is unchanged from the previous commit (one `getLIDForPN` lookup
per decrypt — same as before — but now correctly placed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:27:35 -03:00
Renato Alcara 79844a9d29 fix(signal): use raw JID as transaction key to stop Bad MAC loop on own DSM
PRODUCTION BUG: After WhatsApp rolled out LID-addressed DSM (deviceSentMessage),
own messages flooded the inbound pipeline with perpetual `Bad MAC` errors,
delaying delivery to the consumer by ~60s. Pedro's fork (Feb/6 snapshot, 484
commits behind) does NOT have this bug — diff isolated the regression to this
file.

ROOT CAUSE: `decryptMessage` was locking on `canonicalJid` (resolved via
`resolveCanonicalJid`, which awaits `lidMapping.getLIDForPN`) but the
`SessionCipher` is constructed from `jidToSignalProtocolAddress(jid)` — the
RAW jid. So:

  1. The mutex lock key (canonical LID) and the session-storage key (raw
     JID/PN-derived) drift apart. Two concurrent decrypts for the same
     logical session — one arriving as PN, one as LID — acquire DIFFERENT
     locks and interleave reads/writes on the same Signal session record,
     corrupting the ratchet. Symptom: perpetual `Bad MAC Error`.

  2. Each decrypt also paid the cost of an extra `await getLIDForPN()` round
     trip to the LID store on the hot path — a needless await per message.

`encryptMessage` had the same bug.

FIX: Lock on the raw `jid` (matches what `SessionCipher` actually uses for
storage). PN/LID race protection still happens upstream in `migrateSession`
+ `storeLIDPNMappings`, which run BEFORE decrypt and ensure the session is
already stored under the correct format by the time we acquire the lock.

`resolveCanonicalJid` is now unused — removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:57:40 -03:00
5 changed files with 19 additions and 3130 deletions
+1 -163
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package proto;
/// WhatsApp Version: 2.3000.1038469210
/// WhatsApp Version: 2.3000.1038164556
message ADVDeviceIdentity {
optional uint32 rawId = 1;
@@ -14,7 +14,6 @@ message ADVDeviceIdentity {
enum ADVEncryptionType {
E2EE = 0;
HOSTED = 1;
NON_E2EE = 2;
}
message ADVKeyIndexList {
optional uint32 rawId = 1;
@@ -62,7 +61,6 @@ message AIHomeState {
ANIMATE_PHOTO = 2;
ANALYZE_FILE = 3;
COLLABORATE = 4;
OPEN_GREETING_CARD = 5;
}
}
@@ -249,16 +247,6 @@ message AIRichResponseUnifiedResponse {
optional bytes data = 1;
}
enum AISubscriptionRequestType {
UNSPECIFIED = 0;
THINK_HARD = 1;
IMAGE_GEN = 2;
VIDEO_GEN = 3;
}
message AISubscriptionUpsellMetadata {
optional AISubscriptionRequestType requestType = 1;
}
message AIThreadInfo {
optional AIThreadServerInfo serverInfo = 1;
optional AIThreadClientInfo clientInfo = 2;
@@ -671,8 +659,6 @@ message BotMetadata {
optional BotInfrastructureDiagnostics botInfrastructureDiagnostics = 37;
optional AIMediaCollectionMetadata aiMediaCollectionMetadata = 38;
optional BotCommandMetadata commandMetadata = 39;
optional BotResolvedToolCallMetadata resolvedToolCallMetadata = 40;
optional AISubscriptionUpsellMetadata subscriptionUpsellMetadata = 41;
optional bytes internalMetadata = 999;
}
@@ -911,11 +897,6 @@ message BotRenderingMetadata {
}
message BotResolvedToolCallMetadata {
optional string toolCallId = 1;
optional string resolutionDataSerialized = 2;
}
message BotSessionMetadata {
optional string sessionId = 1;
optional BotSessionSource sessionSource = 2;
@@ -1151,7 +1132,6 @@ message ClientPayload {
optional int32 preacksCount = 45;
optional int32 processingQueueSize = 46;
repeated string pairedPeripherals = 47;
optional bytes testIsolationId = 48;
enum AccountType {
DEFAULT = 0;
GUEST = 1;
@@ -1247,7 +1227,6 @@ message ClientPayload {
optional string deviceExpId = 14;
optional DeviceType deviceType = 15;
optional string deviceModelType = 16;
optional DistributionChannel distributionChannel = 17;
message AppVersion {
optional uint32 primary = 1;
optional uint32 secondary = 2;
@@ -1263,12 +1242,6 @@ message ClientPayload {
WEARABLE = 3;
VR = 4;
}
enum DistributionChannel {
APPSTORE = 0;
WEBSITE = 1;
TESTFLIGHT = 2;
INTERNAL = 3;
}
enum Platform {
ANDROID = 0;
IOS = 1;
@@ -1438,9 +1411,6 @@ message ContextInfo {
optional MediaDomainInfo mediaDomainInfo = 74;
optional PartiallySelectedContent partiallySelectedContent = 75;
optional uint32 afterReadDuration = 76;
optional CrossAppSource crossAppSource = 77;
optional BusinessInteractionPills businessInteractionPills = 78;
optional string posterStatusId = 79;
message AdReplyInfo {
optional string advertiserName = 1;
optional MediaType mediaType = 2;
@@ -1453,47 +1423,10 @@ message ContextInfo {
}
}
message BusinessInteractionPills {
optional string businessJid = 1;
repeated Pill pills = 2;
optional EntryPoint entryPoint = 3;
enum EntryPoint {
ENTRY_POINT_UNKNOWN = 0;
P2P_LINK_SHARE = 1;
CONTACT_CARD_SHARING = 2;
PHONE_NUMBER = 3;
STATUS = 4;
IN_THREAD_CONTEXT_CARD = 5;
}
message Pill {
optional ContextInfo.BusinessInteractionPills.PillType pillType = 1;
optional string actionUrl = 2;
}
enum PillType {
UNKNOWN = 0;
VIEW_BUSINESS = 1;
CHAT = 2;
CALL = 3;
CATALOG = 4;
CHANNEL = 5;
BOOK_APPOINTMENT = 6;
OFFERS = 7;
BESTSELLERS = 8;
MENU = 9;
ABOUT = 10;
}
}
message BusinessMessageForwardInfo {
optional string businessOwnerJid = 1;
}
enum CrossAppSource {
CROSS_APP_SOURCE_UNKNOWN = 0;
CROSS_APP_SOURCE_INSTAGRAM = 1;
CROSS_APP_SOURCE_FACEBOOK = 2;
}
message DataSharingContext {
optional bool showMmDisclosure = 1;
optional string encryptedSignalTokenConsented = 2;
@@ -1702,7 +1635,6 @@ message Conversation {
optional bool isMarketingMessageThread = 55;
optional bool isSenderNewAccount = 56;
optional uint32 afterReadDuration = 57;
optional bool isSenderSuspicious = 58;
enum EndOfHistoryTransferType {
COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY = 0;
COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY = 1;
@@ -2416,8 +2348,6 @@ message Message {
optional PollAddOptionMessage pollAddOptionMessage = 121;
optional EventInviteMessage eventInviteMessage = 122;
optional GroupRootKeyShare groupRootKeyShare = 123;
optional P2PPaymentReminderNotification p2PPaymentReminderNotification = 124;
optional SplitPaymentMessage splitPaymentMessage = 125;
message AlbumMessage {
optional uint32 expectedImageCount = 2;
optional uint32 expectedVideoCount = 3;
@@ -2598,41 +2528,6 @@ message Message {
optional string id = 2;
}
message ChatCustomImageWallpaper {
optional string directPath = 1;
optional bytes mediaKey = 2;
optional bytes fileEncSha256 = 3;
optional bytes fileSha256 = 4;
optional float dimLevel = 5;
}
message ChatDefaultWallpaper {
optional bool isDoodleEnabled = 1;
}
message ChatSolidColorWallpaper {
optional string colorLight = 1;
optional string colorDark = 2;
optional bool isDoodleEnabled = 3;
}
message ChatStockImageWallpaper {
optional string stockImageId = 1;
optional float dimLevel = 2;
}
message ChatThemeSetting {
optional int64 settingTimestampMs = 1;
optional bool clearTheme = 2;
optional string colorSchemeId = 3;
oneof wallpaper {
Message.ChatDefaultWallpaper defaultWallpaper = 10;
Message.ChatSolidColorWallpaper solidColor = 11;
Message.ChatStockImageWallpaper stockImage = 12;
Message.ChatCustomImageWallpaper customImage = 13;
}
}
message CloudAPIThreadControlNotification {
optional CloudAPIThreadControl status = 1;
optional int64 senderNotificationTimestampMs = 2;
@@ -2742,7 +2637,6 @@ message Message {
optional int64 startTime = 5;
optional string caption = 6;
optional bool isCanceled = 7;
optional int64 endTime = 8;
}
message EventMessage {
@@ -3325,35 +3219,6 @@ message Message {
}
}
message P2PPaymentReminderNotification {
optional string reminderId = 1;
optional Money amount = 2;
optional ReminderFrequency frequency = 3;
optional int64 nextReminderTimestamp = 4;
optional int64 expiryTimestamp = 5;
optional ReminderState state = 6;
optional string description = 7;
optional string creatorJid = 8;
optional string receiverJid = 9;
optional string upiId = 10;
optional int64 createdTimestamp = 11;
enum ReminderFrequency {
UNKNOWN_FREQUENCY = 0;
WEEKLY = 1;
BIWEEKLY = 2;
MONTHLY = 3;
CUSTOM = 4;
}
enum ReminderState {
UNKNOWN_STATE = 0;
ACTIVE = 1;
PAUSED = 2;
STOPPED = 3;
EXPIRED = 4;
CANCELLED = 5;
}
}
message PaymentExtendedMetadata {
optional uint32 type = 1;
optional string platform = 2;
@@ -3763,7 +3628,6 @@ message Message {
optional MemberLabel memberLabel = 27;
optional AIMediaCollectionMessage aiMediaCollectionMessage = 28;
optional uint32 afterReadDuration = 29;
optional Message.ChatThemeSetting chatThemeSetting = 30;
enum Type {
REVOKE = 0;
EPHEMERAL_SETTING = 3;
@@ -3793,7 +3657,6 @@ message Message {
GROUP_MEMBER_LABEL_CHANGE = 30;
AI_MEDIA_COLLECTION_MESSAGE = 31;
MESSAGE_UNSCHEDULE = 32;
CHAT_THEME_SETTING = 34;
}
}
@@ -3885,26 +3748,6 @@ message Message {
optional bytes axolotlSenderKeyDistributionMessage = 2;
}
message SplitPaymentMessage {
optional string splitId = 1;
optional Money totalAmount = 2;
optional string description = 3;
optional string requesterJid = 4;
repeated Message.SplitPaymentParticipant participants = 5;
optional int64 createdAtMs = 6;
optional ContextInfo contextInfo = 17;
}
message SplitPaymentParticipant {
optional string jid = 1;
optional Money amount = 2;
optional SplitPaymentStatus status = 3;
enum SplitPaymentStatus {
PENDING = 0;
PAID = 1;
}
}
message StatusNotificationMessage {
optional MessageKey responseMessageKey = 1;
optional MessageKey originalMessageKey = 2;
@@ -4855,7 +4698,6 @@ message StatusAttribution {
SHARECHAT = 9;
GOOGLE_PHOTOS = 10;
SOUNDCLOUD = 11;
SHAZAM = 12;
}
}
@@ -4913,7 +4755,6 @@ message StatusAttribution {
LAYOUTS = 8;
NEWSLETTER_STATUS = 9;
STATUS_CLOSE_SHARING = 10;
PAID_PARTNERSHIP = 11;
}
}
@@ -5425,7 +5266,6 @@ message SyncActionValue {
repeated string keywords = 3;
optional int32 count = 4;
optional bool deleted = 5;
repeated string associatedLabelIds = 6;
}
message RecentEmojiWeightsAction {
@@ -5943,8 +5783,6 @@ message WebMessageInfo {
optional string hsmTag = 79;
optional uint64 ephemeralExpirationTimestamp = 80;
optional ScheduledMessageMetadata scheduledMessageMetadata = 81;
optional string decisionId = 82;
repeated string decisionSources = 83;
enum BizPrivacyStatus {
E2EE = 0;
FB = 2;
+5 -377
View File
@@ -28,8 +28,7 @@ export namespace proto {
enum ADVEncryptionType {
E2EE = 0,
HOSTED = 1,
NON_E2EE = 2
HOSTED = 1
}
interface IADVKeyIndexList {
@@ -177,8 +176,7 @@ export namespace proto {
CREATE_IMAGE = 1,
ANIMATE_PHOTO = 2,
ANALYZE_FILE = 3,
COLLABORATE = 4,
OPEN_GREETING_CARD = 5
COLLABORATE = 4
}
}
}
@@ -707,29 +705,6 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string;
}
enum AISubscriptionRequestType {
UNSPECIFIED = 0,
THINK_HARD = 1,
IMAGE_GEN = 2,
VIDEO_GEN = 3
}
interface IAISubscriptionUpsellMetadata {
requestType?: (proto.AISubscriptionRequestType|null);
}
class AISubscriptionUpsellMetadata implements IAISubscriptionUpsellMetadata {
constructor(p?: proto.IAISubscriptionUpsellMetadata);
public requestType?: (proto.AISubscriptionRequestType|null);
public static create(properties?: proto.IAISubscriptionUpsellMetadata): proto.AISubscriptionUpsellMetadata;
public static encode(m: proto.IAISubscriptionUpsellMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.AISubscriptionUpsellMetadata;
public static fromObject(d: { [k: string]: any }): proto.AISubscriptionUpsellMetadata;
public static toObject(m: proto.AISubscriptionUpsellMetadata, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IAIThreadInfo {
serverInfo?: (proto.AIThreadInfo.IAIThreadServerInfo|null);
clientInfo?: (proto.AIThreadInfo.IAIThreadClientInfo|null);
@@ -1727,8 +1702,6 @@ export namespace proto {
botInfrastructureDiagnostics?: (proto.IBotInfrastructureDiagnostics|null);
aiMediaCollectionMetadata?: (proto.IAIMediaCollectionMetadata|null);
commandMetadata?: (proto.IBotCommandMetadata|null);
resolvedToolCallMetadata?: (proto.IBotResolvedToolCallMetadata|null);
subscriptionUpsellMetadata?: (proto.IAISubscriptionUpsellMetadata|null);
internalMetadata?: (Uint8Array|null);
}
@@ -1772,8 +1745,6 @@ export namespace proto {
public botInfrastructureDiagnostics?: (proto.IBotInfrastructureDiagnostics|null);
public aiMediaCollectionMetadata?: (proto.IAIMediaCollectionMetadata|null);
public commandMetadata?: (proto.IBotCommandMetadata|null);
public resolvedToolCallMetadata?: (proto.IBotResolvedToolCallMetadata|null);
public subscriptionUpsellMetadata?: (proto.IAISubscriptionUpsellMetadata|null);
public internalMetadata?: (Uint8Array|null);
public static create(properties?: proto.IBotMetadata): proto.BotMetadata;
public static encode(m: proto.IBotMetadata, w?: $protobuf.Writer): $protobuf.Writer;
@@ -2323,24 +2294,6 @@ export namespace proto {
}
}
interface IBotResolvedToolCallMetadata {
toolCallId?: (string|null);
resolutionDataSerialized?: (string|null);
}
class BotResolvedToolCallMetadata implements IBotResolvedToolCallMetadata {
constructor(p?: proto.IBotResolvedToolCallMetadata);
public toolCallId?: (string|null);
public resolutionDataSerialized?: (string|null);
public static create(properties?: proto.IBotResolvedToolCallMetadata): proto.BotResolvedToolCallMetadata;
public static encode(m: proto.IBotResolvedToolCallMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.BotResolvedToolCallMetadata;
public static fromObject(d: { [k: string]: any }): proto.BotResolvedToolCallMetadata;
public static toObject(m: proto.BotResolvedToolCallMetadata, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IBotSessionMetadata {
sessionId?: (string|null);
sessionSource?: (proto.BotSessionSource|null);
@@ -2929,7 +2882,6 @@ export namespace proto {
preacksCount?: (number|null);
processingQueueSize?: (number|null);
pairedPeripherals?: (string[]|null);
testIsolationId?: (Uint8Array|null);
}
class ClientPayload implements IClientPayload {
@@ -2969,7 +2921,6 @@ export namespace proto {
public preacksCount?: (number|null);
public processingQueueSize?: (number|null);
public pairedPeripherals: string[];
public testIsolationId?: (Uint8Array|null);
public static create(properties?: proto.IClientPayload): proto.ClientPayload;
public static encode(m: proto.IClientPayload, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ClientPayload;
@@ -3132,7 +3083,6 @@ export namespace proto {
deviceExpId?: (string|null);
deviceType?: (proto.ClientPayload.UserAgent.DeviceType|null);
deviceModelType?: (string|null);
distributionChannel?: (proto.ClientPayload.UserAgent.DistributionChannel|null);
}
class UserAgent implements IUserAgent {
@@ -3153,7 +3103,6 @@ export namespace proto {
public deviceExpId?: (string|null);
public deviceType?: (proto.ClientPayload.UserAgent.DeviceType|null);
public deviceModelType?: (string|null);
public distributionChannel?: (proto.ClientPayload.UserAgent.DistributionChannel|null);
public static create(properties?: proto.ClientPayload.IUserAgent): proto.ClientPayload.UserAgent;
public static encode(m: proto.ClientPayload.IUserAgent, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ClientPayload.UserAgent;
@@ -3197,13 +3146,6 @@ export namespace proto {
VR = 4
}
enum DistributionChannel {
APPSTORE = 0,
WEBSITE = 1,
TESTFLIGHT = 2,
INTERNAL = 3
}
enum Platform {
ANDROID = 0,
IOS = 1,
@@ -3469,9 +3411,6 @@ export namespace proto {
mediaDomainInfo?: (proto.IMediaDomainInfo|null);
partiallySelectedContent?: (proto.ContextInfo.IPartiallySelectedContent|null);
afterReadDuration?: (number|null);
crossAppSource?: (proto.ContextInfo.CrossAppSource|null);
businessInteractionPills?: (proto.ContextInfo.IBusinessInteractionPills|null);
posterStatusId?: (string|null);
}
class ContextInfo implements IContextInfo {
@@ -3535,9 +3474,6 @@ export namespace proto {
public mediaDomainInfo?: (proto.IMediaDomainInfo|null);
public partiallySelectedContent?: (proto.ContextInfo.IPartiallySelectedContent|null);
public afterReadDuration?: (number|null);
public crossAppSource?: (proto.ContextInfo.CrossAppSource|null);
public businessInteractionPills?: (proto.ContextInfo.IBusinessInteractionPills|null);
public posterStatusId?: (string|null);
public static create(properties?: proto.IContextInfo): proto.ContextInfo;
public static encode(m: proto.IContextInfo, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ContextInfo;
@@ -3580,70 +3516,6 @@ export namespace proto {
}
}
interface IBusinessInteractionPills {
businessJid?: (string|null);
pills?: (proto.ContextInfo.BusinessInteractionPills.IPill[]|null);
entryPoint?: (proto.ContextInfo.BusinessInteractionPills.EntryPoint|null);
}
class BusinessInteractionPills implements IBusinessInteractionPills {
constructor(p?: proto.ContextInfo.IBusinessInteractionPills);
public businessJid?: (string|null);
public pills: proto.ContextInfo.BusinessInteractionPills.IPill[];
public entryPoint?: (proto.ContextInfo.BusinessInteractionPills.EntryPoint|null);
public static create(properties?: proto.ContextInfo.IBusinessInteractionPills): proto.ContextInfo.BusinessInteractionPills;
public static encode(m: proto.ContextInfo.IBusinessInteractionPills, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ContextInfo.BusinessInteractionPills;
public static fromObject(d: { [k: string]: any }): proto.ContextInfo.BusinessInteractionPills;
public static toObject(m: proto.ContextInfo.BusinessInteractionPills, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
namespace BusinessInteractionPills {
enum EntryPoint {
ENTRY_POINT_UNKNOWN = 0,
P2P_LINK_SHARE = 1,
CONTACT_CARD_SHARING = 2,
PHONE_NUMBER = 3,
STATUS = 4,
IN_THREAD_CONTEXT_CARD = 5
}
interface IPill {
pillType?: (proto.ContextInfo.BusinessInteractionPills.PillType|null);
actionUrl?: (string|null);
}
class Pill implements IPill {
constructor(p?: proto.ContextInfo.BusinessInteractionPills.IPill);
public pillType?: (proto.ContextInfo.BusinessInteractionPills.PillType|null);
public actionUrl?: (string|null);
public static create(properties?: proto.ContextInfo.BusinessInteractionPills.IPill): proto.ContextInfo.BusinessInteractionPills.Pill;
public static encode(m: proto.ContextInfo.BusinessInteractionPills.IPill, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ContextInfo.BusinessInteractionPills.Pill;
public static fromObject(d: { [k: string]: any }): proto.ContextInfo.BusinessInteractionPills.Pill;
public static toObject(m: proto.ContextInfo.BusinessInteractionPills.Pill, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
enum PillType {
UNKNOWN = 0,
VIEW_BUSINESS = 1,
CHAT = 2,
CALL = 3,
CATALOG = 4,
CHANNEL = 5,
BOOK_APPOINTMENT = 6,
OFFERS = 7,
BESTSELLERS = 8,
MENU = 9,
ABOUT = 10
}
}
interface IBusinessMessageForwardInfo {
businessOwnerJid?: (string|null);
}
@@ -3660,12 +3532,6 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string;
}
enum CrossAppSource {
CROSS_APP_SOURCE_UNKNOWN = 0,
CROSS_APP_SOURCE_INSTAGRAM = 1,
CROSS_APP_SOURCE_FACEBOOK = 2
}
interface IDataSharingContext {
showMmDisclosure?: (boolean|null);
encryptedSignalTokenConsented?: (string|null);
@@ -4055,7 +3921,6 @@ export namespace proto {
isMarketingMessageThread?: (boolean|null);
isSenderNewAccount?: (boolean|null);
afterReadDuration?: (number|null);
isSenderSuspicious?: (boolean|null);
}
class Conversation implements IConversation {
@@ -4117,7 +3982,6 @@ export namespace proto {
public isMarketingMessageThread?: (boolean|null);
public isSenderNewAccount?: (boolean|null);
public afterReadDuration?: (number|null);
public isSenderSuspicious?: (boolean|null);
public static create(properties?: proto.IConversation): proto.Conversation;
public static encode(m: proto.IConversation, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Conversation;
@@ -5861,8 +5725,6 @@ export namespace proto {
pollAddOptionMessage?: (proto.Message.IPollAddOptionMessage|null);
eventInviteMessage?: (proto.Message.IEventInviteMessage|null);
groupRootKeyShare?: (proto.IGroupRootKeyShare|null);
p2PPaymentReminderNotification?: (proto.Message.IP2PPaymentReminderNotification|null);
splitPaymentMessage?: (proto.Message.ISplitPaymentMessage|null);
}
class Message implements IMessage {
@@ -5970,8 +5832,6 @@ export namespace proto {
public pollAddOptionMessage?: (proto.Message.IPollAddOptionMessage|null);
public eventInviteMessage?: (proto.Message.IEventInviteMessage|null);
public groupRootKeyShare?: (proto.IGroupRootKeyShare|null);
public p2PPaymentReminderNotification?: (proto.Message.IP2PPaymentReminderNotification|null);
public splitPaymentMessage?: (proto.Message.ISplitPaymentMessage|null);
public static create(properties?: proto.IMessage): proto.Message;
public static encode(m: proto.IMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message;
@@ -6480,113 +6340,6 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatCustomImageWallpaper {
directPath?: (string|null);
mediaKey?: (Uint8Array|null);
fileEncSha256?: (Uint8Array|null);
fileSha256?: (Uint8Array|null);
dimLevel?: (number|null);
}
class ChatCustomImageWallpaper implements IChatCustomImageWallpaper {
constructor(p?: proto.Message.IChatCustomImageWallpaper);
public directPath?: (string|null);
public mediaKey?: (Uint8Array|null);
public fileEncSha256?: (Uint8Array|null);
public fileSha256?: (Uint8Array|null);
public dimLevel?: (number|null);
public static create(properties?: proto.Message.IChatCustomImageWallpaper): proto.Message.ChatCustomImageWallpaper;
public static encode(m: proto.Message.IChatCustomImageWallpaper, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatCustomImageWallpaper;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatCustomImageWallpaper;
public static toObject(m: proto.Message.ChatCustomImageWallpaper, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatDefaultWallpaper {
isDoodleEnabled?: (boolean|null);
}
class ChatDefaultWallpaper implements IChatDefaultWallpaper {
constructor(p?: proto.Message.IChatDefaultWallpaper);
public isDoodleEnabled?: (boolean|null);
public static create(properties?: proto.Message.IChatDefaultWallpaper): proto.Message.ChatDefaultWallpaper;
public static encode(m: proto.Message.IChatDefaultWallpaper, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatDefaultWallpaper;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatDefaultWallpaper;
public static toObject(m: proto.Message.ChatDefaultWallpaper, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatSolidColorWallpaper {
colorLight?: (string|null);
colorDark?: (string|null);
isDoodleEnabled?: (boolean|null);
}
class ChatSolidColorWallpaper implements IChatSolidColorWallpaper {
constructor(p?: proto.Message.IChatSolidColorWallpaper);
public colorLight?: (string|null);
public colorDark?: (string|null);
public isDoodleEnabled?: (boolean|null);
public static create(properties?: proto.Message.IChatSolidColorWallpaper): proto.Message.ChatSolidColorWallpaper;
public static encode(m: proto.Message.IChatSolidColorWallpaper, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatSolidColorWallpaper;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatSolidColorWallpaper;
public static toObject(m: proto.Message.ChatSolidColorWallpaper, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatStockImageWallpaper {
stockImageId?: (string|null);
dimLevel?: (number|null);
}
class ChatStockImageWallpaper implements IChatStockImageWallpaper {
constructor(p?: proto.Message.IChatStockImageWallpaper);
public stockImageId?: (string|null);
public dimLevel?: (number|null);
public static create(properties?: proto.Message.IChatStockImageWallpaper): proto.Message.ChatStockImageWallpaper;
public static encode(m: proto.Message.IChatStockImageWallpaper, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatStockImageWallpaper;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatStockImageWallpaper;
public static toObject(m: proto.Message.ChatStockImageWallpaper, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatThemeSetting {
settingTimestampMs?: (number|Long|null);
clearTheme?: (boolean|null);
colorSchemeId?: (string|null);
defaultWallpaper?: (proto.Message.IChatDefaultWallpaper|null);
solidColor?: (proto.Message.IChatSolidColorWallpaper|null);
stockImage?: (proto.Message.IChatStockImageWallpaper|null);
customImage?: (proto.Message.IChatCustomImageWallpaper|null);
}
class ChatThemeSetting implements IChatThemeSetting {
constructor(p?: proto.Message.IChatThemeSetting);
public settingTimestampMs?: (number|Long|null);
public clearTheme?: (boolean|null);
public colorSchemeId?: (string|null);
public defaultWallpaper?: (proto.Message.IChatDefaultWallpaper|null);
public solidColor?: (proto.Message.IChatSolidColorWallpaper|null);
public stockImage?: (proto.Message.IChatStockImageWallpaper|null);
public customImage?: (proto.Message.IChatCustomImageWallpaper|null);
public wallpaper?: ("defaultWallpaper"|"solidColor"|"stockImage"|"customImage");
public static create(properties?: proto.Message.IChatThemeSetting): proto.Message.ChatThemeSetting;
public static encode(m: proto.Message.IChatThemeSetting, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatThemeSetting;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatThemeSetting;
public static toObject(m: proto.Message.ChatThemeSetting, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface ICloudAPIThreadControlNotification {
status?: (proto.Message.CloudAPIThreadControlNotification.CloudAPIThreadControl|null);
senderNotificationTimestampMs?: (number|Long|null);
@@ -6891,7 +6644,6 @@ export namespace proto {
startTime?: (number|Long|null);
caption?: (string|null);
isCanceled?: (boolean|null);
endTime?: (number|Long|null);
}
class EventInviteMessage implements IEventInviteMessage {
@@ -6903,7 +6655,6 @@ export namespace proto {
public startTime?: (number|Long|null);
public caption?: (string|null);
public isCanceled?: (boolean|null);
public endTime?: (number|Long|null);
public static create(properties?: proto.Message.IEventInviteMessage): proto.Message.EventInviteMessage;
public static encode(m: proto.Message.IEventInviteMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.EventInviteMessage;
@@ -8424,62 +8175,6 @@ export namespace proto {
}
}
interface IP2PPaymentReminderNotification {
reminderId?: (string|null);
amount?: (proto.IMoney|null);
frequency?: (proto.Message.P2PPaymentReminderNotification.ReminderFrequency|null);
nextReminderTimestamp?: (number|Long|null);
expiryTimestamp?: (number|Long|null);
state?: (proto.Message.P2PPaymentReminderNotification.ReminderState|null);
description?: (string|null);
creatorJid?: (string|null);
receiverJid?: (string|null);
upiId?: (string|null);
createdTimestamp?: (number|Long|null);
}
class P2PPaymentReminderNotification implements IP2PPaymentReminderNotification {
constructor(p?: proto.Message.IP2PPaymentReminderNotification);
public reminderId?: (string|null);
public amount?: (proto.IMoney|null);
public frequency?: (proto.Message.P2PPaymentReminderNotification.ReminderFrequency|null);
public nextReminderTimestamp?: (number|Long|null);
public expiryTimestamp?: (number|Long|null);
public state?: (proto.Message.P2PPaymentReminderNotification.ReminderState|null);
public description?: (string|null);
public creatorJid?: (string|null);
public receiverJid?: (string|null);
public upiId?: (string|null);
public createdTimestamp?: (number|Long|null);
public static create(properties?: proto.Message.IP2PPaymentReminderNotification): proto.Message.P2PPaymentReminderNotification;
public static encode(m: proto.Message.IP2PPaymentReminderNotification, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.P2PPaymentReminderNotification;
public static fromObject(d: { [k: string]: any }): proto.Message.P2PPaymentReminderNotification;
public static toObject(m: proto.Message.P2PPaymentReminderNotification, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
namespace P2PPaymentReminderNotification {
enum ReminderFrequency {
UNKNOWN_FREQUENCY = 0,
WEEKLY = 1,
BIWEEKLY = 2,
MONTHLY = 3,
CUSTOM = 4
}
enum ReminderState {
UNKNOWN_STATE = 0,
ACTIVE = 1,
PAUSED = 2,
STOPPED = 3,
EXPIRED = 4,
CANCELLED = 5
}
}
interface IPaymentExtendedMetadata {
type?: (number|null);
platform?: (string|null);
@@ -9628,7 +9323,6 @@ export namespace proto {
memberLabel?: (proto.IMemberLabel|null);
aiMediaCollectionMessage?: (proto.IAIMediaCollectionMessage|null);
afterReadDuration?: (number|null);
chatThemeSetting?: (proto.Message.IChatThemeSetting|null);
}
class ProtocolMessage implements IProtocolMessage {
@@ -9659,7 +9353,6 @@ export namespace proto {
public memberLabel?: (proto.IMemberLabel|null);
public aiMediaCollectionMessage?: (proto.IAIMediaCollectionMessage|null);
public afterReadDuration?: (number|null);
public chatThemeSetting?: (proto.Message.IChatThemeSetting|null);
public static create(properties?: proto.Message.IProtocolMessage): proto.Message.ProtocolMessage;
public static encode(m: proto.Message.IProtocolMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ProtocolMessage;
@@ -9699,8 +9392,7 @@ export namespace proto {
AI_QUERY_FANOUT = 29,
GROUP_MEMBER_LABEL_CHANGE = 30,
AI_MEDIA_COLLECTION_MESSAGE = 31,
MESSAGE_UNSCHEDULE = 32,
CHAT_THEME_SETTING = 34
MESSAGE_UNSCHEDULE = 32
}
}
@@ -9952,62 +9644,6 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface ISplitPaymentMessage {
splitId?: (string|null);
totalAmount?: (proto.IMoney|null);
description?: (string|null);
requesterJid?: (string|null);
participants?: (proto.Message.ISplitPaymentParticipant[]|null);
createdAtMs?: (number|Long|null);
contextInfo?: (proto.IContextInfo|null);
}
class SplitPaymentMessage implements ISplitPaymentMessage {
constructor(p?: proto.Message.ISplitPaymentMessage);
public splitId?: (string|null);
public totalAmount?: (proto.IMoney|null);
public description?: (string|null);
public requesterJid?: (string|null);
public participants: proto.Message.ISplitPaymentParticipant[];
public createdAtMs?: (number|Long|null);
public contextInfo?: (proto.IContextInfo|null);
public static create(properties?: proto.Message.ISplitPaymentMessage): proto.Message.SplitPaymentMessage;
public static encode(m: proto.Message.ISplitPaymentMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.SplitPaymentMessage;
public static fromObject(d: { [k: string]: any }): proto.Message.SplitPaymentMessage;
public static toObject(m: proto.Message.SplitPaymentMessage, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface ISplitPaymentParticipant {
jid?: (string|null);
amount?: (proto.IMoney|null);
status?: (proto.Message.SplitPaymentParticipant.SplitPaymentStatus|null);
}
class SplitPaymentParticipant implements ISplitPaymentParticipant {
constructor(p?: proto.Message.ISplitPaymentParticipant);
public jid?: (string|null);
public amount?: (proto.IMoney|null);
public status?: (proto.Message.SplitPaymentParticipant.SplitPaymentStatus|null);
public static create(properties?: proto.Message.ISplitPaymentParticipant): proto.Message.SplitPaymentParticipant;
public static encode(m: proto.Message.ISplitPaymentParticipant, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.SplitPaymentParticipant;
public static fromObject(d: { [k: string]: any }): proto.Message.SplitPaymentParticipant;
public static toObject(m: proto.Message.SplitPaymentParticipant, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
namespace SplitPaymentParticipant {
enum SplitPaymentStatus {
PENDING = 0,
PAID = 1
}
}
interface IStatusNotificationMessage {
responseMessageKey?: (proto.IMessageKey|null);
originalMessageKey?: (proto.IMessageKey|null);
@@ -12362,8 +11998,7 @@ export namespace proto {
APPLE_MUSIC = 8,
SHARECHAT = 9,
GOOGLE_PHOTOS = 10,
SOUNDCLOUD = 11,
SHAZAM = 12
SOUNDCLOUD = 11
}
}
@@ -12497,8 +12132,7 @@ export namespace proto {
AI_CREATED = 7,
LAYOUTS = 8,
NEWSLETTER_STATUS = 9,
STATUS_CLOSE_SHARING = 10,
PAID_PARTNERSHIP = 11
STATUS_CLOSE_SHARING = 10
}
}
@@ -14035,7 +13669,6 @@ export namespace proto {
keywords?: (string[]|null);
count?: (number|null);
deleted?: (boolean|null);
associatedLabelIds?: (string[]|null);
}
class QuickReplyAction implements IQuickReplyAction {
@@ -14045,7 +13678,6 @@ export namespace proto {
public keywords: string[];
public count?: (number|null);
public deleted?: (boolean|null);
public associatedLabelIds: string[];
public static create(properties?: proto.SyncActionValue.IQuickReplyAction): proto.SyncActionValue.QuickReplyAction;
public static encode(m: proto.SyncActionValue.IQuickReplyAction, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.QuickReplyAction;
@@ -15327,8 +14959,6 @@ export namespace proto {
hsmTag?: (string|null);
ephemeralExpirationTimestamp?: (number|Long|null);
scheduledMessageMetadata?: (proto.IScheduledMessageMetadata|null);
decisionId?: (string|null);
decisionSources?: (string[]|null);
}
class WebMessageInfo implements IWebMessageInfo {
@@ -15403,8 +15033,6 @@ export namespace proto {
public hsmTag?: (string|null);
public ephemeralExpirationTimestamp?: (number|Long|null);
public scheduledMessageMetadata?: (proto.IScheduledMessageMetadata|null);
public decisionId?: (string|null);
public decisionSources: string[];
public static create(properties?: proto.IWebMessageInfo): proto.WebMessageInfo;
public static encode(m: proto.IWebMessageInfo, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.WebMessageInfo;
-2549
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
{"version":[2,3000,1038487394]}
{"version":[2,3000,1038189736]}
+12 -40
View File
@@ -67,61 +67,33 @@ export const DECRYPTION_RETRY_CONFIG = {
}
/**
* Retry options for decryption operations.
*
* IMPORTANT fail-fast policy for decryption:
* Both `sessionRecordErrors` ('No matching sessions found', etc.) and
* `corruptedSessionErrors` ('Bad MAC', 'MessageCounterError', missing keys)
* return `false` from `shouldRetry`. Rationale:
*
* 1. libsignal already scanned ALL stored sessions for the JID before
* throwing retrying immediately gives the SAME result (no new session
* record materialises in the 200-800ms backoff window).
* 2. The real recovery flow is upstream: failed decrypt retry receipt to
* WA phone re-sends as `pkmsg` libsignal builds a fresh session
* next message decrypts cleanly. That handshake takes ~300ms total.
* 3. Wrapping decrypt in 3 attempts × exponential backoff (200ms400ms800ms
* 1.4 s per failed message) just blocks the inbound buffer pipeline. At
* the rate own DSM messages flood in after a LID/PN mismatch, this
* compounds to tens of seconds of accumulated delay before live messages
* from real contacts can even reach the consumer.
*
* Only truly *unknown* errors get a single retry those might be transient
* (network blip, unexpected exception) and a quick 200ms retry is cheap.
*
* If we ever need transient-error retries again (e.g. the storage layer adds
* an async race that benefits from re-reading), set `sessionRecordErrors` to
* `attempt < 1` here, NOT `attempt < 3` one extra read at most.
* Retry options for decryption operations
* Uses exponential backoff with jitter to handle transient failures
*/
export const DECRYPTION_RETRY_OPTIONS: RetryOptions = {
maxAttempts: 2,
baseDelay: 200, // 200ms base delay (only used for unknown errors below)
maxDelay: 2000,
maxAttempts: 3,
baseDelay: 200, // 200ms base delay
maxDelay: 2000, // 2s max delay
backoffStrategy: 'exponential',
backoffMultiplier: 2,
jitter: 0.2,
collectMetrics: false,
jitter: 0.2, // 20% jitter
collectMetrics: false, // No Prometheus metrics
operationName: 'message_decryption',
shouldRetry: (error: Error, attempt: number) => {
const errorMsg = error?.message || ''
// Session record errors: libsignal already exhausted all stored sessions.
// Retrying immediately gives the same result; the real recovery path
// is the upstream retry-receipt → pkmsg flow. Fail fast.
// Always retry on session record errors (session might be syncing)
if (DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(err => errorMsg.includes(err))) {
return false
return attempt < 3 // Retry up to 3 times
}
// Corrupted session errors: Bad MAC / counter errors. Same reasoning —
// the keys are wrong and won't right themselves on retry.
// Don't retry on corrupted session errors (need cleanup first)
if (DECRYPTION_RETRY_CONFIG.corruptedSessionErrors.some(err => errorMsg.includes(err))) {
return false
}
// Unknown errors: one retry in case it was a transient blip.
// `attempt` is 1-based (retry-utils starts the loop at 1), so `attempt < 2`
// allows the second pass and returns false on the third.
return attempt < 2
// Retry other transient errors
return attempt < 2 // Retry up to 2 times for unknown errors
}
}