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 +2 -2
- package/README.md +2 -2
- package/dist/index.cjs +78 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -13
- package/dist/index.d.ts +44 -13
- package/dist/index.js +78 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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/
|
|
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(
|
|
3980
|
-
this.replyTo =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
6182
|
-
|
|
6183
|
-
|
|
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
|
|
8037
|
-
const
|
|
8038
|
-
const
|
|
8039
|
-
this.icdcService.setCookies(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 ? {
|
|
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({
|