@whanext/core 0.19.5 → 0.19.7

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
@@ -370,7 +370,7 @@ var CommandConcurrencyController = class {
370
370
  recoverable: true
371
371
  });
372
372
  } else if (strategy === "queue" && state.active >= max) {
373
- await new Promise((resolve3) => state.queue.push(resolve3));
373
+ await new Promise((resolve4) => state.queue.push(resolve4));
374
374
  }
375
375
  const controller = new AbortController();
376
376
  state.controllers.add(controller);
@@ -1617,8 +1617,8 @@ var SqliteMuteStore = class {
1617
1617
  }
1618
1618
  let database;
1619
1619
  try {
1620
- const require2 = createRequire(import.meta.url);
1621
- const sqlite = require2("node:sqlite");
1620
+ const require3 = createRequire(import.meta.url);
1621
+ const sqlite = require3("node:sqlite");
1622
1622
  database = new sqlite.DatabaseSync(databasePath);
1623
1623
  database.exec("PRAGMA journal_mode = WAL");
1624
1624
  database.exec("PRAGMA busy_timeout = 5000");
@@ -2203,12 +2203,12 @@ var WhaNextApp = class {
2203
2203
  this.logger.info("Login started");
2204
2204
  let unsubscribe = () => void 0;
2205
2205
  let timer;
2206
- const connected = new Promise((resolve3, reject) => {
2206
+ const connected = new Promise((resolve4, reject) => {
2207
2207
  unsubscribe = this.#provider.on("connection", (update) => {
2208
- if (update.state === "connected") resolve3();
2208
+ if (update.state === "connected") resolve4();
2209
2209
  if (update.state === "closed") {
2210
2210
  reject(
2211
- new WhaNextError(
2211
+ update.error instanceof WhaNextError ? update.error : new WhaNextError(
2212
2212
  "CONNECTION_FAILED",
2213
2213
  "WhatsApp closed the connection before login completed.",
2214
2214
  {
@@ -2375,8 +2375,9 @@ var Browser = /* @__PURE__ */ ((Browser2) => {
2375
2375
  })(Browser || {});
2376
2376
 
2377
2377
  // src/provider/zapo/zapo-provider.ts
2378
- import { mkdir } from "fs/promises";
2379
- import { join } from "path";
2378
+ import { mkdir, stat } from "fs/promises";
2379
+ import { createRequire as createRequire2 } from "module";
2380
+ import { basename, dirname as dirname2, join, resolve as resolve2 } from "path";
2380
2381
  import { createMediaProcessor } from "@zapo-js/media-utils";
2381
2382
  import { createSqliteStore } from "@zapo-js/store-sqlite";
2382
2383
  import {
@@ -2706,6 +2707,25 @@ function toNumber(value) {
2706
2707
  }
2707
2708
 
2708
2709
  // src/provider/zapo/zapo-provider.ts
2710
+ var require2 = createRequire2(import.meta.url);
2711
+ var sharedZapoStores = /* @__PURE__ */ new Map();
2712
+ var sharedZapoStorePromises = /* @__PURE__ */ new Map();
2713
+ var fatalDisconnectReasons = /* @__PURE__ */ new Set([
2714
+ "stream_error_replaced",
2715
+ "stream_error_device_removed",
2716
+ "stream_error_force_logout",
2717
+ "failure_not_authorized",
2718
+ "failure_banned",
2719
+ "failure_locked",
2720
+ "failure_client_too_old",
2721
+ "failure_bad_user_agent",
2722
+ "primary_identity_key_change"
2723
+ ]);
2724
+ var fatalDisconnectCodes = /* @__PURE__ */ new Set([401, 403, 405, 406, 409, 516]);
2725
+ var sharedMediaProcessor = createMediaProcessor();
2726
+ var messageSnapshotRetentionSeconds = 7 * 24 * 60 * 60;
2727
+ var messageSnapshotMaxPerSession = 2e4;
2728
+ var messageSnapshotPruneInterval = 256;
2709
2729
  var ZapoProvider = class {
2710
2730
  #options;
2711
2731
  #events = new TypedEventEmitter();
@@ -2716,14 +2736,19 @@ var ZapoProvider = class {
2716
2736
  #handledProtocolStore = /* @__PURE__ */ new Set();
2717
2737
  #callCreatorStore = /* @__PURE__ */ new Map();
2718
2738
  #messageCacheSize;
2739
+ #protocolMutationQueue = Promise.resolve();
2719
2740
  #client;
2741
+ #storeEntry;
2742
+ #storePath;
2720
2743
  #intentionalClose = false;
2744
+ #closedNotified = false;
2721
2745
  #reconnectAttempt = 0;
2722
2746
  #reconnectTimer;
2723
2747
  #connectPromise;
2724
2748
  #pairingRequired = false;
2725
2749
  #connected = false;
2726
2750
  #connectedAtSeconds = 0;
2751
+ #messageSnapshotsSincePrune = 0;
2727
2752
  #pairingReady = Promise.resolve();
2728
2753
  #resolvePairingReady;
2729
2754
  constructor(options) {
@@ -2736,6 +2761,7 @@ var ZapoProvider = class {
2736
2761
  }
2737
2762
  async connect() {
2738
2763
  this.#intentionalClose = false;
2764
+ this.#closedNotified = false;
2739
2765
  const client = await this.#ensureClient();
2740
2766
  if (this.#connected || this.#connectPromise) {
2741
2767
  return;
@@ -2755,12 +2781,16 @@ var ZapoProvider = class {
2755
2781
  }
2756
2782
  const client = this.#client;
2757
2783
  this.#connectPromise = void 0;
2758
- if (client) {
2759
- await client.disconnect();
2760
- } else {
2761
- await this.#events.emit("connection", { state: "closed" });
2784
+ try {
2785
+ if (client) {
2786
+ await client.disconnect();
2787
+ } else {
2788
+ await this.#emitClosedOnce();
2789
+ }
2790
+ } finally {
2791
+ this.#connected = false;
2792
+ await this.#releaseStore();
2762
2793
  }
2763
- this.#connected = false;
2764
2794
  }
2765
2795
  getCurrentUserIds() {
2766
2796
  const credentials = this.#client?.getCredentials();
@@ -2819,7 +2849,7 @@ var ZapoProvider = class {
2819
2849
  return this.#sent(result, chatId);
2820
2850
  }
2821
2851
  async repostMessage(source, chatId, options = {}) {
2822
- const original = this.#findStoredMessage(this.#toZapoKey(source));
2852
+ const original = await this.#findStoredMessage(this.#toZapoKey(source));
2823
2853
  if (!original?.message) {
2824
2854
  throw new WhaNextError(
2825
2855
  "MESSAGE_NOT_FOUND",
@@ -2849,7 +2879,7 @@ var ZapoProvider = class {
2849
2879
  return this.#sent(result, key.chatId);
2850
2880
  }
2851
2881
  async downloadMedia(key) {
2852
- const message = this.#messageKeyStore.get(key) ?? this.#findStoredMessage(this.#toZapoKey(key));
2882
+ const message = this.#messageKeyStore.get(key) ?? await this.#findStoredMessage(this.#toZapoKey(key));
2853
2883
  if (!message?.message) {
2854
2884
  throw new WhaNextError(
2855
2885
  "MEDIA_NOT_AVAILABLE",
@@ -2993,34 +3023,24 @@ var ZapoProvider = class {
2993
3023
  }
2994
3024
  async #ensureClient() {
2995
3025
  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
- });
3026
+ const authPath = resolve2(this.#options.auth);
3027
+ const sessionId = this.#sessionId();
3028
+ const authPathExisted = await fileExists(authPath);
3029
+ const legacyStorePath = join(authPath, "state.sqlite");
3030
+ const canShareByDirectory = sessionId !== "default" && basename(authPath) === sessionId;
3031
+ const storePath = canShareByDirectory ? join(dirname2(authPath), "state.sqlite") : legacyStorePath;
3032
+ const resetSharedSession = canShareByDirectory && !authPathExisted && await fileExists(storePath);
3033
+ await mkdir(authPath, { recursive: true });
3034
+ const storeEntry = await acquireSharedZapoStore(
3035
+ storePath,
3036
+ sessionId,
3037
+ legacyStorePath,
3038
+ resetSharedSession,
3039
+ this.#logger
3040
+ );
3021
3041
  const client = new WaClient({
3022
- store,
3023
- sessionId: this.#options.sessionId ?? "default",
3042
+ store: storeEntry.store,
3043
+ sessionId,
3024
3044
  markOnlineOnConnect: false,
3025
3045
  deviceBrowser: this.#deviceBrowser(),
3026
3046
  deviceOsDisplayName: this.#deviceOsDisplayName(),
@@ -3029,8 +3049,10 @@ var ZapoProvider = class {
3029
3049
  autoDecrypt: true,
3030
3050
  persistAllSecrets: true
3031
3051
  },
3032
- media: { processor: createMediaProcessor() }
3052
+ media: { processor: sharedMediaProcessor }
3033
3053
  }, new WhaNextZapoLogger(this.#logger));
3054
+ this.#storeEntry = storeEntry;
3055
+ this.#storePath = storePath;
3034
3056
  this.#client = client;
3035
3057
  this.#bind(client);
3036
3058
  return client;
@@ -3045,10 +3067,19 @@ var ZapoProvider = class {
3045
3067
  client.on("auth_paired", () => {
3046
3068
  this.#pairingRequired = false;
3047
3069
  });
3070
+ client.on("auth_passkey_required", ({ hasSigner }) => {
3071
+ if (hasSigner) return;
3072
+ const error = new WhaNextError(
3073
+ "AUTH_PASSKEY_REQUIRED",
3074
+ "WhatsApp requires a passkey assertion to link this account, but no passkey signer is configured.",
3075
+ { recoverable: false }
3076
+ );
3077
+ void this.#stopTerminalConnection(client, error);
3078
+ });
3048
3079
  client.on("message", (event) => {
3049
3080
  const protocol = this.#protocolMessage(event);
3050
3081
  if (protocol && this.#isMessageMutationProtocol(protocol.type)) {
3051
- this.#handleProtocolEvent({
3082
+ this.#enqueueProtocolEvent({
3052
3083
  ...event,
3053
3084
  protocolMessage: protocol
3054
3085
  });
@@ -3069,7 +3100,7 @@ var ZapoProvider = class {
3069
3100
  });
3070
3101
  });
3071
3102
  client.on("message_protocol", (event) => {
3072
- this.#handleProtocolEvent(event);
3103
+ this.#enqueueProtocolEvent(event);
3073
3104
  });
3074
3105
  client.on("message_addon", (event) => {
3075
3106
  this.#handleAddonEvent(event);
@@ -3139,7 +3170,7 @@ var ZapoProvider = class {
3139
3170
  ...protocolKey?.participant !== void 0 ? { participant: protocolKey.participant } : event.key.participant !== void 0 ? { participant: event.key.participant } : {},
3140
3171
  ...protocolKey?.participantAlt !== void 0 ? { participantAlt: protocolKey.participantAlt } : event.key.participantAlt !== void 0 ? { participantAlt: event.key.participantAlt } : {}
3141
3172
  };
3142
- this.#handleProtocolEvent({
3173
+ void this.#handleProtocolEvent({
3143
3174
  key: event.key,
3144
3175
  ...event.timestampSeconds !== void 0 ? { timestampSeconds: event.timestampSeconds } : {},
3145
3176
  protocolMessage: {
@@ -3212,7 +3243,15 @@ var ZapoProvider = class {
3212
3243
  if (timestamp === void 0) return false;
3213
3244
  return timestamp < this.#connectedAtSeconds - 3;
3214
3245
  }
3215
- #handleProtocolEvent(event) {
3246
+ #enqueueProtocolEvent(event) {
3247
+ this.#protocolMutationQueue = this.#protocolMutationQueue.then(() => this.#handleProtocolEvent(event)).catch((error) => {
3248
+ this.#logger.debug("Could not process a Zapo protocol mutation.", {
3249
+ error: error instanceof Error ? error : new Error(String(error)),
3250
+ messageId: event.key.id ?? void 0
3251
+ });
3252
+ });
3253
+ }
3254
+ async #handleProtocolEvent(event) {
3216
3255
  if (this.#shouldIgnoreProtocolEvent(event)) {
3217
3256
  this.#logger.debug("Ignored protocol mutation from before the current live connection.", {
3218
3257
  messageId: event.key.id ?? void 0,
@@ -3240,7 +3279,7 @@ var ZapoProvider = class {
3240
3279
  ...participant !== void 0 ? { participant } : {},
3241
3280
  ...participantAlt !== void 0 ? { participantAlt } : {}
3242
3281
  };
3243
- const stored = this.#findStoredMessage(target);
3282
+ const stored = await this.#findStoredMessage(target);
3244
3283
  if (!target.remoteJid && stored?.key.remoteJid) target.remoteJid = stored.key.remoteJid;
3245
3284
  if (!target.remoteJid) return;
3246
3285
  const type = protocol.type;
@@ -3335,20 +3374,26 @@ var ZapoProvider = class {
3335
3374
  this.#connected = true;
3336
3375
  this.#connectedAtSeconds = Math.floor(Date.now() / 1e3);
3337
3376
  this.#reconnectAttempt = 0;
3377
+ this.#closedNotified = false;
3338
3378
  await this.#events.emit("connection", { state: "connected" });
3339
3379
  return;
3340
3380
  }
3341
3381
  this.#connectPromise = void 0;
3342
3382
  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
- });
3383
+ const details = disconnectDetails(event.reason, event.code);
3384
+ const error = connectionError(event.reason, details);
3385
+ if (this.#intentionalClose || event.isLogout === true || shouldStopAutomaticReconnect(details)) {
3386
+ if (!this.#intentionalClose && shouldStopAutomaticReconnect(details)) {
3387
+ this.#logger.warn("WhatsApp closed the session with a non-retryable reason.", {
3388
+ ...details.reason ? { reason: details.reason } : {},
3389
+ ...details.code !== void 0 ? { code: details.code } : {}
3390
+ });
3391
+ }
3392
+ await this.#emitClosedOnce(error);
3393
+ await this.#releaseStore();
3349
3394
  return;
3350
3395
  }
3351
- await this.#scheduleReconnect(error);
3396
+ await this.#scheduleReconnect(error, reconnectDelayOverride(details));
3352
3397
  }
3353
3398
  #startConnect(client) {
3354
3399
  const promise = client.connect();
@@ -3357,20 +3402,65 @@ var ZapoProvider = class {
3357
3402
  if (this.#connectPromise !== promise) return;
3358
3403
  this.#connectPromise = void 0;
3359
3404
  if (this.#intentionalClose) return;
3360
- const normalized = error instanceof Error ? error : new Error(String(error));
3405
+ const details = disconnectDetails(error);
3406
+ const normalized = connectionError(error, details) ?? (error instanceof Error ? error : new Error(String(error)));
3361
3407
  this.#logger.warn("WhatsApp connection attempt failed.", { error: normalized });
3362
- await this.#scheduleReconnect(normalized);
3408
+ if (shouldStopAutomaticReconnect(details)) {
3409
+ await this.#stopTerminalConnection(client, normalized);
3410
+ return;
3411
+ }
3412
+ await this.#scheduleReconnect(normalized, reconnectDelayOverride(details));
3413
+ });
3414
+ }
3415
+ async #stopTerminalConnection(client, error) {
3416
+ if (client !== this.#client) return;
3417
+ this.#intentionalClose = true;
3418
+ if (this.#reconnectTimer) {
3419
+ clearTimeout(this.#reconnectTimer);
3420
+ this.#reconnectTimer = void 0;
3421
+ }
3422
+ this.#connectPromise = void 0;
3423
+ this.#connected = false;
3424
+ await this.#emitClosedOnce(error);
3425
+ this.#client = void 0;
3426
+ try {
3427
+ await client.disconnect();
3428
+ } catch (disconnectError) {
3429
+ this.#logger.debug("Could not close a terminal Zapo connection cleanly.", {
3430
+ error: disconnectError instanceof Error ? disconnectError : new Error(String(disconnectError))
3431
+ });
3432
+ } finally {
3433
+ await this.#releaseStore();
3434
+ }
3435
+ }
3436
+ async #emitClosedOnce(error) {
3437
+ if (this.#closedNotified) return;
3438
+ this.#closedNotified = true;
3439
+ await this.#events.emit("connection", {
3440
+ state: "closed",
3441
+ ...error ? { error } : {}
3363
3442
  });
3364
3443
  }
3365
- async #scheduleReconnect(error) {
3444
+ async #releaseStore() {
3445
+ const entry = this.#storeEntry;
3446
+ const storePath = this.#storePath;
3447
+ const sessionId = this.#sessionId();
3448
+ this.#storeEntry = void 0;
3449
+ this.#storePath = void 0;
3450
+ this.#client = void 0;
3451
+ if (!entry || !storePath) return;
3452
+ await releaseSharedZapoStore(storePath, sessionId, entry, this.#logger);
3453
+ }
3454
+ #sessionId() {
3455
+ return this.#options.sessionId ?? "default";
3456
+ }
3457
+ async #scheduleReconnect(error, delayOverrideMs) {
3366
3458
  if (this.#reconnectTimer) return;
3367
3459
  const options = this.#options.reconnect;
3368
3460
  const maxAttempts = options?.maxAttempts ?? 10;
3369
3461
  if (options?.enabled === false || this.#reconnectAttempt >= maxAttempts) {
3370
- await this.#events.emit("connection", {
3371
- state: "closed",
3372
- ...error ? { error } : {}
3373
- });
3462
+ await this.#emitClosedOnce(error);
3463
+ await this.#releaseStore();
3374
3464
  return;
3375
3465
  }
3376
3466
  this.#reconnectAttempt += 1;
@@ -3381,11 +3471,13 @@ var ZapoProvider = class {
3381
3471
  });
3382
3472
  const initial = options?.initialDelayMs ?? 1e3;
3383
3473
  const maximum = options?.maxDelayMs ?? 3e4;
3384
- const delay = Math.min(maximum, initial * 2 ** (this.#reconnectAttempt - 1));
3474
+ const exponential = Math.min(maximum, initial * 2 ** (this.#reconnectAttempt - 1));
3475
+ const delay = delayOverrideMs ?? exponential;
3476
+ const jitter = delay > 0 ? Math.floor(Math.random() * 250) : 0;
3385
3477
  this.#reconnectTimer = setTimeout(() => {
3386
3478
  this.#reconnectTimer = void 0;
3387
3479
  void this.connect();
3388
- }, delay + Math.floor(Math.random() * 250));
3480
+ }, delay + jitter);
3389
3481
  }
3390
3482
  async #sendButtons(chatId, content, replyTo) {
3391
3483
  this.#validateButtons(content);
@@ -3718,7 +3810,7 @@ var ZapoProvider = class {
3718
3810
  #groupParticipantIds(event) {
3719
3811
  return uniqueIdentities((event.participants ?? []).map((participant) => participant.jid ?? participant.lidJid ?? participant.phoneJid));
3720
3812
  }
3721
- #findStoredMessage(key) {
3813
+ async #findStoredMessage(key) {
3722
3814
  const direct = this.#messageStore.get(this.#messageStoreKey(key));
3723
3815
  if (direct) return direct;
3724
3816
  if (!key.id) return void 0;
@@ -3734,18 +3826,103 @@ var ZapoProvider = class {
3734
3826
  this.#remember(storedQuoted);
3735
3827
  return storedQuoted;
3736
3828
  }
3737
- return void 0;
3829
+ return this.#readArchivedMessage(key);
3738
3830
  }
3739
3831
  #remember(message) {
3740
3832
  if (!message.key.id || !message.message) return;
3741
3833
  const key = this.#messageStoreKey(message.key);
3742
3834
  this.#messageStore.delete(key);
3743
3835
  this.#messageStore.set(key, message);
3836
+ void this.#archiveMessage(message);
3744
3837
  while (this.#messageStore.size > this.#messageCacheSize) {
3745
3838
  const oldest = this.#messageStore.keys().next().value;
3746
3839
  if (oldest) this.#messageStore.delete(oldest);
3747
3840
  }
3748
3841
  }
3842
+ async #archiveMessage(message) {
3843
+ const id = message.key.id;
3844
+ const chatId = message.key.remoteJid;
3845
+ const content = message.message;
3846
+ const entry = this.#storeEntry;
3847
+ if (!id || !chatId || !content || !entry) return;
3848
+ try {
3849
+ const participantId = message.key.participant ?? message.key.participantAlt ?? null;
3850
+ const timestampSeconds = toSeconds(message.timestampSeconds) ?? null;
3851
+ const messageBytes = Buffer.from(proto.Message.encode(content).finish());
3852
+ const storedAtSeconds = Math.floor(Date.now() / 1e3);
3853
+ const sessionId = this.#sessionId();
3854
+ entry.snapshotUpsert.run(
3855
+ sessionId,
3856
+ id,
3857
+ chatId,
3858
+ participantId,
3859
+ message.key.fromMe === true ? 1 : 0,
3860
+ timestampSeconds,
3861
+ messageBytes,
3862
+ storedAtSeconds
3863
+ );
3864
+ this.#messageSnapshotsSincePrune += 1;
3865
+ if (this.#messageSnapshotsSincePrune >= messageSnapshotPruneInterval) {
3866
+ this.#messageSnapshotsSincePrune = 0;
3867
+ entry.snapshotPruneAge.run(
3868
+ sessionId,
3869
+ storedAtSeconds - messageSnapshotRetentionSeconds
3870
+ );
3871
+ entry.snapshotPruneOverflow.run(
3872
+ sessionId,
3873
+ sessionId,
3874
+ messageSnapshotMaxPerSession
3875
+ );
3876
+ }
3877
+ } catch (error) {
3878
+ this.#logger.debug("Could not archive a message snapshot for mutation recovery.", {
3879
+ error: error instanceof Error ? error : new Error(String(error)),
3880
+ messageId: id,
3881
+ chatId
3882
+ });
3883
+ }
3884
+ }
3885
+ async #readArchivedMessage(key) {
3886
+ const id = key.id;
3887
+ const entry = this.#storeEntry;
3888
+ if (!id || !entry) return void 0;
3889
+ try {
3890
+ const archived = entry.snapshotGet.get(
3891
+ this.#sessionId(),
3892
+ id
3893
+ );
3894
+ const messageBytes = bytesField(archived?.message_bytes);
3895
+ if (!archived || !messageBytes) return void 0;
3896
+ const archivedChatId = stringValue(archived.chat_id);
3897
+ const remoteJid = key.remoteJid ?? archivedChatId;
3898
+ if (!remoteJid) return void 0;
3899
+ const participant = key.participant ?? key.participantAlt ?? stringValue(archived.participant_id);
3900
+ const archivedFromMe = booleanValue(archived.from_me);
3901
+ const timestampSeconds = numberValue(archived.timestamp_seconds);
3902
+ const fromMe = key.fromMe ?? archivedFromMe;
3903
+ const restored = {
3904
+ key: {
3905
+ ...key,
3906
+ id,
3907
+ remoteJid,
3908
+ ...fromMe !== void 0 ? { fromMe } : {},
3909
+ ...participant ? { participant } : {}
3910
+ },
3911
+ message: proto.Message.decode(messageBytes),
3912
+ ...timestampSeconds !== void 0 ? { timestampSeconds } : {}
3913
+ };
3914
+ const cacheKey = this.#messageStoreKey(restored.key);
3915
+ this.#messageStore.delete(cacheKey);
3916
+ this.#messageStore.set(cacheKey, restored);
3917
+ return restored;
3918
+ } catch (error) {
3919
+ this.#logger.debug("Could not restore a message snapshot for mutation recovery.", {
3920
+ error: error instanceof Error ? error : new Error(String(error)),
3921
+ messageId: id
3922
+ });
3923
+ return void 0;
3924
+ }
3925
+ }
3749
3926
  #messageStoreKey(key) {
3750
3927
  return `${key.remoteJid ?? ""}:${key.id ?? ""}:${key.participant ?? key.participantAlt ?? ""}`;
3751
3928
  }
@@ -3788,8 +3965,8 @@ var ZapoProvider = class {
3788
3965
  }
3789
3966
  #preparePairingGate() {
3790
3967
  this.#pairingRequired = false;
3791
- this.#pairingReady = new Promise((resolve3) => {
3792
- this.#resolvePairingReady = resolve3;
3968
+ this.#pairingReady = new Promise((resolve4) => {
3969
+ this.#resolvePairingReady = resolve4;
3793
3970
  });
