Compare commits

...

9 Commits

Author SHA1 Message Date
Renato Alcara 50226ae389 fix(decrypt): correct 1-based attempt check for unknown-error retry
Codex P2 review caught: `retry()` in retry-utils.ts iterates with
`for (let attempt = 1; ...)`, so the `attempt` passed to `shouldRetry` on
the first failure is 1, not 0. The previous `attempt < 1` was therefore
always false → no retry on unknown errors, contradicting the inline policy
comment ("one retry in case it was a transient blip").

Use `attempt < 2` so the SECOND pass happens (initial + 1 retry = 2 total
attempts, which matches `maxAttempts: 2`) and the third pass is refused.

The session-record / corrupted-session branches above already return false
unconditionally and are not affected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 10:34:04 -03:00
Renato Alcara 56ca70bd75 perf(decrypt): fail-fast on session-record errors to clear pipeline backpressure
PRODUCTION BUG (still present after PR #396): inbound messages from the
smartphone take ~60s to surface in the consumer (zpro). PR #396 fixed the
PN/LID lock-vs-storage drift but did NOT clear the latency. Diff against the
known-good Pedro snapshot pinpointed the residual cause: this retry wrapper.

ROOT CAUSE:
`DECRYPTION_RETRY_OPTIONS.shouldRetry` retries 3× with exponential backoff
(200 ms → 400 ms → 800 ms ≈ 1.4 s per failed message) on
`'No matching sessions found'` and friends. After WhatsApp's LID/DSM rollout
own DSM messages flood in for sessions stored under the legacy `_1.0` format
that no longer matches the LID-addressed envelope, so EVERY DSM hits this
path. At ~30 DSM/min the accumulated backoff is ~42 s — enough to block
real-contact messages from reaching the buffer flush.

The Pedro snapshot (Feb 6 2026, 484 commits behind, confirmed fast in prod
on the same auth state) has NO retry wrapper at all: try once → fail → send
retry receipt → phone re-sends as `pkmsg` → fresh session → next message
decrypts cleanly. Total recovery ~300 ms, no pipeline backpressure.

WHY THE RETRIES WERE USELESS:
1. libsignal already scanned every stored session for the JID before throwing
   `No matching sessions found`. Re-running the same lookup 200 ms later
   gives the same answer — no new session record materialises in that window.
2. Bad MAC / counter errors mean the keys are simply wrong; retry doesn't
   regenerate keys. (This branch was already correctly returning false.)

THIS PATCH:
- `sessionRecordErrors` now also returns `false` from `shouldRetry` (matches
  the existing `corruptedSessionErrors` policy).
- Unknown errors retry exactly once (was twice) — quick blip recovery only.
- `maxAttempts` lowered to 2 to match.
- Big block comment captures the rationale so the next person doesn't add
  retries back hoping it'll help.

Recovery still happens — just upstream, via the retry-receipt → pkmsg flow,
which is what WhatsApp protocol intends and what Pedro's working snapshot
relies on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 10:30:22 -03:00
Renato Alcara bb78f61c6a chore: update WhatsApp Web version to v2.3000.1038487394 (#401)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-30 06:39:44 -03:00
github-actions[bot] e1f6066db4 chore: update proto/version to v2.3000.1038469210 (#400)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-04-30 01:13:09 -03:00
Renato Alcara a193402832 chore: update WhatsApp Web version to v2.3000.1038387592 (#399)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-29 06:39:45 -03:00
github-actions[bot] c4ff320513 chore: update proto/version to v2.3000.1038358494 (#398)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-04-29 01:11:25 -03:00
Renato Alcara ba914b4336 fix(signal): lock on canonical wire address to stop Bad MAC loop on own DSM (#396)
fix(signal): lock on canonical wire address to stop Bad MAC loop on own DSM (#396)
2026-04-29 00:50:47 -03:00
Renato Alcara cca28756ee chore: update WhatsApp Web version to v2.3000.1038294217 (#397)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-28 06:44:51 -03:00
github-actions[bot] 095fbf121f chore: update proto/version to v2.3000.1038260195 (#394)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-04-28 01:14:03 -03:00
6 changed files with 3217 additions and 74 deletions
+163 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package proto;
/// WhatsApp Version: 2.3000.1038164556
/// WhatsApp Version: 2.3000.1038469210
message ADVDeviceIdentity {
optional uint32 rawId = 1;
@@ -14,6 +14,7 @@ message ADVDeviceIdentity {
enum ADVEncryptionType {
E2EE = 0;
HOSTED = 1;
NON_E2EE = 2;
}
message ADVKeyIndexList {
optional uint32 rawId = 1;
@@ -61,6 +62,7 @@ message AIHomeState {
ANIMATE_PHOTO = 2;
ANALYZE_FILE = 3;
COLLABORATE = 4;
OPEN_GREETING_CARD = 5;
}
}
@@ -247,6 +249,16 @@ 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;
@@ -659,6 +671,8 @@ 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;
}
@@ -897,6 +911,11 @@ message BotRenderingMetadata {
}
message BotResolvedToolCallMetadata {
optional string toolCallId = 1;
optional string resolutionDataSerialized = 2;
}
message BotSessionMetadata {
optional string sessionId = 1;
optional BotSessionSource sessionSource = 2;
@@ -1132,6 +1151,7 @@ message ClientPayload {
optional int32 preacksCount = 45;
optional int32 processingQueueSize = 46;
repeated string pairedPeripherals = 47;
optional bytes testIsolationId = 48;
enum AccountType {
DEFAULT = 0;
GUEST = 1;
@@ -1227,6 +1247,7 @@ 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;
@@ -1242,6 +1263,12 @@ message ClientPayload {
WEARABLE = 3;
VR = 4;
}
enum DistributionChannel {
APPSTORE = 0;
WEBSITE = 1;
TESTFLIGHT = 2;
INTERNAL = 3;
}
enum Platform {
ANDROID = 0;
IOS = 1;
@@ -1411,6 +1438,9 @@ 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;
@@ -1423,10 +1453,47 @@ 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;
@@ -1635,6 +1702,7 @@ 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;
@@ -2348,6 +2416,8 @@ 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;
@@ -2528,6 +2598,41 @@ 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;
@@ -2637,6 +2742,7 @@ message Message {
optional int64 startTime = 5;
optional string caption = 6;
optional bool isCanceled = 7;
optional int64 endTime = 8;
}
message EventMessage {
@@ -3219,6 +3325,35 @@ 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;
@@ -3628,6 +3763,7 @@ 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;
@@ -3657,6 +3793,7 @@ message Message {
GROUP_MEMBER_LABEL_CHANGE = 30;
AI_MEDIA_COLLECTION_MESSAGE = 31;
MESSAGE_UNSCHEDULE = 32;
CHAT_THEME_SETTING = 34;
}
}
@@ -3748,6 +3885,26 @@ 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;
@@ -4698,6 +4855,7 @@ message StatusAttribution {
SHARECHAT = 9;
GOOGLE_PHOTOS = 10;
SOUNDCLOUD = 11;
SHAZAM = 12;
}
}
@@ -4755,6 +4913,7 @@ message StatusAttribution {
LAYOUTS = 8;
NEWSLETTER_STATUS = 9;
STATUS_CLOSE_SHARING = 10;
PAID_PARTNERSHIP = 11;
}
}
@@ -5266,6 +5425,7 @@ message SyncActionValue {
repeated string keywords = 3;
optional int32 count = 4;
optional bool deleted = 5;
repeated string associatedLabelIds = 6;
}
message RecentEmojiWeightsAction {
@@ -5783,6 +5943,8 @@ 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;
+377 -5
View File
@@ -28,7 +28,8 @@ export namespace proto {
enum ADVEncryptionType {
E2EE = 0,
HOSTED = 1
HOSTED = 1,
NON_E2EE = 2
}
interface IADVKeyIndexList {
@@ -176,7 +177,8 @@ export namespace proto {
CREATE_IMAGE = 1,
ANIMATE_PHOTO = 2,
ANALYZE_FILE = 3,
COLLABORATE = 4
COLLABORATE = 4,
OPEN_GREETING_CARD = 5
}
}
}
@@ -705,6 +707,29 @@ 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);
@@ -1702,6 +1727,8 @@ 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);
}
@@ -1745,6 +1772,8 @@ 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;
@@ -2294,6 +2323,24 @@ 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);
@@ -2882,6 +2929,7 @@ export namespace proto {
preacksCount?: (number|null);
processingQueueSize?: (number|null);
pairedPeripherals?: (string[]|null);
testIsolationId?: (Uint8Array|null);
}
class ClientPayload implements IClientPayload {
@@ -2921,6 +2969,7 @@ 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;
@@ -3083,6 +3132,7 @@ 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 {
@@ -3103,6 +3153,7 @@ 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;
@@ -3146,6 +3197,13 @@ export namespace proto {
VR = 4
}
enum DistributionChannel {
APPSTORE = 0,
WEBSITE = 1,
TESTFLIGHT = 2,
INTERNAL = 3
}
enum Platform {
ANDROID = 0,
IOS = 1,
@@ -3411,6 +3469,9 @@ 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 {
@@ -3474,6 +3535,9 @@ 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;
@@ -3516,6 +3580,70 @@ 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);
}
@@ -3532,6 +3660,12 @@ 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);
@@ -3921,6 +4055,7 @@ export namespace proto {
isMarketingMessageThread?: (boolean|null);
isSenderNewAccount?: (boolean|null);
afterReadDuration?: (number|null);
isSenderSuspicious?: (boolean|null);
}
class Conversation implements IConversation {
@@ -3982,6 +4117,7 @@ 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;
@@ -5725,6 +5861,8 @@ 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 {
@@ -5832,6 +5970,8 @@ 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;
@@ -6340,6 +6480,113 @@ 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);
@@ -6644,6 +6891,7 @@ export namespace proto {
startTime?: (number|Long|null);
caption?: (string|null);
isCanceled?: (boolean|null);
endTime?: (number|Long|null);
}
class EventInviteMessage implements IEventInviteMessage {
@@ -6655,6 +6903,7 @@ 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;
@@ -8175,6 +8424,62 @@ 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);
@@ -9323,6 +9628,7 @@ export namespace proto {
memberLabel?: (proto.IMemberLabel|null);
aiMediaCollectionMessage?: (proto.IAIMediaCollectionMessage|null);
afterReadDuration?: (number|null);
chatThemeSetting?: (proto.Message.IChatThemeSetting|null);
}
class ProtocolMessage implements IProtocolMessage {
@@ -9353,6 +9659,7 @@ 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;
@@ -9392,7 +9699,8 @@ export namespace proto {
AI_QUERY_FANOUT = 29,
GROUP_MEMBER_LABEL_CHANGE = 30,
AI_MEDIA_COLLECTION_MESSAGE = 31,
MESSAGE_UNSCHEDULE = 32
MESSAGE_UNSCHEDULE = 32,
CHAT_THEME_SETTING = 34
}
}
@@ -9644,6 +9952,62 @@ 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);
@@ -11998,7 +12362,8 @@ export namespace proto {
APPLE_MUSIC = 8,
SHARECHAT = 9,
GOOGLE_PHOTOS = 10,
SOUNDCLOUD = 11
SOUNDCLOUD = 11,
SHAZAM = 12
}
}
@@ -12132,7 +12497,8 @@ export namespace proto {
AI_CREATED = 7,
LAYOUTS = 8,
NEWSLETTER_STATUS = 9,
STATUS_CLOSE_SHARING = 10
STATUS_CLOSE_SHARING = 10,
PAID_PARTNERSHIP = 11
}
}
@@ -13669,6 +14035,7 @@ export namespace proto {
keywords?: (string[]|null);
count?: (number|null);
deleted?: (boolean|null);
associatedLabelIds?: (string[]|null);
}
class QuickReplyAction implements IQuickReplyAction {
@@ -13678,6 +14045,7 @@ 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;
@@ -14959,6 +15327,8 @@ export namespace proto {
hsmTag?: (string|null);
ephemeralExpirationTimestamp?: (number|Long|null);
scheduledMessageMetadata?: (proto.IScheduledMessageMetadata|null);
decisionId?: (string|null);
decisionSources?: (string[]|null);
}
class WebMessageInfo implements IWebMessageInfo {
@@ -15033,6 +15403,8 @@ 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,1038189736]}
{"version":[2,3000,1038487394]}
+87 -55
View File
@@ -299,25 +299,6 @@ export function makeLibSignalRepository(
// Promise instead of each spawning their own DB transactions.
const migrationInFlight = new Map<string, Promise<{ migrated: number; skipped: number; total: number }>>()
// Resolve PN JID to its canonical LID JID for transaction locking.
// This prevents PN/LID race conditions where concurrent operations for the
// same logical contact acquire different mutex locks because one uses PN
// and the other uses LID. (Aligned with WABA behavior — all operations use LID internally.)
const resolveCanonicalJid = async(jid: string): Promise<string> => {
if (isAnyLidUser(jid)) {
return jid
}
if (isAnyPnUser(jid)) {
const lid = await lidMapping.getLIDForPN(jid)
if (lid) {
return lid
}
}
return jid
}
const repository: SignalRepositoryWithLIDStore = {
decryptGroupMessage({ group, authorJid, msg }) {
const senderName = jidToSignalSenderKeyName(group, authorJid)
@@ -360,6 +341,14 @@ export function makeLibSignalRepository(
},
async decryptMessage({ jid, type, ciphertext }) {
const addr = jidToSignalProtocolAddress(jid)
// Wire address = the EXACT key that signalStorage's loadSession/storeSession
// uses internally (it canonicalizes PN→LID via resolveLIDSignalAddress).
// We MUST lock on this same key — locking on the raw `jid` lets two
// concurrent ops for the same logical contact (one arriving as PN, one as
// LID) acquire DIFFERENT mutex slots and interleave on the same session
// record, corrupting the ratchet → perpetual `Bad MAC` on own DSM after
// the WhatsApp PN→LID DSM rollout.
const wireAddr = await resolveLIDSignalAddress(addr.toString(), lidMapping)
const session = new libsignal.SessionCipher(storage, addr)
async function doDecrypt() {
@@ -408,24 +397,23 @@ export function makeLibSignalRepository(
return result
}
// Use canonical JID (PN→LID resolved) as transaction key to prevent
// PN/LID race conditions on the same logical session.
const canonicalJid = await resolveCanonicalJid(jid)
return parsedKeys.transaction(async () => {
return await doDecrypt()
}, canonicalJid)
}, wireAddr)
},
async encryptMessage({ jid, data }) {
const addr = jidToSignalProtocolAddress(jid)
// See decryptMessage above for the rationale: lock on the wire address,
// not the raw JID, so concurrent PN/LID ops on the same session serialize.
const wireAddr = await resolveLIDSignalAddress(addr.toString(), lidMapping)
const cipher = new libsignal.SessionCipher(storage, addr)
const canonicalJid = await resolveCanonicalJid(jid)
return parsedKeys.transaction(async () => {
const { type: sigType, body } = await cipher.encrypt(data)
const type = sigType === 3 ? 'pkmsg' : 'msg'
return { type, ciphertext: Buffer.from(body, 'binary') }
}, canonicalJid)
}, wireAddr)
},
async encryptGroupMessage({ group, meId, data }) {
@@ -453,10 +441,16 @@ export function makeLibSignalRepository(
async injectE2ESession({ jid, session }) {
logger.trace({ jid }, 'injecting E2EE session')
const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid))
const addr = jidToSignalProtocolAddress(jid)
// Same wire-address locking as encrypt/decrypt — `SessionBuilder.initOutgoing`
// writes via `storeSession` which canonicalizes, so the lock key must be
// the wire address for the mutex to actually serialize concurrent ops on
// the same logical session.
const wireAddr = await resolveLIDSignalAddress(addr.toString(), lidMapping)
const cipher = new libsignal.SessionBuilder(storage, addr)
return parsedKeys.transaction(async () => {
await cipher.initOutgoing(session)
}, jid)
}, wireAddr)
},
jidToSignalProtocolAddress(jid) {
return jidToSignalProtocolAddress(jid).toString()
@@ -733,6 +727,65 @@ const jidToSignalSenderKeyName = (group: string, user: string): SenderKeyName =>
return new SenderKeyName(group, jidToSignalProtocolAddress(user))
}
/**
* Resolve a Signal protocol address (string form `signalUser.device`, where
* `signalUser` is `user` for the WhatsApp domain or `user_domainType` for any
* other domain e.g. `12345_1.0` for a LID device 0) to its canonical *wire*
* address the exact key that `loadSession`/`storeSession` uses when
* reading/writing `keys.get('session', [...])`.
*
* This is the SAME key the storage layer uses internally, so callers that need
* a transaction lock around session mutations MUST lock on this resolved
* address (not the raw JID), otherwise the lock key and storage key drift
* apart and concurrent ops on the same session interleave ratchet
* corruption perpetual `Bad MAC` errors.
*
* Behavior: if the input already targets a LID domain, returns it unchanged
* (cheap fast path no awaits). Otherwise looks up the PNLID mapping; if
* found, returns the LID-form Signal address. If no mapping exists, returns
* the input unchanged.
*
* Residual race window: the lock and the storage's internal re-resolution
* each call this function once. If a PNLID mapping is added between those
* two calls (microsecond window), the lock could be on the PN address while
* storage canonicalizes to LID. In production this is mitigated upstream:
* `messages-recv.ts` runs `migrateSession()` synchronously BEFORE
* `decryptMessage()` is called, so the mapping is already populated by the
* time we lock. Eliminating the residual race entirely would require storage
* to NOT re-resolve, which is a bigger refactor (other call sites depend on
* the current contract).
*
* Top-level (not inside `signalStorage`) so `makeLibSignalRepository` can
* compute the same wire address for transaction locking.
*/
const resolveLIDSignalAddress = async (id: string, lidMapping: LIDMappingStore): Promise<string> => {
if (id.includes('.')) {
const [deviceId, device] = id.split('.')
if (!deviceId) {
throw new Error(`Malformed signal address (empty user portion before '.'): "${id}"`)
}
if (device === undefined || device === '') {
throw new Error(`Malformed signal address (empty device portion after '.'): "${id}"`)
}
const [user, domainType_] = deviceId.split('_')
const domainType = parseInt(domainType_ || '0')
if (domainType === WAJIDDomains.LID || domainType === WAJIDDomains.HOSTED_LID) return id
const pnJid = `${user!}${device !== '0' ? `:${device}` : ''}@${domainType === WAJIDDomains.HOSTED ? 'hosted' : 's.whatsapp.net'}`
const lidForPN = await lidMapping.getLIDForPN(pnJid)
if (lidForPN) {
const lidAddr = jidToSignalProtocolAddress(lidForPN)
return lidAddr.toString()
}
}
return id
}
/**
* Extended SignalStorage with identity key management
* This type adds identity key operations to the standard Signal storage
@@ -764,30 +817,9 @@ function signalStorage(
ev?: BaileysEventEmitter,
logger?: ILogger
): ExtendedSignalStorage {
// Shared function to resolve PN signal address to LID if mapping exists
const resolveLIDSignalAddress = async (id: string): Promise<string> => {
if (id.includes('.')) {
const [deviceId, device] = id.split('.')
if (!deviceId) {
throw new Error('Missing device ID')
}
const [user, domainType_] = deviceId.split('_')
const domainType = parseInt(domainType_ || '0')
if (domainType === WAJIDDomains.LID || domainType === WAJIDDomains.HOSTED_LID) return id
const pnJid = `${user!}${device !== '0' ? `:${device}` : ''}@${domainType === WAJIDDomains.HOSTED ? 'hosted' : 's.whatsapp.net'}`
const lidForPN = await lidMapping.getLIDForPN(pnJid)
if (lidForPN) {
const lidAddr = jidToSignalProtocolAddress(lidForPN)
return lidAddr.toString()
}
}
return id
}
// Bind the top-level `resolveLIDSignalAddress` to this instance's lidMapping
// so call sites below stay readable.
const resolveAddr = (id: string) => resolveLIDSignalAddress(id, lidMapping)
// Delayed PreKey deletion: grace period to handle race conditions
// where two pkmsg with the same preKeyId arrive nearly simultaneously.
@@ -799,7 +831,7 @@ function signalStorage(
return {
loadSession: async (id: string) => {
try {
const wireJid = await resolveLIDSignalAddress(id)
const wireJid = await resolveAddr(id)
const { [wireJid]: sess } = await keys.get('session', [wireJid])
if (sess) {
@@ -812,7 +844,7 @@ function signalStorage(
return null
},
storeSession: async (id: string, session: libsignal.SessionRecord) => {
const wireJid = await resolveLIDSignalAddress(id)
const wireJid = await resolveAddr(id)
await keys.set({ session: { [wireJid]: session.serialize() } })
},
isTrustedIdentity: () => {
@@ -889,7 +921,7 @@ function signalStorage(
const timer = metrics.signalIdentityKeyOperations?.startTimer({ operation: 'load' })
try {
const wireJid = await resolveLIDSignalAddress(id)
const wireJid = await resolveAddr(id)
// Check cache first
const cached = identityKeyCache.get(wireJid)
@@ -923,7 +955,7 @@ function signalStorage(
const timer = metrics.signalIdentityKeyOperations?.startTimer({ operation: 'save' })
try {
const wireJid = await resolveLIDSignalAddress(id)
const wireJid = await resolveAddr(id)
const currentFingerprint = generateKeyFingerprint(identityKey)
// Load existing key (from cache or storage)
+40 -12
View File
@@ -67,33 +67,61 @@ export const DECRYPTION_RETRY_CONFIG = {
}
/**
* Retry options for decryption operations
* Uses exponential backoff with jitter to handle transient failures
* 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.
*/
export const DECRYPTION_RETRY_OPTIONS: RetryOptions = {
maxAttempts: 3,
baseDelay: 200, // 200ms base delay
maxDelay: 2000, // 2s max delay
maxAttempts: 2,
baseDelay: 200, // 200ms base delay (only used for unknown errors below)
maxDelay: 2000,
backoffStrategy: 'exponential',
backoffMultiplier: 2,
jitter: 0.2, // 20% jitter
collectMetrics: false, // No Prometheus metrics
jitter: 0.2,
collectMetrics: false,
operationName: 'message_decryption',
shouldRetry: (error: Error, attempt: number) => {
const errorMsg = error?.message || ''
// Always retry on session record errors (session might be syncing)
// 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.
if (DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(err => errorMsg.includes(err))) {
return attempt < 3 // Retry up to 3 times
return false
}
// Don't retry on corrupted session errors (need cleanup first)
// Corrupted session errors: Bad MAC / counter errors. Same reasoning —
// the keys are wrong and won't right themselves on retry.
if (DECRYPTION_RETRY_CONFIG.corruptedSessionErrors.some(err => errorMsg.includes(err))) {
return false
}
// Retry other transient errors
return attempt < 2 // Retry up to 2 times for unknown errors
// 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
}
}