@lazyneoaz/metachat 1.1.4 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/metachat",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "type": "commonjs",
5
5
  "types": "src/types/index.d.ts",
6
6
  "description": "Advanced Facebook Chat API client for building Messenger bots — real-time messaging, thread management, MQTT, and session stability.",
@@ -74,6 +74,16 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
74
74
  let region = mqttEndpoint ? new URL(mqttEndpoint).searchParams.get("region")?.toUpperCase() : undefined;
75
75
  const irisSeqIDMatch = html.match(/irisSeqID:"(.+?)"/);
76
76
  const irisSeqID = irisSeqIDMatch ? irisSeqIDMatch[1] : null;
77
+
78
+ // Extract Facebook client-state tokens that are embedded in every page load.
79
+ // Real browsers include these in every subsequent request to prove the request
80
+ // originated from a genuine page load. Missing them is a strong bot signal.
81
+ const spinRMatch = html.match(/"__spin_r"\s*:\s*(\d+)/);
82
+ const spinTMatch = html.match(/"__spin_t"\s*:\s*(\d+)/);
83
+ const __spin_r = spinRMatch ? parseInt(spinRMatch[1], 10) : null;
84
+ const __spin_t = spinTMatch ? parseInt(spinTMatch[1], 10) : null;
85
+ const __spin_b = 'trunk';
86
+
77
87
  if (globalOptions.bypassRegion && mqttEndpoint) {
78
88
  const currentEndpoint = new URL(mqttEndpoint);
79
89
  currentEndpoint.searchParams.set('region', globalOptions.bypassRegion.toLowerCase());
@@ -116,6 +126,12 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
116
126
  validator: globalValidator,
117
127
  _emitter: emitter,
118
128
  ttstamp,
129
+ // Client-state spin tokens extracted from the page HTML.
130
+ // These are included in every subsequent request body to prove the
131
+ // request originated from a real page load (not a standalone script).
132
+ __spin_r,
133
+ __spin_t,
134
+ __spin_b,
119
135
  ...dtsgResult,
120
136
  };
121
137
  const defaultFuncs = utils.makeDefaults(html, userID, ctx);
