@whanext/core 0.19.4 → 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 {
@@ -2388,9 +2389,13 @@ import {
2388
2389
  // src/provider/zapo/normalize-message.ts
2389
2390
  function unwrapZapoMessageContent(input) {
2390
2391
  if (!input) return void 0;
2391
- const nested = input.ephemeralMessage?.message ?? input.viewOnceMessage?.message ?? input.viewOnceMessageV2?.message ?? viewOnceV2ExtensionMessage(input) ?? input.deviceSentMessage?.message ?? input.documentWithCaptionMessage?.message;
2392
+ const nested = input.ephemeralMessage?.message ?? input.viewOnceMessage?.message ?? input.viewOnceMessageV2?.message ?? viewOnceV2ExtensionMessage(input) ?? input.deviceSentMessage?.message ?? input.documentWithCaptionMessage?.message ?? editedWrapperMessage(input);
2392
2393
  return nested ? unwrapZapoMessageContent(nested) : input;
2393
2394
  }
2395
+ function editedWrapperMessage(input) {
2396
+ const edited = input.editedMessage;
2397
+ return edited?.message ?? void 0;
2398
+ }
2394
2399
  function viewOnceV2ExtensionMessage(input) {
2395
2400
  const extension = input.viewOnceMessageV2Extension;
2396
2401
  return extension?.message ?? void 0;
@@ -2702,6 +2707,25 @@ function toNumber(value) {
2702
2707
  }
2703
2708
 
2704
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;
2705
2729
  var ZapoProvider = class {
2706
2730
  #options;
2707
2731
  #events = new TypedEventEmitter();
@@ -2712,14 +2736,19 @@ var ZapoProvider = class {
2712
2736
  #handledProtocolStore = /* @__PURE__ */ new Set();
2713
2737
  #callCreatorStore = /* @__PURE__ */ new Map();
2714
2738
  #messageCacheSize;
2739
+ #protocolMutationQueue = Promise.resolve();
2715
2740
  #client;
2741
+ #storeEntry;
2742
+ #storePath;
2716
2743
  #intentionalClose = false;
2744
+ #closedNotified = false;
2717
2745
  #reconnectAttempt = 0;
2718
2746
  #reconnectTimer;
2719
2747
  #connectPromise;
2720
2748
  #pairingRequired = false;
2721
2749
  #connected = false;
2722
2750
  #connectedAtSeconds = 0;
2751
+ #messageSnapshotsSincePrune = 0;
2723
2752
  #pairingReady = Promise.resolve();
2724
2753
  #resolvePairingReady;
2725
2754
  constructor(options) {
@@ -2732,6 +2761,7 @@ var ZapoProvider = class {
2732
2761
  }
2733
2762
  async connect() {
2734
2763
  this.#intentionalClose = false;
2764
+ this.#closedNotified = false;
2735
2765
  const client = await this.#ensureClient();
2736
2766
  if (this.#connected || this.#connectPromise) {
2737
2767
  return;
@@ -2751,12 +2781,16 @@ var ZapoProvider = class {
2751
2781
  }
2752
2782
  const client = this.#client;
2753
2783
  this.#connectPromise = void 0;
2754
- if (client) {
2755
- await client.disconnect();
2756
- } else {
2757
- 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();
2758
2793
  }
2759
- this.#connected = false;
2760
2794
  }
2761
2795
  getCurrentUserIds() {
2762
2796
  const credentials = this.#client?.getCredentials();
@@ -2815,7 +2849,7 @@ var ZapoProvider = class {
2815
2849
  return this.#sent(result, chatId);
2816
2850
  }
2817
2851
  async repostMessage(source, chatId, options = {}) {
2818
- const original = this.#findStoredMessage(this.#toZapoKey(source));
2852
+ const original = await this.#findStoredMessage(this.#toZapoKey(source));
2819
2853
  if (!original?.message) {
2820
2854
  throw new WhaNextError(
2821
2855
  "MESSAGE_NOT_FOUND",
@@ -2845,7 +2879,7 @@ var ZapoProvider = class {
2845
2879
  return this.#sent(result, key.chatId);
2846
2880
  }
2847
2881
  async downloadMedia(key) {
2848
- const message = this.#messageKeyStore.get(key) ?? this.#findStoredMessage(this.#toZapoKey(key));
2882
+ const message = this.#messageKeyStore.get(key) ?? await this.#findStoredMessage(this.#toZapoKey(key));
2849
2883
  if (!message?.message) {
2850
2884
  throw new WhaNextError(
2851
2885
  "MEDIA_NOT_AVAILABLE",
@@ -2989,34 +3023,24 @@ var ZapoProvider = class {
2989
3023
  }
2990
3024
  async #ensureClient() {
2991
3025
  if (this.#client) return this.#client;
2992
- await mkdir(this.#options.auth, { recursive: true });
2993
- const store = createStore({
2994
- backends: {
2995
- sqlite: createSqliteStore({
2996
- path: join(this.#options.auth, "state.sqlite"),
2997
- driver: "auto"
2998
- })
2999
- },
3000
- providers: {
3001
- auth: "sqlite",
3002
- signal: "sqlite",
3003
- preKey: "sqlite",
3004
- session: "sqlite",
3005
- identity: "sqlite",
3006
- senderKey: "sqlite",
3007
- appState: "sqlite",
3008
- privacyToken: "sqlite",
3009
- messages: "none",
3010
- threads: "none",
3011
- contacts: "none"
3012
- },
3013
- cacheProviders: {
3014
- messageSecret: "sqlite"
3015
- }
3016
- });
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
+ );
3017
3041
  const client = new WaClient({
3018
- store,
3019
- sessionId: this.#options.sessionId ?? "default",
3042
+ store: storeEntry.store,
3043
+ sessionId,
3020
3044
  markOnlineOnConnect: false,
3021
3045
  deviceBrowser: this.#deviceBrowser(),
3022
3046
  deviceOsDisplayName: this.#deviceOsDisplayName(),
@@ -3025,8 +3049,10 @@ var ZapoProvider = class {
3025
3049
  autoDecrypt: true,
3026
3050
  persistAllSecrets: true
3027
3051
  },
3028
- media: { processor: createMediaProcessor() }
3052
+ media: { processor: sharedMediaProcessor }
3029
3053
  }, new WhaNextZapoLogger(this.#logger));
3054
+ this.#storeEntry = storeEntry;
3055
+ this.#storePath = storePath;
3030
3056
  this.#client = client;
3031
3057
  this.#bind(client);
3032
3058
  return client;
@@ -3041,7 +3067,24 @@ var ZapoProvider = class {
3041
3067
  client.on("auth_paired", () => {
3042
3068
  this.#pairingRequired = false;
3043
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
+ });
3044
3079
  client.on("message", (event) => {
3080
+ const protocol = this.#protocolMessage(event);
3081
+ if (protocol && this.#isMessageMutationProtocol(protocol.type)) {
3082
+ this.#enqueueProtocolEvent({
3083
+ ...event,
3084
+ protocolMessage: protocol
3085
+ });
3086
+ return;
3087
+ }
3045
3088
  this.#handleMessage(event);
3046
3089
  });
3047
3090
  client.on("message_send", (event) => {
@@ -3057,7 +3100,7 @@ var ZapoProvider = class {
3057
3100
  });
3058
3101
  });
3059
3102
  client.on("message_protocol", (event) => {
3060
- this.#handleProtocolEvent(event);
3103
+ this.#enqueueProtocolEvent(event);
3061
3104
  });
3062
3105
  client.on("message_addon", (event) => {
3063
3106
  this.#handleAddonEvent(event);
@@ -3109,26 +3152,56 @@ var ZapoProvider = class {
3109
3152
  void this.#events.emit("message", message);
3110
3153
  }
3111
3154
  #handleAddonEvent(event) {
3112
- if (event.kind !== "message_edit" || !event.targetMessageId) return;
3113
- const editedMessage = this.#addonEditedMessage(event.decrypted);
3155
+ const decrypted = this.#addonRecord(event.decrypted);
3156
+ const protocol = this.#addonProtocolMessage(decrypted);
3157
+ const kind = event.kind ?? this.#stringField(decrypted, "kind") ?? this.#stringField(decrypted, "type");
3158
+ const isEdit = kind === "message_edit" || protocol?.type === proto.Message.ProtocolMessage.Type.MESSAGE_EDIT;
3159
+ if (!isEdit) return;
3160
+ const targetMessageId = event.targetMessageId ?? this.#stringField(decrypted, "targetMessageId") ?? this.#stringField(decrypted, "targetMessageID") ?? protocol?.key?.id ?? void 0;
3161
+ if (!targetMessageId) return;
3162
+ const editedMessage = protocol?.editedMessage ?? this.#addonEditedMessage(event.decrypted);
3114
3163
  if (!editedMessage) return;
3164
+ const protocolKey = protocol?.key;
3115
3165
  const target = {
3116
- id: event.targetMessageId,
3117
- ...event.key.remoteJid !== void 0 ? { remoteJid: event.key.remoteJid } : {},
3118
- ...event.key.fromMe !== void 0 ? { fromMe: event.key.fromMe } : {},
3119
- ...event.key.participant !== void 0 ? { participant: event.key.participant } : {},
3120
- ...event.key.participantAlt !== void 0 ? { participantAlt: event.key.participantAlt } : {}
3166
+ ...protocolKey ?? {},
3167
+ id: targetMessageId,
3168
+ ...protocolKey?.remoteJid !== void 0 ? { remoteJid: protocolKey.remoteJid } : event.key.remoteJid !== void 0 ? { remoteJid: event.key.remoteJid } : {},
3169
+ ...protocolKey?.fromMe !== void 0 ? { fromMe: protocolKey.fromMe } : event.key.fromMe !== void 0 ? { fromMe: event.key.fromMe } : {},
3170
+ ...protocolKey?.participant !== void 0 ? { participant: protocolKey.participant } : event.key.participant !== void 0 ? { participant: event.key.participant } : {},
3171
+ ...protocolKey?.participantAlt !== void 0 ? { participantAlt: protocolKey.participantAlt } : event.key.participantAlt !== void 0 ? { participantAlt: event.key.participantAlt } : {}
3121
3172
  };
3122
- this.#handleProtocolEvent({
3173
+ void this.#handleProtocolEvent({
3123
3174
  key: event.key,
3124
- ...event.offline !== void 0 ? { offline: event.offline } : {},
3175
+ ...event.timestampSeconds !== void 0 ? { timestampSeconds: event.timestampSeconds } : {},
3125
3176
  protocolMessage: {
3126
3177
  type: proto.Message.ProtocolMessage.Type.MESSAGE_EDIT,
3127
3178
  key: target,
3128
- editedMessage
3179
+ editedMessage,
3180
+ ...protocol?.timestampMs !== void 0 ? { timestampMs: protocol.timestampMs } : {}
3129
3181
  }
3130
3182
  });
3131
3183
  }
3184
+ #addonRecord(value) {
3185
+ return value && typeof value === "object" ? value : void 0;
3186
+ }
3187
+ #stringField(record, field) {
3188
+ const value = record?.[field];
3189
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3190
+ }
3191
+ #addonProtocolMessage(record) {
3192
+ const direct = record?.protocolMessage;
3193
+ if (direct && typeof direct === "object") {
3194
+ return direct;
3195
+ }
3196
+ const message = record?.message;
3197
+ if (message && typeof message === "object") {
3198
+ const nested = message.protocolMessage;
3199
+ if (nested && typeof nested === "object") {
3200
+ return nested;
3201
+ }
3202
+ }
3203
+ return void 0;
3204
+ }
3132
3205
  #addonEditedMessage(value) {
3133
3206
  if (!value || typeof value !== "object") return void 0;
3134
3207
  const record = value;
@@ -3138,24 +3211,65 @@ var ZapoProvider = class {
3138
3211
  if (edited2 && typeof edited2 === "object") return edited2;
3139
3212
  }
3140
3213
  const edited = record.editedMessage;
3141
- if (edited && typeof edited === "object") return edited;
3214
+ if (edited && typeof edited === "object") {
3215
+ const nested = edited.message;
3216
+ return nested && typeof nested === "object" ? nested : edited;
3217
+ }
3142
3218
  const message = record.message;
3143
- if (message && typeof message === "object") return message;
3219
+ if (message && typeof message === "object") {
3220
+ const messageRecord = message;
3221
+ const nestedEdited = messageRecord.editedMessage;
3222
+ if (nestedEdited && typeof nestedEdited === "object") {
3223
+ const nested = nestedEdited.message;
3224
+ if (nested && typeof nested === "object") return nested;
3225
+ }
3226
+ return message;
3227
+ }
3144
3228
  return value;
3145
3229
  }
3146
- #handleProtocolEvent(event) {
3147
- if (this.#isOfflineMessage(event)) {
3148
- this.#logger.debug("Ignored protocol event queued before the current live connection.", {
3230
+ #protocolMessage(event) {
3231
+ if (event.protocolMessage) return event.protocolMessage;
3232
+ const content = unwrapZapoMessageContent(event.message);
3233
+ return content?.protocolMessage;
3234
+ }
3235
+ #isMessageMutationProtocol(type) {
3236
+ return type === proto.Message.ProtocolMessage.Type.REVOKE || type === proto.Message.ProtocolMessage.Type.MESSAGE_EDIT;
3237
+ }
3238
+ #shouldIgnoreProtocolEvent(event) {
3239
+ if (this.#options.processOfflineMessages === true) return false;
3240
+ if (!this.#connected) return true;
3241
+ if (!this.#connectedAtSeconds || event.timestampSeconds == null) return false;
3242
+ const timestamp = toSeconds(event.timestampSeconds);
3243
+ if (timestamp === void 0) return false;
3244
+ return timestamp < this.#connectedAtSeconds - 3;
3245
+ }
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) {
3255
+ if (this.#shouldIgnoreProtocolEvent(event)) {
3256
+ this.#logger.debug("Ignored protocol mutation from before the current live connection.", {
3149
3257
  messageId: event.key.id ?? void 0,
3150
3258
  chatId: event.key.remoteJid ?? void 0,
3151
3259
  timestampSeconds: event.timestampSeconds ?? void 0
3152
3260
  });
3153
3261
  return;
3154
3262
  }
3155
- const protocol = event.protocolMessage ?? event.message?.protocolMessage;
3156
- if (!protocol) return;
3263
+ const protocol = this.#protocolMessage(event);
3264
+ if (!protocol || !this.#isMessageMutationProtocol(protocol.type)) return;
3157
3265
  const protocolKey = protocol.key;
3158
- if (!protocolKey?.id) return;
3266
+ if (!protocolKey?.id) {
3267
+ this.#logger.debug("Ignored Zapo protocol mutation without a target message id.", {
3268
+ messageId: event.key.id ?? void 0,
3269
+ protocolType: protocol.type ?? void 0
3270
+ });
3271
+ return;
3272
+ }
3159
3273
  const remoteJid = protocolKey.remoteJid ?? event.key.remoteJid;
3160
3274
  const participant = protocolKey.participant ?? event.key.participant;
3161
3275
  const participantAlt = protocolKey.participantAlt ?? event.key.participantAlt;
@@ -3165,9 +3279,10 @@ var ZapoProvider = class {
3165
3279
  ...participant !== void 0 ? { participant } : {},
3166
3280
  ...participantAlt !== void 0 ? { participantAlt } : {}
3167
3281
  };
3282
+ const stored = await this.#findStoredMessage(target);
3283
+ if (!target.remoteJid && stored?.key.remoteJid) target.remoteJid = stored.key.remoteJid;
3168
3284
  if (!target.remoteJid) return;
3169
- const stored = this.#findStoredMessage(target);
3170
- const type = protocol?.type;
3285
+ const type = protocol.type;
3171
3286
  const mutationKey = this.#protocolDeliveryKey(event, target, type);
3172
3287
  if (mutationKey && this.#handledProtocolStore.has(mutationKey)) {
3173
3288
  this.#logger.debug("Ignored duplicate Zapo protocol event.", {
@@ -3181,6 +3296,12 @@ var ZapoProvider = class {
3181
3296
  const previous2 = stored ? normalizeZapoMessage(stored) : void 0;
3182
3297
  const deletedByMe = event.key.fromMe === true;
3183
3298
  const deletedById = event.key.participant ?? event.key.participantAlt ?? (deletedByMe ? this.getCurrentUserIds()[0] : event.key.remoteJid ?? void 0);
3299
+ if (!previous2) {
3300
+ this.#logger.debug("Zapo revoke target was not present in the recent message cache.", {
3301
+ targetMessageId: target.id ?? void 0,
3302
+ chatId: target.remoteJid ?? void 0
3303
+ });
3304
+ }
3184
3305
  void this.#events.emit("messageDeleted", {
3185
3306
  key: previous2?.keys ?? normalizeZapoKey(target),
3186
3307
  ...previous2 ? { message: previous2 } : {},
@@ -3190,9 +3311,8 @@ var ZapoProvider = class {
3190
3311
  });
3191
3312
  return;
3192
3313
  }
3193
- if (type !== proto.Message.ProtocolMessage.Type.MESSAGE_EDIT || !protocol.editedMessage) {
3194
- return;
3195
- }
3314
+ const editedContent = protocol.editedMessage ? this.#unwrapEditedContent(protocol.editedMessage) : void 0;
3315
+ if (!editedContent) return;
3196
3316
  const pushName = event.pushName ?? stored?.pushName;
3197
3317
  const edited = {
3198
3318
  ...stored ?? {},
@@ -3200,7 +3320,7 @@ var ZapoProvider = class {
3200
3320
  ...stored?.key ?? {},
3201
3321
  ...target
3202
3322
  },
3203
- message: protocol.editedMessage,
3323
+ message: editedContent,
3204
3324
  timestampSeconds: toSeconds(protocol.timestampMs) ?? event.timestampSeconds ?? stored?.timestampSeconds ?? Math.floor(Date.now() / 1e3),
3205
3325
  ...pushName !== void 0 ? { pushName } : {}
3206
3326
  };
@@ -3211,6 +3331,12 @@ var ZapoProvider = class {
3211
3331
  this.#remember(edited);
3212
3332
  const editedByMe = event.key.fromMe === true;
3213
3333
  const editedById = event.key.participant ?? event.key.participantAlt ?? (editedByMe ? this.getCurrentUserIds()[0] : event.key.remoteJid ?? void 0);
3334
+ if (!previous) {
3335
+ this.#logger.debug("Zapo edit target was not present in the recent message cache.", {
3336
+ targetMessageId: target.id ?? void 0,
3337
+ chatId: target.remoteJid ?? void 0
3338
+ });
3339
+ }
3214
3340
  void this.#events.emit("messageEdited", {
3215
3341
  key: message.keys,
3216
3342
  ...previous ? { previous } : {},
@@ -3220,6 +3346,10 @@ var ZapoProvider = class {
3220
3346
  editedAt: message.timestamp
3221
3347
  });
3222
3348
  }
3349
+ #unwrapEditedContent(message) {
3350
+ const wrapper = message.editedMessage;
3351
+ return wrapper?.message ?? message;
3352
+ }
3223
3353
  #handleGroupEvent(event) {
3224
3354
  const groupId = event.groupJid;
3225
3355
  if (!groupId) return;
@@ -3244,20 +3374,26 @@ var ZapoProvider = class {
3244
3374
  this.#connected = true;
3245
3375
  this.#connectedAtSeconds = Math.floor(Date.now() / 1e3);
3246
3376
  this.#reconnectAttempt = 0;
3377
+ this.#closedNotified = false;
3247
3378
  await this.#events.emit("connection", { state: "connected" });
3248
3379
  return;
3249
3380
  }
3250
3381
  this.#connectPromise = void 0;
3251
3382
  this.#connected = false;
3252
- const error = event.reason instanceof Error ? event.reason : event.reason ? new Error(String(event.reason)) : void 0;
3253
- if (this.#intentionalClose || event.isLogout) {
3254
- await this.#events.emit("connection", {
3255
- state: "closed",
3256
- ...error ? { error } : {}
3257
- });
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();
3258
3394
  return;
3259
3395
  }
3260
- await this.#scheduleReconnect(error);
3396
+ await this.#scheduleReconnect(error, reconnectDelayOverride(details));
3261
3397
  }