3794
3971
  }
3795
3972
  #deviceBrowser() {
@@ -3831,6 +4008,395 @@ var ZapoProvider = class {
3831
4008
  }
3832
4009
  }
3833
4010
  };
4011
+ function disconnectDetails(value, explicitCode) {
4012
+ let code = typeof explicitCode === "number" ? explicitCode : void 0;
4013
+ let reason;
4014
+ const queue = [value];
4015
+ const seen = /* @__PURE__ */ new Set();
4016
+ for (let depth = 0; queue.length > 0 && depth < 12; depth += 1) {
4017
+ const current = queue.shift();
4018
+ if (typeof current === "string") {
4019
+ if (/^\d+$/.test(current)) {
4020
+ code ??= Number(current);
4021
+ } else {
4022
+ reason ??= current;
4023
+ }
4024
+ continue;
4025
+ }
4026
+ if (!current || typeof current !== "object") continue;
4027
+ if (seen.has(current)) continue;
4028
+ seen.add(current);
4029
+ const record = current;
4030
+ const directCode = record.code;
4031
+ const statusCode = record.statusCode;
4032
+ if (typeof directCode === "number") code ??= directCode;
4033
+ if (typeof statusCode === "number") code ??= statusCode;
4034
+ const directReason = record.reason;
4035
+ const failureReason = record.failureReason;
4036
+ if (typeof directReason === "string") {
4037
+ if (/^\d+$/.test(directReason)) code ??= Number(directReason);
4038
+ else reason ??= directReason;
4039
+ }
4040
+ if (typeof failureReason === "string") reason ??= failureReason;
4041
+ const output = record.output;
4042
+ const data = record.data;
4043
+ if (output && typeof output === "object") queue.push(output);
4044
+ if (data && typeof data === "object") queue.push(data);
4045
+ if (record.cause !== void 0) queue.push(record.cause);
4046
+ }
4047
+ return {
4048
+ ...reason ? { reason } : {},
4049
+ ...code !== void 0 ? { code } : {}
4050
+ };
4051
+ }
4052
+ function shouldStopAutomaticReconnect(details) {
4053
+ if (details.reason === "client_disconnected") return true;
4054
+ if (details.reason && fatalDisconnectReasons.has(details.reason)) return true;
4055
+ return details.code !== void 0 && fatalDisconnectCodes.has(details.code);
4056
+ }
4057
+ function reconnectDelayOverride(details) {
4058
+ if (details.code === 515) return 0;
4059
+ if (details.code === 402) return 6e4;
4060
+ return void 0;
4061
+ }
4062
+ function connectionError(value, details) {
4063
+ if (value === void 0 && details.code === void 0 && details.reason === void 0) {
4064
+ return void 0;
4065
+ }
4066
+ 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";
4067
+ const cause = value instanceof Error ? value : details.code !== void 0 ? {
4068
+ output: { statusCode: details.code },
4069
+ data: { reason: String(details.code) },
4070
+ ...value !== void 0 ? { cause: value } : {}
4071
+ } : value;
4072
+ 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.";
4073
+ return new WhaNextError(
4074
+ authExpired ? "AUTH_EXPIRED" : "CONNECTION_FAILED",
4075
+ message,
4076
+ {
4077
+ ...cause !== void 0 ? { cause } : {},
4078
+ context: {
4079
+ ...details.reason ? { reason: details.reason } : {},
4080
+ ...details.code !== void 0 ? { statusCode: details.code } : {}
4081
+ },
4082
+ recoverable: details.code === 402 || details.code === 500 || details.code === 503 || details.code === 515
4083
+ }
4084
+ );
4085
+ }
4086
+ async function acquireSharedZapoStore(storePath, sessionId, legacyStorePath, resetSession, logger) {
4087
+ if (storePath === legacyStorePath) {
4088
+ const entry2 = createZapoStoreEntry(storePath);
4089
+ entry2.refs = 1;
4090
+ entry2.sessionRefs.set(sessionId, 1);
4091
+ return entry2;
4092
+ }
4093
+ let pending = sharedZapoStorePromises.get(storePath);
4094
+ if (!pending) {
4095
+ pending = (async () => {
4096
+ await mkdir(dirname2(storePath), { recursive: true });
4097
+ const entry2 = createZapoStoreEntry(storePath);
4098
+ sharedZapoStores.set(storePath, entry2);
4099
+ return entry2;
4100
+ })();
4101
+ sharedZapoStorePromises.set(storePath, pending);
4102
+ }
4103
+ let entry;
4104
+ try {
4105
+ entry = await pending;
4106
+ } catch (error) {
4107
+ if (sharedZapoStorePromises.get(storePath) === pending) {
4108
+ sharedZapoStorePromises.delete(storePath);
4109
+ }
4110
+ throw error;
4111
+ }
4112
+ entry.refs += 1;
4113
+ entry.sessionRefs.set(sessionId, (entry.sessionRefs.get(sessionId) ?? 0) + 1);
4114
+ if (resetSession || await fileExists(legacyStorePath)) {
4115
+ entry.migrationQueue = entry.migrationQueue.then(async () => {
4116
+ if (resetSession) {
4117
+ clearZapoSessionData(storePath, sessionId);
4118
+ entry.snapshotClearSession.run(sessionId);
4119
+ }
4120
+ if (await fileExists(legacyStorePath)) {
4121
+ await migrateLegacyZapoStore(storePath, legacyStorePath, sessionId, logger);
4122
+ }
4123
+ });
4124
+ await entry.migrationQueue;
4125
+ }
4126
+ return entry;
4127
+ }
4128
+ function createZapoStoreEntry(storePath) {
4129
+ const store = createStore({
4130
+ backends: {
4131
+ sqlite: createSqliteStore({
4132
+ path: storePath,
4133
+ driver: "auto"
4134
+ })
4135
+ },
4136
+ providers: {
4137
+ auth: "sqlite",
4138
+ signal: "sqlite",
4139
+ preKey: "sqlite",
4140
+ session: "sqlite",
4141
+ identity: "sqlite",
4142
+ senderKey: "sqlite",
4143
+ appState: "sqlite",
4144
+ privacyToken: "sqlite",
4145
+ messages: "none",
4146
+ threads: "none",
4147
+ contacts: "none"
4148
+ },
4149
+ cacheProviders: {
4150
+ messageSecret: "sqlite"
4151
+ }
4152
+ });
4153
+ const Database = require2("better-sqlite3");
4154
+ const snapshotDb = new Database(messageSnapshotStorePath(storePath));
4155
+ snapshotDb.exec(`
4156
+ PRAGMA busy_timeout = 5000;
4157
+ CREATE TABLE IF NOT EXISTS whanext_message_snapshots (
4158
+ session_id TEXT NOT NULL,
4159
+ message_id TEXT NOT NULL,
4160
+ chat_id TEXT NOT NULL,
4161
+ participant_id TEXT,
4162
+ from_me INTEGER NOT NULL,
4163
+ timestamp_seconds INTEGER,
4164
+ message_bytes BLOB NOT NULL,
4165
+ stored_at_seconds INTEGER NOT NULL,
4166
+ PRIMARY KEY (session_id, message_id)
4167
+ );
4168
+ CREATE INDEX IF NOT EXISTS idx_whanext_message_snapshots_chat
4169
+ ON whanext_message_snapshots (session_id, chat_id);
4170
+ `);
4171
+ const snapshotColumns = snapshotDb.prepare(
4172
+ "PRAGMA table_info(whanext_message_snapshots)"
4173
+ ).all().map((column) => stringField2(column, "name")).filter((name) => name !== void 0);
4174
+ if (!snapshotColumns.includes("stored_at_seconds")) {
4175
+ snapshotDb.exec(
4176
+ "ALTER TABLE whanext_message_snapshots ADD COLUMN stored_at_seconds INTEGER NOT NULL DEFAULT 0"
4177
+ );
4178
+ }
4179
+ snapshotDb.exec(`
4180
+ CREATE INDEX IF NOT EXISTS idx_whanext_message_snapshots_retention
4181
+ ON whanext_message_snapshots (session_id, stored_at_seconds);
4182
+ `);
4183
+ return {
4184
+ store,
4185
+ snapshotDb,
4186
+ snapshotUpsert: snapshotDb.prepare(`
4187
+ INSERT INTO whanext_message_snapshots (
4188
+ session_id, message_id, chat_id, participant_id, from_me,
4189
+ timestamp_seconds, message_bytes, stored_at_seconds
4190
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
4191
+ ON CONFLICT(session_id, message_id) DO UPDATE SET
4192
+ chat_id = excluded.chat_id,
4193
+ participant_id = excluded.participant_id,
4194
+ from_me = excluded.from_me,
4195
+ timestamp_seconds = excluded.timestamp_seconds,
4196
+ message_bytes = excluded.message_bytes,
4197
+ stored_at_seconds = excluded.stored_at_seconds
4198
+ `),
4199
+ snapshotGet: snapshotDb.prepare(`
4200
+ SELECT chat_id, participant_id, from_me, timestamp_seconds, message_bytes
4201
+ FROM whanext_message_snapshots
4202
+ WHERE session_id = ? AND message_id = ?
4203
+ LIMIT 1
4204
+ `),
4205
+ snapshotPruneAge: snapshotDb.prepare(`
4206
+ DELETE FROM whanext_message_snapshots
4207
+ WHERE session_id = ? AND stored_at_seconds < ?
4208
+ `),
4209
+ snapshotPruneOverflow: snapshotDb.prepare(`
4210
+ DELETE FROM whanext_message_snapshots
4211
+ WHERE session_id = ?
4212
+ AND message_id IN (
4213
+ SELECT message_id
4214
+ FROM whanext_message_snapshots
4215
+ WHERE session_id = ?
4216
+ ORDER BY stored_at_seconds DESC
4217
+ LIMIT -1 OFFSET ?
4218
+ )
4219
+ `),
4220
+ snapshotClearSession: snapshotDb.prepare(`
4221
+ DELETE FROM whanext_message_snapshots
4222
+ WHERE session_id = ?
4223
+ `),
4224
+ refs: 0,
4225
+ sessionRefs: /* @__PURE__ */ new Map(),
4226
+ migrationQueue: Promise.resolve()
4227
+ };
4228
+ }
4229
+ function messageSnapshotStorePath(storePath) {
4230
+ return join(dirname2(storePath), "whanext-messages.sqlite");
4231
+ }
4232
+ async function releaseSharedZapoStore(storePath, sessionId, entry, logger) {
4233
+ const currentSessionRefs = entry.sessionRefs.get(sessionId) ?? 0;
4234
+ if (currentSessionRefs <= 1) {
4235
+ entry.sessionRefs.delete(sessionId);
4236
+ try {
4237
+ await entry.store.session(sessionId).destroy();
4238
+ } catch (error) {
4239
+ logger.warn("Could not release the Zapo session store cleanly.", {
4240
+ error: error instanceof Error ? error : new Error(String(error)),
4241
+ sessionId
4242
+ });
4243
+ }
4244
+ } else {
4245
+ entry.sessionRefs.set(sessionId, currentSessionRefs - 1);
4246
+ }
4247
+ entry.refs = Math.max(0, entry.refs - 1);
4248
+ if (entry.refs > 0) return;
4249
+ if (sharedZapoStores.get(storePath) === entry) {
4250
+ sharedZapoStores.delete(storePath);
4251
+ sharedZapoStorePromises.delete(storePath);
4252
+ }
4253
+ try {
4254
+ await entry.store.destroy();
4255
+ } catch (error) {
4256
+ logger.warn("Could not close the shared Zapo store cleanly.", {
4257
+ error: error instanceof Error ? error : new Error(String(error)),
4258
+ storePath
4259
+ });
4260
+ } finally {
4261
+ try {
4262
+ entry.snapshotDb.close();
4263
+ } catch (error) {
4264
+ logger.debug("Could not close the WhaNext message snapshot database cleanly.", {
4265
+ error: error instanceof Error ? error : new Error(String(error)),
4266
+ storePath
4267
+ });
4268
+ }
4269
+ }
4270
+ }
4271
+ function clearZapoSessionData(storePath, sessionId) {
4272
+ let db;
4273
+ try {
4274
+ const Database = require2("better-sqlite3");
4275
+ db = new Database(storePath);
4276
+ const tables = db.prepare(
4277
+ "SELECT name FROM main.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
4278
+ ).all();
4279
+ db.exec("BEGIN IMMEDIATE");
4280
+ try {
4281
+ for (const row of tables) {
4282
+ const table = stringField2(row, "name");
4283
+ if (!table || table === "wa_migrations" || table === "whanext_core_migrations") {
4284
+ continue;
4285
+ }
4286
+ const quoted = sqliteIdentifier(table);
4287
+ const columns = db.prepare(`PRAGMA main.table_info(${quoted})`).all().map((column) => stringField2(column, "name")).filter((name) => name !== void 0);
4288
+ if (!columns.includes("session_id")) continue;
4289
+ db.prepare(`DELETE FROM main.${quoted} WHERE session_id = ?`).run(sessionId);
4290
+ }
4291
+ db.exec("COMMIT");
4292
+ } catch (error) {
4293
+ db.exec("ROLLBACK");
4294
+ throw error;
4295
+ }
4296
+ } finally {
4297
+ db?.close();
4298
+ }
4299
+ }
4300
+ async function migrateLegacyZapoStore(storePath, legacyStorePath, sessionId, logger) {
4301
+ let db;
4302
+ try {
4303
+ const Database = require2("better-sqlite3");
4304
+ db = new Database(storePath);
4305
+ db.exec(`ATTACH DATABASE ${sqliteString(legacyStorePath)} AS legacy`);
4306
+ db.exec(`
4307
+ CREATE TABLE IF NOT EXISTS whanext_core_migrations (
4308
+ migration_key TEXT PRIMARY KEY,
4309
+ migrated_at INTEGER NOT NULL
4310
+ )
4311
+ `);
4312
+ const migrationKey = `shared-store:${resolve2(legacyStorePath)}:${sessionId}`;
4313
+ const migrated = db.prepare(
4314
+ "SELECT migration_key FROM whanext_core_migrations WHERE migration_key = ?"
4315
+ ).get(migrationKey);
4316
+ if (migrated) return;
4317
+ const mainTables = new Set(
4318
+ db.prepare("SELECT name FROM main.sqlite_master WHERE type = 'table'").all().map((row) => stringField2(row, "name")).filter((name) => name !== void 0)
4319
+ );
4320
+ const legacyTables = db.prepare(
4321
+ "SELECT name FROM legacy.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
4322
+ ).all();
4323
+ db.exec("BEGIN IMMEDIATE");
4324
+ try {
4325
+ for (const row of legacyTables) {
4326
+ const table = stringField2(row, "name");
4327
+ if (!table || !mainTables.has(table) || table === "wa_migrations" || table === "whanext_core_migrations") {
4328
+ continue;
4329
+ }
4330
+ const quoted = sqliteIdentifier(table);
4331
+ const mainColumns = db.prepare(`PRAGMA main.table_info(${quoted})`).all().map((column) => stringField2(column, "name")).filter((name) => name !== void 0);
4332
+ const legacyColumns = new Set(
4333
+ db.prepare(`PRAGMA legacy.table_info(${quoted})`).all().map((column) => stringField2(column, "name")).filter((name) => name !== void 0)
4334
+ );
4335
+ const columns = mainColumns.filter((column) => legacyColumns.has(column));
4336
+ if (!columns.includes("session_id") || columns.length === 0) continue;
4337
+ const selected = columns.map(sqliteIdentifier).join(", ");
4338
+ db.prepare(
4339
+ `INSERT OR REPLACE INTO main.${quoted} (${selected}) SELECT ${selected} FROM legacy.${quoted} WHERE session_id = ?`
4340
+ ).run(sessionId);
4341
+ }
4342
+ db.prepare(
4343
+ "INSERT OR REPLACE INTO whanext_core_migrations (migration_key, migrated_at) VALUES (?, ?)"
4344
+ ).run(migrationKey, Date.now());
4345
+ db.exec("COMMIT");
4346
+ } catch (error) {
4347
+ db.exec("ROLLBACK");
4348
+ throw error;
4349
+ }
4350
+ } catch (error) {
4351
+ logger.warn("Could not migrate the legacy per-account Zapo store; keeping the shared store active.", {
4352
+ error: error instanceof Error ? error : new Error(String(error)),
4353
+ sessionId,
4354
+ legacyStorePath,
4355
+ storePath
4356
+ });
4357
+ } finally {
4358
+ if (db) {
4359
+ try {
4360
+ db.exec("DETACH DATABASE legacy");
4361
+ } catch {
4362
+ }
4363
+ db.close();
4364
+ }
4365
+ }
4366
+ }
4367
+ async function fileExists(path2) {
4368
+ try {
4369
+ await stat(path2);
4370
+ return true;
4371
+ } catch {
4372
+ return false;
4373
+ }
4374
+ }
4375
+ function sqliteIdentifier(value) {
4376
+ return `"${value.replaceAll('"', '""')}"`;
4377
+ }
4378
+ function sqliteString(value) {
4379
+ return `'${value.replaceAll("'", "''")}'`;
4380
+ }
4381
+ function stringField2(record, field) {
4382
+ const value = record[field];
4383
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4384
+ }
4385
+ function stringValue(value) {
4386
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4387
+ }
4388
+ function numberValue(value) {
4389
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4390
+ }
4391
+ function booleanValue(value) {
4392
+ if (value === true || value === 1) return true;
4393
+ if (value === false || value === 0) return false;
4394
+ return void 0;
4395
+ }
4396
+ function bytesField(value) {
4397
+ if (value instanceof Uint8Array) return value;
4398
+ return void 0;
4399
+ }
3834
4400
  var WhaNextZapoLogger = class _WhaNextZapoLogger {
3835
4401
  level;
3836
4402
  #logger;
@@ -3903,7 +4469,7 @@ async function create(options = {}) {
3903
4469
  // src/app/multi-app.ts
3904
4470
  import {
3905
4471
  join as join2,
3906
- resolve as resolve2
4472
+ resolve as resolve3
3907
4473
  } from "path";
3908
4474
  var MultiCommandRouter = class {
3909
4475
  #apps;
@@ -4048,7 +4614,7 @@ async function createMulti(options) {
4048
4614
  }
4049
4615
  normalizedIds.add(normalizedId);
4050
4616
  if (account.provider === void 0) {
4051
- const authPath = resolve2(account.auth ?? join2(authRoot, account.id));
4617
+ const authPath = resolve3(account.auth ?? join2(authRoot, account.id));
4052
4618
  if (authPaths.has(authPath)) {
4053
4619
  throw new WhaNextError(
4054
4620
  "ARGUMENT_INVALID",