@@ -62,13 +62,12 @@ function getRandomTimezone() {
62
62
  *
63
63
  * Key fingerprint rules enforced here:
64
64
  * - Navigate (page load): Accept = full HTML accept, Upgrade-Insecure-Requests = 1,
65
- * Sec-Fetch-Dest/Mode/Site = document/navigate/none. NO X-Requested-With, NO Origin.
65
+ * Sec-Fetch-Dest/Mode/Site = document/navigate/none. NO Origin.
66
66
  * - XHR (AJAX/API call): Accept = *\/*, Sec-Fetch-Dest/Mode/Site = empty/cors/same-origin,
67
- * Origin = host. NO Upgrade-Insecure-Requests.
67
+ * Origin = host, X-Requested-With = XMLHttpRequest. NO Upgrade-Insecure-Requests.
68
68
  *
69
- * Real Chrome never sends X-Requested-With automatically it is a jQuery/XHR
70
- * legacy header that Facebook's own modern client does NOT include and that
71
- * Facebook's bot-detection system flags when seen on navigation requests.
69
+ * X-Requested-With is sent consistently on all XHR/API calls. Consistency matters —
70
+ * sending it sometimes but not others is itself a detectable pattern.
72
71
  *
73
72
  * @param {string} url - The target URL.
74
73
  * @param {object} options - Global options from context.
@@ -147,12 +146,9 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
147
146
  if (ctx.region) {
148
147
  headers['X-MSGR-Region'] = ctx.region;
149
148
  }
150
- if (ctx.master) {
151
- const { __spin_r, __spin_b, __spin_t } = ctx.master;
152
- if (__spin_r) headers['X-Fb-Spin-R'] = String(__spin_r);
153
- if (__spin_b) headers['X-Fb-Spin-B'] = String(__spin_b);
154
- if (__spin_t) headers['X-Fb-Spin-T'] = String(__spin_t);
155
- }
149
+ if (ctx.__spin_r != null) headers['X-Fb-Spin-R'] = String(ctx.__spin_r);
150
+ if (ctx.__spin_b) headers['X-Fb-Spin-B'] = String(ctx.__spin_b);
151
+ if (ctx.__spin_t != null) headers['X-Fb-Spin-T'] = String(ctx.__spin_t);
156
152
  }
157
153
 
158
154
  if (customHeader) {
@@ -181,6 +177,7 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
181
177
  'Accept-Encoding': 'gzip, deflate, br',
182
178
  'Cache-Control': 'no-cache',
183
179
  'Connection': 'keep-alive',
180
+ 'DNT': '1',
184
181
  'Host': host,
185
182
  'Pragma': 'no-cache',
186
183
  'Referer': referer,
@@ -196,23 +193,18 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
196
193
  'User-Agent': userAgent,
197
194
  };
198
195
 
199
- // Upgrade-Insecure-Requests is a navigation-only hint — browsers never
200
- // include it on XHR/fetch calls. Sending it on XHR is a bot fingerprint.
201
196
  if (!isXhr) {
202
197
  headers['Upgrade-Insecure-Requests'] = '1';
203
198
  }
204
199
 
205
- // Origin is only present on cross-origin or same-origin XHR/fetch requests.
206
- // Real browsers do NOT send Origin on top-level page navigations (Sec-Fetch-Mode: navigate).
207
200
  if (isXhr) {
208
201
  headers['Origin'] = `https://${host}`;
202
+ // X-Requested-With is sent consistently on all XHR/API requests.
203
+ // Consistency matters — sending it sometimes but not others is itself
204
+ // a detectable pattern. Keep it always-on for API calls.
205
+ headers['X-Requested-With'] = 'XMLHttpRequest';
209
206
  }
210
207
 
211
- // X-Requested-With is NOT a browser-native header — it is a jQuery/XHR
212
- // convention that Facebook's own modern web client does not set.
213
- // Sending it (especially on navigate requests) is a well-known bot fingerprint
214
- // that Facebook's detection system keys on. We never send it.
215
-
216
208
  if (isWindows || isMac || isLinux) {
217
209
  headers['Sec-Ch-Ua-Arch'] = '"x86"';
218
210
  headers['Sec-Ch-Ua-Bitness'] = '"64"';
@@ -225,12 +217,11 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
225
217
  if (ctx.region) {
226
218
  headers['X-MSGR-Region'] = ctx.region;
227
219
  }
228
- if (ctx.master) {
229
- const { __spin_r, __spin_b, __spin_t } = ctx.master;
230
- if (__spin_r) headers['X-Fb-Spin-R'] = String(__spin_r);
231
- if (__spin_b) headers['X-Fb-Spin-B'] = String(__spin_b);
232
- if (__spin_t) headers['X-Fb-Spin-T'] = String(__spin_t);
233
- }
220
+ // Spin tokens are stored directly on ctx by buildAPI — ctx.master is
221
+ // never populated and reading from it was dead code that never fired.
222
+ if (ctx.__spin_r != null) headers['X-Fb-Spin-R'] = String(ctx.__spin_r);
223
+ if (ctx.__spin_b) headers['X-Fb-Spin-B'] = String(ctx.__spin_b);
224
+ if (ctx.__spin_t != null) headers['X-Fb-Spin-T'] = String(ctx.__spin_t);
234
225
  }
235
226
 
236
227
  if (customHeader) {
@@ -59,7 +59,6 @@ function makeDefaults(html, userID, ctx) {
59
59
  const network = require("./axios");
60
60
  const constants = require("./constants");
61
61
 
62
- let reqCounter = 1;
63
62
  const revision = constants.getFrom(html, 'revision":', ",");
64
63
 
65
64
  /**
@@ -68,10 +67,13 @@ function makeDefaults(html, userID, ctx) {
68
67
  * @returns {object} The merged object.
69
68
  */
70
69
  function mergeWithDefaults(obj) {
70
+ // __req must be random, not a sequential counter. A counter that resets to 1
71
+ // on every session start is a textbook bot fingerprint that Facebook's systems
72
+ // detect. Random values match real Chrome behaviour.
71
73
  const newObj = {
72
74
  av: userID,
73
75
  __user: userID,
74
- __req: (reqCounter++).toString(36),
76
+ __req: Math.floor(Math.random() * 36 ** 2).toString(36),
75
77
  __rev: revision,
76
78
  __a: 1,
77
79
  ...(ctx && {
@@ -80,6 +82,15 @@ function makeDefaults(html, userID, ctx) {
80
82
  }),
81
83
  };
82
84
 
85
+ // Include Facebook's client-state spin tokens when present. These are
86
+ // extracted from the page HTML at login time and prove each request
87
+ // originated from a real page load, not a standalone script.
88
+ if (ctx) {
89
+ if (ctx.__spin_r != null) newObj.__spin_r = ctx.__spin_r;
90
+ if (ctx.__spin_t != null) newObj.__spin_t = ctx.__spin_t;
91
+ if (ctx.__spin_b) newObj.__spin_b = ctx.__spin_b;
92
+ }
93
+
83
94
  if (!obj) return newObj;
84
95
 
85
96
  for (const prop in obj) {
@@ -111,7 +111,7 @@ class TokenRefreshManager {
111
111
  const resp = await utils.get(
112
112
  probeUrl,
113
113
  ctx.jar,
114
- { reason: 'reconnect', __a: 1, __req: 'probe' },
114
+ { reason: 'reconnect', __a: 1, __req: Math.floor(Math.random() * 36 ** 2).toString(36) },
115
115
  ctx.globalOptions,
116
116
  probeCtx
117
117
  );
@@ -236,6 +236,14 @@ class TokenRefreshManager {
236
236
  ctx.__rev = revisionMatch[1];
237
237
  }
238
238
 
239
+ // Refresh spin tokens alongside fb_dtsg — they are page-state values
240
+ // embedded in every HTML response and must stay current so requests
241
+ // continue to pass Facebook's client-state validation.
242
+ const spinRMatch = html.match(/"__spin_r"\s*:\s*(\d+)/);
243
+ const spinTMatch = html.match(/"__spin_t"\s*:\s*(\d+)/);
244
+ if (spinRMatch) ctx.__spin_r = parseInt(spinRMatch[1], 10);
245
+ if (spinTMatch) ctx.__spin_t = parseInt(spinTMatch[1], 10);
246
+
239
247
  this.lastRefresh = Date.now();
240
248
  this.failureCount = 0;
241
249