zapo-js 1.6.2 → 1.7.0

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.
Files changed (141) hide show
  1. package/dist/abprops-spec.d.ts +10 -0
  2. package/dist/abprops-spec.js +25 -0
  3. package/dist/client/WaClient.d.ts +11 -0
  4. package/dist/client/WaClient.js +60 -0
  5. package/dist/client/WaClientFactory.d.ts +2 -2
  6. package/dist/client/WaClientFactory.js +50 -12
  7. package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
  8. package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
  9. package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
  10. package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  11. package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
  12. package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
  13. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
  14. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +217 -30
  15. package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
  16. package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
  17. package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
  18. package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
  19. package/dist/client/events/dirty.d.ts +3 -0
  20. package/dist/client/events/dirty.js +12 -7
  21. package/dist/client/events/offline.d.ts +9 -0
  22. package/dist/client/events/offline.js +95 -0
  23. package/dist/client/events/privacy.d.ts +45 -0
  24. package/dist/client/events/privacy.js +130 -0
  25. package/dist/client/messaging/group-metadata.d.ts +12 -0
  26. package/dist/client/messaging/group-metadata.js +24 -8
  27. package/dist/client/messaging/link-preview.d.ts +7 -0
  28. package/dist/client/messaging/link-preview.js +27 -0
  29. package/dist/client/newsletter/content.d.ts +4 -1
  30. package/dist/client/newsletter/content.js +43 -18
  31. package/dist/client/newsletter/messaging.d.ts +2 -0
  32. package/dist/client/newsletter/messaging.js +2 -1
  33. package/dist/client/newsletter/mex.d.ts +2 -2
  34. package/dist/client/persistence/WriteBehindPersistence.js +2 -1
  35. package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
  36. package/dist/client/persistence/ephemeral-setting.js +120 -0
  37. package/dist/client/persistence/group-history.d.ts +29 -0
  38. package/dist/client/persistence/group-history.js +149 -0
  39. package/dist/client/persistence/history-blob.d.ts +25 -0
  40. package/dist/client/persistence/history-blob.js +38 -0
  41. package/dist/client/persistence/history-sync.d.ts +2 -0
  42. package/dist/client/persistence/history-sync.js +31 -29
  43. package/dist/client/types.d.ts +150 -16
  44. package/dist/crypto/core/xeddsa.js +16 -0
  45. package/dist/crypto/curves/X25519.js +13 -0
  46. package/dist/crypto/nativeBackend.d.ts +20 -0
  47. package/dist/crypto/nativeBackend.js +95 -0
  48. package/dist/esm/abprops-spec.js +15 -0
  49. package/dist/esm/client/WaClient.js +60 -0
  50. package/dist/esm/client/WaClientFactory.js +51 -13
  51. package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
  52. package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  53. package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
  54. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +218 -32
  55. package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
  56. package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
  57. package/dist/esm/client/events/dirty.js +12 -7
  58. package/dist/esm/client/events/offline.js +92 -0
  59. package/dist/esm/client/events/privacy.js +123 -0
  60. package/dist/esm/client/messaging/group-metadata.js +25 -9
  61. package/dist/esm/client/messaging/link-preview.js +29 -2
  62. package/dist/esm/client/newsletter/content.js +44 -19
  63. package/dist/esm/client/newsletter/messaging.js +2 -1
  64. package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
  65. package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
  66. package/dist/esm/client/persistence/group-history.js +145 -0
  67. package/dist/esm/client/persistence/history-blob.js +34 -0
  68. package/dist/esm/client/persistence/history-sync.js +24 -22
  69. package/dist/esm/crypto/core/xeddsa.js +17 -1
  70. package/dist/esm/crypto/curves/X25519.js +13 -0
  71. package/dist/esm/crypto/nativeBackend.js +93 -0
  72. package/dist/esm/index.js +3 -1
  73. package/dist/esm/message/context-info.js +39 -0
  74. package/dist/esm/message/encode/content.js +20 -4
  75. package/dist/esm/message/encode/media-payload.js +3 -1
  76. package/dist/esm/message/kinds/group-history.js +36 -0
  77. package/dist/esm/message/primitives/incoming.js +18 -6
  78. package/dist/esm/protocol/abprops.js +18 -162
  79. package/dist/esm/protocol/constants.js +4 -3
  80. package/dist/esm/protocol/defaults.js +6 -0
  81. package/dist/esm/protocol/media.js +1 -0
  82. package/dist/esm/protocol/message.js +8 -0
  83. package/dist/esm/protocol/notification.js +1 -0
  84. package/dist/esm/protocol/privacy.js +70 -4
  85. package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
  86. package/dist/esm/store/createStore.js +19 -2
  87. package/dist/esm/store/index.js +1 -0
  88. package/dist/esm/store/memory/chat-metadata.store.js +66 -0
  89. package/dist/esm/store/noop.store.js +8 -0
  90. package/dist/esm/transport/node/builders/privacy.js +63 -4
  91. package/dist/esm/util/coercion.js +12 -4
  92. package/dist/esm/util/index.js +1 -1
  93. package/dist/index.d.ts +11 -6
  94. package/dist/index.js +15 -2
  95. package/dist/media/types.d.ts +1 -1
  96. package/dist/message/context-info.d.ts +10 -0
  97. package/dist/message/context-info.js +40 -0
  98. package/dist/message/encode/content.d.ts +7 -3
  99. package/dist/message/encode/content.js +20 -4
  100. package/dist/message/encode/media-payload.d.ts +5 -2
  101. package/dist/message/encode/media-payload.js +3 -1
  102. package/dist/message/kinds/group-history.d.ts +30 -0
  103. package/dist/message/kinds/group-history.js +40 -0
  104. package/dist/message/primitives/incoming.d.ts +5 -0
  105. package/dist/message/primitives/incoming.js +18 -6
  106. package/dist/protocol/abprops.d.ts +27 -140
  107. package/dist/protocol/abprops.js +18 -163
  108. package/dist/protocol/constants.d.ts +4 -3
  109. package/dist/protocol/constants.js +10 -3
  110. package/dist/protocol/defaults.d.ts +6 -0
  111. package/dist/protocol/defaults.js +6 -0
  112. package/dist/protocol/media.d.ts +1 -0
  113. package/dist/protocol/media.js +1 -0
  114. package/dist/protocol/message.d.ts +6 -0
  115. package/dist/protocol/message.js +9 -0
  116. package/dist/protocol/notification.d.ts +1 -0
  117. package/dist/protocol/notification.js +1 -0
  118. package/dist/protocol/privacy.d.ts +52 -0
  119. package/dist/protocol/privacy.js +71 -5
  120. package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
  121. package/dist/store/contracts/chat-metadata.store.js +2 -0
  122. package/dist/store/contracts/group-metadata.store.d.ts +7 -0
  123. package/dist/store/contracts/thread.store.d.ts +6 -0
  124. package/dist/store/createStore.d.ts +10 -2
  125. package/dist/store/createStore.js +18 -1
  126. package/dist/store/index.d.ts +3 -1
  127. package/dist/store/index.js +3 -1
  128. package/dist/store/memory/chat-metadata.store.d.ts +26 -0
  129. package/dist/store/memory/chat-metadata.store.js +70 -0
  130. package/dist/store/noop.store.d.ts +2 -0
  131. package/dist/store/noop.store.js +9 -1
  132. package/dist/store/types.d.ts +190 -39
  133. package/dist/transport/node/builders/privacy.d.ts +26 -2
  134. package/dist/transport/node/builders/privacy.js +63 -3
  135. package/dist/util/coercion.d.ts +6 -0
  136. package/dist/util/coercion.js +13 -4
  137. package/dist/util/index.d.ts +1 -1
  138. package/dist/util/index.js +2 -1
  139. package/package.json +3 -1
  140. package/spec/abprops/index.d.ts +4402 -0
  141. package/spec/abprops/index.js +4372 -0