3262
3398
  #startConnect(client) {
3263
3399
  const promise = client.connect();
@@ -3266,20 +3402,65 @@ var ZapoProvider = class {
3266
3402
  if (this.#connectPromise !== promise) return;
3267
3403
  this.#connectPromise = void 0;
3268
3404
  if (this.#intentionalClose) return;
3269
- 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)));
3270
3407
  this.#logger.warn("WhatsApp connection attempt failed.", { error: normalized });
3271
- 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 } : {}
3272
3442
  });
3273
3443
  }
3274
- 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) {
3275
3458
  if (this.#reconnectTimer) return;
3276
3459
  const options = this.#options.reconnect;
3277
3460
  const maxAttempts = options?.maxAttempts ?? 10;
3278
3461
  if (options?.enabled === false || this.#reconnectAttempt >= maxAttempts) {
3279
- await this.#events.emit("connection", {
3280
- state: "closed",
3281
- ...error ? { error } : {}
3282
- });
3462
+ await this.#emitClosedOnce(error);
3463
+ await this.#releaseStore();
3283
3464
  return;
3284
3465
  }
3285
3466
  this.#reconnectAttempt += 1;
@@ -3290,11 +3471,13 @@ var ZapoProvider = class {
3290
3471
  });
3291
3472
  const initial = options?.initialDelayMs ?? 1e3;
