@whanext/core 0.19.5 → 0.19.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.js CHANGED
@@ -90,11 +90,40 @@ function toWhaNextError(error, context) {
90
90
  return error;
91
91
  }
92
92
  const message = error instanceof Error ? error.message : "An unknown error occurred.";
93
+ if (isReachoutTimelockError(error)) {
94
+ return new WhaNextError(
95
+ "MESSAGE_REACHOUT_LOCKED",
96
+ "WhatsApp rejected the outgoing message (ack 463: reach-out time-lock).",
97
+ {
98
+ cause: error,
99
+ context: {
100
+ ...context ?? {},
101
+ ackCode: 463
102
+ },
103
+ recoverable: false
104
+ }
105
+ );
106
+ }
93
107
  return new WhaNextError("UNKNOWN_ERROR", message, {
94
108
  cause: error,
95
109
  ...context ? { context } : {}
96
110
  });
97
111
  }
112
+ function isReachoutTimelockError(error) {
113
+ const seen = /* @__PURE__ */ new Set();
114
+ let current = error;
115
+ for (let depth = 0; depth < 8 && current !== void 0 && current !== null; depth += 1) {
116
+ if (seen.has(current)) return false;
117
+ seen.add(current);
118
+ const message = current instanceof Error ? current.message : typeof current === "string" ? current : void 0;
119
+ if (message && /negative publish ack:.*\berror=463\b/i.test(message)) {
120
+ return true;
121
+ }
122
+ if (typeof current !== "object") return false;
123
+ current = Reflect.get(current, "cause");
124
+ }
125
+ return false;
126
+ }
98
127
 
99
128
  // src/models/identity.ts
100
129
  function normalizeIdentity(identity) {
@@ -370,7 +399,7 @@ var CommandConcurrencyController = class {
370
399
  recoverable: true
371
400
  });
372
401
  } else if (strategy === "queue" && state.active >= max) {
373
- await new Promise((resolve3) => state.queue.push(resolve3));
402
+ await new Promise((resolve4) => state.queue.push(resolve4));
374
403
  }
375
404
  const controller = new AbortController();
376
405
  state.controllers.add(controller);
@@ -1617,8 +1646,8 @@ var SqliteMuteStore = class {
1617
1646
  }
1618
1647
  let database;
