fb-messenger-e2ee 0.1.1 → 0.1.3

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.
Files changed (111) hide show
  1. package/DOCS.md +41 -3
  2. package/README.md +27 -2
  3. package/dist/index.cjs +9017 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +910 -0
  6. package/dist/index.d.ts +910 -0
  7. package/dist/index.js +9049 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +19 -4
  10. package/bun.lock +0 -1014
  11. package/examples/basic-usage.ts +0 -52
  12. package/jest.config.ts +0 -18
  13. package/src/config/env.ts +0 -15
  14. package/src/controllers/client.controller.ts +0 -805
  15. package/src/controllers/dgw-handler.ts +0 -171
  16. package/src/controllers/e2ee-handler.ts +0 -832
  17. package/src/controllers/event-mapper.ts +0 -327
  18. package/src/core/client.ts +0 -151
  19. package/src/e2ee/application/e2ee-client.ts +0 -376
  20. package/src/e2ee/application/fanout-planner.ts +0 -79
  21. package/src/e2ee/application/outbound-message-cache.ts +0 -58
  22. package/src/e2ee/application/prekey-maintenance.ts +0 -55
  23. package/src/e2ee/application/retry-manager.ts +0 -156
  24. package/src/e2ee/facebook/facebook-protocol-utils.ts +0 -230
  25. package/src/e2ee/facebook/icdc-payload.ts +0 -31
  26. package/src/e2ee/index.ts +0 -76
  27. package/src/e2ee/internal.ts +0 -27
  28. package/src/e2ee/media/media-crypto.ts +0 -147
  29. package/src/e2ee/media/media-upload.ts +0 -90
  30. package/src/e2ee/message/builders/client-payload.ts +0 -54
  31. package/src/e2ee/message/builders/consumer-application.ts +0 -362
  32. package/src/e2ee/message/builders/message-application.ts +0 -64
  33. package/src/e2ee/message/builders/message-transport.ts +0 -84
  34. package/src/e2ee/message/codecs/protobuf-codecs.ts +0 -101
  35. package/src/e2ee/message/constants.ts +0 -4
  36. package/src/e2ee/message/message-builder.ts +0 -15
  37. package/src/e2ee/signal/prekey-manager.ts +0 -140
  38. package/src/e2ee/signal/signal-manager.ts +0 -365
  39. package/src/e2ee/store/device-json.ts +0 -47
  40. package/src/e2ee/store/device-repository.ts +0 -12
  41. package/src/e2ee/store/device-store.ts +0 -538
  42. package/src/e2ee/transport/binary/decoder.ts +0 -180
  43. package/src/e2ee/transport/binary/encoder.ts +0 -143
  44. package/src/e2ee/transport/binary/stanzas.ts +0 -97
  45. package/src/e2ee/transport/binary/tokens.ts +0 -64
  46. package/src/e2ee/transport/binary/wa-binary.ts +0 -8
  47. package/src/e2ee/transport/dgw/dgw-socket.ts +0 -345
  48. package/src/e2ee/transport/noise/noise-handshake.ts +0 -417
  49. package/src/e2ee/transport/noise/noise-socket.ts +0 -230
  50. package/src/index.ts +0 -34
  51. package/src/models/client.ts +0 -26
  52. package/src/models/config.ts +0 -14
  53. package/src/models/domain.ts +0 -299
  54. package/src/models/e2ee.ts +0 -295
  55. package/src/models/media.ts +0 -41
  56. package/src/models/messaging.ts +0 -88
  57. package/src/models/thread.ts +0 -69
  58. package/src/repositories/session.repository.ts +0 -20
  59. package/src/services/auth.service.ts +0 -55
  60. package/src/services/e2ee.service.ts +0 -174
  61. package/src/services/facebook-gateway.service.ts +0 -245
  62. package/src/services/icdc.service.ts +0 -177
  63. package/src/services/media.service.ts +0 -304
  64. package/src/services/messaging.service.ts +0 -28
  65. package/src/services/thread.service.ts +0 -199
  66. package/src/types/advanced-types.ts +0 -61
  67. package/src/types/fca-unofficial.d.ts +0 -212
  68. package/src/types/protocol-types.ts +0 -43
  69. package/src/utils/fca-utils.ts +0 -30
  70. package/src/utils/logger.ts +0 -24
  71. package/src/utils/mime.ts +0 -51
  72. package/tests/.env.example +0 -87
  73. package/tests/data/1x1.png +0 -0
  74. package/tests/data/example.txt +0 -1
  75. package/tests/data/file_example.mp3 +0 -0
  76. package/tests/data/file_example.mp4 +0 -0
  77. package/tests/integration.test.ts +0 -498
  78. package/tests/script/echo-e2ee.ts +0 -174
  79. package/tests/script/send-e2ee-media.ts +0 -227
  80. package/tests/script/send-e2ee-reaction.ts +0 -108
  81. package/tests/script/send-e2ee-unsend.ts +0 -115
  82. package/tests/script/send-typing.ts +0 -107
  83. package/tests/script/test-group-send.ts +0 -105
  84. package/tests/setup.ts +0 -3
  85. package/tests/types.test.ts +0 -57
  86. package/tests/unit/controllers/dgw-handler.test.ts +0 -60
  87. package/tests/unit/controllers/e2ee-handler.test.ts +0 -293
  88. package/tests/unit/controllers/event-mapper.test.ts +0 -252
  89. package/tests/unit/e2ee/application-helpers.test.ts +0 -418
  90. package/tests/unit/e2ee/device-store.test.ts +0 -126
  91. package/tests/unit/e2ee/facebook-protocol-utils.test.ts +0 -134
  92. package/tests/unit/e2ee/media-crypto.test.ts +0 -55
  93. package/tests/unit/e2ee/media-upload.test.ts +0 -60
  94. package/tests/unit/e2ee/message-builder.test.ts +0 -42
  95. package/tests/unit/e2ee/message-builders.test.ts +0 -230
  96. package/tests/unit/e2ee/noise-handshake.test.ts +0 -260
  97. package/tests/unit/e2ee/prekey-manager.test.ts +0 -55
  98. package/tests/unit/e2ee/wa-binary.test.ts +0 -127
  99. package/tests/unit/services/e2ee.service.test.ts +0 -101
  100. package/tests/unit/services/facebook-gateway.service.test.ts +0 -138
  101. package/tests/unit/services/media.service.test.ts +0 -169
  102. package/tests/unit/utils/fca-utils.test.ts +0 -48
  103. package/tsconfig.json +0 -19
  104. /package/{src/e2ee/message/proto → proto}/ArmadilloApplication.proto +0 -0
  105. /package/{src/e2ee/message/proto → proto}/ArmadilloICDC.proto +0 -0
  106. /package/{src/e2ee/message/proto → proto}/ConsumerApplication.proto +0 -0
  107. /package/{src/e2ee/message/proto → proto}/MessageApplication.proto +0 -0
  108. /package/{src/e2ee/message/proto → proto}/MessageTransport.proto +0 -0
  109. /package/{src/e2ee/message/proto → proto}/WACommon.proto +0 -0
  110. /package/{src/e2ee/message/proto → proto}/WAMediaTransport.proto +0 -0
  111. /package/{src/e2ee/message/proto → proto}/proto-writer.ts +0 -0