3292
3473
  const maximum = options?.maxDelayMs ?? 3e4;
3293
- 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;
3294
3477
  this.#reconnectTimer = setTimeout(() => {
3295
3478
  this.#reconnectTimer = void 0;
3296
3479
  void this.connect();
3297
- }, delay + Math.floor(Math.random() * 250));
3480
+ }, delay + jitter);
3298
3481
  }
3299
3482
  async #sendButtons(chatId, content, replyTo) {
3300
3483
  this.#validateButtons(content);
@@ -3627,7 +3810,7 @@ var ZapoProvider = class {
3627
3810
  #groupParticipantIds(event) {
3628
3811
  return uniqueIdentities((event.participants ?? []).map((participant) => participant.jid ?? participant.lidJid ?? participant.phoneJid));
3629
3812
  }
3630
- #findStoredMessage(key) {
3813
+ async #findStoredMessage(key) {
3631
3814
  const direct = this.#messageStore.get(this.#messageStoreKey(key));
3632
3815
  if (direct) return direct;
3633
3816
  if (!key.id) return void 0;
@@ -3643,18 +3826,103 @@ var ZapoProvider = class {
3643
3826
  this.#remember(storedQuoted);
3644
3827
  return storedQuoted;
3645
3828
  }
3646
- return void 0;
3829
+ return this.#readArchivedMessage(key);
3647
3830
  }
