@lazyneoaz/metachat 4.0.7 → 4.0.8

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.cjs CHANGED
@@ -25142,13 +25142,15 @@ function getHeaders(url2, options, ctx, customHeader) {
25142
25142
  headers["priority"] = "u=0, i";
25143
25143
  }
25144
25144
  if (isXhr) {
25145
- headers["X-Requested-With"] = "XMLHttpRequest";
25146
- headers["Pragma"] = "no-cache";
25147
- headers["Cache-Control"] = "no-cache";
25145
+ if (!isGraphQl) {
25146
+ headers["X-Requested-With"] = "XMLHttpRequest";
25147
+ headers["Pragma"] = "no-cache";
25148
+ headers["Cache-Control"] = "no-cache";
25149
+ }
25148
25150
  headers["priority"] = "u=1, i";
25149
25151
  }
25150
25152
  if (isGraphQl) {
25151
- headers["x-asbd-id"] = "129477";
25153
+ headers["x-asbd-id"] = String(ctx?.asbdId || DEFAULT_ASBD_ID);
25152
25154
  const lsd = ctx?.lsd || ctx?.fb_lsd;
25153
25155
  if (lsd) headers["x-fb-lsd"] = sanitizeHeaderValue2(lsd);
25154
25156
  const friendlyName = customHeader?.["x-fb-friendly-name"] || customHeader?.["X-Fb-Friendly-Name"];
@@ -25219,10 +25221,11 @@ function getFingerprintParams(options) {
25219
25221
  dpr: String(profile.devicePixelRatio)
25220
25222
  };
25221
25223
  }
25222
- var meta;
25224
+ var DEFAULT_ASBD_ID, meta;
25223
25225
  var init_headers = __esm({
25224
25226
  "src/utils/headers.ts"() {
25225
25227
  init_user_agents();
25228
+ DEFAULT_ASBD_ID = "359341";
25226
25229
  meta = (prop2) => new RegExp(`<meta property="${prop2}" content="([^"]*)"`);
25227
25230
  }
25228
25231
  });