@@ -1,90 +0,0 @@
1
- import type { MediaUploadConfig, MediaUploadResult, MmsTypeStr } from "../../models/media.ts";
2
- export type { MediaUploadConfig, MediaUploadResult, MmsTypeStr };
3
-
4
- export function toMediaUploadToken(fileEncSHA256: Buffer): string {
5
- return fileEncSHA256.toString("base64").replace(/\+/g, "-").replace(/\//g, "_");
6
- }
7
-
8
- /**
9
- * Callback to refresh media upload config (e.g., re-query media_conn).
10
- */
11
- export type RefreshUploadConfigFn = () => Promise<MediaUploadConfig>;
12
-
13
- export interface UploadMediaOptions {
14
- /** Optional callback to refresh auth config on 401. */
15
- refreshConfig?: RefreshUploadConfigFn;
16
- /** Max retry attempts after refresh (default 1). */
17
- maxRetries?: number;
18
- }
19
-
20
- /**
21
- * Upload encrypted media bytes to Facebook's upload CDN.
22
- * Supports retry on 401 by refreshing the upload config.
23
- */
24
- export async function uploadMedia(
25
- config: MediaUploadConfig,
26
- data: Buffer,
27
- fileEncSHA256: Buffer,
28
- mmsType: MmsTypeStr,
29
- options?: UploadMediaOptions,
30
- ): Promise<MediaUploadResult> {
31
- if (!config.auth) {
32
- throw new Error("Missing media upload auth token; query media_conn before uploading E2EE media");
33
- }
34
-
35
- const token = toMediaUploadToken(fileEncSHA256);
36
- const maxRetries = options?.maxRetries ?? 1;
37
- let currentConfig = config;
38
-
39
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
40
- const uploadUrl = `https://${currentConfig.host}/wa-msgr/mms/${mmsType}/${token}?auth=${encodeURIComponent(currentConfig.auth)}&token=${encodeURIComponent(token)}`;
41
-
42
- const resp = await fetch(uploadUrl, {
43
- method: "POST",
44
- headers: {
45
- "Content-Type": "application/octet-stream",
46
- "Content-Length": String(data.length),
47
- "Origin": "https://www.facebook.com",
48
- "Referer": "https://www.facebook.com/",
49
- },
50
- body: data,
51
- });
52
-
53
- if (resp.ok) {
54
- const json = await resp.json() as Record<string, unknown>;
55
- const stringField = (...keys: string[]): string => {
56
- for (const key of keys) {
57
- const value = json[key];
58
- if (typeof value === "string") return value;
59
- if (typeof value === "number") return String(value);
60
- }
61
- return "";
62
- };
63
-
64
- return {
65
- url: stringField("url"),
66
- directPath: stringField("direct_path", "directPath"),
67
- handle: stringField("handle"),
68
- objectId: stringField("object_id", "objectID", "objectId"),
69
- };
70
- }
71
-
72
- // On 401, try to refresh config and retry
73
- if (resp.status === 401 && attempt < maxRetries && options?.refreshConfig) {
74
- const body = await resp.text().catch(() => "");
75
- console.warn(`Media upload 401 (attempt ${attempt + 1}), refreshing config...`, body.slice(0, 200));
76
- currentConfig = await options.refreshConfig();
77
- if (!currentConfig.auth) {
78
- throw new Error("Media upload refresh returned empty auth token");
79
- }
80
- continue;
81
- }
82
-
83
- // Non-401 or max retries exceeded
84
- const body = await resp.text().catch(() => "");
85
- throw new Error(`Media upload failed: HTTP ${resp.status} - ${body}`);
86
- }
87
-
88
- // Should not reach here, but just in case
89
- throw new Error("Media upload failed after retries");
90
- }
@@ -1,54 +0,0 @@
1
- import { ProtoWriter } from "../proto/proto-writer.ts";
2
-
3
- export interface ClientPayloadOptions {
4
- username: bigint;
5
- deviceId: number;
6
- fbCat?: Buffer;
7
- fbUserAgent?: Buffer;
8
- fbAppID?: bigint;
9
- fbDeviceID?: Buffer;
10
- fbCatBase64?: string;
11
- }
12
-
13
- // ClientPayload encoding (Handshake Step 3)
14
-
15
- export function encodeClientPayload(opts: ClientPayloadOptions): Buffer {
16
- // AppVersion: 301.0.2
17
- const appVersion = new ProtoWriter()
18
- .varint(1, 301)
19
- .varint(2, 0)
20
- .varint(3, 2)
21
- .build();
22
-
23
- // UserAgent
24
- const userAgent = new ProtoWriter()
25
- .varint(1, 32) // Platform = BLUE_WEB (32)
26
- .bytes(2, appVersion)
27
- .string(3, "000") // mcc
28
- .string(4, "000") // mnc
29
- .string(5, "") // osVersion
30
- .string(6, "Linux") // manufacturer (OSName)
31
- .string(7, "Chrome") // device (BrowserName)
32
- .string(8, "") // osBuildNumber
33
- .varint(10, 3) // releaseChannel = DEBUG
34
- .string(11, "en") // localeLanguage
35
- .string(12, "en") // localeCountry
36
- .build();
37
-
38
- const UserAgentStr = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
39
-
40
- // ClientPayload
41
- let w = new ProtoWriter()
42
- .uint64_varint(1, opts.username) // field 1
43
- .bool(3, false) // field 3: passive
44
- .bytes(5, userAgent) // field 5
45
- .varint(12, 1) // field 12: connectType (WIFI_UNKNOWN)
46
- .varint(13, 1) // field 13: connectReason (USER_ACTIVATED)
47
- .varint(18, opts.deviceId) // field 18: device
48
- .varint(20, 1) // field 20: product (MESSENGER)
49
- .bytes(21, opts.fbCatBase64 ? Buffer.from(opts.fbCatBase64) : Buffer.alloc(0)) // field 21: fbCat (as base64 string bytes)
50
- .bytes(22, opts.fbUserAgent ?? Buffer.from(UserAgentStr)) // field 22: fbUserAgent
51
- .bool(33, true); // field 33: pull
52
-
53
- return w.build();
54
- }
@@ -1,362 +0,0 @@
1
- import type { MediaFields } from "../../../models/e2ee.ts";
2
- import { ProtoWriter } from "../proto/proto-writer.ts";
3
-
4
- export type { MediaFields };
5
-
6
- // ConsumerApplication encoding
7
-
8
-
9
- // MessageBuilder (Pattern 3: Builder Pattern with Type Safety)
10
-
11
- type ContentType =
12
- | { type: "text"; text: string }
13
- | { type: "image"; media: MediaFields }
14
- | { type: "video"; media: MediaFields }
15
- | { type: "audio"; media: MediaFields }
16
- | { type: "document"; media: MediaFields }
17
- | { type: "sticker"; media: MediaFields }
18
- | { type: "reaction"; emoji: string; targetId: string }
19
- | { type: "edit"; text: string; targetId: string }
20
- | { type: "revoke"; targetId: string; fromMe: boolean };
21
-
22
- export class MessageBuilder {
23
- private content?: ContentType;
24
- private replyTo?: { id: string; senderJid: string };
25
-
26
- setReply(id: string, senderJid: string): this {
27
- this.replyTo = { id, senderJid };
28
- return this;
29
- }
30
-
31
- getReply() {
32
- return this.replyTo;
33
- }
34
-
35
- setText(text: string): this {
36
- this.content = { type: "text", text };
37
- return this;
38
- }
39
-
40
- setImage(media: MediaFields): this {
41
- this.content = { type: "image", media };
42
- return this;
43
- }
44
-
45
- setVideo(media: MediaFields): this {
46
- this.content = { type: "video", media };
47
- return this;
48
- }
49
-
50
- setAudio(media: MediaFields): this {
51
- this.content = { type: "audio", media };
52
- return this;
53
- }
54
-
55
- setDocument(media: MediaFields): this {
56
- this.content = { type: "document", media };
57
- return this;
58
- }
59
-
60
- setSticker(media: MediaFields): this {
61
- this.content = { type: "sticker", media };
62
- return this;
63
- }
64
-
65
- setReaction(emoji: string, targetId: string): this {
66
- this.content = { type: "reaction", emoji, targetId };
67
- return this;
68
- }
69
-
70
- setEdit(text: string, targetId: string): this {
71
- this.content = { type: "edit", text, targetId };
72
- return this;
73
- }
74
-
75
- setRevoke(targetId: string, fromMe: boolean): this {
76
- this.content = { type: "revoke", targetId, fromMe };
77
- return this;
78
- }
79
-
80
- build(): Buffer {
81
- if (!this.content) throw new Error("Message content not set");
82
-
83
- switch (this.content.type) {
84
- case "text":
85
- return encodeTextMessage(this.content.text);
86
- case "image":
87
- return encodeImageMessage(this.content.media);
88
- case "video":
89
- return encodeVideoMessage(this.content.media);
90
- case "audio":
91
- return encodeAudioMessage(this.content.media);
92
- case "document":
93
- return encodeDocumentMessage(this.content.media);
94
- case "sticker":
95
- return encodeStickerMessage(this.content.media);
96
- case "reaction":
97
- return encodeReactionMessage(this.content.targetId, this.content.emoji);
98
- case "edit":
99
- return encodeEditMessage(this.content.targetId, this.content.text);
100
- case "revoke":
101
- return encodeRevokeMessage(this.content.targetId, this.content.fromMe);
102
- default:
103
- throw new Error("Unknown content type");
104
- }
105
- }
106
- }
107
-
108
- /**
109
- * Encode a ConsumerApplication text message.
110
- * Field 1 = Payload { field 1 = Content { field 1 = MessageText { field 1 = text } } }
111
- */
112
- export function encodeTextMessage(text: string): Buffer {
113
- const msgText = encodeMessageText(text);
114
- const content = new ProtoWriter().bytes(1, msgText).build(); // oneof content field 1 = messageText
115
- const payload = new ProtoWriter().bytes(1, content).build(); // oneof payload field 1 = Content
116
- return new ProtoWriter().bytes(1, payload).build(); // ConsumerApplication { payload }
117
- }
118
-
119
- function encodeMessageText(text: string): Buffer {
120
- return new ProtoWriter().string(1, text).build();
121
- }
122
-
123
- const MEDIA_TRANSPORT_VERSION = 1;
124
-
125
- function encodeMediaSubProtocol(payload: Buffer): Buffer {
126
- return new ProtoWriter()
127
- .bytes(1, payload)
128
- .varint(2, MEDIA_TRANSPORT_VERSION)
129
- .build();
130
- }
131
-
132
- function mediaKeyTimestampSeconds(m: MediaFields): bigint {
133
- const seconds = m.mediaKeyTimestamp ?? Math.floor(Date.now() / 1000);
134
- return BigInt(Math.max(0, Math.trunc(seconds)));
135
- }
136
-
137
- function optionalDimension(value: number | undefined): number | undefined {
138
- return value === undefined ? undefined : Math.max(0, Math.trunc(value));
139
- }
140
-
141
- function encodeDownloadableThumbnailMetadata(m: MediaFields): Buffer | undefined {
142
- const width = optionalDimension(m.width);
143
- const height = optionalDimension(m.height);
144
- if (width === undefined && height === undefined) return undefined;
145
-
146
- let thumbnail = new ProtoWriter();
147
- if (width !== undefined) thumbnail = thumbnail.varint(3, width);
148
- if (height !== undefined) thumbnail = thumbnail.varint(4, height);
149
- return thumbnail.build();
150
- }
151
-
152
- /**
153
- * Encode WAMediaTransport.WAMediaTransport.
154
- *
155
- * Modern Messenger media messages no longer carry a flat media payload directly in
156
- * ImageMessage/VideoMessage/etc. They carry a WACommon.SubProtocol whose payload
157
- * is a concrete media transport (ImageTransport, VideoTransport, ...). Each media
158
- * transport then nests this common WAMediaTransport with the encrypted-file
159
- * checksums, key, direct path, mimetype, file length and upload object ID.
160
- */
161
- function encodeCommonMediaTransport(m: MediaFields, includeThumbnailMetadata: boolean): Buffer {
162
- const integral = new ProtoWriter()
163
- .bytes(1, m.fileSHA256)
164
- .bytes(2, m.mediaKey)
165
- .bytes(3, m.fileEncSHA256)
166
- .string(4, m.directPath)
167
- .uint64_varint(5, mediaKeyTimestampSeconds(m))
168
- .build();
169
-
170
- let ancillary = new ProtoWriter()
171
- .uint64_varint(1, BigInt(Math.max(0, Math.trunc(m.fileLength))))
172
- .string(2, m.mimeType);
173
-
174
- const thumbnail = includeThumbnailMetadata ? encodeDownloadableThumbnailMetadata(m) : undefined;
175
- if (thumbnail) ancillary = ancillary.bytes(3, thumbnail);
176
- if (m.objectId) ancillary = ancillary.string(4, m.objectId);
177
-
178
- return new ProtoWriter()
179
- .bytes(1, integral)
180
- .bytes(2, ancillary.build())
181
- .build();
182
- }
183
-
184
- function encodeMediaTransportIntegral(commonTransport: Buffer): Buffer {
185
- return new ProtoWriter().bytes(1, commonTransport).build();
186
- }
187
-
188
- function encodeImageTransportPayload(m: MediaFields): Buffer {
189
- const width = optionalDimension(m.width);
190
- const height = optionalDimension(m.height);
191
- const transport = encodeCommonMediaTransport(m, true);
192
-
193
- let ancillary = new ProtoWriter();
194
- if (height !== undefined) ancillary = ancillary.varint(1, height);
195
- if (width !== undefined) ancillary = ancillary.varint(2, width);
196
-
197
- return new ProtoWriter()
198
- .bytes(1, encodeMediaTransportIntegral(transport))
199
- .bytes(2, ancillary.build())
200
- .build();
201
- }
202
-
203
- function encodeVideoTransportPayload(m: MediaFields): Buffer {
204
- const width = optionalDimension(m.width);
205
- const height = optionalDimension(m.height);
206
- const seconds = optionalDimension(m.seconds);
207
- const transport = encodeCommonMediaTransport(m, true);
208
-
209
- let ancillary = new ProtoWriter();
210
- if (seconds !== undefined) ancillary = ancillary.varint(1, seconds);
211
- // Native Messenger sends gifPlayback explicitly as false for normal videos.
212
- ancillary = ancillary.bool(3, false);
213
- if (height !== undefined) ancillary = ancillary.varint(4, height);
214
- if (width !== undefined) ancillary = ancillary.varint(5, width);
215
-
216
- return new ProtoWriter()
217
- .bytes(1, encodeMediaTransportIntegral(transport))
218
- .bytes(2, ancillary.build())
219
- .build();
220
- }
221
-
222
- function encodeAudioTransportPayload(m: MediaFields): Buffer {
223
- const seconds = optionalDimension(m.seconds);
224
- const transport = encodeCommonMediaTransport(m, false);
225
-
226
- let ancillary = new ProtoWriter();
227
- if (seconds !== undefined) ancillary = ancillary.varint(1, seconds);
228
-
229
- return new ProtoWriter()
230
- .bytes(1, encodeMediaTransportIntegral(transport))
231
- .bytes(2, ancillary.build())
232
- .build();
233
- }
234
-
235
- function encodeDocumentTransportPayload(m: MediaFields): Buffer {
236
- const transport = encodeCommonMediaTransport(m, false);
237
- return new ProtoWriter()
238
- .bytes(1, encodeMediaTransportIntegral(transport))
239
- .bytes(2, Buffer.alloc(0))
240
- .build();
241
- }
242
-
243
- function encodeStickerTransportPayload(m: MediaFields): Buffer {
244
- const width = optionalDimension(m.width);
245
- const height = optionalDimension(m.height);
246
- const transport = encodeCommonMediaTransport(m, true);
247
-
248
- const integral = new ProtoWriter()
249
- .bytes(1, transport)
250
- .build();
251
-
252
- let ancillary = new ProtoWriter();
253
- if (height !== undefined) ancillary = ancillary.varint(2, height);
254
- if (width !== undefined) ancillary = ancillary.varint(3, width);
255
-
256
- return new ProtoWriter()
257
- .bytes(1, integral)
258
- .bytes(2, ancillary.build())
259
- .build();
260
- }
261
-
262
- /** Encode a ConsumerApplication image message. */
263
- export function encodeImageMessage(m: MediaFields): Buffer {
264
- let w = new ProtoWriter().bytes(1, encodeMediaSubProtocol(encodeImageTransportPayload(m)));
265
- if (m.caption) w = w.bytes(2, encodeMessageText(m.caption));
266
- const content = new ProtoWriter().bytes(2, w.build()).build();
267
- const payload = new ProtoWriter().bytes(1, content).build();
268
- return new ProtoWriter().bytes(1, payload).build();
269
- }
270
-
271
- /** Encode a ConsumerApplication video message. */
272
- export function encodeVideoMessage(m: MediaFields): Buffer {
273
- let w = new ProtoWriter().bytes(1, encodeMediaSubProtocol(encodeVideoTransportPayload(m)));
274
- if (m.caption) w = w.bytes(2, encodeMessageText(m.caption));
275
- const content = new ProtoWriter().bytes(9, w.build()).build();
276
- const payload = new ProtoWriter().bytes(1, content).build();
277
- return new ProtoWriter().bytes(1, payload).build();
278
- }
279
-
280
- /** Encode a ConsumerApplication audio/voice message. */
281
- export function encodeAudioMessage(m: MediaFields): Buffer {
282
- let w = new ProtoWriter().bytes(1, encodeMediaSubProtocol(encodeAudioTransportPayload(m)));
283
- if (m.ptt) w = w.bool(2, true);
284
- const content = new ProtoWriter().bytes(8, w.build()).build();
285
- const payload = new ProtoWriter().bytes(1, content).build();
286
- return new ProtoWriter().bytes(1, payload).build();
287
- }
288
-
289
- /** Encode a ConsumerApplication document message. */
290
- export function encodeDocumentMessage(m: MediaFields): Buffer {
291
- let w = new ProtoWriter().bytes(1, encodeMediaSubProtocol(encodeDocumentTransportPayload(m)));
292
- if (m.fileName) w = w.string(2, m.fileName);
293
- const content = new ProtoWriter().bytes(7, w.build()).build();
294
- const payload = new ProtoWriter().bytes(1, content).build();
295
- return new ProtoWriter().bytes(1, payload).build();
296
- }
297
-
298
- /** Encode a ConsumerApplication sticker message. */
299
- export function encodeStickerMessage(m: MediaFields): Buffer {
300
- const stickerMsg = new ProtoWriter()
301
- .bytes(1, encodeMediaSubProtocol(encodeStickerTransportPayload(m)))
302
- .build();
303
- const content = new ProtoWriter().bytes(12, stickerMsg).build();
304
- const payload = new ProtoWriter().bytes(1, content).build();
305
- return new ProtoWriter().bytes(1, payload).build();
306
- }
307
-
308
- export interface MessageKeyOptions {
309
- remoteJid?: string;
310
- fromMe?: boolean;
311
- participant?: string;
312
- }
313
-
314
- export interface ReactionMessageKeyOptions extends MessageKeyOptions {
315
- senderTimestampMs?: number;
316
- }
317
-
318
- function encodeMessageKey(messageId: string, keyOpts: MessageKeyOptions = {}): Buffer {
319
- let key = new ProtoWriter();
320
- if (keyOpts.remoteJid) key = key.string(1, keyOpts.remoteJid);
321
- if (typeof keyOpts.fromMe === "boolean") key = key.bool(2, keyOpts.fromMe);
322
- key = key.string(3, messageId);
323
- if (keyOpts.participant) key = key.string(4, keyOpts.participant);
324
- return key.build();
325
- }
326
-
327
- /** Encode a reaction message. */
328
- export function encodeReactionMessage(targetMessageId: string, emoji: string, keyOpts: ReactionMessageKeyOptions = {}): Buffer {
329
- const reaction = new ProtoWriter()
330
- .bytes(1, encodeMessageKey(targetMessageId, keyOpts))
331
- .string(2, emoji)
332
- .uint64_varint(4, BigInt(keyOpts.senderTimestampMs ?? Date.now()))
333
- .build();
334
- const content = new ProtoWriter().bytes(16, reaction).build();
335
- const payload = new ProtoWriter().bytes(1, content).build();
336
- return new ProtoWriter().bytes(1, payload).build();
337
- }
338
-
339
- /** Encode a message edit. */
340
- export function encodeEditMessage(targetMessageId: string, newText: string): Buffer {
341
- const key = new ProtoWriter().string(3, targetMessageId).build();
342
- const msgText = new ProtoWriter().string(1, newText).build();
343
- const edit = new ProtoWriter()
344
- .bytes(1, key)
345
- .bytes(2, msgText)
346
- .uint64_varint(3, BigInt(Date.now()))
347
- .build();
348
- const content = new ProtoWriter().bytes(19, edit).build();
349
- const payload = new ProtoWriter().bytes(1, content).build();
350
- return new ProtoWriter().bytes(1, payload).build();
351
- }
352
-
353
- /** Encode a revoke (unsend) message. */
354
- export function encodeRevokeMessage(messageId: string, keyOptsOrFromMe: MessageKeyOptions | boolean = true): Buffer {
355
- const keyOpts = typeof keyOptsOrFromMe === "boolean"
356
- ? { fromMe: keyOptsOrFromMe }
357
- : keyOptsOrFromMe;
358
- const revoke = new ProtoWriter().bytes(1, encodeMessageKey(messageId, keyOpts)).build();
359
- const applicationData = new ProtoWriter().bytes(1, revoke).build();
360
- const payload = new ProtoWriter().bytes(2, applicationData).build();
361
- return new ProtoWriter().bytes(1, payload).build();
362
- }
@@ -1,64 +0,0 @@
1
- import { createHmac, randomBytes } from "node:crypto";
2
- import { FB_CONSUMER_MESSAGE_VERSION } from "../constants.ts";
3
- import { ProtoWriter } from "../proto/proto-writer.ts";
4
-
5
- // MessageApplication encoding
6
-
7
- /**
8
- * Wrap a ConsumerApplication payload into a MessageApplication.
9
- * Returns (messageApp bytes, frankingKey, frankingTag).
10
- */
11
- export function encodeMessageApplication(
12
- consumerAppBytes: Buffer,
13
- replyTo?: { id: string; senderJid: string }
14
- ): {
15
- messageApp: Buffer;
16
- frankingKey: Buffer;
17
- frankingTag: Buffer;
18
- } {
19
- const frankingKey = randomBytes(32);
20
-
21
- // SubProtocol { payload=consumerAppBytes, version=FB_CONSUMER_MESSAGE_VERSION }
22
- const subProtocol = new ProtoWriter()
23
- .bytes(1, consumerAppBytes)
24
- .varint(2, FB_CONSUMER_MESSAGE_VERSION)
25
- .build();
26
-
27
- // MessageApplication.SubProtocolPayload {
28
- // futureProof = PLACEHOLDER (field 1)
29
- // consumerMessage = WACommon.SubProtocol (field 2)
30
- // }
31
- const payloadSubProto = new ProtoWriter()
32
- .varint(1, 0)
33
- .bytes(2, subProtocol)
34
- .build();
35
-
36
- // MessageApplication.Payload { subProtocol = payloadSubProto } (field 4)
37
- const appPayload = new ProtoWriter().bytes(4, payloadSubProto).build();
38
-
39
- // MessageApplication_Metadata { frankingKey=8, frankingVersion=9, quotedMessage=10 }
40
- let metadataWriter = new ProtoWriter()
41
- .bytes(8, frankingKey)
42
- .varint(9, 0); // frankingVersion
43
-
44
- if (replyTo) {
45
- const quoted = new ProtoWriter()
46
- .string(1, replyTo.id)
47
- .string(2, replyTo.senderJid)
48
- .build();
49
- metadataWriter = metadataWriter.bytes(10, quoted);
50
- }
51
-
52
- const metadata = metadataWriter.build();
53
-
54
- // MessageApplication { payload, metadata }
55
- const messageApp = new ProtoWriter()
56
- .bytes(1, appPayload)
57
- .bytes(2, metadata)
58
- .build();
59
-
60
- // frankingTag = HMAC-SHA256(frankingKey, messageApp)
61
- const frankingTag = createHmac("sha256", frankingKey).update(messageApp).digest();
62
-
63
- return { messageApp, frankingKey, frankingTag };
64
- }
@@ -1,84 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- import type { MessageTransportOptions } from "../../../models/e2ee.ts";
3
- import { FB_MESSAGE_APPLICATION_VERSION } from "../constants.ts";
4
- import { ProtoWriter } from "../proto/proto-writer.ts";
5
-
6
- export type { MessageTransportOptions };
7
-
8
- // MessageTransport encoding (plaintext before Signal encryption)
9
-
10
-
11
- /**
12
- * Encode the MessageTransport protobuf that will be fed into Signal cipher.
13
- */
14
- export function encodeMessageTransport(opts: MessageTransportOptions): Buffer {
15
- const padding = opts.padding ?? generatePadding();
16
-
17
- let payload: Buffer | undefined;
18
- if (opts.messageApp) {
19
- // Payload.ApplicationPayload (SubProtocol)
20
- const appPayload = new ProtoWriter()
21
- .bytes(1, opts.messageApp)
22
- .varint(2, FB_MESSAGE_APPLICATION_VERSION)
23
- .build();
24
-
25
- // Payload
26
- payload = new ProtoWriter()
27
- .bytes(1, appPayload)
28
- .varint(3, 0) // futureProof PLACEHOLDER
29
- .build();
30
- }
31
-
32
- // Protocol.Integral
33
- let integral = new ProtoWriter().bytes(1, padding);
34
- if (opts.dsm) {
35
- const dsmMsg = new ProtoWriter()
36
- .string(1, opts.dsm.destinationJid)
37
- .string(2, opts.dsm.phash)
38
- .build();
39
- integral = integral.bytes(2, dsmMsg);
40
- }
41
-
42
- // Protocol.Ancillary
43
- let ancillary = new ProtoWriter();
44
- if (opts.skdm) {
45
- const skdmMsg = new ProtoWriter()
46
- .string(1, opts.skdm.groupId)
47
- .bytes(2, opts.skdm.skdmBytes)
48
- .build();
49
- ancillary = ancillary.bytes(2, skdmMsg);
50
- }
51
- if (opts.backupDirective) {
52
- const actionType = opts.backupDirective.actionType === "REMOVE" ? 2 : 1;
53
- const backupDirectiveMsg = new ProtoWriter()
54
- .string(1, opts.backupDirective.messageId)
55
- .varint(2, actionType)
56
- .build();
57
- ancillary = ancillary.bytes(5, backupDirectiveMsg);
58
- }
59
- // Protocol
60
- const protocol = new ProtoWriter()
61
- .bytes(1, integral.build())
62
- .bytes(2, ancillary.build())
63
- .build();
64
-
65
- // MessageTransport
66
- const transport = new ProtoWriter();
67
- if (payload) transport.bytes(1, payload);
68
- return transport
69
- .bytes(2, protocol)
70
- .build();
71
- }
72
-
73
- // Helpers
74
-
75
- /**
76
- * Generate a random padding buffer.
77
- * Uses random length 1–255 and stores the padding length in the final byte.
78
- */
79
- function generatePadding(): Buffer {
80
- const len = (randomBytes(1)[0]! & 0xff) || 1;
81
- const pad = randomBytes(len);
82
- pad[len - 1] = len; // last byte = length (PKCS7-style)
83
- return pad;
84
- }