@@ -2,8 +2,10 @@ import type { AppStateCollectionName } from '../appstate/types';
2
2
  import type { DataForKey, WaAppstateActionKey, WaAppstateIndexArgs } from '../appstate-spec';
3
3
  import type { WaShortcakeAssertionSigner } from '../auth/pairing/WaShortcakeFlow';
4
4
  import type { WaAuthClientOptions, WaAuthCredentials, WaAuthDangerousOptions, WaAuthSocketOptions } from '../auth/types';
5
+ import type { WaPrivacyAccountSyncResult } from './coordinators/WaPrivacyCoordinator';
5
6
  import type { WaCallGroupParticipant, WaCallType } from './events/call';
6
7
  import type { IncomingPresenceType, PresenceLastSeen } from './events/presence';
8
+ import type { WaBlocklistResult } from './events/privacy';
7
9
  import type { CompanionHostPersistence } from './persistence/companion-host';
8
10
  import type { WaClientPluginDefinition } from './plugins/types';
9
11
  import type { WaMediaProcessor } from '../media/processor';
@@ -148,7 +150,9 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
148
150
  * on-demand backfill triggered by `message.requestHistorySync`) are
149
151
  * downloaded and emitted as `history_sync_chunk` events. Set
150
152
  * `enabled: false` to drop them; `requireFullSync: true` asks the
151
- * primary device for a full history download instead of just recent.
153
+ * primary device for a full history download instead of just recent;
154
+ * `groupBundles: true` opts into the group-history bundle a member may
155
+ * share after somebody joins a group.
152
156
  */
153
157
  readonly history?: WaHistorySyncOptions;
154
158
  /**
@@ -315,6 +319,14 @@ export interface WaHistorySyncOptions {
315
319
  */
316
320
  readonly enabled?: boolean;
317
321
  readonly requireFullSync?: boolean;
322
+ /**
323
+ * Whether to download and process the group-history bundle a member may
324
+ * share after somebody joins a group, emitting `group_history_bundle`.
325
+ * **Off by default** - a bundle is media a third party pushes at this
326
+ * account unprompted, so fetching it is opt-in. Bundles addressed to
327
+ * other members are dropped either way.
328
+ */
329
+ readonly groupBundles?: boolean;
318
330
  }