@@ -75560,10 +75563,13 @@ __export(utils_exports2, {
75560
75563
  NUM_TO_MONTH: () => NUM_TO_MONTH,
75561
75564
  NetworkError: () => NetworkError,
75562
75565
  NotFoundError: () => NotFoundError,
75566
+ PING_GRACE_MS: () => PING_GRACE_MS,
75563
75567
  PermissionError: () => PermissionError,
75564
75568
  RateLimitError: () => RateLimitError,
75569
+ RealtimeHealth: () => RealtimeHealth,
75565
75570
  RecoverySupervisor: () => RecoverySupervisor,
75566
75571
  SESSION_COOKIE_NAMES: () => SESSION_COOKIE_NAMES,
75572
+ SILENT_WINDOW_MS: () => SILENT_WINDOW_MS,
75567
75573
  TimeoutError: () => TimeoutError,
75568
75574
  ValidationError: () => ValidationError,
75569
75575
  WINDOW_MS: () => WINDOW_MS,
@@ -75588,6 +75594,7 @@ __export(utils_exports2, {
75588
75594
  delay: () => delay2,
75589
75595
  ensureCometTokens: () => ensureCometTokens,
75590
75596
  error: () => error,
75597
+ extractAsbdId: () => extractAsbdId,
75591
75598
  extractAttachmentIds: () => extractAttachmentIds,
75592
75599
  extractCookies: () => extractCookies,
75593
75600
  extractCookiesFromBody: () => extractCookiesFromBody,
@@ -75638,6 +75645,7 @@ __export(utils_exports2, {
75638
75645
  invalidateCometTokens: () => invalidateCometTokens,
75639
75646
  isAuthFailure: () => isAuthFailure,
75640
75647
  isBreakerOpen: () => isBreakerOpen,
75648
+ isLoggedOutDocument: () => isLoggedOutDocument,
75641
75649
  isReadableStream: () => isReadableStream2,
75642
75650
  isStaleTokenError: () => isStaleTokenError,
75643
75651
  json: () => json2,
@@ -75675,6 +75683,7 @@ __export(utils_exports2, {
75675
75683
  setProxy: () => setProxy2,
75676
75684
  sleep: () => sleep,
75677
75685
  startLiveness: () => startLiveness,
75686
+ startRealtimeSupervisor: () => startRealtimeSupervisor,
75678
75687
  startRecovery: () => startRecovery,
75679
75688
  success: () => success,
75680
75689
  tripBreaker: () => tripBreaker,
@@ -91032,6 +91041,9 @@ function extractLsd(html3) {
91032
91041
  function extractDtsg(html3) {
91033
91042
  return getFrom3(html3, '["DTSGInitialData",[],{"token":"', '"}') || getFrom3(html3, 'name="fb_dtsg" value="', '"') || (html3.match(/"DTSGInitialData",\[\],\{"token":"([^"]+)"/) || [])[1] || "";
91034
91043
  }
91044
+ function extractAsbdId(html3) {
91045
+ return (html3.match(/"asbd_id":(\d+)/) || [])[1] || (html3.match(/x-asbd-id["']?\s*[:=]\s*["']?(\d+)/i) || [])[1] || (html3.match(/"ASBD_ID":(\d+)/) || [])[1] || "";
91046
+ }
91035
91047
  function extractSpin(html3) {
91036
91048
  const out = {};
91037
91049
  const spinB = (html3.match(/"__spin_b":"([^"]+)"/) || [])[1];
@@ -91098,6 +91110,8 @@ async function ensureCometTokens(ctx, defaultFuncs, force = false) {
91098
91110
  const spin = extractSpin(html3);
91099
91111
  if (Object.keys(spin).length) ctx.__spin = { ...ctx.__spin || {}, ...spin };
91100
91112
  if (spin.__rev) ctx.revision = spin.__rev;
91113
+ const asbd = extractAsbdId(html3);
91114
+ if (asbd) ctx.asbdId = asbd;
91101
91115
  ctx.lsd = lsd;
91102
91116
  ctx.fb_dtsg = dtsg;
91103
91117
  ctx.jazoest = recomputeJazoest(dtsg);
@@ -91368,6 +91382,13 @@ function isAuthFailure(value) {
91368
91382
  }
91369
91383
  return false;
91370
91384
  }
91385
+ function isLoggedOutDocument(html3) {
91386
+ if (typeof html3 !== "string" || html3.length < 200) return false;
91387
+ if (!/<html/i.test(html3)) return false;
91388
+ const hasAuthedTokens = /DTSGInitialData|"LSD",\[\]/.test(html3);
91389
+ const looksLikeLogin = /name="(?:email|pass)"|id="email"|"login_form"|LoginForm|login_form/i.test(html3) || /Continue as |Use another profile|Create new account/i.test(html3) || /You must log in to continue|Please log in|log in to continue/i.test(html3);
91390
+ return looksLikeLogin && !hasAuthedTokens;
91391
+ }
91371
91392
  async function withSessionRetry(ctx, defaultFuncs, operation, options = {}) {
91372
91393
  const attempts = Math.max(1, options.attempts ?? 2);
91373
91394
  let lastError;
@@ -91474,6 +91495,8 @@ async function warmUpSession(ctx, getter, options = {}) {
91474
91495
  const spin = extractSpin(html3);
91475
91496
  if (Object.keys(spin).length) ctx.__spin = { ...ctx.__spin || {}, ...spin };
91476
91497
  if (spin.__rev) ctx.revision = spin.__rev;
91498
+ const asbd = extractAsbdId(html3);
91499
+ if (asbd) ctx.asbdId = asbd;
91477
91500
  tokensSeeded = true;
91478
91501
  }
91479
91502
  } else if (url2.includes("/messages")) {
@@ -91673,7 +91696,15 @@ var RecoverySupervisor = class {
91673
91696
  async runOnce() {
91674
91697
  this.state.checks += 1;
91675
91698
  this.emit("check", `health check #${this.state.checks}`);
91676
- if (this.runtime.isAuthenticated()) {
91699
+ let healthy = this.runtime.isAuthenticated();
91700
+ if (healthy && this.runtime.probeLiveness) {
91701
+ try {
91702
+ healthy = await this.runtime.probeLiveness();
91703
+ } catch {
91704
+ healthy = true;
91705
+ }
91706
+ }
91707
+ if (healthy) {
91677
91708
  this.markHealthy("session authenticated");
91678
91709
  return;
91679
91710
  }
@@ -91772,6 +91803,131 @@ function startRecovery(ctx, runtime, options = {}) {
91772
91803
  return supervisor;
91773
91804
  }
91774
91805
 
91806
+ // src/utils/realtime-health.ts
91807
+ init_pacing();
91808
+ var SILENT_WINDOW_MS = 6 * 60 * 1e3;
91809
+ var PING_GRACE_MS = 45 * 1e3;
91810
+ var RealtimeHealth = class {
91811
+ constructor(now = () => Date.now()) {
91812
+ this.now = now;
91813
+ this.frames = 0;
91814
+ this.lastFrameAt = 0;
91815
+ this.connectedAt = 0;
91816
+ this.probes = 0;
91817
+ this.forcedReconnects = 0;
91818
+ this.lastPingAt = 0;
91819
+ this.startedAt = Date.now();
91820
+ this.startedAt = this.now();
91821
+ }
91822
+ /** Record any inbound frame (message, pong, anything from the broker). */
91823
+ noteFrame() {
91824
+ this.frames += 1;
91825
+ this.lastFrameAt = this.now();
91826
+ }
91827
+ /** Record a successful connect. */
91828
+ noteConnected() {
91829
+ this.connectedAt = this.now();
91830
+ this.lastFrameAt = this.now();
91831
+ }
91832
+ /** Record that the transport reports itself disconnected. */
91833
+ noteDisconnected() {
91834
+ this.lastFrameAt = 0;
91835
+ }
91836
+ /** Record an active ping being sent. */
91837
+ notePing() {
91838
+ this.lastPingAt = this.now();
91839
+ }
91840
+ sinceLastFrame() {
91841
+ const base = this.lastFrameAt || this.connectedAt || this.startedAt;
91842
+ return this.now() - base;
91843
+ }
91844
+ /**
91845
+ * Decide what to do about the current socket.
91846
+ *
91847
+ * - `disconnected` — the transport already knows it is down; reconnect.
91848
+ * - `dead` — connected on paper, silent past the window, and a ping raised no
91849
+ * traffic within the grace period; reconnect.
91850
+ * - `suspect` — silent past the window but a ping was only just sent; wait.
91851
+ * - `quiet` — silent but inside the window; normal.
91852
+ * - `healthy` — recent traffic.
91853
+ */
91854
+ verdict(connected) {
91855
+ if (!connected) return "disconnected";
91856
+ const silent = this.sinceLastFrame();
91857
+ if (silent < SILENT_WINDOW_MS / 2) return "healthy";
91858
+ if (silent < SILENT_WINDOW_MS) return "quiet";
91859
+ if (this.lastPingAt && this.now() - this.lastPingAt < PING_GRACE_MS) return "suspect";
91860
+ return "dead";
91861
+ }
91862
+ /** A snapshot for diagnostics. */
91863
+ state(connected) {
91864
+ return {
91865
+ connected,
91866
+ sinceLastFrameMs: this.sinceLastFrame(),
91867
+ frames: this.frames,
91868
+ probes: this.probes,
91869
+ forcedReconnects: this.forcedReconnects,
91870
+ verdict: this.verdict(connected)
91871
+ };
91872
+ }
91873
+ noteProbe() {
91874
+ this.probes += 1;
91875
+ }
91876
+ noteForcedReconnect() {
91877
+ this.forcedReconnects += 1;
91878
+ }
91879
+ };
91880
+ function startRealtimeSupervisor(options) {
91881
+ const health = new RealtimeHealth();
91882
+ const interval = options.intervalMs ?? 3 * 60 * 1e3;
91883
+ let stopped = false;
91884
+ let timer = null;
91885
+ const evaluate = () => {
91886
+ const connected = options.isConnected();
91887
+ const verdict = health.verdict(connected);
91888
+ health.noteProbe();
91889
+ if (verdict === "dead") {
91890
+ const pingSent = options.ping ? options.ping() : false;
91891
+ if (pingSent) {
91892
+ health.notePing();
91893
+ } else {
91894
+ health.noteForcedReconnect();
91895
+ options.reconnect("realtime socket silent and unpinnable");
91896
+ }
91897
+ }
91898
+ const state = health.state(connected);
91899
+ try {
91900
+ options.onVerdict?.(state);
91901
+ } catch {
91902
+ }
91903
+ return state;
91904
+ };
91905
+ const schedule = () => {
91906
+ if (stopped) return;
91907
+ timer = setTimeout(
91908
+ () => {
91909
+ timer = null;
91910
+ evaluate();
91911
+ schedule();
91912
+ },
91913
+ Math.max(1e3, humanDelay(interval, interval * 0.3))
91914
+ );
91915
+ if (timer && typeof timer.unref === "function") timer.unref();
91916
+ };
91917
+ schedule();
91918
+ return {
91919
+ health,
91920
+ checkNow: evaluate,
91921
+ stop: () => {
91922
+ stopped = true;
91923
+ if (timer) {
91924
+ clearTimeout(timer);
91925
+ timer = null;
91926
+ }
91927
+ }
91928
+ };
91929
+ }
91930
+
91775
91931
  // src/utils/index.ts
91776
91932
  async function json2(url2, jar2, qs2, options, ctx, customHeader) {
91777
91933
  try {
@@ -91989,6 +92145,7 @@ async function buildAPI(html3, jar2, netData, globalOptions, fbLinkFunc, errorRe
91989
92145
  const lsd = lsdConfig && lsdConfig.token || extractLsd(html3) || void 0;
91990
92146
  const spin = extractSpin(html3);
91991
92147
  const revision = spin.__rev || getFrom(html3, 'revision":', ",").trim();
92148
+ const asbdId = extractAsbdId(html3) || void 0;
91992
92149
  const clientIDData = findConfig("MqttWebDeviceID");
91993
92150
  const clientID = clientIDData ? clientIDData.clientID : void 0;
91994
92151
  const mqttConfigData = findConfig("MqttWebConfig");
@@ -92030,7 +92187,8 @@ async function buildAPI(html3, jar2, netData, globalOptions, fbLinkFunc, errorRe
92030
92187
  ...dtsgResult,
92031
92188
  ...lsd ? { lsd } : {},
92032
92189
  ...Object.keys(spin).length ? { __spin: spin } : {},
92033
- ...revision ? { revision } : {}
92190
+ ...revision ? { revision } : {},
92191
+ ...asbdId ? { asbdId } : {}
92034
92192
  };
92035
92193
  if (lsd && dtsg) {
92036
92194
  ctx.__cometTokens = { lsd, fb_dtsg: dtsg, fetchedAt: Date.now(), failures: 0 };
@@ -100728,7 +100886,9 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100728
100886
  attempts: 0,
100729
100887
  delay: MQTT_CONFIG.INITIAL_RETRY_DELAY,
100730
100888
  throttleDelay: MQTT_CONFIG.THROTTLE_DELAY,
100731
- generation: 0
100889
+ generation: 0,
100890
+ health: null,
100891
+ healthSupervisor: null
100732
100892
  };
100733
100893
  const log2 = (...args) => log(...args);
100734
100894
  function clearReconnectTimer() {
@@ -100742,6 +100902,13 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100742
100902
  clearInterval(lifecycle.presenceInterval);
100743
100903
  lifecycle.presenceInterval = null;
100744
100904
  }
100905
+ if (lifecycle.healthSupervisor) {
100906
+ try {
100907
+ lifecycle.healthSupervisor.stop();
100908
+ } catch (_3) {
100909
+ }
100910
+ lifecycle.healthSupervisor = null;
100911
+ }
100745
100912
  clearReconnectTimer();
100746
100913
  }
100747
100914
  function destroyClient() {
@@ -100869,6 +101036,8 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100869
101036
  });
100870
101037
  lifecycle.client = client2;
100871
101038
  ctx.mqttClient = client2;
101039
+ const health = new RealtimeHealth();
101040
+ lifecycle.health = health;
100872
101041
  let connected = false;
100873
101042
  let closing = false;
100874
101043
  let throttled = false;
@@ -100888,12 +101057,14 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100888
101057
  if (generation !== lifecycle.generation) return;
100889
101058
  connected = false;
100890
101059
  ctx.mqttConnected = false;
101060
+ health.noteDisconnected();
100891
101061
  });
100892
101062
  client2.on("close", () => {
100893
101063
  if (generation !== lifecycle.generation) return;
100894
101064
  if (connected) log2("MQTT connection closed.");
100895
101065
  connected = false;
100896
101066
  ctx.mqttConnected = false;
101067
+ health.noteDisconnected();
100897
101068
  if (closing) return;
100898
101069
  closing = true;
100899
101070
  if (authFailure) {
@@ -100911,6 +101082,7 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100911
101082
  client2.on("connect", () => {
100912
101083
  if (generation !== lifecycle.generation) return;
100913
101084
  connected = true;
101085
+ health.noteConnected();
100914
101086
  client2.subscribe(MQTT_TOPICS, (subErr) => {
100915
101087
  if (subErr) {
100916
101088
  error("Failed to subscribe to MQTT topics:", subErr);
@@ -100947,6 +101119,49 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100947
101119
  ctx.mqttConnected = true;
100948
101120
  ctx.lastMqttError = null;
100949
101121
  resetBackoff();
101122
+ if (lifecycle.healthSupervisor) {
101123
+ try {
101124
+ lifecycle.healthSupervisor.stop();
101125
+ } catch (_3) {
101126
+ }
101127
+ }
101128
+ lifecycle.healthSupervisor = startRealtimeSupervisor({
101129
+ isConnected: () => generation === lifecycle.generation && client2.connected,
101130
+ reconnect: (reason) => {
101131
+ if (generation !== lifecycle.generation) return;
101132
+ if (lifecycle.stopped || ctx.loggedIn === false) return;
101133
+ log2(`MQTT ${reason}; recycling the socket`);
101134
+ try {
101135
+ client2.end(true);
101136
+ } catch (_3) {
101137
+ }
101138
+ const gen = generation;
101139
+ setTimeout(() => {
101140
+ if (gen !== lifecycle.generation) return;
101141
+ if (lifecycle.stopped || ctx.loggedIn === false) return;
101142
+ if (lifecycle.client && lifecycle.client.connected) return;
101143
+ if (lifecycle.reconnectTimer) return;
101144
+ scheduleReconnect("realtime socket recycled");
101145
+ }, 5e3).unref?.();
101146
+ },
101147
+ ping: () => {
101148
+ try {
101149
+ client2.ping();
101150
+ return true;
101151
+ } catch {
101152
+ return false;
101153
+ }
101154
+ },
101155
+ onVerdict: (state) => {
101156
+ if (state.verdict === "dead") {
101157
+ warn(
101158
+ "MQTT health",
101159
+ `silent for ${Math.round(state.sinceLastFrameMs / 1e3)}s; probing`
101160
+ );
101161
+ }
101162
+ }
101163
+ });
101164
+ ctx.__mqttHealth = () => lifecycle.health ? lifecycle.health.state(generation === lifecycle.generation && !!client2.connected) : null;
100950
101165
  api.getBotInitialData().then((botData) => {
100951
101166
  if (botData) {
100952
101167
  log(`Logged in as: ${botData.name || "Facebook User"} (${botData.uid || ctx.userID})`);
@@ -100957,6 +101172,7 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100957
101172
  });
100958
101173
  client2.on("message", async (topic, message, _packet) => {
100959
101174
  try {
101175
+ health.noteFrame();
100960
101176
  ctx.lastMessageTime = Date.now();
100961
101177
  ctx.messageCount = (ctx.messageCount || 0) + 1;
100962
101178
  const jsonMessage = JSON.parse(message.toString());
@@ -101042,6 +101258,16 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
101042
101258
  this.stop();
101043
101259
  if (typeof cb === "function") cb();
101044
101260
  }
101261
+ /** Live connection-health snapshot, for diagnostics/alerting. */
101262
+ getHealth() {
101263
+ if (!lifecycle.health) return null;
101264
+ const connected = !lifecycle.stopped && !!lifecycle.client && !!lifecycle.client.connected;
101265
+ return lifecycle.health.state(connected);
101266
+ }
101267
+ /** Is this listener currently stopped? */
101268
+ isStopped() {
101269
+ return lifecycle.stopped;
101270
+ }
101045
101271
  }
101046
101272
  const msgEmitter = new MessageEmitter();
101047
101273
  lifecycle.stopped = false;
@@ -101363,6 +101589,22 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
101363
101589
  {
101364
101590
  ctx,
101365
101591
  isAuthenticated: () => hasAuthenticatedSession(ctx.jar),
101592
+ probeLiveness: async () => {
101593
+ try {
101594
+ const res = await defaultFuncs.get(
101595
+ "https://www.facebook.com/",
101596
+ ctx.jar,
101597
+ null,
101598
+ ctx.globalOptions,
101599
+ { noRef: true }
101600
+ );
101601
+ const html3 = typeof res === "string" ? res : res && res.body;
101602
+ if (typeof html3 !== "string" || !html3) return true;
101603
+ return !isLoggedOutDocument(html3);
101604
+ } catch {
101605
+ return true;
101606
+ }
101607
+ },
101366
101608
  refresh: () => repairSession(ctx, defaultFuncs),
101367
101609
  canRelogin: Boolean(credentials.email && credentials.password),
101368
101610
  relogin: async () => {
@@ -101591,7 +101833,10 @@ lodash/lodash.js:
101591
101833
  *)
101592
101834
  */
101593
101835
 
101836
+ exports.PING_GRACE_MS = PING_GRACE_MS;
101837
+ exports.RealtimeHealth = RealtimeHealth;
101594
101838
  exports.RecoverySupervisor = RecoverySupervisor;
101839
+ exports.SILENT_WINDOW_MS = SILENT_WINDOW_MS;
101595
101840
  exports.acquireCookies = acquireCookies;
101596
101841
  exports.applySetCookie = applySetCookie;
101597
101842
  exports.awaitProactiveSlot = awaitProactiveSlot;
@@ -101612,6 +101857,7 @@ exports.humanDelay = humanDelay;
101612
101857
  exports.invalidateCometTokens = invalidateCometTokens;
101613
101858
  exports.isAuthFailure = isAuthFailure;
101614
101859
  exports.isBreakerOpen = isBreakerOpen;
101860
+ exports.isLoggedOutDocument = isLoggedOutDocument;
101615
101861
  exports.isStaleTokenError = isStaleTokenError;
101616
101862
  exports.login = login;
101617
101863
  exports.missingSessionCookies = missingSessionCookies;
@@ -101632,6 +101878,7 @@ exports.retryDelay = retryDelay;
101632
101878
  exports.safetySnapshot = safetySnapshot;
101633
101879
  exports.sleep = sleep;
101634
101880
  exports.startLiveness = startLiveness;
101881
+ exports.startRealtimeSupervisor = startRealtimeSupervisor;
101635
101882
  exports.startRecovery = startRecovery;
101636
101883
  exports.tripBreaker = tripBreaker;
101637
101884
  exports.warmUpSession = warmUpSession;
package/dist/index.d.mts CHANGED
@@ -145,6 +145,8 @@ interface ApiContext$1 {
145
145
  fb_dtsg?: string;
146
146
  jazoest?: string;
147
147
  lsd?: string;
148
+ /** App-scoped abuse id (`x-asbd-id`) scraped from the current document. */
149
+ asbdId?: string;
148
150
  ttstamp?: string;
149
151
  threadTypeCache?: Record<string, boolean>;
150
152
  lastMessageTime?: number;
@@ -733,6 +735,16 @@ declare function refreshSessionCookies(ctx: any, defaultFuncs: any, options?: {
733
735
  }>;
734
736
  /** Detect an auth/document-token failure in a thrown or returned payload. */
735
737
  declare function isAuthFailure(value: any): boolean;
738
+ /**
739
+ * Detect a logged-out document.
740
+ *
741
+ * When Facebook invalidates a session it does not always answer with an auth
742
+ * error code: it serves the login page, or (when the browser/device is still
743
+ * trusted) the one-tap "Continue as <name>" interstitial. Both look like a
744
+ * normal `200 text/html`, so a session can silently become useless. Recognising
745
+ * the document lets the caller re-authenticate instead of limping along.
746
+ */
747
+ declare function isLoggedOutDocument(html: any): boolean;
736
748
  /**
737
749
  * Wrap a network operation with one repair-and-retry pass.
738
750
  *
@@ -1007,6 +1019,13 @@ interface RecoveryRuntime {
1007
1019
  ctx: any;
1008
1020
  /** True when the jar still holds a usable session. Cheap, synchronous. */
1009
1021
  isAuthenticated: () => boolean;
1022
+ /**
1023
+ * Active server-side liveness check. Distinct from {@link isAuthenticated}:
1024
+ * the jar can still look authenticated locally while Facebook has already
1025
+ * invalidated the session server-side (the "Continue as <name>" page). When
1026
+ * provided, the supervisor trusts this over the cheap local check.
1027
+ */
1028
+ probeLiveness?: () => Promise<boolean>;
1010
1029
  /** Refresh cookies/tokens in place; resolves to whether auth survived. */
1011
1030
  refresh: () => Promise<boolean>;
1012
1031
  /**
@@ -1084,6 +1103,107 @@ declare class RecoverySupervisor {
1084
1103
  */
1085
1104
  declare function startRecovery(ctx: any, runtime: RecoveryRuntime, options?: RecoveryOptions): RecoverySupervisor;
1086
1105
 
1106
+ /**
1107
+ * MQTT connection health.
1108
+ *
1109
+ * A Messenger realtime socket fails in two ways:
1110
+ *
1111
+ * 1. **Loudly** — TCP drop, broker close, auth error. mqtt.js raises `close`
1112
+ * and the listener reconnects. Easy.
1113
+ * 2. **Silently** — the socket stays "connected" but Facebook has stopped
1114
+ * routing events to it (a half-open connection, a broker-side idle reap, a
1115
+ * revoked session). No error is emitted, `client.connected` stays `true`,
1116
+ * and from the bot's point of view it is online while receiving nothing.
1117
+ *
1118
+ * Case 2 is why bots are traditionally torn down and restarted on a timer:
1119
+ * there was no way to tell a healthy quiet socket from a dead one. This module
1120
+ * provides that signal, so the socket can stay up indefinitely and only ever be
1121
+ * recycled when it is *actually* broken.
1122
+ *
1123
+ * The detector is deliberately conservative — real accounts genuinely go quiet
1124
+ * for long stretches, so it only acts when the socket has been silent for a
1125
+ * long window AND an active ping fails to elicit any traffic.
1126
+ */
1127
+ /** No traffic and no pong for this long => the socket is suspect. */
1128
+ declare const SILENT_WINDOW_MS: number;
1129
+ /** How long to wait for traffic after an active ping before declaring death. */
1130
+ declare const PING_GRACE_MS: number;
1131
+ interface RealtimeHealthState {
1132
+ /** Whether the transport currently reports itself connected. */
1133
+ connected: boolean;
1134
+ /** ms since the last inbound frame (or since connect when none seen yet). */
1135
+ sinceLastFrameMs: number;
1136
+ /** Total inbound frames observed. */
1137
+ frames: number;
1138
+ /** Completed health probes. */
1139
+ probes: number;
1140
+ /** Times the health check forced a reconnect. */
1141
+ forcedReconnects: number;
1142
+ /** The last determined verdict. */
1143
+ verdict: "healthy" | "quiet" | "suspect" | "dead" | "disconnected";
1144
+ }
1145
+ /**
1146
+ * Tracks liveness of one realtime connection. Instances are cheap and are
1147
+ * recreated per connection generation by the listener.
1148
+ */
1149
+ declare class RealtimeHealth {
1150
+ private now;
1151
+ private frames;
1152
+ private lastFrameAt;
1153
+ private connectedAt;
1154
+ private probes;
1155
+ private forcedReconnects;
1156
+ private lastPingAt;
1157
+ private startedAt;
1158
+ constructor(now?: () => number);
1159
+ /** Record any inbound frame (message, pong, anything from the broker). */
1160
+ noteFrame(): void;
1161
+ /** Record a successful connect. */
1162
+ noteConnected(): void;
1163
+ /** Record that the transport reports itself disconnected. */
1164
+ noteDisconnected(): void;
1165
+ /** Record an active ping being sent. */
1166
+ notePing(): void;
1167
+ sinceLastFrame(): number;
1168
+ /**
1169
+ * Decide what to do about the current socket.
1170
+ *
1171
+ * - `disconnected` — the transport already knows it is down; reconnect.
1172
+ * - `dead` — connected on paper, silent past the window, and a ping raised no
1173
+ * traffic within the grace period; reconnect.
1174
+ * - `suspect` — silent past the window but a ping was only just sent; wait.
1175
+ * - `quiet` — silent but inside the window; normal.
1176
+ * - `healthy` — recent traffic.
1177
+ */
1178
+ verdict(connected: boolean): RealtimeHealthState["verdict"];
1179
+ /** A snapshot for diagnostics. */
1180
+ state(connected: boolean): RealtimeHealthState;
1181
+ noteProbe(): void;
1182
+ noteForcedReconnect(): void;
1183
+ }
1184
+ interface RealtimeSupervisorOptions {
1185
+ /** How often to evaluate the socket. Jittered. Default 3 min. */
1186
+ intervalMs?: number;
1187
+ /** Ask the transport to reconnect (fresh socket) — must be idempotent. */
1188
+ reconnect: (reason: string) => void;
1189
+ /** Send an MQTT ping. Return false if it could not be sent. */
1190
+ ping?: () => boolean;
1191
+ /** Whether the transport currently reports connected. */
1192
+ isConnected: () => boolean;
1193
+ /** Observe verdict changes (logging). */
1194
+ onVerdict?: (state: RealtimeHealthState) => void;
1195
+ }
1196
+ /**
1197
+ * A lightweight supervisor that keeps ONE realtime socket honest: it pings a
1198
+ * long-quiet socket and recycles only a genuinely dead one. It never restarts a
1199
+ * socket that is simply idle, which is what lets the connection live for days.
1200
+ */
1201
+ declare function startRealtimeSupervisor(options: RealtimeSupervisorOptions): {
1202
+ health: RealtimeHealth;
1203
+ stop: () => void;
1204
+ checkNow: () => RealtimeHealthState;
1205
+ };
1206
+
1087
1207
  /** Extract the `CurrentUserInitialData` object from a profile HTML document. */
1088
1208
  declare function extractCurrentUser(html: any): Record<string, any> | null;
1089
1209
 
@@ -1125,4 +1245,4 @@ declare const _default: typeof login$1 & {
1125
1245
  default: typeof login$1;
1126
1246
  };
1127
1247
 
1128
- export { type API, type AddedStickerPackInfo, type AiTheme, type AnyAttachment, type ApiContext, type ApiInternal, type Attachment, type AudioAttachment, type BrowserProfile, type Callback, type CommentMessage, type CommentResult, type Coordinates, type DefaultFuncs, type EmojiEvent, type Event, type FileAttachment, type GroupNameEvent, type ListenEvent, type LivenessHandle, type LivenessState, type LoginCredentials, type LoginOptions, type Mention, type Message, type MessageID, type MessageObject, type MessageReply, type MusicTag, type MusicTrack, type NicknameEvent, type PhotoAttachment, type Reaction, type RecoveryEvent, type RecoveryOptions, type RecoveryRuntime, type RecoveryState, RecoverySupervisor, type SearchMusicOptions, type SearchMusicResult, type ShareAttachment, type ShareResult, type StickerAttachment, type StickerInfo, type StickerPackInfo, type ThreadID, type ThreadInfo, type ThreadThemeEvent, type TypingIndicator, type UnsendMessageEvent, type UserID, type UserInfo, type VideoAttachment, type WarmUpResult, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, collectSessionCookies, _default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isStaleTokenError, login$1 as login, missingSessionCookies, normalizeBrowserProfile, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, safetySnapshot, sleep, startLiveness, startRecovery, tripBreaker, warmUpSession, withCometTokens, withSessionRetry };
1248
+ export { type API, type AddedStickerPackInfo, type AiTheme, type AnyAttachment, type ApiContext, type ApiInternal, type Attachment, type AudioAttachment, type BrowserProfile, type Callback, type CommentMessage, type CommentResult, type Coordinates, type DefaultFuncs, type EmojiEvent, type Event, type FileAttachment, type GroupNameEvent, type ListenEvent, type LivenessHandle, type LivenessState, type LoginCredentials, type LoginOptions, type Mention, type Message, type MessageID, type MessageObject, type MessageReply, type MusicTag, type MusicTrack, type NicknameEvent, PING_GRACE_MS, type PhotoAttachment, type Reaction, RealtimeHealth, type RealtimeHealthState, type RecoveryEvent, type RecoveryOptions, type RecoveryRuntime, type RecoveryState, RecoverySupervisor, SILENT_WINDOW_MS, type SearchMusicOptions, type SearchMusicResult, type ShareAttachment, type ShareResult, type StickerAttachment, type StickerInfo, type StickerPackInfo, type ThreadID, type ThreadInfo, type ThreadThemeEvent, type TypingIndicator, type UnsendMessageEvent, type UserID, type UserInfo, type VideoAttachment, type WarmUpResult, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, collectSessionCookies, _default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isLoggedOutDocument, isStaleTokenError, login$1 as login, missingSessionCookies, normalizeBrowserProfile, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, safetySnapshot, sleep, startLiveness, startRealtimeSupervisor, startRecovery, tripBreaker, warmUpSession, withCometTokens, withSessionRetry };
package/dist/index.d.ts CHANGED
@@ -145,6 +145,8 @@ interface ApiContext$1 {
145
145
  fb_dtsg?: string;
146
146
  jazoest?: string;
147
147
  lsd?: string;
148
+ /** App-scoped abuse id (`x-asbd-id`) scraped from the current document. */
149
+ asbdId?: string;
148
150
  ttstamp?: string;
149
151
  threadTypeCache?: Record<string, boolean>;
150
152
  lastMessageTime?: number;
@@ -733,6 +735,16 @@ declare function refreshSessionCookies(ctx: any, defaultFuncs: any, options?: {
733
735
  }>;
734
736
  /** Detect an auth/document-token failure in a thrown or returned payload. */
735
737
  declare function isAuthFailure(value: any): boolean;
738
+ /**
739
+ * Detect a logged-out document.
740
+ *
741
+ * When Facebook invalidates a session it does not always answer with an auth
742
+ * error code: it serves the login page, or (when the browser/device is still
743
+ * trusted) the one-tap "Continue as <name>" interstitial. Both look like a
744
+ * normal `200 text/html`, so a session can silently become useless. Recognising
745
+ * the document lets the caller re-authenticate instead of limping along.
746
+ */
747
+ declare function isLoggedOutDocument(html: any): boolean;
736
748
  /**
737
749
  * Wrap a network operation with one repair-and-retry pass.
738
750
  *
@@ -1007,6 +1019,13 @@ interface RecoveryRuntime {
1007
1019
  ctx: any;
1008
1020
  /** True when the jar still holds a usable session. Cheap, synchronous. */
1009
1021
  isAuthenticated: () => boolean;
1022
+ /**
1023
+ * Active server-side liveness check. Distinct from {@link isAuthenticated}:
1024
+ * the jar can still look authenticated locally while Facebook has already
1025
+ * invalidated the session server-side (the "Continue as <name>" page). When
1026
+ * provided, the supervisor trusts this over the cheap local check.
1027
+ */
1028
+ probeLiveness?: () => Promise<boolean>;
1010
1029
  /** Refresh cookies/tokens in place; resolves to whether auth survived. */
1011
1030
  refresh: () => Promise<boolean>;
1012
1031
  /**
@@ -1084,6 +1103,107 @@ declare class RecoverySupervisor {
1084
1103
  */
1085
1104
  declare function startRecovery(ctx: any, runtime: RecoveryRuntime, options?: RecoveryOptions): RecoverySupervisor;
1086
1105
 
1106
+ /**
1107
+ * MQTT connection health.
1108
+ *
1109
+ * A Messenger realtime socket fails in two ways:
1110
+ *
1111
+ * 1. **Loudly** — TCP drop, broker close, auth error. mqtt.js raises `close`
1112
+ * and the listener reconnects. Easy.
1113
+ * 2. **Silently** — the socket stays "connected" but Facebook has stopped
1114
+ * routing events to it (a half-open connection, a broker-side idle reap, a
1115
+ * revoked session). No error is emitted, `client.connected` stays `true`,
1116
+ * and from the bot's point of view it is online while receiving nothing.
1117
+ *
1118
+ * Case 2 is why bots are traditionally torn down and restarted on a timer:
1119
+ * there was no way to tell a healthy quiet socket from a dead one. This module
1120
+ * provides that signal, so the socket can stay up indefinitely and only ever be
1121
+ * recycled when it is *actually* broken.
1122
+ *
1123
+ * The detector is deliberately conservative — real accounts genuinely go quiet
1124
+ * for long stretches, so it only acts when the socket has been silent for a
1125
+ * long window AND an active ping fails to elicit any traffic.
1126
+ */
1127
+ /** No traffic and no pong for this long => the socket is suspect. */
1128
+ declare const SILENT_WINDOW_MS: number;
1129
+ /** How long to wait for traffic after an active ping before declaring death. */
1130
+ declare const PING_GRACE_MS: number;
1131
+ interface RealtimeHealthState {
1132
+ /** Whether the transport currently reports itself connected. */
1133
+ connected: boolean;
1134
+ /** ms since the last inbound frame (or since connect when none seen yet). */
1135
+ sinceLastFrameMs: number;
1136
+ /** Total inbound frames observed. */
1137
+ frames: number;
1138
+ /** Completed health probes. */
1139
+ probes: number;
1140
+ /** Times the health check forced a reconnect. */
1141
+ forcedReconnects: number;
1142
+ /** The last determined verdict. */
1143
+ verdict: "healthy" | "quiet" | "suspect" | "dead" | "disconnected";
1144
+ }
1145
+ /**
1146
+ * Tracks liveness of one realtime connection. Instances are cheap and are
1147
+ * recreated per connection generation by the listener.
1148
+ */
1149
+ declare class RealtimeHealth {
1150
+ private now;
1151
+ private frames;
1152
+ private lastFrameAt;
1153
+ private connectedAt;
1154
+ private probes;
1155
+ private forcedReconnects;
1156
+ private lastPingAt;
1157
+ private startedAt;
1158
+ constructor(now?: () => number);
1159
+ /** Record any inbound frame (message, pong, anything from the broker). */
1160
+ noteFrame(): void;
1161
+ /** Record a successful connect. */
1162
+ noteConnected(): void;
1163
+ /** Record that the transport reports itself disconnected. */
1164
+ noteDisconnected(): void;
1165
+ /** Record an active ping being sent. */
1166
+ notePing(): void;
1167
+ sinceLastFrame(): number;
1168
+ /**
1169
+ * Decide what to do about the current socket.
1170
+ *
1171
+ * - `disconnected` — the transport already knows it is down; reconnect.
1172
+ * - `dead` — connected on paper, silent past the window, and a ping raised no
1173
+ * traffic within the grace period; reconnect.
1174
+ * - `suspect` — silent past the window but a ping was only just sent; wait.
1175
+ * - `quiet` — silent but inside the window; normal.
1176
+ * - `healthy` — recent traffic.
1177
+ */
1178
+ verdict(connected: boolean): RealtimeHealthState["verdict"];
1179
+ /** A snapshot for diagnostics. */
1180
+ state(connected: boolean): RealtimeHealthState;
1181
+ noteProbe(): void;
1182
+ noteForcedReconnect(): void;
1183
+ }
1184
+ interface RealtimeSupervisorOptions {
1185
+ /** How often to evaluate the socket. Jittered. Default 3 min. */
1186
+ intervalMs?: number;
1187
+ /** Ask the transport to reconnect (fresh socket) — must be idempotent. */
1188
+ reconnect: (reason: string) => void;
1189
+ /** Send an MQTT ping. Return false if it could not be sent. */
1190
+ ping?: () => boolean;
1191
+ /** Whether the transport currently reports connected. */
1192
+ isConnected: () => boolean;
1193
+ /** Observe verdict changes (logging). */
1194
+ onVerdict?: (state: RealtimeHealthState) => void;
1195
+ }
1196
+ /**
1197
+ * A lightweight supervisor that keeps ONE realtime socket honest: it pings a
1198
+ * long-quiet socket and recycles only a genuinely dead one. It never restarts a
1199
+ * socket that is simply idle, which is what lets the connection live for days.
1200
+ */
1201
+ declare function startRealtimeSupervisor(options: RealtimeSupervisorOptions): {
1202
+ health: RealtimeHealth;
1203
+ stop: () => void;
1204
+ checkNow: () => RealtimeHealthState;
1205
+ };
1206
+
1087
1207
  /** Extract the `CurrentUserInitialData` object from a profile HTML document. */
1088
1208
  declare function extractCurrentUser(html: any): Record<string, any> | null;
1089
1209
 
@@ -1125,4 +1245,4 @@ declare const _default: typeof login$1 & {
1125
1245
  default: typeof login$1;
1126
1246
  };
1127
1247
 
1128
- export { type API, type AddedStickerPackInfo, type AiTheme, type AnyAttachment, type ApiContext, type ApiInternal, type Attachment, type AudioAttachment, type BrowserProfile, type Callback, type CommentMessage, type CommentResult, type Coordinates, type DefaultFuncs, type EmojiEvent, type Event, type FileAttachment, type GroupNameEvent, type ListenEvent, type LivenessHandle, type LivenessState, type LoginCredentials, type LoginOptions, type Mention, type Message, type MessageID, type MessageObject, type MessageReply, type MusicTag, type MusicTrack, type NicknameEvent, type PhotoAttachment, type Reaction, type RecoveryEvent, type RecoveryOptions, type RecoveryRuntime, type RecoveryState, RecoverySupervisor, type SearchMusicOptions, type SearchMusicResult, type ShareAttachment, type ShareResult, type StickerAttachment, type StickerInfo, type StickerPackInfo, type ThreadID, type ThreadInfo, type ThreadThemeEvent, type TypingIndicator, type UnsendMessageEvent, type UserID, type UserInfo, type VideoAttachment, type WarmUpResult, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, collectSessionCookies, _default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isStaleTokenError, login$1 as login, missingSessionCookies, normalizeBrowserProfile, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, safetySnapshot, sleep, startLiveness, startRecovery, tripBreaker, warmUpSession, withCometTokens, withSessionRetry };
1248
+ export { type API, type AddedStickerPackInfo, type AiTheme, type AnyAttachment, type ApiContext, type ApiInternal, type Attachment, type AudioAttachment, type BrowserProfile, type Callback, type CommentMessage, type CommentResult, type Coordinates, type DefaultFuncs, type EmojiEvent, type Event, type FileAttachment, type GroupNameEvent, type ListenEvent, type LivenessHandle, type LivenessState, type LoginCredentials, type LoginOptions, type Mention, type Message, type MessageID, type MessageObject, type MessageReply, type MusicTag, type MusicTrack, type NicknameEvent, PING_GRACE_MS, type PhotoAttachment, type Reaction, RealtimeHealth, type RealtimeHealthState, type RecoveryEvent, type RecoveryOptions, type RecoveryRuntime, type RecoveryState, RecoverySupervisor, SILENT_WINDOW_MS, type SearchMusicOptions, type SearchMusicResult, type ShareAttachment, type ShareResult, type StickerAttachment, type StickerInfo, type StickerPackInfo, type ThreadID, type ThreadInfo, type ThreadThemeEvent, type TypingIndicator, type UnsendMessageEvent, type UserID, type UserInfo, type VideoAttachment, type WarmUpResult, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, collectSessionCookies, _default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isLoggedOutDocument, isStaleTokenError, login$1 as login, missingSessionCookies, normalizeBrowserProfile, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, safetySnapshot, sleep, startLiveness, startRealtimeSupervisor, startRecovery, tripBreaker, warmUpSession, withCometTokens, withSessionRetry };
package/dist/index.mjs CHANGED
@@ -25107,13 +25107,15 @@ function getHeaders(url2, options, ctx, customHeader) {
25107
25107
  headers["priority"] = "u=0, i";
25108
25108
  }
25109
25109
  if (isXhr) {
25110
- headers["X-Requested-With"] = "XMLHttpRequest";
25111
- headers["Pragma"] = "no-cache";
25112
- headers["Cache-Control"] = "no-cache";
25110
+ if (!isGraphQl) {
25111
+ headers["X-Requested-With"] = "XMLHttpRequest";
25112
+ headers["Pragma"] = "no-cache";
25113
+ headers["Cache-Control"] = "no-cache";
25114
+ }
25113
25115
  headers["priority"] = "u=1, i";
25114
25116
  }
25115
25117
  if (isGraphQl) {
25116
- headers["x-asbd-id"] = "129477";
25118
+ headers["x-asbd-id"] = String(ctx?.asbdId || DEFAULT_ASBD_ID);
25117
25119
  const lsd = ctx?.lsd || ctx?.fb_lsd;
25118
25120
  if (lsd) headers["x-fb-lsd"] = sanitizeHeaderValue2(lsd);
25119
25121
  const friendlyName = customHeader?.["x-fb-friendly-name"] || customHeader?.["X-Fb-Friendly-Name"];
@@ -25184,10 +25186,11 @@ function getFingerprintParams(options) {
25184
25186
  dpr: String(profile.devicePixelRatio)
25185
25187
  };
25186
25188
  }
25187
- var meta;
25189
+ var DEFAULT_ASBD_ID, meta;
25188
25190
  var init_headers = __esm({
25189
25191
  "src/utils/headers.ts"() {
25190
25192
  init_user_agents();
25193
+ DEFAULT_ASBD_ID = "359341";
25191
25194
  meta = (prop2) => new RegExp(`<meta property="${prop2}" content="([^"]*)"`);
25192
25195
  }
25193
25196
  });
@@ -75525,10 +75528,13 @@ __export(utils_exports2, {
75525
75528
  NUM_TO_MONTH: () => NUM_TO_MONTH,
75526
75529
  NetworkError: () => NetworkError,
75527
75530
  NotFoundError: () => NotFoundError,
75531
+ PING_GRACE_MS: () => PING_GRACE_MS,
75528
75532
  PermissionError: () => PermissionError,
75529
75533
  RateLimitError: () => RateLimitError,
75534
+ RealtimeHealth: () => RealtimeHealth,
75530
75535
  RecoverySupervisor: () => RecoverySupervisor,
75531
75536
  SESSION_COOKIE_NAMES: () => SESSION_COOKIE_NAMES,
75537
+ SILENT_WINDOW_MS: () => SILENT_WINDOW_MS,
75532
75538
  TimeoutError: () => TimeoutError,
75533
75539
  ValidationError: () => ValidationError,
75534
75540
  WINDOW_MS: () => WINDOW_MS,
@@ -75553,6 +75559,7 @@ __export(utils_exports2, {
75553
75559
  delay: () => delay2,
75554
75560
  ensureCometTokens: () => ensureCometTokens,
75555
75561
  error: () => error,
75562
+ extractAsbdId: () => extractAsbdId,
75556
75563
  extractAttachmentIds: () => extractAttachmentIds,
75557
75564
  extractCookies: () => extractCookies,
75558
75565
  extractCookiesFromBody: () => extractCookiesFromBody,
@@ -75603,6 +75610,7 @@ __export(utils_exports2, {
75603
75610
  invalidateCometTokens: () => invalidateCometTokens,
75604
75611
  isAuthFailure: () => isAuthFailure,
75605
75612
  isBreakerOpen: () => isBreakerOpen,
75613
+ isLoggedOutDocument: () => isLoggedOutDocument,
75606
75614
  isReadableStream: () => isReadableStream2,
75607
75615
  isStaleTokenError: () => isStaleTokenError,
75608
75616
  json: () => json2,
@@ -75640,6 +75648,7 @@ __export(utils_exports2, {
75640
75648
  setProxy: () => setProxy2,
75641
75649
  sleep: () => sleep,
75642
75650
  startLiveness: () => startLiveness,
75651
+ startRealtimeSupervisor: () => startRealtimeSupervisor,
75643
75652
  startRecovery: () => startRecovery,
75644
75653
  success: () => success,
75645
75654
  tripBreaker: () => tripBreaker,
@@ -90997,6 +91006,9 @@ function extractLsd(html3) {
90997
91006
  function extractDtsg(html3) {
90998
91007
  return getFrom3(html3, '["DTSGInitialData",[],{"token":"', '"}') || getFrom3(html3, 'name="fb_dtsg" value="', '"') || (html3.match(/"DTSGInitialData",\[\],\{"token":"([^"]+)"/) || [])[1] || "";
90999
91008
  }
91009
+ function extractAsbdId(html3) {
91010
+ return (html3.match(/"asbd_id":(\d+)/) || [])[1] || (html3.match(/x-asbd-id["']?\s*[:=]\s*["']?(\d+)/i) || [])[1] || (html3.match(/"ASBD_ID":(\d+)/) || [])[1] || "";
91011
+ }
91000
91012
  function extractSpin(html3) {
91001
91013
  const out = {};
91002
91014
  const spinB = (html3.match(/"__spin_b":"([^"]+)"/) || [])[1];
@@ -91063,6 +91075,8 @@ async function ensureCometTokens(ctx, defaultFuncs, force = false) {
91063
91075
  const spin = extractSpin(html3);
91064
91076
  if (Object.keys(spin).length) ctx.__spin = { ...ctx.__spin || {}, ...spin };
91065
91077
  if (spin.__rev) ctx.revision = spin.__rev;
91078
+ const asbd = extractAsbdId(html3);
91079
+ if (asbd) ctx.asbdId = asbd;
91066
91080
  ctx.lsd = lsd;
91067
91081
  ctx.fb_dtsg = dtsg;
91068
91082
  ctx.jazoest = recomputeJazoest(dtsg);
@@ -91333,6 +91347,13 @@ function isAuthFailure(value) {
91333
91347
  }
91334
91348
  return false;
91335
91349
  }
91350
+ function isLoggedOutDocument(html3) {
91351
+ if (typeof html3 !== "string" || html3.length < 200) return false;
91352
+ if (!/<html/i.test(html3)) return false;
91353
+ const hasAuthedTokens = /DTSGInitialData|"LSD",\[\]/.test(html3);
91354
+ const looksLikeLogin = /name="(?:email|pass)"|id="email"|"login_form"|LoginForm|login_form/i.test(html3) || /Continue as |Use another profile|Create new account/i.test(html3) || /You must log in to continue|Please log in|log in to continue/i.test(html3);
91355
+ return looksLikeLogin && !hasAuthedTokens;
91356
+ }
91336
91357
  async function withSessionRetry(ctx, defaultFuncs, operation, options = {}) {
91337
91358
  const attempts = Math.max(1, options.attempts ?? 2);
91338
91359
  let lastError;
@@ -91439,6 +91460,8 @@ async function warmUpSession(ctx, getter, options = {}) {
91439
91460
  const spin = extractSpin(html3);
91440
91461
  if (Object.keys(spin).length) ctx.__spin = { ...ctx.__spin || {}, ...spin };
91441
91462
  if (spin.__rev) ctx.revision = spin.__rev;
91463
+ const asbd = extractAsbdId(html3);
91464
+ if (asbd) ctx.asbdId = asbd;
91442
91465
  tokensSeeded = true;
91443
91466
  }
91444
91467
  } else if (url2.includes("/messages")) {
@@ -91638,7 +91661,15 @@ var RecoverySupervisor = class {
91638
91661
  async runOnce() {
91639
91662
  this.state.checks += 1;
91640
91663
  this.emit("check", `health check #${this.state.checks}`);
91641
- if (this.runtime.isAuthenticated()) {
91664
+ let healthy = this.runtime.isAuthenticated();
91665
+ if (healthy && this.runtime.probeLiveness) {
91666
+ try {
91667
+ healthy = await this.runtime.probeLiveness();
91668
+ } catch {
91669
+ healthy = true;
91670
+ }
91671
+ }
91672
+ if (healthy) {
91642
91673
  this.markHealthy("session authenticated");
91643
91674
  return;
91644
91675
  }
@@ -91737,6 +91768,131 @@ function startRecovery(ctx, runtime, options = {}) {
91737
91768
  return supervisor;
91738
91769
  }
91739
91770
 
91771
+ // src/utils/realtime-health.ts
91772
+ init_pacing();
91773
+ var SILENT_WINDOW_MS = 6 * 60 * 1e3;
91774
+ var PING_GRACE_MS = 45 * 1e3;
91775
+ var RealtimeHealth = class {
91776
+ constructor(now = () => Date.now()) {
91777
+ this.now = now;
91778
+ this.frames = 0;
91779
+ this.lastFrameAt = 0;
91780
+ this.connectedAt = 0;
91781
+ this.probes = 0;
91782
+ this.forcedReconnects = 0;
91783
+ this.lastPingAt = 0;
91784
+ this.startedAt = Date.now();
91785
+ this.startedAt = this.now();
91786
+ }
91787
+ /** Record any inbound frame (message, pong, anything from the broker). */
91788
+ noteFrame() {
91789
+ this.frames += 1;
91790
+ this.lastFrameAt = this.now();
91791
+ }
91792
+ /** Record a successful connect. */
91793
+ noteConnected() {
91794
+ this.connectedAt = this.now();
91795
+ this.lastFrameAt = this.now();
91796
+ }
91797
+ /** Record that the transport reports itself disconnected. */
91798
+ noteDisconnected() {
91799
+ this.lastFrameAt = 0;
91800
+ }
91801
+ /** Record an active ping being sent. */
91802
+ notePing() {
91803
+ this.lastPingAt = this.now();
91804
+ }
91805
+ sinceLastFrame() {
91806
+ const base = this.lastFrameAt || this.connectedAt || this.startedAt;
91807
+ return this.now() - base;
91808
+ }
91809
+ /**
91810
+ * Decide what to do about the current socket.
91811
+ *
91812
+ * - `disconnected` — the transport already knows it is down; reconnect.
91813
+ * - `dead` — connected on paper, silent past the window, and a ping raised no
91814
+ * traffic within the grace period; reconnect.
91815
+ * - `suspect` — silent past the window but a ping was only just sent; wait.
91816
+ * - `quiet` — silent but inside the window; normal.
91817
+ * - `healthy` — recent traffic.
91818
+ */
91819
+ verdict(connected) {
91820
+ if (!connected) return "disconnected";
91821
+ const silent = this.sinceLastFrame();
91822
+ if (silent < SILENT_WINDOW_MS / 2) return "healthy";
91823
+ if (silent < SILENT_WINDOW_MS) return "quiet";
91824
+ if (this.lastPingAt && this.now() - this.lastPingAt < PING_GRACE_MS) return "suspect";
91825
+ return "dead";
91826
+ }
91827
+ /** A snapshot for diagnostics. */
91828
+ state(connected) {
91829
+ return {
91830
+ connected,
91831
+ sinceLastFrameMs: this.sinceLastFrame(),
91832
+ frames: this.frames,
91833
+ probes: this.probes,
91834
+ forcedReconnects: this.forcedReconnects,
91835
+ verdict: this.verdict(connected)
91836
+ };
91837
+ }
91838
+ noteProbe() {
91839
+ this.probes += 1;
91840
+ }
91841
+ noteForcedReconnect() {
91842
+ this.forcedReconnects += 1;
91843
+ }
91844
+ };
91845
+ function startRealtimeSupervisor(options) {
91846
+ const health = new RealtimeHealth();
91847
+ const interval = options.intervalMs ?? 3 * 60 * 1e3;
91848
+ let stopped = false;
91849
+ let timer = null;
91850
+ const evaluate = () => {
91851
+ const connected = options.isConnected();
91852
+ const verdict = health.verdict(connected);
91853
+ health.noteProbe();
91854
+ if (verdict === "dead") {
91855
+ const pingSent = options.ping ? options.ping() : false;
91856
+ if (pingSent) {
91857
+ health.notePing();
91858
+ } else {
91859
+ health.noteForcedReconnect();
91860
+ options.reconnect("realtime socket silent and unpinnable");
91861
+ }
91862
+ }
91863
+ const state = health.state(connected);
91864
+ try {
91865
+ options.onVerdict?.(state);
91866
+ } catch {
91867
+ }
91868
+ return state;
91869
+ };
91870
+ const schedule = () => {
91871
+ if (stopped) return;
91872
+ timer = setTimeout(
91873
+ () => {
91874
+ timer = null;
91875
+ evaluate();
91876
+ schedule();
91877
+ },
91878
+ Math.max(1e3, humanDelay(interval, interval * 0.3))
91879
+ );
91880
+ if (timer && typeof timer.unref === "function") timer.unref();
91881
+ };
91882
+ schedule();
91883
+ return {
91884
+ health,
91885
+ checkNow: evaluate,
91886
+ stop: () => {
91887
+ stopped = true;
91888
+ if (timer) {
91889
+ clearTimeout(timer);
91890
+ timer = null;
91891
+ }
91892
+ }
91893
+ };
91894
+ }
91895
+
91740
91896
  // src/utils/index.ts
91741
91897
  async function json2(url2, jar2, qs2, options, ctx, customHeader) {
91742
91898
  try {
@@ -91954,6 +92110,7 @@ async function buildAPI(html3, jar2, netData, globalOptions, fbLinkFunc, errorRe
91954
92110
  const lsd = lsdConfig && lsdConfig.token || extractLsd(html3) || void 0;
91955
92111
  const spin = extractSpin(html3);
91956
92112
  const revision = spin.__rev || getFrom(html3, 'revision":', ",").trim();
92113
+ const asbdId = extractAsbdId(html3) || void 0;
91957
92114
  const clientIDData = findConfig("MqttWebDeviceID");
91958
92115
  const clientID = clientIDData ? clientIDData.clientID : void 0;
91959
92116
  const mqttConfigData = findConfig("MqttWebConfig");
@@ -91995,7 +92152,8 @@ async function buildAPI(html3, jar2, netData, globalOptions, fbLinkFunc, errorRe
91995
92152
  ...dtsgResult,
91996
92153
  ...lsd ? { lsd } : {},
91997
92154
  ...Object.keys(spin).length ? { __spin: spin } : {},
91998
- ...revision ? { revision } : {}
92155
+ ...revision ? { revision } : {},
92156
+ ...asbdId ? { asbdId } : {}
91999
92157
  };
92000
92158
  if (lsd && dtsg) {
92001
92159
  ctx.__cometTokens = { lsd, fb_dtsg: dtsg, fetchedAt: Date.now(), failures: 0 };
@@ -100693,7 +100851,9 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100693
100851
  attempts: 0,
100694
100852
  delay: MQTT_CONFIG.INITIAL_RETRY_DELAY,
100695
100853
  throttleDelay: MQTT_CONFIG.THROTTLE_DELAY,
100696
- generation: 0
100854
+ generation: 0,
100855
+ health: null,
100856
+ healthSupervisor: null
100697
100857
  };
100698
100858
  const log2 = (...args) => log(...args);
100699
100859
  function clearReconnectTimer() {
@@ -100707,6 +100867,13 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100707
100867
  clearInterval(lifecycle.presenceInterval);
100708
100868
  lifecycle.presenceInterval = null;
100709
100869
  }
100870
+ if (lifecycle.healthSupervisor) {
100871
+ try {
100872
+ lifecycle.healthSupervisor.stop();
100873
+ } catch (_3) {
100874
+ }
100875
+ lifecycle.healthSupervisor = null;
100876
+ }
100710
100877
  clearReconnectTimer();
100711
100878
  }
100712
100879
  function destroyClient() {
@@ -100834,6 +101001,8 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100834
101001
  });
100835
101002
  lifecycle.client = client2;
100836
101003
  ctx.mqttClient = client2;
101004
+ const health = new RealtimeHealth();
101005
+ lifecycle.health = health;
100837
101006
  let connected = false;
100838
101007
  let closing = false;
100839
101008
  let throttled = false;
@@ -100853,12 +101022,14 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100853
101022
  if (generation !== lifecycle.generation) return;
100854
101023
  connected = false;
100855
101024
  ctx.mqttConnected = false;
101025
+ health.noteDisconnected();
100856
101026
  });
100857
101027
  client2.on("close", () => {
100858
101028
  if (generation !== lifecycle.generation) return;
100859
101029
  if (connected) log2("MQTT connection closed.");
100860
101030
  connected = false;
100861
101031
  ctx.mqttConnected = false;
101032
+ health.noteDisconnected();
100862
101033
  if (closing) return;
100863
101034
  closing = true;
100864
101035
  if (authFailure) {
@@ -100876,6 +101047,7 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100876
101047
  client2.on("connect", () => {
100877
101048
  if (generation !== lifecycle.generation) return;
100878
101049
  connected = true;
101050
+ health.noteConnected();
100879
101051
  client2.subscribe(MQTT_TOPICS, (subErr) => {
100880
101052
  if (subErr) {
100881
101053
  error("Failed to subscribe to MQTT topics:", subErr);
@@ -100912,6 +101084,49 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100912
101084
  ctx.mqttConnected = true;
100913
101085
  ctx.lastMqttError = null;
100914
101086
  resetBackoff();
101087
+ if (lifecycle.healthSupervisor) {
101088
+ try {
101089
+ lifecycle.healthSupervisor.stop();
101090
+ } catch (_3) {
101091
+ }
101092
+ }
101093
+ lifecycle.healthSupervisor = startRealtimeSupervisor({
101094
+ isConnected: () => generation === lifecycle.generation && client2.connected,
101095
+ reconnect: (reason) => {
101096
+ if (generation !== lifecycle.generation) return;
101097
+ if (lifecycle.stopped || ctx.loggedIn === false) return;
101098
+ log2(`MQTT ${reason}; recycling the socket`);
101099
+ try {
101100
+ client2.end(true);
101101
+ } catch (_3) {
101102
+ }
101103
+ const gen = generation;
101104
+ setTimeout(() => {
101105
+ if (gen !== lifecycle.generation) return;
101106
+ if (lifecycle.stopped || ctx.loggedIn === false) return;
101107
+ if (lifecycle.client && lifecycle.client.connected) return;
101108
+ if (lifecycle.reconnectTimer) return;
101109
+ scheduleReconnect("realtime socket recycled");
101110
+ }, 5e3).unref?.();
101111
+ },
101112
+ ping: () => {
101113
+ try {
101114
+ client2.ping();
101115
+ return true;
101116
+ } catch {
101117
+ return false;
101118
+ }
101119
+ },
101120
+ onVerdict: (state) => {
101121
+ if (state.verdict === "dead") {
101122
+ warn(
101123
+ "MQTT health",
101124
+ `silent for ${Math.round(state.sinceLastFrameMs / 1e3)}s; probing`
101125
+ );
101126
+ }
101127
+ }
101128
+ });
101129
+ ctx.__mqttHealth = () => lifecycle.health ? lifecycle.health.state(generation === lifecycle.generation && !!client2.connected) : null;
100915
101130
  api.getBotInitialData().then((botData) => {
100916
101131
  if (botData) {
100917
101132
  log(`Logged in as: ${botData.name || "Facebook User"} (${botData.uid || ctx.userID})`);
@@ -100922,6 +101137,7 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
100922
101137
  });
100923
101138
  client2.on("message", async (topic, message, _packet) => {
100924
101139
  try {
101140
+ health.noteFrame();
100925
101141
  ctx.lastMessageTime = Date.now();
100926
101142
  ctx.messageCount = (ctx.messageCount || 0) + 1;
100927
101143
  const jsonMessage = JSON.parse(message.toString());
@@ -101007,6 +101223,16 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
101007
101223
  this.stop();
101008
101224
  if (typeof cb === "function") cb();
101009
101225
  }
101226
+ /** Live connection-health snapshot, for diagnostics/alerting. */
101227
+ getHealth() {
101228
+ if (!lifecycle.health) return null;
101229
+ const connected = !lifecycle.stopped && !!lifecycle.client && !!lifecycle.client.connected;
101230
+ return lifecycle.health.state(connected);
101231
+ }
101232
+ /** Is this listener currently stopped? */
101233
+ isStopped() {
101234
+ return lifecycle.stopped;
101235
+ }
101010
101236
  }
101011
101237
  const msgEmitter = new MessageEmitter();
101012
101238
  lifecycle.stopped = false;
@@ -101328,6 +101554,22 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
101328
101554
  {
101329
101555
  ctx,
101330
101556
  isAuthenticated: () => hasAuthenticatedSession(ctx.jar),
101557
+ probeLiveness: async () => {
101558
+ try {
101559
+ const res = await defaultFuncs.get(
101560
+ "https://www.facebook.com/",
101561
+ ctx.jar,
101562
+ null,
101563
+ ctx.globalOptions,
101564
+ { noRef: true }
101565
+ );
101566
+ const html3 = typeof res === "string" ? res : res && res.body;
101567
+ if (typeof html3 !== "string" || !html3) return true;
101568
+ return !isLoggedOutDocument(html3);
101569
+ } catch {
101570
+ return true;
101571
+ }
101572
+ },
101331
101573
  refresh: () => repairSession(ctx, defaultFuncs),
101332
101574
  canRelogin: Boolean(credentials.email && credentials.password),
101333
101575
  relogin: async () => {
@@ -101556,4 +101798,4 @@ lodash/lodash.js:
101556
101798
  *)
101557
101799
  */
101558
101800
 
101559
- export { RecoverySupervisor, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, collectSessionCookies, index_default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isStaleTokenError, login, missingSessionCookies, normalizeBrowserProfile, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, public_exports as publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, safetySnapshot, sleep, startLiveness, startRecovery, tripBreaker, warmUpSession, withCometTokens, withSessionRetry };
101801
+ export { PING_GRACE_MS, RealtimeHealth, RecoverySupervisor, SILENT_WINDOW_MS, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, collectSessionCookies, index_default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isLoggedOutDocument, isStaleTokenError, login, missingSessionCookies, normalizeBrowserProfile, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, public_exports as publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, safetySnapshot, sleep, startLiveness, startRealtimeSupervisor, startRecovery, tripBreaker, warmUpSession, withCometTokens, withSessionRetry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/metachat",
3
- "version": "4.0.7",
3
+ "version": "4.0.8",
4
4
  "description": "A modern TypeScript Facebook Chat API client for building reliable Messenger bots, with built-in anti-detection protection, automatic cookie refresh, music search, and AI theme support.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -57,7 +57,7 @@
57
57
  "prepublishOnly": "npm run build",
58
58
  "lint": "npm run typecheck",
59
59
  "format": "prettier --write \"src/**/*.ts\" \"index.ts\"",
60
- "test": "npm run build && node test/smoke.test.cjs && node test/smoke.test.mjs && node test/music-search.test.cjs && node test/mqtt-lifecycle.test.cjs && node test/session-reliability.test.cjs && node test/anti-detection.test.cjs && node test/account-safety.test.cjs && node test/liveness.test.cjs && node test/recovery.test.cjs && node test/cookie-acquisition.test.cjs",
60
+ "test": "npm run build && node test/smoke.test.cjs && node test/smoke.test.mjs && node test/music-search.test.cjs && node test/mqtt-lifecycle.test.cjs && node test/session-reliability.test.cjs && node test/anti-detection.test.cjs && node test/account-safety.test.cjs && node test/liveness.test.cjs && node test/recovery.test.cjs && node test/cookie-acquisition.test.cjs && node test/realtime-health.test.cjs",
61
61
  "audit": "npm audit",
62
62
  "audit:fix": "npm audit fix"
63
63
  },