fb-messenger-e2ee 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -225,6 +225,13 @@ interface SendMessageInput {
225
225
  threadId: string;
226
226
  text: string;
227
227
  replyToMessageId?: string;
228
+ /**
229
+ * JID of the original message sender for E2EE reply (QuotedMessage.participant field).
230
+ * - For DM: the bare JID of the peer (e.g. "123456789.0@msgr"); if omitted, defaults to the peer's JID.
231
+ * - For group: the full member JID who sent the original message (e.g. "123456789.160@msgr"); required
232
+ * for the server to correctly thread the reply in the group conversation.
233
+ */
234
+ replyToSenderJid?: string;
228
235
  }
229
236
  interface SendMediaInput {
230
237
  threadId: string;
@@ -234,6 +241,11 @@ interface SendMediaInput {
234
241
  mimeType?: string;
235
242
  caption?: string;
236
243
  replyToMessageId?: string;
244
+ /**
245
+ * JID of the original message sender for E2EE reply (QuotedMessage.participant field).
246
+ * See SendMessageInput.replyToSenderJid for details.
247
+ */
248
+ replyToSenderJid?: string;
237
249
  /** Optional media width in pixels for E2EE image/video/sticker payloads. */
238
250
  width?: number;
239
251
  /** Optional media height in pixels for E2EE image/video/sticker payloads. */
@@ -749,6 +761,38 @@ declare class DeviceStore implements IdentityKeyStore, SessionStore, PreKeyStore
749
761
  getPreKeyCount(): number;
750
762
  }
751
763
 
764
+ /**
765
+ * Reply-to (quoted message) metadata for MessageApplication.Metadata.QuotedMessage.
766
+ *
767
+ * Proto schema (WAMsgApplication.proto):
768
+ * message QuotedMessage {
769
+ * optional string stanzaID = 1; // message ID of the quoted message
770
+ * optional string remoteJID = 2; // chat JID (thread) that contains the quoted message
771
+ * optional string participant = 3; // sender JID of the quoted message (required for groups)
772
+ * }
773
+ */
774
+ interface ReplyToMeta {
775
+ /** Message ID (stanzaID) of the quoted message. */
776
+ messageId: string;
777
+ /** Chat JID (remoteJID) — the thread where the quoted message lives. */
778
+ chatJid: string;
779
+ /**
780
+ * Sender JID (participant) of the quoted message.
781
+ * - For DM: same as chatJid (the peer's bare JID).
782
+ * - For group: the specific member JID who sent the original message.
783
+ */
784
+ senderJid: string;
785
+ }
786
+ /**
787
+ * Wrap a ConsumerApplication payload into a MessageApplication.
788
+ * Returns (messageApp bytes, frankingKey, frankingTag).
789
+ */
790
+ declare function encodeMessageApplication(consumerAppBytes: Buffer, replyTo?: ReplyToMeta): {
791
+ messageApp: Buffer;
792
+ frankingKey: Buffer;
793
+ frankingTag: Buffer;
794
+ };
795
+
752
796
  /**
753
797
  * Encode a ConsumerApplication text message.
754
798
  * Field 1 = Payload { field 1 = Content { field 1 = MessageText { field 1 = text } } }
@@ -779,19 +823,6 @@ declare function encodeEditMessage(targetMessageId: string, newText: string): Bu
779
823
  /** Encode a revoke (unsend) message. */
780
824
  declare function encodeRevokeMessage(messageId: string, keyOptsOrFromMe?: MessageKeyOptions | boolean): Buffer;
781
825
 
782
- /**
783
- * Wrap a ConsumerApplication payload into a MessageApplication.
784
- * Returns (messageApp bytes, frankingKey, frankingTag).
785
- */
786
- declare function encodeMessageApplication(consumerAppBytes: Buffer, replyTo?: {
787
- id: string;
788
- senderJid: string;
789
- }): {
790
- messageApp: Buffer;
791
- frankingKey: Buffer;
792
- frankingTag: Buffer;
793
- };
794
-
795
826
  /**
796
827
  * Encode the MessageTransport protobuf that will be fed into Signal cipher.
797
828
  */
package/dist/index.d.ts CHANGED
@@ -225,6 +225,13 @@ interface SendMessageInput {
225
225
  threadId: string;
226
226
  text: string;
227
227
  replyToMessageId?: string;
228
+ /**
229
+ * JID of the original message sender for E2EE reply (QuotedMessage.participant field).
230
+ * - For DM: the bare JID of the peer (e.g. "123456789.0@msgr"); if omitted, defaults to the peer's JID.
231
+ * - For group: the full member JID who sent the original message (e.g. "123456789.160@msgr"); required
232
+ * for the server to correctly thread the reply in the group conversation.
233
+ */
234
+ replyToSenderJid?: string;
228
235
  }
229
236
  interface SendMediaInput {
230
237
  threadId: string;
@@ -234,6 +241,11 @@ interface SendMediaInput {
234
241
  mimeType?: string;
235
242
  caption?: string;
236
243
  replyToMessageId?: string;
244
+ /**
245
+ * JID of the original message sender for E2EE reply (QuotedMessage.participant field).
246
+ * See SendMessageInput.replyToSenderJid for details.
247
+ */
248
+ replyToSenderJid?: string;
237
249
  /** Optional media width in pixels for E2EE image/video/sticker payloads. */
238
250
  width?: number;
239
251
  /** Optional media height in pixels for E2EE image/video/sticker payloads. */
@@ -749,6 +761,38 @@ declare class DeviceStore implements IdentityKeyStore, SessionStore, PreKeyStore
749
761
  getPreKeyCount(): number;
750
762
  }
751
763
 
764
+ /**
765
+ * Reply-to (quoted message) metadata for MessageApplication.Metadata.QuotedMessage.
766
+ *
767
+ * Proto schema (WAMsgApplication.proto):
768
+ * message QuotedMessage {
769
+ * optional string stanzaID = 1; // message ID of the quoted message
770
+ * optional string remoteJID = 2; // chat JID (thread) that contains the quoted message
771
+ * optional string participant = 3; // sender JID of the quoted message (required for groups)
772
+ * }
773
+ */
774
+ interface ReplyToMeta {
775
+ /** Message ID (stanzaID) of the quoted message. */
776
+ messageId: string;
777
+ /** Chat JID (remoteJID) — the thread where the quoted message lives. */
778
+ chatJid: string;
779
+ /**
780
+ * Sender JID (participant) of the quoted message.
781
+ * - For DM: same as chatJid (the peer's bare JID).
782
+ * - For group: the specific member JID who sent the original message.
783
+ */
784
+ senderJid: string;
785
+ }
786
+ /**
787
+ * Wrap a ConsumerApplication payload into a MessageApplication.
788
+ * Returns (messageApp bytes, frankingKey, frankingTag).
789
+ */
790
+ declare function encodeMessageApplication(consumerAppBytes: Buffer, replyTo?: ReplyToMeta): {
791
+ messageApp: Buffer;
792
+ frankingKey: Buffer;
793
+ frankingTag: Buffer;
794
+ };
795
+
752
796
  /**
753
797
  * Encode a ConsumerApplication text message.
754
798
  * Field 1 = Payload { field 1 = Content { field 1 = MessageText { field 1 = text } } }
@@ -779,19 +823,6 @@ declare function encodeEditMessage(targetMessageId: string, newText: string): Bu
779
823
  /** Encode a revoke (unsend) message. */
780
824
  declare function encodeRevokeMessage(messageId: string, keyOptsOrFromMe?: MessageKeyOptions | boolean): Buffer;
781
825
 
782
- /**
783
- * Wrap a ConsumerApplication payload into a MessageApplication.
784
- * Returns (messageApp bytes, frankingKey, frankingTag).
785
- */
786
- declare function encodeMessageApplication(consumerAppBytes: Buffer, replyTo?: {
787
- id: string;
788
- senderJid: string;
789
- }): {
790
- messageApp: Buffer;
791
- frankingKey: Buffer;
792
- frankingTag: Buffer;
793
- };
794
-
795
826
  /**
796
827
  * Encode the MessageTransport protobuf that will be fed into Signal cipher.
797
828
  */
package/dist/index.js CHANGED
@@ -3969,8 +3969,8 @@ init_esm_shims();
3969
3969
  var MessageBuilder = class {
3970
3970
  content;
3971
3971
  replyTo;
3972
- setReply(id, senderJid) {
3973
- this.replyTo = { id, senderJid };
3972
+ setReply(replyTo) {
3973
+ this.replyTo = replyTo;
3974
3974
  return this;
3975
3975
  }
3976
3976
  getReply() {
@@ -4194,7 +4194,7 @@ function encodeMessageApplication(consumerAppBytes, replyTo) {
4194
4194
  const appPayload = new ProtoWriter().bytes(4, payloadSubProto).build();
4195
4195
  let metadataWriter = new ProtoWriter().bytes(8, frankingKey).varint(9, 0);
4196
4196
  if (replyTo) {
4197
- const quoted = new ProtoWriter().string(1, replyTo.id).string(2, replyTo.senderJid).build();
4197
+ const quoted = new ProtoWriter().string(1, replyTo.messageId).string(2, replyTo.chatJid).string(3, replyTo.senderJid).build();
4198
4198
  metadataWriter = metadataWriter.bytes(10, quoted);
4199
4199
  }
4200
4200
  const metadata = metadataWriter.build();
@@ -5597,7 +5597,16 @@ var E2EEClient = class {
5597
5597
  async buildDMTextFanoutPayloads(opts) {
5598
5598
  const builder = new MessageBuilder().setText(opts.text);
5599
5599
  if (opts.replyToId && opts.replyToSenderJid) {
5600
- builder.setReply(opts.replyToId, opts.replyToSenderJid);
5600
+ const replyTo = {
5601
+ messageId: opts.replyToId,
5602
+ // chatJid = remoteJID: the thread/chat that holds the quoted message
5603
+ // For DM, this is the peer's bare JID (toJid).
5604
+ chatJid: opts.toJid,
5605
+ // senderJid = participant: who sent the original message.
5606
+ // Caller must supply the correct sender JID via replyToSenderJid.
5607
+ senderJid: opts.replyToSenderJid
5608
+ };
5609
+ builder.setReply(replyTo);
5601
5610
  }
5602
5611
  const consumerApp = builder.build();
5603
5612
  const { messageApp, frankingTag } = encodeMessageApplication(consumerApp, builder.getReply());
@@ -5632,7 +5641,14 @@ var E2EEClient = class {
5632
5641
  async encryptGroupText(groupJid, selfJid, text, messageId, replyToId, replyToSenderJid) {
5633
5642
  const builder = new MessageBuilder().setText(text);
5634
5643
  if (replyToId && replyToSenderJid) {
5635
- builder.setReply(replyToId, replyToSenderJid);
5644
+ const replyTo = {
5645
+ messageId: replyToId,
5646
+ // chatJid = remoteJID: for a group message, this is the group JID
5647
+ chatJid: groupJid,
5648
+ // participant: the specific member who sent the original message
5649
+ senderJid: replyToSenderJid
5650
+ };
5651
+ builder.setReply(replyTo);
5636
5652
  }
5637
5653
  const consumerApp = builder.build();
5638
5654
  const { messageApp, frankingTag } = encodeMessageApplication(consumerApp, builder.getReply());
@@ -5985,6 +6001,9 @@ var EncryptedFrameSocket = class {
5985
6001
  const len = header.readUIntBE(0, 3);
5986
6002
  logger.debug("FacebookE2EESocket", `RAW frame header: ${header.toString("hex")} (len=${len})`);
5987
6003
  const payload = await this.ws.readRaw(len);
6004
+ if (!payload) {
6005
+ throw new Error("Socket closed while reading frame payload");
6006
+ }
5988
6007
  try {
5989
6008
  const decrypted = this.decryptFrame(payload);
5990
6009
  logger.debug("FacebookE2EESocket", `Decrypt successful, result length: ${decrypted.length}`);
@@ -6124,11 +6143,12 @@ var FacebookE2EESocket = class extends EventEmitter2 {
6124
6143
  url;
6125
6144
  heartbeatInterval = null;
6126
6145
  isConnected = false;
6146
+ cookieHeader = "";
6127
6147
  constructor(endpoint) {
6128
6148
  super();
6129
6149
  this.url = endpoint;
6130
6150
  }
6131
- async connect(noisePrivKey, authPayload) {
6151
+ async connect(noisePrivKey, authPayload, cookies) {
6132
6152
  return new Promise((resolve, reject) => {
6133
6153
  try {
6134
6154
  let readFromBuffer2 = function(len) {
@@ -6153,19 +6173,25 @@ var FacebookE2EESocket = class extends EventEmitter2 {
6153
6173
  return res;
6154
6174
  };
6155
6175
  var readFromBuffer = readFromBuffer2;
6176
+ if (cookies) this.cookieHeader = cookies;
6156
6177
  const wsUrl = new URL(this.url);
6157
6178
  wsUrl.searchParams.set("cid", "client-" + Date.now());
6158
6179
  const UserAgentStr = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
6180
+ const wsHeaders = {
6181
+ "Origin": "https://www.facebook.com",
6182
+ "User-Agent": UserAgentStr
6183
+ };
6184
+ if (this.cookieHeader) {
6185
+ wsHeaders["Cookie"] = this.cookieHeader;
6186
+ }
6159
6187
  this.ws = new WebSocket(wsUrl.toString(), void 0, {
6160
- headers: {
6161
- "Origin": "https://www.facebook.com",
6162
- "User-Agent": UserAgentStr
6163
- }
6188
+ headers: wsHeaders
6164
6189
  });
6165
6190
  this.ws.binaryType = "arraybuffer";
6166
6191
  let handshakeResolved = false;
6167
6192
  let streamBuffer = [];
6168
6193
  let streamLen = 0;
6194
+ let waitingRejectFn = null;
6169
6195
  let waitingResolver = null;
6170
6196
  let waitingLen = 0;
6171
6197
  this.ws.addEventListener("message", (ev) => {
@@ -6190,8 +6216,9 @@ var FacebookE2EESocket = class extends EventEmitter2 {
6190
6216
  const targetLen = len || 0;
6191
6217
  if (targetLen === 0) {
6192
6218
  if (streamLen > 0) return Promise.resolve(readFromBuffer2(streamLen));
6193
- return new Promise((resolve2) => {
6219
+ return new Promise((resolve2, reject2) => {
6194
6220
  waitingLen = 1;
6221
+ waitingRejectFn = reject2;
6195
6222
  waitingResolver = () => resolve2(readFromBuffer2(streamLen));
6196
6223
  });
6197
6224
  }
@@ -6201,8 +6228,9 @@ var FacebookE2EESocket = class extends EventEmitter2 {
6201
6228
  if (this.ws?.readyState !== WebSocket.OPEN) {
6202
6229
  return Promise.reject(new Error("WebSocket not open"));
6203
6230
  }
6204
- return new Promise((resolve2) => {
6231
+ return new Promise((resolve2, reject2) => {
6205
6232
  waitingLen = targetLen;
6233
+ waitingRejectFn = reject2;
6206
6234
  waitingResolver = resolve2;
6207
6235
  });
6208
6236
  },
@@ -6211,10 +6239,11 @@ var FacebookE2EESocket = class extends EventEmitter2 {
6211
6239
  this.ws.addEventListener("close", () => {
6212
6240
  this.isConnected = false;
6213
6241
  this.stopHeartbeat();
6214
- if (waitingResolver) {
6215
- waitingResolver(null);
6216
- waitingResolver = null;
6242
+ if (waitingRejectFn) {
6243
+ waitingRejectFn(new Error("WebSocket closed while waiting for data"));
6244
+ waitingRejectFn = null;
6217
6245
  }
6246
+ waitingResolver = null;
6218
6247
  this.emit("disconnected");
6219
6248
  });
6220
6249
  this.ws.addEventListener("open", async () => {
@@ -8066,10 +8095,10 @@ var ClientController = class {
8066
8095
  logger.debug("ClientController", "Fetching CAT...");
8067
8096
  const fbCat = await this.gateway.fetchCAT(this.requireApi());
8068
8097
  if (!ds.jidDevice) {
8069
- const api = this.requireApi();
8070
- const appState = api.getAppState?.() || [];
8071
- const cookieStr = appState.map((c) => `${c.key}=${c.value}`).join("; ");
8072
- this.icdcService.setCookies(cookieStr);
8098
+ const api2 = this.requireApi();
8099
+ const appState2 = api2.getAppState?.() || [];
8100
+ const cookieStr2 = appState2.map((c) => `${c.key}=${c.value}`).join("; ");
8101
+ this.icdcService.setCookies(cookieStr2);
8073
8102
  logger.info("ClientController", "Registering new device via ICDC...");
8074
8103
  const waDeviceId = await this.icdcService.register(userId, fbCat, "2220391788200892", ds);
8075
8104
  ds.jidDevice = waDeviceId;
@@ -8118,7 +8147,12 @@ var ClientController = class {
8118
8147
  logger.error("E2EE", "Frame error:", err);
8119
8148
  }
8120
8149
  });
8121
- await noiseSocket.connect(ds.noiseKeyPriv, clientPayload);
8150
+ const api = this.requireApi();
8151
+ const appState = api.getAppState?.() || [];
8152
+ const cookieStr = appState.map((c) => `${c.key}=${c.value}`).join("; ");
8153
+ logger.info("ClientController", "FCA appState length:", appState.length);
8154
+ logger.info("ClientController", "Cookie string snippet:", cookieStr.substring(0, 100));
8155
+ await noiseSocket.connect(ds.noiseKeyPriv, clientPayload, cookieStr || void 0);
8122
8156
  this.e2eeSocket = noiseSocket;
8123
8157
  await new Promise((resolve, reject) => {
8124
8158
  const timeout = setTimeout(() => reject(new Error("Handshake timeout")), 1e4);
@@ -8241,15 +8275,25 @@ var ClientController = class {
8241
8275
  if (this.e2eeConnected && isE2EE) {
8242
8276
  let messageId;
8243
8277
  if (isGroup) {
8244
- messageId = await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
8278
+ messageId = await this.sendE2EEGroupText(
8279
+ input.threadId,
8280
+ input.text,
8281
+ input.replyToMessageId,
8282
+ input.replyToSenderJid
8283
+ );
8245
8284
  } else {
8246
- messageId = await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
8285
+ messageId = await this.sendE2EEText(
8286
+ input.threadId,
8287
+ input.text,
8288
+ input.replyToMessageId,
8289
+ input.replyToSenderJid
8290
+ );
8247
8291
  }
8248
8292
  return { messageId, timestampMs: now() };
8249
8293
  }
8250
8294
  return this.messagingService.sendText(this.requireApi(), input);
8251
8295
  }
8252
- async sendE2EEText(threadId, text, replyToMessageId) {
8296
+ async sendE2EEText(threadId, text, replyToMessageId, replyToSenderJid) {
8253
8297
  if (!this.e2eeSocket) throw new Error("E2EE not connected");
8254
8298
  const e2eeClient = this.e2eeService.getClient();
8255
8299
  const selfJid = this.getSelfE2EEJid();
@@ -8261,7 +8305,9 @@ var ClientController = class {
8261
8305
  text,
8262
8306
  isGroup: false,
8263
8307
  replyToId: replyToMessageId,
8264
- replyToSenderJid: replyToMessageId ? toJid : void 0
8308
+ // For DM: participant = sender of original msg. If caller knows who sent it, use that;
8309
+ // otherwise fall back to the peer's JID (correct for messages they sent to us).
8310
+ replyToSenderJid: replyToMessageId ? replyToSenderJid ?? toJid : void 0
8265
8311
  });
8266
8312
  const participantNodes = [];
8267
8313
  const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
@@ -8307,7 +8353,7 @@ var ClientController = class {
8307
8353
  logger.info("ClientController", `E2EE DM message sent to ${toJid} with ${participantNodes.length} devices`);
8308
8354
  return messageId;
8309
8355
  }
8310
- async sendE2EEGroupText(groupJid, text, replyToMessageId) {
8356
+ async sendE2EEGroupText(groupJid, text, replyToMessageId, replyToSenderJid) {
8311
8357
  if (!this.e2eeSocket) throw new Error("E2EE not connected");
8312
8358
  const e2eeClient = this.e2eeService.getClient();
8313
8359
  const selfJid = this.getSelfE2EEJid();
@@ -8323,7 +8369,8 @@ var ClientController = class {
8323
8369
  text,
8324
8370
  messageId,
8325
8371
  replyToMessageId,
8326
- void 0
8372
+ replyToSenderJid
8373
+ // pass through — undefined when not replying
8327
8374
  );
8328
8375
  const participantNodes = [];
8329
8376
  for (const deviceJid of deviceJids) {
@@ -8663,7 +8710,11 @@ var ClientController = class {
8663
8710
  const consumerApp = buildMessage(media.mediaFields);
8664
8711
  const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(
8665
8712
  consumerApp,
8666
- input.replyToMessageId ? { id: input.replyToMessageId, senderJid: toJid } : void 0
8713
+ input.replyToMessageId ? {
8714
+ messageId: input.replyToMessageId,
8715
+ chatJid: toJid,
8716
+ senderJid: input.replyToSenderJid ?? toJid
8717
+ } : void 0
8667
8718
  );
8668
8719
  const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
8669
8720
  const selfDevicePayload = e2eeClient.buildMessageTransport({