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/DOCS.md CHANGED
@@ -18,8 +18,8 @@ new FBClient(options: ClientOptions)
18
18
 
19
19
  | Option | Type | Description |
20
20
  |---|---|---|
21
- | `appStatePath` | `string` | Path to Facebook appState/cookies JSON. |
22
- | `appState` | `any[] \| string` | Optional in-memory appState alternative. |
21
+ | `appStatePath` | `string` | Path to Facebook appState JSON (contains the active login cookies/session data). |
22
+ | `appState` | `any[] \| string` | Optional in-memory appState/cookies alternative (cookies string or array). |
23
23
  | `sessionStorePath` | `string` | Optional path for login/session metadata used by E2EE bootstrap. |
24
24
  | `platform` | `"facebook" \| "messenger"` | Login platform hint. Defaults to `"facebook"`. |
25
25
 
package/README.md CHANGED
@@ -28,7 +28,7 @@
28
28
  - **Language**: [TypeScript](https://www.typescriptlang.org/)
29
29
  - **Encryption**: [@signalapp/libsignal-client](https://github.com/signalapp/libsignal-client)
30
30
  - **Protocol**: ProtobufJS + manual WA-binary/protobuf encoders
31
- - **Auth bootstrap bridge**: [fca-unofficial](https://github.com/VangBanLaNhat/fca-unofficial) is used internally only for appState login/CAT bootstrap
31
+ - **Auth bootstrap bridge**: [fca-unofficial](https://github.com/VangBanLaNhat/fca-unofficial) is used internally only for appState login/CAT bootstrap (where appState represents the active Facebook login cookies)
32
32
 
33
33
  ---
34
34
 
@@ -60,7 +60,7 @@ const { FBClient } = require("fb-messenger-e2ee");
60
60
  import { FBClient } from "fb-messenger-e2ee";
61
61
 
62
62
  const client = new FBClient({
63
- appStatePath: "./appstate.json",
63
+ appStatePath: "./appstate.json", // Path to Facebook appState JSON (contains active login cookies/session data)
64
64
  sessionStorePath: "./session.json",
65
65
  platform: "facebook",
66
66
  });
package/dist/index.cjs CHANGED
@@ -3976,8 +3976,8 @@ init_cjs_shims();
3976
3976
  var MessageBuilder = class {
3977
3977
  content;
3978
3978
  replyTo;
3979
- setReply(id, senderJid) {
3980
- this.replyTo = { id, senderJid };
3979
+ setReply(replyTo) {
3980
+ this.replyTo = replyTo;
3981
3981
  return this;
3982
3982
  }
3983
3983
  getReply() {
@@ -4201,7 +4201,7 @@ function encodeMessageApplication(consumerAppBytes, replyTo) {
4201
4201
  const appPayload = new ProtoWriter().bytes(4, payloadSubProto).build();
4202
4202
  let metadataWriter = new ProtoWriter().bytes(8, frankingKey).varint(9, 0);
4203
4203
  if (replyTo) {
4204
- const quoted = new ProtoWriter().string(1, replyTo.id).string(2, replyTo.senderJid).build();
4204
+ const quoted = new ProtoWriter().string(1, replyTo.messageId).string(2, replyTo.chatJid).string(3, replyTo.senderJid).build();
4205
4205
  metadataWriter = metadataWriter.bytes(10, quoted);
4206
4206
  }
4207
4207
  const metadata = metadataWriter.build();
@@ -5573,7 +5573,16 @@ var E2EEClient = class {
5573
5573
  async buildDMTextFanoutPayloads(opts) {
5574
5574
  const builder = new MessageBuilder().setText(opts.text);
5575
5575
  if (opts.replyToId && opts.replyToSenderJid) {
5576
- builder.setReply(opts.replyToId, opts.replyToSenderJid);
5576
+ const replyTo = {
5577
+ messageId: opts.replyToId,
5578
+ // chatJid = remoteJID: the thread/chat that holds the quoted message
5579
+ // For DM, this is the peer's bare JID (toJid).
5580
+ chatJid: opts.toJid,
5581
+ // senderJid = participant: who sent the original message.
5582
+ // Caller must supply the correct sender JID via replyToSenderJid.
5583
+ senderJid: opts.replyToSenderJid
5584
+ };
5585
+ builder.setReply(replyTo);
5577
5586
  }
5578
5587
  const consumerApp = builder.build();
5579
5588
  const { messageApp, frankingTag } = encodeMessageApplication(consumerApp, builder.getReply());
@@ -5608,7 +5617,14 @@ var E2EEClient = class {
5608
5617
  async encryptGroupText(groupJid, selfJid, text, messageId, replyToId, replyToSenderJid) {
5609
5618
  const builder = new MessageBuilder().setText(text);
5610
5619
  if (replyToId && replyToSenderJid) {
5611
- builder.setReply(replyToId, replyToSenderJid);
5620
+ const replyTo = {
5621
+ messageId: replyToId,
5622
+ // chatJid = remoteJID: for a group message, this is the group JID
5623
+ chatJid: groupJid,
5624
+ // participant: the specific member who sent the original message
5625
+ senderJid: replyToSenderJid
5626
+ };
5627
+ builder.setReply(replyTo);
5612
5628
  }
5613
5629
  const consumerApp = builder.build();
5614
5630
  const { messageApp, frankingTag } = encodeMessageApplication(consumerApp, builder.getReply());
@@ -5952,6 +5968,9 @@ var EncryptedFrameSocket = class {
5952
5968
  const len = header.readUIntBE(0, 3);
5953
5969
  logger.debug("FacebookE2EESocket", `RAW frame header: ${header.toString("hex")} (len=${len})`);
5954
5970
  const payload = await this.ws.readRaw(len);
5971
+ if (!payload) {
5972
+ throw new Error("Socket closed while reading frame payload");
5973
+ }
5955
5974
  try {
5956
5975
  const decrypted = this.decryptFrame(payload);
5957
5976
  logger.debug("FacebookE2EESocket", `Decrypt successful, result length: ${decrypted.length}`);
@@ -6091,11 +6110,12 @@ var FacebookE2EESocket = class extends import_node_events2.EventEmitter {
6091
6110
  url;
6092
6111
  heartbeatInterval = null;
6093
6112
  isConnected = false;
6113
+ cookieHeader = "";
6094
6114
  constructor(endpoint) {
6095
6115
  super();
6096
6116
  this.url = endpoint;
6097
6117
  }
6098
- async connect(noisePrivKey, authPayload) {
6118
+ async connect(noisePrivKey, authPayload, cookies) {
6099
6119
  return new Promise((resolve, reject) => {
6100
6120
  try {
6101
6121
  let readFromBuffer2 = function(len) {
@@ -6120,19 +6140,25 @@ var FacebookE2EESocket = class extends import_node_events2.EventEmitter {
6120
6140
  return res;
6121
6141
  };
6122
6142
  var readFromBuffer = readFromBuffer2;
6143
+ if (cookies) this.cookieHeader = cookies;
6123
6144
  const wsUrl = new URL(this.url);
6124
6145
  wsUrl.searchParams.set("cid", "client-" + Date.now());
6125
6146
  const UserAgentStr = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
6147
+ const wsHeaders = {
6148
+ "Origin": "https://www.facebook.com",
6149
+ "User-Agent": UserAgentStr
6150
+ };
6151
+ if (this.cookieHeader) {
6152
+ wsHeaders["Cookie"] = this.cookieHeader;
6153
+ }
6126
6154
  this.ws = new WebSocket(wsUrl.toString(), void 0, {
6127
- headers: {
6128
- "Origin": "https://www.facebook.com",
6129
- "User-Agent": UserAgentStr
6130
- }
6155
+ headers: wsHeaders
6131
6156
  });
6132
6157
  this.ws.binaryType = "arraybuffer";
6133
6158
  let handshakeResolved = false;
6134
6159
  let streamBuffer = [];
6135
6160
  let streamLen = 0;
6161
+ let waitingRejectFn = null;
6136
6162
  let waitingResolver = null;
6137
6163
  let waitingLen = 0;
6138
6164
  this.ws.addEventListener("message", (ev) => {
@@ -6157,8 +6183,9 @@ var FacebookE2EESocket = class extends import_node_events2.EventEmitter {
6157
6183
  const targetLen = len || 0;
6158
6184
  if (targetLen === 0) {
6159
6185
  if (streamLen > 0) return Promise.resolve(readFromBuffer2(streamLen));
6160
- return new Promise((resolve2) => {
6186
+ return new Promise((resolve2, reject2) => {
6161
6187
  waitingLen = 1;
6188
+ waitingRejectFn = reject2;
6162
6189
  waitingResolver = () => resolve2(readFromBuffer2(streamLen));
6163
6190
  });
6164
6191
  }
@@ -6168,8 +6195,9 @@ var FacebookE2EESocket = class extends import_node_events2.EventEmitter {
6168
6195
  if (this.ws?.readyState !== WebSocket.OPEN) {
6169
6196
  return Promise.reject(new Error("WebSocket not open"));
6170
6197
  }
6171
- return new Promise((resolve2) => {
6198
+ return new Promise((resolve2, reject2) => {
6172
6199
  waitingLen = targetLen;
6200
+ waitingRejectFn = reject2;
6173
6201
  waitingResolver = resolve2;
6174
6202
  });
6175
6203
  },
@@ -6178,10 +6206,11 @@ var FacebookE2EESocket = class extends import_node_events2.EventEmitter {
6178
6206
  this.ws.addEventListener("close", () => {
6179
6207
  this.isConnected = false;
6180
6208
  this.stopHeartbeat();
6181
- if (waitingResolver) {
6182
- waitingResolver(null);
6183
- waitingResolver = null;
6209
+ if (waitingRejectFn) {
6210
+ waitingRejectFn(new Error("WebSocket closed while waiting for data"));
6211
+ waitingRejectFn = null;
6184
6212
  }
6213
+ waitingResolver = null;
6185
6214
  this.emit("disconnected");
6186
6215
  });
6187
6216
  this.ws.addEventListener("open", async () => {
@@ -8033,10 +8062,10 @@ var ClientController = class {
8033
8062
  logger.debug("ClientController", "Fetching CAT...");
8034
8063
  const fbCat = await this.gateway.fetchCAT(this.requireApi());
8035
8064
  if (!ds.jidDevice) {
8036
- const api = this.requireApi();
8037
- const appState = api.getAppState?.() || [];
8038
- const cookieStr = appState.map((c) => `${c.key}=${c.value}`).join("; ");
8039
- this.icdcService.setCookies(cookieStr);
8065
+ const api2 = this.requireApi();
8066
+ const appState2 = api2.getAppState?.() || [];
8067
+ const cookieStr2 = appState2.map((c) => `${c.key}=${c.value}`).join("; ");
8068
+ this.icdcService.setCookies(cookieStr2);
8040
8069
  logger.info("ClientController", "Registering new device via ICDC...");
8041
8070
  const waDeviceId = await this.icdcService.register(userId, fbCat, "2220391788200892", ds);
8042
8071
  ds.jidDevice = waDeviceId;
@@ -8085,7 +8114,12 @@ var ClientController = class {
8085
8114
  logger.error("E2EE", "Frame error:", err);
8086
8115
  }
8087
8116
  });
8088
- await noiseSocket.connect(ds.noiseKeyPriv, clientPayload);
8117
+ const api = this.requireApi();
8118
+ const appState = api.getAppState?.() || [];
8119
+ const cookieStr = appState.map((c) => `${c.key}=${c.value}`).join("; ");
8120
+ logger.info("ClientController", "FCA appState length:", appState.length);
8121
+ logger.info("ClientController", "Cookie string snippet:", cookieStr.substring(0, 100));
8122
+ await noiseSocket.connect(ds.noiseKeyPriv, clientPayload, cookieStr || void 0);
8089
8123
  this.e2eeSocket = noiseSocket;
8090
8124
  await new Promise((resolve, reject) => {
8091
8125
  const timeout = setTimeout(() => reject(new Error("Handshake timeout")), 1e4);
@@ -8208,15 +8242,25 @@ var ClientController = class {
8208
8242
  if (this.e2eeConnected && isE2EE) {
8209
8243
  let messageId;
8210
8244
  if (isGroup) {
8211
- messageId = await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
8245
+ messageId = await this.sendE2EEGroupText(
8246
+ input.threadId,
8247
+ input.text,
8248
+ input.replyToMessageId,
8249
+ input.replyToSenderJid
8250
+ );
8212
8251
  } else {
8213
- messageId = await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
8252
+ messageId = await this.sendE2EEText(
8253
+ input.threadId,
8254
+ input.text,
8255
+ input.replyToMessageId,
8256
+ input.replyToSenderJid
8257
+ );
8214
8258
  }
8215
8259
  return { messageId, timestampMs: now() };
8216
8260
  }
8217
8261
  return this.messagingService.sendText(this.requireApi(), input);
8218
8262
  }
8219
- async sendE2EEText(threadId, text, replyToMessageId) {
8263
+ async sendE2EEText(threadId, text, replyToMessageId, replyToSenderJid) {
8220
8264
  if (!this.e2eeSocket) throw new Error("E2EE not connected");
8221
8265
  const e2eeClient = this.e2eeService.getClient();
8222
8266
  const selfJid = this.getSelfE2EEJid();
@@ -8228,7 +8272,9 @@ var ClientController = class {
8228
8272
  text,
8229
8273
  isGroup: false,
8230
8274
  replyToId: replyToMessageId,
8231
- replyToSenderJid: replyToMessageId ? toJid : void 0
8275
+ // For DM: participant = sender of original msg. If caller knows who sent it, use that;
8276
+ // otherwise fall back to the peer's JID (correct for messages they sent to us).
8277
+ replyToSenderJid: replyToMessageId ? replyToSenderJid ?? toJid : void 0
8232
8278
  });
8233
8279
  const participantNodes = [];
8234
8280
  const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
@@ -8274,7 +8320,7 @@ var ClientController = class {
8274
8320
  logger.info("ClientController", `E2EE DM message sent to ${toJid} with ${participantNodes.length} devices`);
8275
8321
  return messageId;
8276
8322
  }
8277
- async sendE2EEGroupText(groupJid, text, replyToMessageId) {
8323
+ async sendE2EEGroupText(groupJid, text, replyToMessageId, replyToSenderJid) {
8278
8324
  if (!this.e2eeSocket) throw new Error("E2EE not connected");
8279
8325
  const e2eeClient = this.e2eeService.getClient();
8280
8326
  const selfJid = this.getSelfE2EEJid();
@@ -8290,7 +8336,8 @@ var ClientController = class {
8290
8336
  text,
8291
8337
  messageId,
8292
8338
  replyToMessageId,
8293
- void 0
8339
+ replyToSenderJid
8340
+ // pass through — undefined when not replying
8294
8341
  );
8295
8342
  const participantNodes = [];
8296
8343
  for (const deviceJid of deviceJids) {
@@ -8630,7 +8677,11 @@ var ClientController = class {
8630
8677
  const consumerApp = buildMessage(media.mediaFields);
8631
8678
  const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(
8632
8679
  consumerApp,
8633
- input.replyToMessageId ? { id: input.replyToMessageId, senderJid: toJid } : void 0
8680
+ input.replyToMessageId ? {
8681
+ messageId: input.replyToMessageId,
8682
+ chatJid: toJid,
8683
+ senderJid: input.replyToSenderJid ?? toJid
8684
+ } : void 0
8634
8685
  );
8635
8686
  const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
8636
8687
  const selfDevicePayload = e2eeClient.buildMessageTransport({