1619
1648
  try {
1620
- const require2 = createRequire(import.meta.url);
1621
- const sqlite = require2("node:sqlite");
1649
+ const require3 = createRequire(import.meta.url);
1650
+ const sqlite = require3("node:sqlite");
1622
1651
  database = new sqlite.DatabaseSync(databasePath);
1623
1652
  database.exec("PRAGMA journal_mode = WAL");
1624
1653
  database.exec("PRAGMA busy_timeout = 5000");
@@ -2203,12 +2232,12 @@ var WhaNextApp = class {
2203
2232
  this.logger.info("Login started");
2204
2233
  let unsubscribe = () => void 0;
2205
2234
  let timer;
2206
- const connected = new Promise((resolve3, reject) => {
2235
+ const connected = new Promise((resolve4, reject) => {
2207
2236
  unsubscribe = this.#provider.on("connection", (update) => {
2208
- if (update.state === "connected") resolve3();
2237
+ if (update.state === "connected") resolve4();
2209
2238
  if (update.state === "closed") {
2210
2239
  reject(
2211
- new WhaNextError(
2240
+ update.error instanceof WhaNextError ? update.error : new WhaNextError(
2212
2241
  "CONNECTION_FAILED",
2213
2242
  "WhatsApp closed the connection before login completed.",
2214
2243
  {
@@ -2375,8 +2404,9 @@ var Browser = /* @__PURE__ */ ((Browser2) => {
2375
2404
  })(Browser || {});
2376
2405
 
2377
2406
  // src/provider/zapo/zapo-provider.ts
2378
- import { mkdir } from "fs/promises";
2379
- import { join } from "path";
2407
+ import { mkdir, stat } from "fs/promises";
2408
+ import { createRequire as createRequire2 } from "module";
2409
+ import { basename, dirname as dirname2, join, resolve as resolve2 } from "path";
2380
2410
  import { createMediaProcessor } from "@zapo-js/media-utils";
2381
2411
  import { createSqliteStore } from "@zapo-js/store-sqlite";
2382
2412
  import {
@@ -2706,6 +2736,25 @@ function toNumber(value) {
2706
2736
  }
2707
2737
 
2708
2738
  // src/provider/zapo/zapo-provider.ts
2739
+ var require2 = createRequire2(import.meta.url);
2740
+ var sharedZapoStores = /* @__PURE__ */ new Map();
2741
+ var sharedZapoStorePromises = /* @__PURE__ */ new Map();
2742
+ var fatalDisconnectReasons = /* @__PURE__ */ new Set([
2743
+ "stream_error_replaced",
2744
+ "stream_error_device_removed",
2745
+ "stream_error_force_logout",
2746
+ "failure_not_authorized",
2747
+ "failure_banned",
2748
+ "failure_locked",
2749
+ "failure_client_too_old",
2750
+ "failure_bad_user_agent",
2751
+ "primary_identity_key_change"
2752
+ ]);
2753
+ var fatalDisconnectCodes = /* @__PURE__ */ new Set([401, 403, 405, 406, 409, 516]);
2754
+ var sharedMediaProcessor = createMediaProcessor();
2755
+ var messageSnapshotRetentionSeconds = 7 * 24 * 60 * 60;
2756
+ var messageSnapshotMaxPerSession = 2e4;
2757
+ var messageSnapshotPruneInterval = 256;
2709
2758
  var ZapoProvider = class {
2710
2759
  #options;
2711
2760
  #events = new TypedEventEmitter();
@@ -2716,14 +2765,19 @@ var ZapoProvider = class {
2716
2765
  #handledProtocolStore = /* @__PURE__ */ new Set();
2717
2766
  #callCreatorStore = /* @__PURE__ */ new Map();
2718
2767
  #messageCacheSize;
2768
+ #protocolMutationQueue = Promise.resolve();
2719
2769
  #client;
2770
+ #storeEntry;
2771
+ #storePath;
2720
2772
  #intentionalClose = false;
2773
+ #closedNotified = false;
2721
2774
  #reconnectAttempt = 0;
2722
2775
  #reconnectTimer;
2723
2776
  #connectPromise;
2724
2777
  #pairingRequired = false;
2725
2778
  #connected = false;
2726
2779
  #connectedAtSeconds = 0;
2780
+ #messageSnapshotsSincePrune = 0;
2727
2781
  #pairingReady = Promise.resolve();
2728
2782
  #resolvePairingReady;
2729
2783
  constructor(options) {
@@ -2736,6 +2790,7 @@ var ZapoProvider = class {
2736
2790
  }
2737
2791
  async connect() {
2738
2792
  this.#intentionalClose = false;
2793
+ this.#closedNotified = false;
2739
2794
  const client = await this.#ensureClient();
2740
2795
  if (this.#connected || this.#connectPromise) {
2741
2796
  return;
@@ -2755,12 +2810,16 @@ var ZapoProvider = class {
2755
2810
  }
2756
2811
  const client = this.#client;
2757
2812
  this.#connectPromise = void 0;
2758
- if (client) {
2759
- await client.disconnect();
2760
- } else {
2761
- await this.#events.emit("connection", { state: "closed" });
2813
+ try {
2814
+ if (client) {
2815
+ await client.disconnect();
2816
+ } else {
2817
+ await this.#emitClosedOnce();
2818
+ }
2819
+ } finally {
2820
+ this.#connected = false;
2821
+ await this.#releaseStore();
2762
2822
  }
2763
- this.#connected = false;
2764
2823
  }
2765
2824
  getCurrentUserIds() {
2766
2825
  const credentials = this.#client?.getCredentials();
@@ -2819,7 +2878,7 @@ var ZapoProvider = class {
2819
2878
  return this.#sent(result, chatId);
2820
2879
  }
2821
2880
  async repostMessage(source, chatId, options = {}) {
2822
- const original = this.#findStoredMessage(this.#toZapoKey(source));
2881
+ const original = await this.#findStoredMessage(this.#toZapoKey(source));
2823
2882
  if (!original?.message) {
2824
2883
  throw new WhaNextError(
2825
2884
  "MESSAGE_NOT_FOUND",
@@ -2849,7 +2908,7 @@ var ZapoProvider = class {
2849
2908
  return this.#sent(result, key.chatId);
2850
2909
  }
2851
2910
  async downloadMedia(key) {
2852
- const message = this.#messageKeyStore.get(key) ?? this.#findStoredMessage(this.#toZapoKey(key));
2911
+ const message = this.#messageKeyStore.get(key) ?? await this.#findStoredMessage(this.#toZapoKey(key));
2853
2912
  if (!message?.message) {
2854
2913
  throw new WhaNextError(
2855
2914
  "MEDIA_NOT_AVAILABLE",
@@ -2993,44 +3052,36 @@ var ZapoProvider = class {
2993
3052
  }
2994
3053
  async #ensureClient() {
2995
3054
  if (this.#client) return this.#client;
2996
- await mkdir(this.#options.auth, { recursive: true });
2997
- const store = createStore({
2998
- backends: {
2999
- sqlite: createSqliteStore({
3000
- path: join(this.#options.auth, "state.sqlite"),
3001
- driver: "auto"
3002
- })
3003
- },
3004
- providers: {
3005
- auth: "sqlite",
3006
- signal: "sqlite",
3007
- preKey: "sqlite",
3008
- session: "sqlite",
3009
- identity: "sqlite",
3010
- senderKey: "sqlite",
3011
- appState: "sqlite",
3012
- privacyToken: "sqlite",
3013
- messages: "none",
3014
- threads: "none",
3015
- contacts: "none"
3016
- },
3017
- cacheProviders: {
3018
- messageSecret: "sqlite"
3019
- }
3020
- });
3055
+ const authPath = resolve2(this.#options.auth);
3056
+ const sessionId = this.#sessionId();
3057
+ const authPathExisted = await fileExists(authPath);
3058
+ const legacyStorePath = join(authPath, "state.sqlite");
3059
+ const canShareByDirectory = sessionId !== "default" && basename(authPath) === sessionId;
3060
+ const storePath = canShareByDirectory ? join(dirname2(authPath), "state.sqlite") : legacyStorePath;
3061
+ const resetSharedSession = canShareByDirectory && !authPathExisted && await fileExists(storePath);
3062
+ await mkdir(authPath, { recursive: true });
3063
+ const storeEntry = await acquireSharedZapoStore(
3064
+ storePath,
3065
+ sessionId,
3066
+ legacyStorePath,
3067
+ resetSharedSession,
3068
+ this.#logger
3069
+ );
3021
3070
  const client = new WaClient({
3022
- store,
3023
- sessionId: this.#options.sessionId ?? "default",
3071
+ store: storeEntry.store,
3072
+ sessionId,
3024
3073
  markOnlineOnConnect: false,
3025
3074
  deviceBrowser: this.#deviceBrowser(),
3026
3075
  deviceOsDisplayName: this.#deviceOsDisplayName(),
3027
- history: { enabled: false },
3076
+ history: { enabled: true, requireFullSync: true },
3028
3077
  addons: {
3029
3078
  autoDecrypt: true,
3030
3079
  persistAllSecrets: true
3031
3080
  },
3032
- media: { processor: createMediaProcessor() }
3081
+ media: { processor: sharedMediaProcessor }
3033
3082
  }, new WhaNextZapoLogger(this.#logger));
3083
+ this.#storeEntry = storeEntry;
3084
+ this.#storePath = storePath;
3034
3085
  this.#client = client;
3035
3086
  this.#bind(client);
3036
3087
  return client;
@@ -3045,10 +3096,19 @@ var ZapoProvider = class {
3045
3096
  client.on("auth_paired", () => {
3046
3097
  this.#pairingRequired = false;
3047
3098
  });
3099
+ client.on("auth_passkey_required", ({ hasSigner }) => {
3100
+ if (hasSigner) return;
3101
+ const error = new WhaNextError(
3102
+ "AUTH_PASSKEY_REQUIRED",
3103
+ "WhatsApp requires a passkey assertion to link this account, but no passkey signer is configured.",
3104
+ { recoverable: false }
3105
+ );
3106
+ void this.#stopTerminalConnection(client, error);
3107
+ });
3048
3108
  client.on("message", (event) => {
3049
3109
  const protocol = this.#protocolMessage(event);
3050
3110
  if (protocol && this.#isMessageMutationProtocol(protocol.type)) {
3051
- this.#handleProtocolEvent({
3111
+ this.#enqueueProtocolEvent({
3052
3112
  ...event,
3053
3113
  protocolMessage: protocol
3054
3114
  });
@@ -3069,7 +3129,7 @@ var ZapoProvider = class {
3069
3129
  });
3070
3130
  });
3071
3131
  client.on("message_protocol", (event) => {
3072
- this.#handleProtocolEvent(event);
3132
+ this.#enqueueProtocolEvent(event);
3073
3133
  });
3074
3134
  client.on("message_addon", (event) => {
3075
3135
  this.#handleAddonEvent(event);
@@ -3139,7 +3199,7 @@ var ZapoProvider = class {
3139
3199
  ...protocolKey?.participant !== void 0 ? { participant: protocolKey.participant } : event.key.participant !== void 0 ? { participant: event.key.participant } : {},
3140
3200
  ...protocolKey?.participantAlt !== void 0 ? { participantAlt: protocolKey.participantAlt } : event.key.participantAlt !== void 0 ? { participantAlt: event.key.participantAlt } : {}
3141
3201
  };
3142
- this.#handleProtocolEvent({
3202
+ void this.#handleProtocolEvent({
3143
3203
  key: event.key,
3144
3204
  ...event.timestampSeconds !== void 0 ? { timestampSeconds: event.timestampSeconds } : {},
3145
3205
  protocolMessage: {
@@ -3212,7 +3272,15 @@ var ZapoProvider = class {
3212
3272
  if (timestamp === void 0) return false;
3213
3273
  return timestamp < this.#connectedAtSeconds - 3;
3214
3274
  }
3215
- #handleProtocolEvent(event) {
3275
+ #enqueueProtocolEvent(event) {
3276
+ this.#protocolMutationQueue = this.#protocolMutationQueue.then(() => this.#handleProtocolEvent(event)).catch((error) => {
3277
+ this.#logger.debug("Could not process a Zapo protocol mutation.", {
3278
+ error: error instanceof Error ? error : new Error(String(error)),
3279
+ messageId: event.key.id ?? void 0
3280
+ });
3281
+ });
3282
+ }
3283
+ async #handleProtocolEvent(event) {
3216
3284
  if (this.#shouldIgnoreProtocolEvent(event)) {
3217
3285
  this.#logger.debug("Ignored protocol mutation from before the current live connection.", {
3218
3286
  messageId: event.key.id ?? void 0,
@@ -3240,7 +3308,7 @@ var ZapoProvider = class {
3240
3308
  ...participant !== void 0 ? { participant } : {},
3241
3309
  ...participantAlt !== void 0 ? { participantAlt } : {}
3242
3310
  };
3243
- const stored = this.#findStoredMessage(target);
3311
+ const stored = await this.#findStoredMessage(target);
3244
3312
  if (!target.remoteJid && stored?.key.remoteJid) target.remoteJid = stored.key.remoteJid;
3245
3313
  if (!target.remoteJid) return;
3246
3314
  const type = protocol.type;
@@ -3335,20 +3403,26 @@ var ZapoProvider = class {
3335
3403
  this.#connected = true;
3336
3404
  this.#connectedAtSeconds = Math.floor(Date.now() / 1e3);
3337
3405
  this.#reconnectAttempt = 0;
3406
+ this.#closedNotified = false;
3338
3407
  await this.#events.emit("connection", { state: "connected" });
3339
3408
  return;
3340
3409
  }
3341
3410
  this.#connectPromise = void 0;
3342
3411
  this.#connected = false;
3343
- const error = event.reason instanceof Error ? event.reason : event.reason ? new Error(String(event.reason)) : void 0;
3344
- if (this.#intentionalClose || event.isLogout) {
3345
- await this.#events.emit("connection", {
3346
- state: "closed",
3347
- ...error ? { error } : {}
3348
- });
3412
+ const details = disconnectDetails(event.reason, event.code);
3413
+ const error = connectionError(event.reason, details);
3414
+ if (this.#intentionalClose || event.isLogout === true || shouldStopAutomaticReconnect(details)) {
3415
+ if (!this.#intentionalClose && shouldStopAutomaticReconnect(details)) {
3416
+ this.#logger.warn("WhatsApp closed the session with a non-retryable reason.", {
3417
+ ...details.reason ? { reason: details.reason } : {},
3418
+ ...details.code !== void 0 ? { code: details.code } : {}
3419
+ });
3420
+ }
3421
+ await this.#emitClosedOnce(error);
3422
+ await this.#releaseStore();
3349
3423
  return;
3350
3424
  }
3351
- await this.#scheduleReconnect(error);
3425
+ await this.#scheduleReconnect(error, reconnectDelayOverride(details));
3352
3426
  }
3353
3427
  #startConnect(client) {
3354
3428
  const promise = client.connect();
@@ -3357,20 +3431,65 @@ var ZapoProvider = class {
3357
3431
  if (this.#connectPromise !== promise) return;
3358
3432
  this.#connectPromise = void 0;
3359
3433
  if (this.#intentionalClose) return;
3360
- const normalized = error instanceof Error ? error : new Error(String(error));
3434
+ const details = disconnectDetails(error);
3435
+ const normalized = connectionError(error, details) ?? (error instanceof Error ? error : new Error(String(error)));
3361
3436
  this.#logger.warn("WhatsApp connection attempt failed.", { error: normalized });
3362
- await this.#scheduleReconnect(normalized);
3437
+ if (shouldStopAutomaticReconnect(details)) {
3438
+ await this.#stopTerminalConnection(client, normalized);
3439
+ return;
3440
+ }
3441
+ await this.#scheduleReconnect(normalized, reconnectDelayOverride(details));
3363
3442
  });
3364
3443
  }
3365
- async #scheduleReconnect(error) {
3444
+ async #stopTerminalConnection(client, error) {
3445
+ if (client !== this.#client) return;
3446
+ this.#intentionalClose = true;
3447
+ if (this.#reconnectTimer) {
3448
+ clearTimeout(this.#reconnectTimer);
3449
+ this.#reconnectTimer = void 0;
3450
+ }
3451
+ this.#connectPromise = void 0;
3452
+ this.#connected = false;
3453
+ await this.#emitClosedOnce(error);
3454
+ this.#client = void 0;
3455
+ try {
3456
+ await client.disconnect();
3457
+ } catch (disconnectError) {
3458
+ this.#logger.debug("Could not close a terminal Zapo connection cleanly.", {
3459
+ error: disconnectError instanceof Error ? disconnectError : new Error(String(disconnectError))
3460
+ });
3461
+ } finally {
3462
+ await this.#releaseStore();
3463
+ }
3464
+ }
3465
+ async #emitClosedOnce(error) {
3466
+ if (this.#closedNotified) return;
3467
+ this.#closedNotified = true;
3468
+ await this.#events.emit("connection", {
3469
+ state: "closed",
3470
+ ...error ? { error } : {}
3471
+ });
3472
+ }
3473
+ async #releaseStore() {
3474
+ const entry = this.#storeEntry;
3475
+ const storePath = this.#storePath;
3476
+ const sessionId = this.#sessionId();
3477
+ this.#storeEntry = void 0;
3478
+ this.#storePath = void 0;
3479
+ this.#client = void 0;
3480
+ if (!entry || !storePath) return;
3481
+ await releaseSharedZapoStore(storePath, sessionId, entry, this.#logger);
3482
+ }
3483
+ #sessionId() {
3484
+ return this.#options.sessionId ?? "default";
3485
+ }
3486
+ async #scheduleReconnect(error, delayOverrideMs) {
3366
3487
  if (this.#reconnectTimer) return;
3367
3488
  const options = this.#options.reconnect;
3368
3489
  const maxAttempts = options?.maxAttempts ?? 10;
3369
3490
  if (options?.enabled === false || this.#reconnectAttempt >= maxAttempts) {
3370
- await this.#events.emit("connection", {
3371
- state: "closed",
3372
- ...error ? { error } : {}
3373
- });
3491
+ await this.#emitClosedOnce(error);
3492
+ await this.#releaseStore();
3374
3493
  return;
3375
3494
  }
3376
3495
  this.#reconnectAttempt += 1;
@@ -3381,11 +3500,13 @@ var ZapoProvider = class {
3381
3500
  });
3382
3501
  const initial = options?.initialDelayMs ?? 1e3;
3383
3502
  const maximum = options?.maxDelayMs ?? 3e4;
3384
- const delay = Math.min(maximum, initial * 2 ** (this.#reconnectAttempt - 1));
3503
+ const exponential = Math.min(maximum, initial * 2 ** (this.#reconnectAttempt - 1));
3504
+ const delay = delayOverrideMs ?? exponential;
3505
+ const jitter = delay > 0 ? Math.floor(Math.random() * 250) : 0;
3385
3506
  this.#reconnectTimer = setTimeout(() => {
3386
3507
  this.#reconnectTimer = void 0;
3387
3508
  void this.connect();
3388
- }, delay + Math.floor(Math.random() * 250));
3509
+ }, delay + jitter);
3389
3510
  }
3390
3511
  async #sendButtons(chatId, content, replyTo) {
3391
3512
  this.#validateButtons(content);
@@ -3718,7 +3839,7 @@ var ZapoProvider = class {
3718
3839
  #groupParticipantIds(event) {
3719
3840
  return uniqueIdentities((event.participants ?? []).map((participant) => participant.jid ?? participant.lidJid ?? participant.phoneJid));
3720
3841
  }
3721
- #findStoredMessage(key) {
3842
+ async #findStoredMessage(key) {
3722
3843
  const direct = this.#messageStore.get(this.#messageStoreKey(key));
3723
3844
  if (direct) return direct;
3724
3845
  if (!key.id) return void 0;
@@ -3734,18 +3855,103 @@ var ZapoProvider = class {
3734
3855
  this.#remember(storedQuoted);
3735
3856
  return storedQuoted;
3736
3857
  }
3737
- return void 0;
3858
+ return this.#readArchivedMessage(key);
3738
3859
  }
3739
3860
  #remember(message) {
3740
3861
  if (!message.key.id || !message.message) return;
3741
3862
  const key = this.#messageStoreKey(message.key);
3742
3863
  this.#messageStore.delete(key);
3743
3864
  this.#messageStore.set(key, message);
3865
+ void this.#archiveMessage(message);
3744
3866
  while (this.#messageStore.size > this.#messageCacheSize) {
3745
3867
  const oldest = this.#messageStore.keys().next().value;
3746
3868
  if (oldest) this.#messageStore.delete(oldest);
3747
3869
  }
3748
3870
  }
3871
+ async #archiveMessage(message) {
3872
+ const id = message.key.id;
3873
+ const chatId = message.key.remoteJid;
3874
+ const content = message.message;
3875
+ const entry = this.#storeEntry;
3876
+ if (!id || !chatId || !content || !entry) return;
3877
+ try {
3878
+ const participantId = message.key.participant ?? message.key.participantAlt ?? null;
3879
+ const timestampSeconds = toSeconds(message.timestampSeconds) ?? null;
3880
+ const messageBytes = Buffer.from(proto.Message.encode(content).finish());
3881
+ const storedAtSeconds = Math.floor(Date.now() / 1e3);
3882
+ const sessionId = this.#sessionId();
3883
+ entry.snapshotUpsert.run(
3884
+ sessionId,
3885
+ id,
3886
+ chatId,
3887
+ participantId,
3888
+ message.key.fromMe === true ? 1 : 0,
3889
+ timestampSeconds,
3890
+ messageBytes,
3891
+ storedAtSeconds
3892
+ );
3893
+ this.#messageSnapshotsSincePrune += 1;
3894
+ if (this.#messageSnapshotsSincePrune >= messageSnapshotPruneInterval) {
3895
+ this.#messageSnapshotsSincePrune = 0;
3896
+ entry.snapshotPruneAge.run(
3897
+ sessionId,
3898
+ storedAtSeconds - messageSnapshotRetentionSeconds
3899
+ );
3900
+ entry.snapshotPruneOverflow.run(
3901
+ sessionId,
3902
+ sessionId,
3903
+ messageSnapshotMaxPerSession
3904
+ );
3905
+ }
3906
+ } catch (error) {
3907
+ this.#logger.debug("Could not archive a message snapshot for mutation recovery.", {
3908
+ error: error instanceof Error ? error : new Error(String(error)),
3909
+ messageId: id,
3910
+ chatId
3911
+ });
3912
+ }
3913
+ }
3914
+ async #readArchivedMessage(key) {
3915
+ const id = key.id;
3916
+ const entry = this.#storeEntry;
3917
+ if (!id || !entry) return void 0;
3918
+ try {
3919
+ const archived = entry.snapshotGet.get(
3920
+ this.#sessionId(),
3921
+ id
3922
+ );
3923
+ const messageBytes = bytesField(archived?.message_bytes);
3924
+ if (!archived || !messageBytes) return void 0;
3925
+ const archivedChatId = stringValue(archived.chat_id);
3926
+ const remoteJid = key.remoteJid ?? archivedChatId;
3927
+ if (!remoteJid) return void 0;
3928
+ const participant = key.participant ?? key.participantAlt ?? stringValue(archived.participant_id);
3929
+ const archivedFromMe = booleanValue(archived.from_me);
3930
+ const timestampSeconds = numberValue(archived.timestamp_seconds);
3931
+ const fromMe = key.fromMe ?? archivedFromMe;
3932
+ const restored = {
3933
+ key: {
3934
+ ...key,
3935
+ id,
3936
+ remoteJid,
3937
+ ...fromMe !== void 0 ? { fromMe } : {},
3938
+ ...participant ? { participant } : {}
3939
+ },
3940
+ message: proto.Message.decode(messageBytes),
3941
+ ...timestampSeconds !== void 0 ? { timestampSeconds } : {}
3942
+ };
3943
+ const cacheKey = this.#messageStoreKey(restored.key);
3944
+ this.#messageStore.delete(cacheKey);
3945
+ this.#messageStore.set(cacheKey, restored);
3946
+ return restored;
3947
+ } catch (error) {
3948
+ this.#logger.debug("Could not restore a message snapshot for mutation recovery.", {
3949
+ error: error instanceof Error ? error : new Error(String(error)),
3950
+ messageId: id
3951
+ });
3952
+ return void 0;
3953
+ }
3954
+ }
3749
3955
  #messageStoreKey(key) {
3750
3956
  return `${key.remoteJid ?? ""}:${key.id ?? ""}:${key.participant ?? key.participantAlt ?? ""}`;
3751
3957
  }
@@ -3788,8 +3994,8 @@ var ZapoProvider = class {
3788
3994
  }
3789
3995
  #preparePairingGate() {
3790
3996
  this.#pairingRequired = false;
3791
- this.#pairingReady = new Promise((resolve3) => {
3792
- this.#resolvePairingReady = resolve3;
3997
+ this.#pairingReady = new Promise((resolve4) => {
3998
+ this.#resolvePairingReady = resolve4;
3793
3999
  });
3794
4000
  }
3795
4001
  #deviceBrowser() {
@@ -3831,6 +4037,395 @@ var ZapoProvider = class {
3831
4037
  }
3832
4038
  }
3833
4039
  };
4040
+ function disconnectDetails(value, explicitCode) {
4041
+ let code = typeof explicitCode === "number" ? explicitCode : void 0;
4042
+ let reason;
4043
+ const queue = [value];
4044
+ const seen = /* @__PURE__ */ new Set();
4045
+ for (let depth = 0; queue.length > 0 && depth < 12; depth += 1) {
4046
+ const current = queue.shift();
4047
+ if (typeof current === "string") {
4048
+ if (/^\d+$/.test(current)) {
4049
+ code ??= Number(current);
4050
+ } else {
4051
+ reason ??= current;
4052
+ }
4053
+ continue;
4054
+ }
4055
+ if (!current || typeof current !== "object") continue;
4056
+ if (seen.has(current)) continue;
4057
+ seen.add(current);
4058
+ const record = current;
4059
+ const directCode = record.code;
4060
+ const statusCode = record.statusCode;
4061
+ if (typeof directCode === "number") code ??= directCode;
4062
+ if (typeof statusCode === "number") code ??= statusCode;
4063
+ const directReason = record.reason;
4064
+ const failureReason = record.failureReason;
4065
+ if (typeof directReason === "string") {
4066
+ if (/^\d+$/.test(directReason)) code ??= Number(directReason);
4067
+ else reason ??= directReason;
4068
+ }
4069
+ if (typeof failureReason === "string") reason ??= failureReason;
4070
+ const output = record.output;
4071
+ const data = record.data;
4072
+ if (output && typeof output === "object") queue.push(output);
4073
+ if (data && typeof data === "object") queue.push(data);
4074
+ if (record.cause !== void 0) queue.push(record.cause);
4075
+ }
4076
+ return {
4077
+ ...reason ? { reason } : {},
4078
+ ...code !== void 0 ? { code } : {}
4079
+ };
4080
+ }
4081
+ function shouldStopAutomaticReconnect(details) {
4082
+ if (details.reason === "client_disconnected") return true;
4083
+ if (details.reason && fatalDisconnectReasons.has(details.reason)) return true;
4084
+ return details.code !== void 0 && fatalDisconnectCodes.has(details.code);
4085
+ }
4086
+ function reconnectDelayOverride(details) {
4087
+ if (details.code === 515) return 0;
4088
+ if (details.code === 402) return 6e4;
4089
+ return void 0;
4090
+ }
4091
+ function connectionError(value, details) {
4092
+ if (value === void 0 && details.code === void 0 && details.reason === void 0) {
4093
+ return void 0;
4094
+ }
4095
+ const authExpired = details.code === 401 || details.code === 516 || details.reason === "failure_not_authorized" || details.reason === "stream_error_device_removed" || details.reason === "stream_error_force_logout" || details.reason === "primary_identity_key_change";
4096
+ const cause = value instanceof Error ? value : details.code !== void 0 ? {
4097
+ output: { statusCode: details.code },
4098
+ data: { reason: String(details.code) },
4099
+ ...value !== void 0 ? { cause: value } : {}
4100
+ } : value;
4101
+ const message = authExpired ? "WhatsApp authorization is no longer valid and the account must be paired again." : details.code === 402 ? "WhatsApp temporarily refused this session. The next reconnect will use an extended backoff." : "WhatsApp closed the connection.";
4102
+ return new WhaNextError(
4103
+ authExpired ? "AUTH_EXPIRED" : "CONNECTION_FAILED",
4104
+ message,
4105
+ {
4106
+ ...cause !== void 0 ? { cause } : {},
4107
+ context: {
4108
+ ...details.reason ? { reason: details.reason } : {},
4109
+ ...details.code !== void 0 ? { statusCode: details.code } : {}
4110
+ },
4111
+ recoverable: details.code === 402 || details.code === 500 || details.code === 503 || details.code === 515
4112
+ }
4113
+ );
4114
+ }
4115
+ async function acquireSharedZapoStore(storePath, sessionId, legacyStorePath, resetSession, logger) {
4116
+ if (storePath === legacyStorePath) {
4117
+ const entry2 = createZapoStoreEntry(storePath);
4118
+ entry2.refs = 1;
4119
+ entry2.sessionRefs.set(sessionId, 1);
4120
+ return entry2;
4121
+ }
4122
+ let pending = sharedZapoStorePromises.get(storePath);
4123
+ if (!pending) {
4124
+ pending = (async () => {
4125
+ await mkdir(dirname2(storePath), { recursive: true });
4126
+ const entry2 = createZapoStoreEntry(storePath);
4127
+ sharedZapoStores.set(storePath, entry2);
4128
+ return entry2;
4129
+ })();
4130
+ sharedZapoStorePromises.set(storePath, pending);
4131
+ }
4132
+ let entry;
4133
+ try {
4134
+ entry = await pending;
4135
+ } catch (error) {
4136
+ if (sharedZapoStorePromises.get(storePath) === pending) {
4137
+ sharedZapoStorePromises.delete(storePath);
4138
+ }
4139
+ throw error;
4140
+ }
4141
+ entry.refs += 1;
4142
+ entry.sessionRefs.set(sessionId, (entry.sessionRefs.get(sessionId) ?? 0) + 1);
4143
+ if (resetSession || await fileExists(legacyStorePath)) {
4144
+ entry.migrationQueue = entry.migrationQueue.then(async () => {
4145
+ if (resetSession) {
4146
+ clearZapoSessionData(storePath, sessionId);
4147
+ entry.snapshotClearSession.run(sessionId);
4148
+ }
4149
+ if (await fileExists(legacyStorePath)) {
4150
+ await migrateLegacyZapoStore(storePath, legacyStorePath, sessionId, logger);
4151
+ }
4152
+ });
4153
+ await entry.migrationQueue;
4154
+ }
4155
+ return entry;
4156
+ }
4157
+ function createZapoStoreEntry(storePath) {
4158
+ const store = createStore({
4159
+ backends: {
4160
+ sqlite: createSqliteStore({
4161
+ path: storePath,
4162
+ driver: "auto"
4163
+ })
4164
+ },
4165
+ providers: {
4166
+ auth: "sqlite",
4167
+ signal: "sqlite",
4168
+ preKey: "sqlite",
4169
+ session: "sqlite",
4170
+ identity: "sqlite",
4171
+ senderKey: "sqlite",
4172
+ appState: "sqlite",
4173
+ privacyToken: "sqlite",
4174
+ messages: "sqlite",
4175
+ threads: "sqlite",
4176
+ contacts: "sqlite"
4177
+ },
4178
+ cacheProviders: {
4179
+ messageSecret: "sqlite"
4180
+ }
4181
+ });
4182
+ const Database = require2("better-sqlite3");
4183
+ const snapshotDb = new Database(messageSnapshotStorePath(storePath));
4184
+ snapshotDb.exec(`
4185
+ PRAGMA busy_timeout = 5000;
4186
+ CREATE TABLE IF NOT EXISTS whanext_message_snapshots (
4187
+ session_id TEXT NOT NULL,
4188
+ message_id TEXT NOT NULL,
4189
+ chat_id TEXT NOT NULL,
4190
+ participant_id TEXT,
4191
+ from_me INTEGER NOT NULL,
4192
+ timestamp_seconds INTEGER,
4193
+ message_bytes BLOB NOT NULL,
4194
+ stored_at_seconds INTEGER NOT NULL,
4195
+ PRIMARY KEY (session_id, message_id)
4196
+ );
4197
+ CREATE INDEX IF NOT EXISTS idx_whanext_message_snapshots_chat
4198
+ ON whanext_message_snapshots (session_id, chat_id);
4199
+ `);
4200
+ const snapshotColumns = snapshotDb.prepare(
4201
+ "PRAGMA table_info(whanext_message_snapshots)"
4202
+ ).all().map((column) => stringField2(column, "name")).filter((name) => name !== void 0);
4203
+ if (!snapshotColumns.includes("stored_at_seconds")) {
4204
+ snapshotDb.exec(
4205
+ "ALTER TABLE whanext_message_snapshots ADD COLUMN stored_at_seconds INTEGER NOT NULL DEFAULT 0"
4206
+ );
4207
+ }
4208
+ snapshotDb.exec(`
4209
+ CREATE INDEX IF NOT EXISTS idx_whanext_message_snapshots_retention
4210
+ ON whanext_message_snapshots (session_id, stored_at_seconds);
4211
+ `);
4212
+ return {
4213
+ store,
4214
+ snapshotDb,
4215
+ snapshotUpsert: snapshotDb.prepare(`
4216
+ INSERT INTO whanext_message_snapshots (
4217
+ session_id, message_id, chat_id, participant_id, from_me,
4218
+ timestamp_seconds, message_bytes, stored_at_seconds
4219
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
4220
+ ON CONFLICT(session_id, message_id) DO UPDATE SET
4221
+ chat_id = excluded.chat_id,
4222
+ participant_id = excluded.participant_id,
4223
+ from_me = excluded.from_me,
4224
+ timestamp_seconds = excluded.timestamp_seconds,
4225
+ message_bytes = excluded.message_bytes,
4226
+ stored_at_seconds = excluded.stored_at_seconds
4227
+ `),
4228
+ snapshotGet: snapshotDb.prepare(`
4229
+ SELECT chat_id, participant_id, from_me, timestamp_seconds, message_bytes
4230
+ FROM whanext_message_snapshots
4231
+ WHERE session_id = ? AND message_id = ?
4232
+ LIMIT 1
4233
+ `),
4234
+ snapshotPruneAge: snapshotDb.prepare(`
4235
+ DELETE FROM whanext_message_snapshots
4236
+ WHERE session_id = ? AND stored_at_seconds < ?
4237
+ `),
4238
+ snapshotPruneOverflow: snapshotDb.prepare(`
4239
+ DELETE FROM whanext_message_snapshots
4240
+ WHERE session_id = ?
4241
+ AND message_id IN (
4242
+ SELECT message_id
4243
+ FROM whanext_message_snapshots
4244
+ WHERE session_id = ?
4245
+ ORDER BY stored_at_seconds DESC
4246
+ LIMIT -1 OFFSET ?
4247
+ )
4248
+ `),
4249
+ snapshotClearSession: snapshotDb.prepare(`
4250
+ DELETE FROM whanext_message_snapshots
4251
+ WHERE session_id = ?
4252
+ `),
4253
+ refs: 0,
4254
+ sessionRefs: /* @__PURE__ */ new Map(),
4255
+ migrationQueue: Promise.resolve()
4256
+ };
4257
+ }
4258
+ function messageSnapshotStorePath(storePath) {
4259
+ return join(dirname2(storePath), "whanext-messages.sqlite");
4260
+ }
4261
+ async function releaseSharedZapoStore(storePath, sessionId, entry, logger) {
4262
+ const currentSessionRefs = entry.sessionRefs.get(sessionId) ?? 0;
4263
+ if (currentSessionRefs <= 1) {
4264
+ entry.sessionRefs.delete(sessionId);
4265
+ try {
4266
+ await entry.store.session(sessionId).destroy();
4267
+ } catch (error) {
4268
+ logger.warn("Could not release the Zapo session store cleanly.", {
4269
+ error: error instanceof Error ? error : new Error(String(error)),
4270
+ sessionId
4271
+ });
4272
+ }
4273
+ } else {
4274
+ entry.sessionRefs.set(sessionId, currentSessionRefs - 1);
4275
+ }
4276
+ entry.refs = Math.max(0, entry.refs - 1);
4277
+ if (entry.refs > 0) return;
4278
+ if (sharedZapoStores.get(storePath) === entry) {
4279
+ sharedZapoStores.delete(storePath);
4280
+ sharedZapoStorePromises.delete(storePath);
4281
+ }
4282
+ try {
4283
+ await entry.store.destroy();
4284
+ } catch (error) {
4285
+ logger.warn("Could not close the shared Zapo store cleanly.", {
4286
+ error: error instanceof Error ? error : new Error(String(error)),
4287
+ storePath
4288
+ });
4289
+ } finally {
4290
+ try {
4291
+ entry.snapshotDb.close();
4292
+ } catch (error) {
4293
+ logger.debug("Could not close the WhaNext message snapshot database cleanly.", {
4294
+ error: error instanceof Error ? error : new Error(String(error)),
4295
+ storePath
4296
+ });
4297
+ }
4298
+ }
4299
+ }
4300
+ function clearZapoSessionData(storePath, sessionId) {
4301
+ let db;
4302
+ try {
4303
+ const Database = require2("better-sqlite3");
4304
+ db = new Database(storePath);
4305
+ const tables = db.prepare(
4306
+ "SELECT name FROM main.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
4307
+ ).all();
4308
+ db.exec("BEGIN IMMEDIATE");
4309
+ try {
4310
+ for (const row of tables) {
4311
+ const table = stringField2(row, "name");
4312
+ if (!table || table === "wa_migrations" || table === "whanext_core_migrations") {
4313
+ continue;
4314
+ }
4315
+ const quoted = sqliteIdentifier(table);
4316
+ const columns = db.prepare(`PRAGMA main.table_info(${quoted})`).all().map((column) => stringField2(column, "name")).filter((name) => name !== void 0);
4317
+ if (!columns.includes("session_id")) continue;
4318
+ db.prepare(`DELETE FROM main.${quoted} WHERE session_id = ?`).run(sessionId);
4319
+ }
4320
+ db.exec("COMMIT");
4321
+ } catch (error) {
4322
+ db.exec("ROLLBACK");
4323
+ throw error;
4324
+ }
4325
+ } finally {
4326
+ db?.close();
4327
+ }
4328
+ }
4329
+ async function migrateLegacyZapoStore(storePath, legacyStorePath, sessionId, logger) {
4330
+ let db;
4331
+ try {
4332
+ const Database = require2("better-sqlite3");
4333
+ db = new Database(storePath);
4334
+ db.exec(`ATTACH DATABASE ${sqliteString(legacyStorePath)} AS legacy`);
4335
+ db.exec(`
4336
+ CREATE TABLE IF NOT EXISTS whanext_core_migrations (
4337
+ migration_key TEXT PRIMARY KEY,
4338
+ migrated_at INTEGER NOT NULL
4339
+ )
4340
+ `);
4341
+ const migrationKey = `shared-store:${resolve2(legacyStorePath)}:${sessionId}`;
4342
+ const migrated = db.prepare(
4343
+ "SELECT migration_key FROM whanext_core_migrations WHERE migration_key = ?"
4344
+ ).get(migrationKey);
4345
+ if (migrated) return;
4346
+ const mainTables = new Set(
4347
+ db.prepare("SELECT name FROM main.sqlite_master WHERE type = 'table'").all().map((row) => stringField2(row, "name")).filter((name) => name !== void 0)
4348
+ );
4349
+ const legacyTables = db.prepare(
4350
+ "SELECT name FROM legacy.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
4351
+ ).all();
4352
+ db.exec("BEGIN IMMEDIATE");
4353
+ try {
4354
+ for (const row of legacyTables) {
4355
+ const table = stringField2(row, "name");
4356
+ if (!table || !mainTables.has(table) || table === "wa_migrations" || table === "whanext_core_migrations") {
4357
+ continue;
4358
+ }
4359
+ const quoted = sqliteIdentifier(table);
4360
+ const mainColumns = db.prepare(`PRAGMA main.table_info(${quoted})`).all().map((column) => stringField2(column, "name")).filter((name) => name !== void 0);
4361
+ const legacyColumns = new Set(
4362
+ db.prepare(`PRAGMA legacy.table_info(${quoted})`).all().map((column) => stringField2(column, "name")).filter((name) => name !== void 0)
4363
+ );
4364
+ const columns = mainColumns.filter((column) => legacyColumns.has(column));
4365
+ if (!columns.includes("session_id") || columns.length === 0) continue;
4366
+ const selected = columns.map(sqliteIdentifier).join(", ");
4367
+ db.prepare(
4368
+ `INSERT OR REPLACE INTO main.${quoted} (${selected}) SELECT ${selected} FROM legacy.${quoted} WHERE session_id = ?`
4369
+ ).run(sessionId);
4370
+ }
4371
+ db.prepare(
4372
+ "INSERT OR REPLACE INTO whanext_core_migrations (migration_key, migrated_at) VALUES (?, ?)"
4373
+ ).run(migrationKey, Date.now());
4374
+ db.exec("COMMIT");
4375
+ } catch (error) {
4376
+ db.exec("ROLLBACK");
4377
+ throw error;
4378
+ }
4379
+ } catch (error) {
4380
+ logger.warn("Could not migrate the legacy per-account Zapo store; keeping the shared store active.", {
4381
+ error: error instanceof Error ? error : new Error(String(error)),
4382
+ sessionId,
4383
+ legacyStorePath,
4384
+ storePath
4385
+ });
4386
+ } finally {
4387
+ if (db) {
4388
+ try {
4389
+ db.exec("DETACH DATABASE legacy");
4390
+ } catch {
4391
+ }
4392
+ db.close();
4393
+ }
4394
+ }
4395
+ }
4396
+ async function fileExists(path2) {
4397
+ try {
4398
+ await stat(path2);
4399
+ return true;
4400
+ } catch {
4401
+ return false;
4402
+ }
4403
+ }
4404
+ function sqliteIdentifier(value) {
4405
+ return `"${value.replaceAll('"', '""')}"`;
4406
+ }
4407
+ function sqliteString(value) {
4408
+ return `'${value.replaceAll("'", "''")}'`;
4409
+ }
4410
+ function stringField2(record, field) {
4411
+ const value = record[field];
4412
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4413
+ }
4414
+ function stringValue(value) {
4415
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4416
+ }
4417
+ function numberValue(value) {
4418
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4419
+ }
4420
+ function booleanValue(value) {
4421
+ if (value === true || value === 1) return true;
4422
+ if (value === false || value === 0) return false;
4423
+ return void 0;
4424
+ }
4425
+ function bytesField(value) {
4426
+ if (value instanceof Uint8Array) return value;
4427
+ return void 0;
4428
+ }
3834
4429
  var WhaNextZapoLogger = class _WhaNextZapoLogger {
3835
4430
  level;
3836
4431
  #logger;
@@ -3903,7 +4498,7 @@ async function create(options = {}) {
3903
4498
  // src/app/multi-app.ts
3904
4499
  import {
3905
4500
  join as join2,
3906
- resolve as resolve2
4501
+ resolve as resolve3
3907
4502
  } from "path";
3908
4503
  var MultiCommandRouter = class {
3909
4504
  #apps;
@@ -4048,7 +4643,7 @@ async function createMulti(options) {
4048
4643
  }
4049
4644
  normalizedIds.add(normalizedId);
4050
4645
  if (account.provider === void 0) {
4051
- const authPath = resolve2(account.auth ?? join2(authRoot, account.id));
4646
+ const authPath = resolve3(account.auth ?? join2(authRoot, account.id));
4052
4647
  if (authPaths.has(authPath)) {
4053
4648
  throw new WhaNextError(
4054
4649
  "ARGUMENT_INVALID",