3648
3831
  #remember(message) {
3649
3832
  if (!message.key.id || !message.message) return;
3650
3833
  const key = this.#messageStoreKey(message.key);
3651
3834
  this.#messageStore.delete(key);
3652
3835
  this.#messageStore.set(key, message);
3836
+ void this.#archiveMessage(message);
3653
3837
  while (this.#messageStore.size > this.#messageCacheSize) {
3654
3838
  const oldest = this.#messageStore.keys().next().value;
3655
3839
  if (oldest) this.#messageStore.delete(oldest);
3656
3840
  }
3657
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
+ }
3658
3926
  #messageStoreKey(key) {
3659
3927
  return `${key.remoteJid ?? ""}:${key.id ?? ""}:${key.participant ?? key.participantAlt ?? ""}`;
3660
3928
  }
@@ -3697,8 +3965,8 @@ var ZapoProvider = class {
3697
3965
  }
3698
3966
  #preparePairingGate() {
3699
3967
  this.#pairingRequired = false;
3700
- this.#pairingReady = new Promise((resolve3) => {
3701
- this.#resolvePairingReady = resolve3;
3968
+ this.#pairingReady = new Promise((resolve4) => {
3969
+ this.#resolvePairingReady = resolve4;
3702
3970
  });
3703
3971
  }
3704
3972
  #deviceBrowser() {
@@ -3740,6 +4008,395 @@ var ZapoProvider = class {
3740
4008
  }
3741
4009
  }
3742
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
+ }
3743
4400
  var WhaNextZapoLogger = class _WhaNextZapoLogger {
3744
4401
  level;
3745
4402
  #logger;
@@ -3812,7 +4469,7 @@ async function create(options = {}) {
3812
4469
  // src/app/multi-app.ts
3813
4470
  import {
3814
4471
  join as join2,
3815
- resolve as resolve2
4472
+ resolve as resolve3
3816
4473
  } from "path";
3817
4474
  var MultiCommandRouter = class {
3818
4475
  #apps;
@@ -3957,7 +4614,7 @@ async function createMulti(options) {
3957
4614
  }
3958
4615
  normalizedIds.add(normalizedId);
3959
4616
  if (account.provider === void 0) {
3960
- const authPath = resolve2(account.auth ?? join2(authRoot, account.id));
4617
+ const authPath = resolve3(account.auth ?? join2(authRoot, account.id));
3961
4618
  if (authPaths.has(authPath)) {
3962
4619
  throw new WhaNextError(
3963
4620
  "ARGUMENT_INVALID",