319
331
  export interface WaSignalMessagePublishInput {
320
332
  readonly to: string;
@@ -375,9 +387,23 @@ export interface WaSendMessageOptions extends WaMessagePublishOptions {
375
387
  */
376
388
  readonly expirationSeconds?: number;
377
389
  /**
378
- * Skip the automatic `ephemeralSettingTimestamp`/`expiration` injection
379
- * applied to messages sent into groups with disappearing-mode on (the cached
380
- * group ephemeral is otherwise fetched and applied for you). Off by default.
390
+ * Unix seconds when disappearing-mode was enabled, sent as
391
+ * `contextInfo.ephemeralSettingTimestamp`. 1:1 only – groups never carry it.
392
+ * Overrides the value the auto-inject resolves from the thread store; omit
393
+ * it unless you have a reason, since a missing timestamp makes the peer warn
394
+ * that the message will not disappear.
395
+ */
396
+ readonly ephemeralSettingTimestamp?: number;
397
+ /**
398
+ * Overrides `contextInfo.disappearingMode.trigger`. The 1:1 auto-inject
399
+ * already sets `CHAT_SETTING`.
400
+ */
401
+ readonly disappearingModeTrigger?: Proto.DisappearingMode.Trigger;
402
+ /**
403
+ * Skip the automatic disappearing-message injection on **group** sends, which
404
+ * would otherwise stamp `contextInfo.expiration` and `disappearingMode` from
405
+ * the group metadata cache. Has no effect on 1:1 – see
406
+ * {@link disableDirectEphemeralAutoInject}. Off by default.
381
407
  *
382
408
  * Relationship with {@link expirationSeconds}: a non-undefined
383
409
  * `expirationSeconds` already short-circuits the auto-inject, so this flag is
@@ -385,6 +411,17 @@ export interface WaSendMessageOptions extends WaMessagePublishOptions {
385
411
  * auto-inject AND not set any expiration yourself.
386
412
  */
387
413
  readonly disableGroupEphemeralAutoInject?: boolean;
414
+ /**
415
+ * Skip the automatic disappearing-message injection on **1:1** sends, which
416
+ * would otherwise stamp `contextInfo.expiration`,
417
+ * `ephemeralSettingTimestamp` and `disappearingMode` from the chat metadata
418
+ * cache. Also skips the cache lookup itself. Has no effect on groups – see
419
+ * {@link disableGroupEphemeralAutoInject}. Off by default.
420
+ *
421
+ * Same relationship with {@link expirationSeconds} as the group flag: a
422
+ * non-undefined `expirationSeconds` already short-circuits the auto-inject.
423
+ */
424
+ readonly disableDirectEphemeralAutoInject?: boolean;
388
425
  /** Raw child nodes appended to the `<message>` stanza. Escape hatch for protocol features the typed API doesn't cover. */
389
426
  readonly customNodes?: readonly BinaryNode[];
390
427
  /** Wrap the outgoing message as view-once. Only valid for image/video/audio content. */
@@ -788,19 +825,26 @@ export interface WaIncomingUnhandledStanzaEvent extends WaIncomingBaseEvent {
788
825
  readonly reason: string;
789
826
  }
790
827
  /**
791
- * Why an incoming message arrived as a content-less placeholder. `view_once`:
792
- * a view-once already consumed elsewhere. `hosted`: a hosted/bot message that
793
- * could not be fanned out. `other`: an `unavailable` marker the lib does not
794
- * categorize yet.
828
+ * Why an incoming message arrived as a content-less placeholder. `view_once`: a
829
+ * view-once already consumed elsewhere. `hosted`: a hosted account whose message
830
+ * could not be fanned out. `bot`: a bot message whose fanout never ran. `other`:
831
+ * a plain fanout placeholder – the only kind the primary device resends.
795
832
  */
796
- export type WaUnavailableMessageKind = 'view_once' | 'hosted' | 'other';
833
+ export type WaUnavailableMessageKind = 'view_once' | 'hosted' | 'bot' | 'other';
797
834
  export interface WaIncomingUnavailableMessageEvent extends Omit<WaIncomingBaseEvent, 'chatJid' | 'stanzaId'> {
798
835
  /** Which flavour of content the server signalled as unavailable. */
799
836
  readonly kind: WaUnavailableMessageKind;
837
+ /**
838
+ * `true` when a resend was queued for the primary device; the payload then
839
+ * arrives as a `message` event with the same key. Best-effort, like wa-web:
840
+ * the request is not persisted, so a failed peer message is not retried.
841
+ * `false` for the unrecoverable flavours, messages past the server age
842
+ * window, and mobile-primary sessions.
843
+ */
844
+ readonly resendRequested: boolean;
800
845
  /**
801
846
  * The message key (chat, stanza id, author, addressing metadata) – same
802
- * shape the `message` event carries, so it can be stored or correlated. There
803
- * is no decrypted `message`: the payload is unavailable and cannot be fetched.
847
+ * shape the `message` event carries, so it can be stored or correlated.
804
848
  */
805
849
  readonly key: WaIncomingMessageKey;
806
850
  /** Stanza `t` attr (seconds since epoch). */
@@ -1032,6 +1076,32 @@ export interface WaHistorySyncChunkEvent {
1032
1076
  /** Server-reported progress, 0–100. */
1033
1077
  readonly progress?: number;
1034
1078
  }
1079
+ /**
1080
+ * A group-history bundle shared with this account after it joined a group,
1081
+ * already downloaded, filtered and persisted. Emitted once per bundle, only
1082
+ * when `history.groupBundles` is enabled.
1083
+ */
1084
+ export interface WaGroupHistoryBundleEvent {
1085
+ readonly groupJid: string;
1086
+ /** Member who shared the history. */
1087
+ readonly senderJid?: string;
1088
+ /** Stanza id of the message that carried the bundle. */
1089
+ readonly bundleMessageId?: string;
1090
+ /** Messages persisted from this bundle, after filtering. */
1091
+ readonly messagesCount: number;
1092
+ /** Pinned messages older than the shared window, exempt from the age cutoff. */
1093
+ readonly outOfWindowPinsCount: number;
1094
+ /** Entries skipped as stubs, foreign-chat, ephemeral-expired or too old. */
1095
+ readonly droppedCount: number;
1096
+ /** Oldest persisted message timestamp, in ms. */
1097
+ readonly oldestTimestampMs?: number;
1098
+ /**
1099
+ * Members the sender addressed the bundle to (PN or LID form). Copied out
1100
+ * of the decoded payload, so mutating it cannot corrupt the message proto
1101
+ * the `message` event handed to the same listener.
1102
+ */
1103
+ readonly historyReceivers: readonly string[];
1104
+ }
1035
1105
  export type WaAppStateMutationSource = 'snapshot' | 'patch' | 'local';
1036
1106
  type MutationEventBase = {
1037
1107
  readonly source: WaAppStateMutationSource;
@@ -1168,11 +1238,11 @@ export interface WaClientEventMap {
1168
1238
  */
1169
1239
  readonly message_protocol: (event: WaIncomingProtocolMessageEvent) => void;
1170
1240
  /**
1171
- * A message the server delivered as a content-less placeholder: the payload
1172
- * is unavailable and cannot be recovered (a view-once already consumed, or a
1173
- * hosted/bot message that could not be fanned out). The lib acks it and emits
1174
- * this instead of a `message` event for the same stanza. Branch on
1175
- * `event.kind`.
1241
+ * A message the server delivered as a content-less placeholder. The lib acks
1242
+ * it and emits this instead of a `message` event for the same stanza. A plain
1243
+ * fanout placeholder is asked back from the primary device and arrives later
1244
+ * as a `message` event (see `event.resendRequested`); a consumed view-once or
1245
+ * a hosted/bot message that could not be fanned out is never resent.
1176
1246
  */
1177
1247
  readonly message_unavailable: (event: WaIncomingUnavailableMessageEvent) => void;
1178
1248
  /**
@@ -1205,6 +1275,28 @@ export interface WaClientEventMap {
1205
1275
  readonly business: (event: WaBusinessEvent) => void;
1206
1276
  /** Profile/group/community picture change notification – the new picture must still be fetched explicitly. */
1207
1277
  readonly picture: (event: WaPictureEvent) => void;
1278
+ /**
1279
+ * The account's privacy, refetched after the primary or another companion
1280
+ * changed it: the full category set plus any disallowed list the server
1281
+ * reported. The two halves are read by separate queries, so a change
1282
+ * landing mid-refresh can leave them describing slightly different server
1283
+ * states - the payload is exactly what
1284
+ * {@link WaPrivacyCoordinator.refreshFromAccountSync} returns.
1285
+ *
1286
+ * Two paths trigger it: the live `account_sync` notification (debounced,
1287
+ * so a burst of changes on the phone collapses into one refresh) and the
1288
+ * `account_sync` dirty bit that catches a session up after being offline.
1289
+ * Neither is trusted for its payload - the values always come from a
1290
+ * fresh read. Categories the library does not model are dropped.
1291
+ */
1292
+ readonly privacy: (event: WaPrivacyAccountSyncResult) => void;
1293
+ /**
1294
+ * The account blocklist after a block/unblock made on another device -
1295
+ * same refetch path as `privacy`, from its own account-sync protocol. The
1296
+ * payload is the full list ({@link WaPrivacyCoordinator.getBlocklist}'s
1297
+ * shape), never a delta.
1298
+ */
1299
+ readonly blocklist: (event: WaBlocklistResult) => void;
1208
1300
  /**
1209
1301
  * A parsed app-state mutation arriving from a sync – chat mute/star/read/
1210
1302
  * pin/archive/contact/label/etc. changed on another device. Inbound only;
@@ -1228,12 +1320,24 @@ export interface WaClientEventMap {
1228
1320
  * Skipped only when `history.enabled` is explicitly `false`.
1229
1321
  */
1230
1322
  readonly history_sync_chunk: (event: WaHistorySyncChunkEvent) => void;
1323
+ /**
1324
+ * A group-history bundle another member shared with this account after it
1325
+ * joined a group, already downloaded and persisted. Requires
1326
+ * `history.groupBundles: true` - the download is opt-in because a third
1327
+ * party triggers it.
1328
+ */
1329
+ readonly group_history_bundle: (event: WaGroupHistoryBundleEvent) => void;
1231
1330
  /**
1232
1331
  * Offline-message queue progress after a reconnect (`'resuming'` ticks
1233
1332
  * with `remainingStanzas`, then `'complete'`). Useful to defer UI updates
1234
1333
  * until the catch-up finishes.
1235
1334
  */
1236
1335
  readonly offline_resume: (event: WaOfflineResumeEvent) => void;
1336
+ /**
1337
+ * Preview manifest of the offline queue, sent just before the flush. Not
1338
+ * guaranteed to arrive - use `offline_resume` for progress, never this.
1339
+ */
1340
+ readonly offline_thread_metadata: (event: WaOfflineThreadMetadataEvent) => void;
1237
1341
  /**
1238
1342
  * Fatal stream-level error from the server (e.g. logged out from another
1239
1343
  * device, stream conflict). The connection will close right after.
@@ -1314,6 +1418,36 @@ export interface WaOfflineResumeEvent {
1314
1418
  /** `true` when triggered by an explicit catch-up request rather than auto-resume on reconnect. */
1315
1419
  readonly forced: boolean;
1316
1420
  }
1421
+ export interface WaOfflineThreadPreview {
1422
+ /** User (`@lid`/`@s.whatsapp.net`) or group (`@g.us`) jid. */
1423
+ readonly jid: string;
1424
+ /** Unix seconds of the most recent queued stanza for this thread. */
1425
+ readonly timestampSeconds: number;
1426
+ }
1427
+ export interface WaOfflineThreadReadWatermark {
1428
+ readonly jid: string;
1429
+ /** Unix seconds up to which the peer has already read this thread. */
1430
+ readonly readTimestampSeconds: number;
1431
+ }
1432
+ /**
1433
+ * Which threads have queued traffic, announced right before the offline flush.
1434
+ * Informational only - every listed thread still delivers its stanzas
1435
+ * normally. All fields but `threads` depend on server gating and are commonly
1436
+ * absent.
1437
+ */
1438
+ export interface WaOfflineThreadMetadataEvent {
1439
+ readonly threads: readonly WaOfflineThreadPreview[];
1440
+ readonly readWatermarks?: readonly WaOfflineThreadReadWatermark[];
1441
+ /** Status-update backlog still queued behind this flush. */
1442
+ readonly pendingStatusMessages?: {
1443
+ readonly count: number;
1444
+ readonly jids: readonly string[];
1445
+ };
1446
+ /** Notification backlog still queued behind this flush. */
1447
+ readonly pendingNotifications?: {
1448
+ readonly count: number;
1449
+ };
1450
+ }
1317
1451
  export interface WaPrivacyTokenUpdateEvent {
1318
1452
  /** Peer whose trusted-contact-token (TC token) changed. */
1319
1453
  readonly jid: string;
@@ -9,17 +9,30 @@ const X25519_1 = require("../curves/X25519");
9
9
  const edwards_1 = require("../math/edwards");
10
10
  const le_1 = require("../math/le");
11
11
  const mod_1 = require("../math/mod");
12
+ const nativeBackend_1 = require("../nativeBackend");
12
13
  const bytes_1 = require("../../util/bytes");
13
14
  const PREFIX_SIGNATURE_RANDOM = new Uint8Array([
14
15
  0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
15
16
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
16
17
  ]);
18
+ const nativeBinding = (() => {
19
+ if (process.env.ZAPO_XEDDSA_FORCE_JS)
20
+ return null;
21
+ const mod = (0, nativeBackend_1.resolveNativeCryptoBackend)();
22
+ if (mod && typeof mod.xeddsaSign === 'function' && typeof mod.xeddsaVerify === 'function') {
23
+ return { xeddsaSign: mod.xeddsaSign, xeddsaVerify: mod.xeddsaVerify };
24
+ }
25
+ return null;
26
+ })();
17
27
  /**
18
28
  * Verifies an XEdDSA signature over `message` against an X25519 (Montgomery)
19
29
  * public key, converting to the Edwards form internally. Returns `false`
20
30
  * on shape mismatch instead of throwing.
21
31
  */
22
32
  async function xeddsaVerify(curvePublicKey, message, signature) {
33
+ if (nativeBinding) {
34
+ return nativeBinding.xeddsaVerify(curvePublicKey, message, signature);
35
+ }
23
36
  if (signature.length !== 64) {
24
37
  return false;
25
38
  }
@@ -44,6 +57,9 @@ async function xeddsaVerify(curvePublicKey, message, signature) {
44
57
  */
45
58
  async function xeddsaSign(privateKey, message) {
46
59
  (0, bytes_1.assertByteLength)(privateKey, 32, `invalid curve25519 private key length ${privateKey.length}`);
60
+ if (nativeBinding) {
61
+ return (0, bytes_1.toBytesView)(nativeBinding.xeddsaSign(privateKey, message));
62
+ }
47
63
  const clampedPrivateKey = (0, X25519_1.clampCurvePrivateKeyInPlace)(privateKey);
48
64
  const privateScalar = (0, le_1.bytesToBigIntLE)(clampedPrivateKey);
49
65
  const encodedPublic = (0, edwards_1.encodeExtendedPoint)((0, edwards_1.scalarMultBase)(privateScalar));
@@ -10,7 +10,17 @@ const constants_1 = require("../curves/constants");
10
10
  const types_1 = require("../curves/types");
11
11
  const constants_2 = require("../math/constants");
12
12
  const fe_1 = require("../math/fe");
13
+ const nativeBackend_1 = require("../nativeBackend");
13
14
  const bytes_1 = require("../../util/bytes");
15
+ const nativeX25519ScalarMult = (() => {
16
+ if (process.env.ZAPO_X25519_FORCE_JS)
17
+ return null;
18
+ const mod = (0, nativeBackend_1.resolveNativeCryptoBackend)();
19
+ if (mod && typeof mod.x25519ScalarMult === 'function') {
20
+ return mod.x25519ScalarMult;
21
+ }
22
+ return null;
23
+ })();
14
24
  const diffieHellmanWithCallback = node_crypto_1.diffieHellman;
15
25
  let diffieHellmanAsync = null;
16
26
  let diffieHellmanAsyncProbed = false;
@@ -120,6 +130,9 @@ class X25519 {
120
130
  static async scalarMult(privKey, pubKey) {
121
131
  (0, bytes_1.assertByteLength)(privKey, 32, 'x25519 private key must be 32 bytes');
122
132
  (0, bytes_1.assertByteLength)(pubKey, 32, 'x25519 public key must be 32 bytes');
133
+ if (nativeX25519ScalarMult) {
134
+ return (0, bytes_1.toBytesView)(nativeX25519ScalarMult(privKey, pubKey));
135
+ }
123
136
  const opts = {
124
137
  privateKey: x25519PrivateKeyObject(privKey),
125
138
  publicKey: x25519PublicKeyObject(pubKey)
@@ -0,0 +1,20 @@
1
+ export interface NativeCryptoModule {
2
+ readonly x25519ScalarMult?: (privateKey: Uint8Array, publicKey: Uint8Array) => Uint8Array;
3
+ readonly xeddsaSign?: (privateKey: Uint8Array, message: Uint8Array) => Uint8Array;
4
+ readonly xeddsaVerify?: (publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => boolean;
5
+ }
6
+ declare const ABSENCE_TOKENS: {
7
+ readonly napi: readonly ["'@zapo-js/native'", "binding.js", "Cannot find native binding"];
8
+ readonly wasm: readonly ["'@zapo-js/native'", "zapo_native_wasm.js"];
9
+ };
10
+ export type NativeBackendKind = keyof typeof ABSENCE_TOKENS;
11
+ /**
12
+ * Classifies a backend load failure: `true` means the backend is simply not
13
+ * available on this install (missing optional package, entry file absent,
14
+ * no prebuilt binary, ESM build without `require`) and the fallback should
15
+ * stay silent; `false` means the backend is present but broken and the
16
+ * degradation deserves a process warning.
17
+ */
18
+ export declare function isBackendAbsence(backend: NativeBackendKind, error: unknown): boolean;
19
+ export declare function resolveNativeCryptoBackend(): NativeCryptoModule | null;
20
+ export {};
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isBackendAbsence = isBackendAbsence;
4
+ exports.resolveNativeCryptoBackend = resolveNativeCryptoBackend;
5
+ /**
6
+ * Resolves the optional native crypto accelerator backend shared by the
7
+ * X25519 and XEdDSA paths. Selected via `ZAPO_NATIVE_BACKEND`:
8
+ *
9
+ * - `auto` (default) – try the NAPI addon first, then the WASM build,
10
+ * then no accelerator.
11
+ * - `napi` – only the compiled Rust NAPI addon (`@zapo-js/native`).
12
+ * - `wasm` – only the wasm-bindgen build of the same Rust core.
13
+ * The shipped artifact targets the web (ESM glue +
14
+ * explicit init) so one build serves browsers and
15
+ * Node; loading it here relies on `require(esm)`,
16
+ * available unflagged on Node 20.19+ / 22.12+.
17
+ * - `js` / `none` – no accelerator; callers fall back to the pure-JS
18
+ * (and `node:crypto`) implementations.
19
+ *
20
+ * The module is resolved once and memoised. A failed backend resolves to
21
+ * `null` (or, under `auto`, falls through to the next one) so callers
22
+ * transparently fall back to JS. An absent backend stays silent - that is
23
+ * the expected shape of an optional accelerator - but a present-yet-broken
24
+ * one (corrupt binary, ABI mismatch, unsupported runtime) emits a process
25
+ * warning with code `ZAPO_NATIVE_LOAD_FAILED` so the degradation is
26
+ * visible. The per-primitive `ZAPO_X25519_FORCE_JS` / `ZAPO_XEDDSA_FORCE_JS`
27
+ * escape hatches are honoured by the callers, not here.
28
+ */
29
+ const node_fs_1 = require("node:fs");
30
+ const primitives_1 = require("../util/primitives");
31
+ const WASM_MODULE_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm.js';
32
+ const WASM_BINARY_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm_bg.wasm';
33
+ function loadWasmBackend() {
34
+ const mod = require(WASM_MODULE_SPECIFIER);
35
+ const wasmBytes = (0, node_fs_1.readFileSync)(require.resolve(WASM_BINARY_SPECIFIER));
36
+ mod.initSync({ module: wasmBytes });
37
+ return mod;
38
+ }
39
+ let cached;
40
+ const ABSENCE_TOKENS = {
41
+ napi: ["'@zapo-js/native'", 'binding.js', 'Cannot find native binding'],
42
+ wasm: ["'@zapo-js/native'", 'zapo_native_wasm.js']
43
+ };
44
+ /**
45
+ * Classifies a backend load failure: `true` means the backend is simply not
46
+ * available on this install (missing optional package, entry file absent,
47
+ * no prebuilt binary, ESM build without `require`) and the fallback should
48
+ * stay silent; `false` means the backend is present but broken and the
49
+ * degradation deserves a process warning.
50
+ */
51
+ function isBackendAbsence(backend, error) {
52
+ if (error instanceof ReferenceError) {
53
+ return error.message.includes('require is not defined');
54
+ }
55
+ const { code, message } = (error ?? {});
56
+ if (typeof message !== 'string')
57
+ return false;
58
+ const notFound = code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND';
59
+ if (!notFound && !message.includes('Cannot find native binding'))
60
+ return false;
61
+ return ABSENCE_TOKENS[backend].some((token) => message.includes(token));
62
+ }
63
+ function warnLoadFailure(backend, error) {
64
+ process.emitWarning('failed to load native crypto accelerator, falling back', {
65
+ code: 'ZAPO_NATIVE_LOAD_FAILED',
66
+ detail: `${backend}: ${(0, primitives_1.toError)(error).message}`
67
+ });
68
+ }
69
+ function resolveNativeCryptoBackend() {
70
+ if (cached !== undefined)
71
+ return cached;
72
+ const backend = (process.env.ZAPO_NATIVE_BACKEND ?? 'auto').toLowerCase();
73
+ cached = null;
74
+ if (backend === 'js' || backend === 'none')
75
+ return cached;
76
+ if (backend === 'napi' || backend === 'auto') {
77
+ try {
78
+ cached = require('@zapo-js/native');
79
+ }
80
+ catch (error) {
81
+ if (!isBackendAbsence('napi', error))
82
+ warnLoadFailure('napi', error);
83
+ }
84
+ }
85
+ if (cached === null && (backend === 'wasm' || backend === 'auto')) {
86
+ try {
87
+ cached = loadWasmBackend();
88
+ }
89
+ catch (error) {
90
+ if (!isBackendAbsence('wasm', error))
91
+ warnLoadFailure('wasm', error);
92
+ }
93
+ }
94
+ return cached;
95
+ }
@@ -0,0 +1,15 @@
1
+ import { WA_ABPROPS_BY_CODE } from '../../spec/abprops/index.js';
2
+ export { WA_ABPROPS, WA_ABPROPS_BY_CODE, WA_ABPROPS_SPECIAL_EARLY, WA_ABPROPS_USED_BEFORE_INIT, WA_GROUP_ABPROPS, WA_GROUP_ABPROPS_BY_CODE } from '../../spec/abprops/index.js';
3
+ // The reverse map is emitted with numeric literal keys, so it cannot be indexed
4
+ // with a plain `number`. Widening it keeps the lookup a single property read and
5
+ // avoids allocating a parallel Map with 2000+ entries at module init.
6
+ const AB_PROP_NAME_BY_CODE = WA_ABPROPS_BY_CODE;
7
+ /**
8
+ * Resolves the client-side prop name for a wire config code.
9
+ *
10
+ * Returns `undefined` for codes this build does not know about - the server
11
+ * advertises props from newer bundles, and WA Web drops those rows too.
12
+ */
13
+ export function resolveAbPropNameByCode(configCode) {
14
+ return AB_PROP_NAME_BY_CODE[configCode];
15
+ }
@@ -1,11 +1,14 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { createIgnoreKeyFilter, validateIgnoreKey } from './messaging/ignore-key.js';
3
+ import { createEphemeralObserver, persistIncomingEphemeralSetting } from './persistence/ephemeral-setting.js';
4
+ import { runGroupHistoryBundle } from './persistence/group-history.js';
3
5
  import { runHistorySyncNotification } from './persistence/history-sync.js';
4
6
  import { persistIncomingMailboxEntities } from './persistence/mailbox.js';
5
7
  import { WriteBehindPersistence } from './persistence/WriteBehindPersistence.js';
6
8
  import { installWaClientPlugins } from './plugins/install.js';
7
9
  import { buildWaClientDependencies, resolveWaClientBase } from './WaClientFactory.js';
8
10
  import { ConsoleLogger } from '../infra/log/ConsoleLogger.js';
11
+ import { unwrapMessage } from '../message/encode/content.js';
9
12
  import { proto } from '../proto.js';
10
13
  import { WA_DEFAULTS, WA_MESSAGE_TYPES } from '../protocol/constants.js';
11
14
  import { normalizeDeviceJid } from '../protocol/jid.js';
@@ -42,6 +45,10 @@ class WaClientImpl extends EventEmitter {
42
45
  threadStore: this.stores.threads,
43
46
  contactStore: this.stores.contacts
44
47
  }, this.logger, this.options.writeBehind);
48
+ this.observeEphemeralSetting = createEphemeralObserver({
49
+ logger: this.logger,
50
+ chatMetadataStore: this.stores.chatMetadata
51
+ });
45
52
  const dependencies = buildWaClientDependencies({
46
53
  base,
47
54
  runtime: {
@@ -182,6 +189,7 @@ class WaClientImpl extends EventEmitter {
182
189
  }
183
190
  try {
184
191
  this.emit('message', event);
192
+ this.observeEphemeralSetting(event);
185
193
  void persistIncomingMailboxEntities({
186
194
  logger: this.logger,
187
195
  writeBehind: this.writeBehind,
@@ -207,6 +215,9 @@ class WaClientImpl extends EventEmitter {
207
215
  });
208
216
  });
209
217
  }
218
+ if (this.options.history?.groupBundles === true && event.message && !event.key.fromMe) {
219
+ this.tryProcessGroupHistoryBundle(event);
220
+ }
210
221
  const protocolMessage = event.message?.protocolMessage;
211
222
  if (!protocolMessage) {
212
223
  return;
@@ -258,6 +269,7 @@ class WaClientImpl extends EventEmitter {
258
269
  await runHistorySyncNotification({
259
270
  logger: this.logger,
260
271
  mediaTransfer: this.mediaTransfer,
272
+ chatMetadataStore: this.stores.chatMetadata,
261
273
  writeBehind: this.writeBehind,
262
274
  emitEvent: this.emit.bind(this),
263
275
  onPrivacyTokens: (conversations) => this.deps.trustedContactToken.hydrateFromHistorySync(conversations),
@@ -270,6 +282,16 @@ class WaClientImpl extends EventEmitter {
270
282
  }
271
283
  return;
272
284
  }
285
+ if (protocolType === proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING) {
286
+ persistIncomingEphemeralSetting({
287
+ logger: this.logger,
288
+ writeBehind: this.writeBehind,
289
+ chatMetadataStore: this.stores.chatMetadata,
290
+ event,
291
+ protocolMessage
292
+ });
293
+ return;
294
+ }
273
295
  if (SYNC_RELATED_PROTOCOL_TYPES.has(protocolType)) {
274
296
  this.logger.info('incoming sync-related protocol message', {
275
297
  id: event.key.id,
@@ -288,6 +310,43 @@ class WaClientImpl extends EventEmitter {
288
310
  this.leaveIncomingHandler();
289
311
  }
290
312
  }
313
+ /**
314
+ * Kicks off the group-history bundle download when the incoming message
315
+ * carries one. Fire-and-forget: the blob can be large, and the incoming
316
+ * handler must not stall behind a CDN fetch.
317
+ *
318
+ * The download outlives the handler that started it, so it takes a slot of
319
+ * its own in the drain accounting - `disconnect()` and `clearStoredState()`
320
+ * must not flush or wipe the stores while a bundle is still writing.
321
+ */
322
+ tryProcessGroupHistoryBundle(event) {
323
+ const bundle = unwrapMessage(event.message ?? {}).messageHistoryBundle;
324
+ const groupJid = event.key.remoteJid;
325
+ if (!bundle || !groupJid) {
326
+ return;
327
+ }
328
+ if (!this.tryEnterIncomingHandler()) {
329
+ return;
330
+ }
331
+ const credentials = this.deps.authClient.getCurrentCredentials();
332
+ void runGroupHistoryBundle({
333
+ logger: this.logger,
334
+ mediaTransfer: this.mediaTransfer,
335
+ writeBehind: this.writeBehind,
336
+ emitEvent: this.emit.bind(this),
337
+ meJid: credentials?.meJid,
338
+ meLid: credentials?.meLid,
339
+ getAbPropNumber: (name) => this.deps.abPropsCoordinator.getConfigValue(name)
340
+ }, {
341
+ bundle,
342
+ groupJid,
343
+ senderJid: event.key.participant ?? undefined,
344
+ bundleMessageId: event.key.id,
345
+ sentAtSeconds: event.timestampSeconds
346
+ }).finally(() => {
347
+ this.leaveIncomingHandler();
348
+ });
349
+ }
291
350
  async queryWithContext(context, node, timeoutMs = this.options.iqTimeoutMs ?? WA_DEFAULTS.IQ_TIMEOUT_MS, contextData = {}, options = {}) {
292
351
  return queryNodeWithContext(async (queryNode, queryTimeoutMs) => this.deps.lowLevelCoordinator.query(queryNode, queryTimeoutMs, options), this.logger, context, node, timeoutMs, contextData);
293
352
  }
@@ -362,6 +421,7 @@ class WaClientImpl extends EventEmitter {
362
421
  * your own backoff.
363
422
  */
364
423
  async disconnect() {
424
+ this.deps.privacyCoordinator.stopAccountSyncRefresh();
365
425
  await this.pauseIncomingEventsAndWaitDrain();
366
426
  const writeBehindFlush = await this.writeBehind.flush(this.options.writeBehind?.flushTimeoutMs);
367
427
  if (writeBehindFlush.remaining > 0) {