zapo-js 1.4.0 → 1.5.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 (94) hide show
  1. package/dist/appstate/types.d.ts +1 -2
  2. package/dist/auth/WaAuthClient.d.ts +10 -0
  3. package/dist/auth/WaAuthClient.js +17 -8
  4. package/dist/auth/credentials-flow.d.ts +8 -0
  5. package/dist/auth/credentials-flow.js +35 -2
  6. package/dist/auth/pairing/companion-host.d.ts +105 -0
  7. package/dist/auth/pairing/companion-host.js +148 -0
  8. package/dist/auth/types.d.ts +20 -9
  9. package/dist/client/WaClient.d.ts +15 -5
  10. package/dist/client/WaClient.js +46 -12
  11. package/dist/client/WaClientFactory.d.ts +3 -0
  12. package/dist/client/WaClientFactory.js +24 -6
  13. package/dist/client/coordinators/WaAppStateMutationCoordinator.d.ts +8 -0
  14. package/dist/client/coordinators/WaAppStateMutationCoordinator.js +37 -1
  15. package/dist/client/coordinators/WaGroupCoordinator.js +6 -1
  16. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +3 -1
  17. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +13 -3
  18. package/dist/client/coordinators/WaMobileCoordinator.d.ts +192 -0
  19. package/dist/client/coordinators/WaMobileCoordinator.js +645 -0
  20. package/dist/client/messaging/companion-host.d.ts +43 -0
  21. package/dist/client/messaging/companion-host.js +61 -0
  22. package/dist/client/messaging/key-protocol.js +25 -1
  23. package/dist/client/persistence/companion-host.d.ts +36 -0
  24. package/dist/client/persistence/companion-host.js +57 -0
  25. package/dist/client/plugins/install.d.ts +2 -1
  26. package/dist/client/plugins/install.js +44 -11
  27. package/dist/client/types.d.ts +71 -7
  28. package/dist/esm/auth/WaAuthClient.js +18 -9
  29. package/dist/esm/auth/credentials-flow.js +36 -3
  30. package/dist/esm/auth/pairing/companion-host.js +140 -0
  31. package/dist/esm/client/WaClient.js +46 -12
  32. package/dist/esm/client/WaClientFactory.js +25 -7
  33. package/dist/esm/client/coordinators/WaAppStateMutationCoordinator.js +37 -1
  34. package/dist/esm/client/coordinators/WaGroupCoordinator.js +6 -1
  35. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +14 -4
  36. package/dist/esm/client/coordinators/WaMobileCoordinator.js +641 -0
  37. package/dist/esm/client/messaging/companion-host.js +56 -0
  38. package/dist/esm/client/messaging/key-protocol.js +25 -1
  39. package/dist/esm/client/persistence/companion-host.js +54 -0
  40. package/dist/esm/client/plugins/install.js +44 -11
  41. package/dist/esm/index.js +5 -2
  42. package/dist/esm/message/WaMessageClient.js +3 -2
  43. package/dist/esm/message/context-info.js +4 -0
  44. package/dist/esm/protocol/constants.js +4 -3
  45. package/dist/esm/protocol/device-identity.js +18 -0
  46. package/dist/esm/protocol/group.js +6 -0
  47. package/dist/esm/protocol/message.js +12 -0
  48. package/dist/esm/protocol/nodes.js +15 -1
  49. package/dist/esm/retry/replay.js +3 -3
  50. package/dist/esm/signal/attestation/WaAdvSignature.js +67 -2
  51. package/dist/esm/signal/attestation/constants.js +2 -0
  52. package/dist/esm/signal/index.js +1 -0
  53. package/dist/esm/transport/index.js +1 -1
  54. package/dist/esm/transport/node/builders/group.js +4 -3
  55. package/dist/esm/transport/node/builders/mobile.js +113 -0
  56. package/dist/esm/transport/noise/WaClientPayload.js +16 -7
  57. package/dist/esm/transport/wa-version-fetcher.js +152 -0
  58. package/dist/esm/util/index.js +1 -0
  59. package/dist/index.d.ts +10 -4
  60. package/dist/index.js +13 -4
  61. package/dist/message/WaMessageClient.js +2 -1
  62. package/dist/message/context-info.d.ts +2 -0
  63. package/dist/message/context-info.js +5 -0
  64. package/dist/protocol/constants.d.ts +6 -4
  65. package/dist/protocol/constants.js +8 -2
  66. package/dist/protocol/device-identity.d.ts +25 -0
  67. package/dist/protocol/device-identity.js +21 -0
  68. package/dist/protocol/group.d.ts +6 -0
  69. package/dist/protocol/group.js +7 -1
  70. package/dist/protocol/message.d.ts +14 -0
  71. package/dist/protocol/message.js +13 -1
  72. package/dist/protocol/nodes.d.ts +14 -0
  73. package/dist/protocol/nodes.js +15 -1
  74. package/dist/retry/replay.js +2 -2
  75. package/dist/signal/attestation/WaAdvSignature.d.ts +50 -1
  76. package/dist/signal/attestation/WaAdvSignature.js +71 -1
  77. package/dist/signal/attestation/constants.d.ts +2 -0
  78. package/dist/signal/attestation/constants.js +3 -1
  79. package/dist/signal/index.d.ts +1 -0
  80. package/dist/signal/index.js +13 -1
  81. package/dist/transport/index.d.ts +2 -2
  82. package/dist/transport/index.js +4 -3
  83. package/dist/transport/node/builders/group.js +4 -3
  84. package/dist/transport/node/builders/mobile.d.ts +68 -0
  85. package/dist/transport/node/builders/mobile.js +120 -0
  86. package/dist/transport/noise/WaClientPayload.js +16 -7
  87. package/dist/transport/wa-version-fetcher.d.ts +92 -0
  88. package/dist/transport/wa-version-fetcher.js +156 -0
  89. package/dist/util/index.d.ts +1 -0
  90. package/dist/util/index.js +3 -1
  91. package/package.json +1 -1
  92. package/dist/esm/transport/wa-web-version-fetcher.js +0 -91
  93. package/dist/transport/wa-web-version-fetcher.d.ts +0 -44
  94. package/dist/transport/wa-web-version-fetcher.js +0 -94
@@ -0,0 +1,56 @@
1
+ import { promisify } from 'node:util';
2
+ import { deflate } from 'node:zlib';
3
+ import { proto } from '../../proto.js';
4
+ const deflateAsync = promisify(deflate);
5
+ /**
6
+ * Serializes a `HistorySync` and zlib-deflates it. The phone uses DEFLATE
7
+ * level 1; any zlib-framed level inflates identically, so it is only a
8
+ * size/speed tradeoff.
9
+ */
10
+ export async function encodeHistorySync(content) {
11
+ const serialized = proto.HistorySync.encode(content).finish();
12
+ const compressed = await deflateAsync(serialized, { level: 1 });
13
+ return new Uint8Array(compressed);
14
+ }
15
+ /**
16
+ * Wraps a deflated `HistorySync` inline in a `HISTORY_SYNC_NOTIFICATION` protocol
17
+ * message. Only small syncs are inlined; larger ones ride an external
18
+ * `md-msg-hist` MMS blob referenced by the same notification.
19
+ */
20
+ export function buildInlineHistorySyncNotification(syncType, deflatedPayload, options = {}) {
21
+ return {
22
+ type: proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION,
23
+ historySyncNotification: {
24
+ syncType,
25
+ chunkOrder: options.chunkOrder ?? 0,
26
+ progress: options.progress ?? 100,
27
+ initialHistBootstrapInlinePayload: deflatedPayload
28
+ }
29
+ };
30
+ }
31
+ /**
32
+ * Builds the `INITIAL_BOOTSTRAP` `HISTORY_SYNC_NOTIFICATION` a mobile-primary
33
+ * pushes to a freshly-linked companion: a deflated `HistorySync` wrapped in a
34
+ * protocol message for delivery as an encrypted peer message. Note
35
+ * `HistorySync.syncType` and `HistorySyncNotification.syncType` are distinct
36
+ * enum types with shared values, so each field uses its own.
37
+ */
38
+ export async function buildHistorySyncBootstrapMessage(input = {}) {
39
+ const historySync = {
40
+ syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
41
+ chunkOrder: 0,
42
+ progress: 100,
43
+ conversations: input.conversations ? [...input.conversations] : [],
44
+ pushnames: input.pushnames ? [...input.pushnames] : [],
45
+ phoneNumberToLidMappings: (input.phoneNumberToLidMappings ?? []).map((mapping) => ({
46
+ pnJid: mapping.pnJid,
47
+ lidJid: mapping.lidJid
48
+ })),
49
+ ...(input.companionMetaNonce !== undefined
50
+ ? { companionMetaNonce: input.companionMetaNonce }
51
+ : {})
52
+ };
53
+ const payload = await encodeHistorySync(historySync);
54
+ const message = buildInlineHistorySyncNotification(proto.Message.HistorySyncType.INITIAL_BOOTSTRAP, payload);
55
+ return { message, payloadBytes: payload.byteLength };
56
+ }
@@ -1,6 +1,15 @@
1
1
  import { proto } from '../../proto.js';
2
2
  import { normalizeDeviceJid } from '../../protocol/jid.js';
3
3
  import { bytesToHex } from '../../util/bytes.js';
4
+ /**
5
+ * Valid app-state sync key ids are exactly 6 bytes: a 2-byte device id followed
6
+ * by a 4-byte big-endian epoch (matching the phone primary's `crypto_info`
7
+ * (device_id, epoch) schema and WhatsApp Web's `handleAppStateSyncKeyShare`,
8
+ * which fatally rejects any share whose keyId byte length is not 6). A primary
9
+ * must never emit a malformed key, so legacy/short key ids are dropped from
10
+ * outgoing shares rather than poisoning the peer's syncd engine.
11
+ */
12
+ const APP_STATE_SYNC_KEY_ID_LENGTH = 6;
4
13
  export function createAppStateSyncKeyProtocol(options) {
5
14
  const { publishProtocolMessageToDevice, fanoutResolver, getCurrentCredentials, logger } = options;
6
15
  const requireCurrentIdentity = (context) => {
@@ -64,7 +73,12 @@ export function createAppStateSyncKeyProtocol(options) {
64
73
  const seenKeyIds = new Set();
65
74
  const keyShareEntries = [];
66
75
  let sharedKeyCount = 0;
76
+ const droppedKeyIds = [];
67
77
  for (const key of keys) {
78
+ if (key.keyId.byteLength !== APP_STATE_SYNC_KEY_ID_LENGTH) {
79
+ droppedKeyIds.push(bytesToHex(key.keyId));
80
+ continue;
81
+ }
68
82
  const keyHex = bytesToHex(key.keyId);
69
83
  if (seenKeyIds.has(keyHex)) {
70
84
  continue;
@@ -81,7 +95,10 @@ export function createAppStateSyncKeyProtocol(options) {
81
95
  });
82
96
  }
83
97
  for (const keyId of missingKeyIds) {
84
- if (keyId.byteLength === 0) {
98
+ if (keyId.byteLength !== APP_STATE_SYNC_KEY_ID_LENGTH) {
99
+ if (keyId.byteLength > 0) {
100
+ droppedKeyIds.push(bytesToHex(keyId));
101
+ }
85
102
  continue;
86
103
  }
87
104
  const keyHex = bytesToHex(keyId);
@@ -93,6 +110,13 @@ export function createAppStateSyncKeyProtocol(options) {
93
110
  keyId: { keyId }
94
111
  });
95
112
  }
113
+ if (droppedKeyIds.length > 0) {
114
+ logger.warn('app-state refusing to share malformed sync keys: keyId byte length must be 6', {
115
+ droppedCount: droppedKeyIds.length,
116
+ totalExpected: keys.length + missingKeyIds.length,
117
+ sample: droppedKeyIds.slice(0, 5)
118
+ });
119
+ }
96
120
  const protocolMessage = {
97
121
  type: proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE,
98
122
  appStateSyncKeyShare: {
@@ -0,0 +1,54 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { base64ToBytes, bytesToBase64 } from '../../util/bytes.js';
4
+ /**
5
+ * File-backed {@link CompanionHostPersistence}: stores the ADV epoch as JSON at
6
+ * `filePath` (the companion identity keys are base64-encoded). Zero native
7
+ * dependencies - the state is tiny (the epoch header plus the linked-companion
8
+ * list). For a database-backed store, implement the two-method
9
+ * `CompanionHostPersistence` contract directly against your DB; there is no need
10
+ * for a dedicated core store domain.
11
+ */
12
+ export function createFileCompanionHostPersistence(filePath) {
13
+ return {
14
+ async load() {
15
+ let raw;
16
+ try {
17
+ raw = await readFile(filePath, 'utf8');
18
+ }
19
+ catch (error) {
20
+ if (error.code === 'ENOENT') {
21
+ return null;
22
+ }
23
+ throw error;
24
+ }
25
+ const parsed = JSON.parse(raw);
26
+ return {
27
+ rawId: parsed.rawId,
28
+ currentKeyIndex: parsed.currentKeyIndex,
29
+ companions: parsed.companions.map((companion) => ({
30
+ deviceJid: companion.deviceJid,
31
+ keyIndex: companion.keyIndex,
32
+ companionIdentityPublicKey: base64ToBytes(companion.companionIdentityPublicKey),
33
+ addedAtSeconds: companion.addedAtSeconds
34
+ }))
35
+ };
36
+ },
37
+ async save(state) {
38
+ const serialized = {
39
+ rawId: state.rawId,
40
+ currentKeyIndex: state.currentKeyIndex,
41
+ companions: state.companions.map((companion) => ({
42
+ deviceJid: companion.deviceJid,
43
+ keyIndex: companion.keyIndex,
44
+ companionIdentityPublicKey: bytesToBase64(companion.companionIdentityPublicKey),
45
+ addedAtSeconds: companion.addedAtSeconds
46
+ }))
47
+ };
48
+ await mkdir(dirname(filePath), { recursive: true });
49
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
50
+ await writeFile(tempPath, JSON.stringify(serialized), 'utf8');
51
+ await rename(tempPath, filePath);
52
+ }
53
+ };
54
+ }
@@ -1,10 +1,35 @@
1
1
  import { isWaClientExposePluginDefinition } from '../plugins/types.js';
2
2
  import { toError } from '../../util/primitives.js';
3
+ const PLUGIN_REGISTRY = Symbol('waClientPluginRegistry');
4
+ /**
5
+ * Per-client store for exposed-plugin state that must survive a
6
+ * disconnect/connect cycle. Each `exposeAs` property is defined on the client
7
+ * exactly once (a non-configurable getter reading `instances`); reinstalls
8
+ * only repopulate the slot, so plugins can be torn down on disconnect and
9
+ * rebuilt on the next connect without redefining reserved members.
10
+ */
11
+ function getPluginRegistry(client) {
12
+ const holder = client;
13
+ const existing = holder[PLUGIN_REGISTRY];
14
+ if (existing) {
15
+ return existing;
16
+ }
17
+ const registry = { instances: new Map(), exposedDefined: new Set() };
18
+ Object.defineProperty(client, PLUGIN_REGISTRY, {
19
+ value: registry,
20
+ enumerable: false,
21
+ configurable: false,
22
+ writable: false
23
+ });
24
+ return registry;
25
+ }
3
26
  /**
4
27
  * Installs {@link WaClientOptions.plugins} on `client`. Returns a dispose
5
- * function invoked by {@link WaClient.disconnect}.
28
+ * function invoked by {@link WaClient.disconnect}. Safe to call again after a
29
+ * dispose to reinstall the same plugins on reconnect.
6
30
  */
7
31
  export function installWaClientPlugins(client, input, plugins) {
32
+ const registry = getPluginRegistry(client);
8
33
  const seenIds = new Set();
9
34
  const seenExposeAs = new Set();
10
35
  const disposeCallbacks = [];
@@ -37,24 +62,32 @@ export function installWaClientPlugins(client, input, plugins) {
37
62
  logger: input.logger.child({ plugin: plugin.id })
38
63
  };
39
64
  if (isWaClientExposePluginDefinition(plugin)) {
40
- if (seenExposeAs.has(plugin.exposeAs)) {
41
- throw new Error(`duplicate wa client plugin exposeAs: ${plugin.exposeAs}`);
65
+ const exposeAs = plugin.exposeAs;
66
+ if (seenExposeAs.has(exposeAs)) {
67
+ throw new Error(`duplicate wa client plugin exposeAs: ${exposeAs}`);
42
68
  }
43
- seenExposeAs.add(plugin.exposeAs);
44
- if (plugin.exposeAs in client) {
45
- throw new Error(`wa client plugin exposeAs "${plugin.exposeAs}" collides with a reserved client member`);
69
+ seenExposeAs.add(exposeAs);
70
+ if (!registry.exposedDefined.has(exposeAs)) {
71
+ if (exposeAs in client) {
72
+ throw new Error(`wa client plugin exposeAs "${exposeAs}" collides with a reserved client member`);
73
+ }
74
+ registry.exposedDefined.add(exposeAs);
75
+ Object.defineProperty(client, exposeAs, {
76
+ get: () => registry.instances.get(exposeAs),
77
+ enumerable: true,
78
+ configurable: false
79
+ });
46
80
  }
47
81
  const instance = plugin.setup(pluginCtx);
48
- Object.defineProperty(client, plugin.exposeAs, {
49
- get: () => instance,
50
- enumerable: true,
51
- configurable: false
82
+ registry.instances.set(exposeAs, instance);
83
+ registerDispose(() => {
84
+ registry.instances.delete(exposeAs);
52
85
  });
53
86
  if (plugin.dispose) {
54
87
  const dispose = plugin.dispose;
55
88
  registerDispose(() => dispose(instance, pluginCtx));
56
89
  }
57
- pluginCtx.logger.debug('wa client plugin installed', { exposeAs: plugin.exposeAs });
90
+ pluginCtx.logger.debug('wa client plugin installed', { exposeAs });
58
91
  }
59
92
  else {
60
93
  plugin.setup(pluginCtx);
package/dist/esm/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  export { WaClient } from './client/index.js';
2
2
  export { defineWaClientPlugin } from './client/plugins/index.js';
3
3
  export { downloadMediaMessage } from './client/media.js';
4
+ export { createFileCompanionHostPersistence } from './client/persistence/companion-host.js';
4
5
  export { parseUsyncResultEnvelope } from './transport/node/builders/usync.js';
5
- export { getContentType, resolveMessageTarget } from './message/encode/content.js';
6
+ export { getContentType, resolveEncMediaType, resolveMessageTarget, unwrapMessage } from './message/encode/content.js';
7
+ export { getContextInfo } from './message/context-info.js';
6
8
  export { resolveMediaPayload } from './message/encode/media-payload.js';
7
9
  export { unpadPkcs7, writeRandomPadMax16 } from './message/encode/padding.js';
8
- export { fetchLatestWaWebVersion } from './transport/wa-web-version-fetcher.js';
10
+ export { fetchLatestWaWebVersion, fetchLatestWaMobileVersion } from './transport/wa-version-fetcher.js';
9
11
  export { ConsoleLogger } from './infra/log/ConsoleLogger.js';
10
12
  export { PinoLogger, createPinoLogger } from './infra/log/PinoLogger.js';
11
13
  export { createNoopLogger } from './infra/log/types.js';
14
+ export { WA_VERSION } from './version-spec.js';
12
15
  export { createStore, WaAuthMemoryStore } from './store/index.js';
13
16
  export { delay } from './util/async.js';
14
17
  export { buildDeviceJid, canonicalizeSignalJid, canonicalizeSignalServer, getLoginIdentity, getWaCompanionPlatformId, getWaMediaHkdfInfo, isBotJid, isBroadcastJid, isGroupJid, isGroupOrBroadcastJid, isHostedDeviceId, isHostedDeviceJid, isHostedServer, isLidJid, isNewsletterJid, isStatusBroadcastJid, isUserJid, normalizeDeviceJid, normalizeRecipientJid, parseJidFull, parsePhoneJid, parseSignalAddressFromJid, signalAddressKey, splitJid, toUserJid, WA_ACCOUNT_SYNC_PROTOCOLS, WA_APP_STATE_COLLECTIONS, WA_APP_STATE_COLLECTION_STATES, WA_APP_STATE_ERROR_CODES, WA_APP_STATE_KDF_INFO, WA_APP_STATE_KEY_TYPES, WA_APP_STATE_SYNC_DATA_TYPE, WA_BROWSERS, WA_BUSINESS_HOURS_DAYS, WA_BUSINESS_HOURS_MODES, WA_COMPANION_PLATFORM_IDS, WA_DEFAULTS, WA_DIRTY_PROTOCOLS, WA_DIRTY_TYPES, WA_DISCONNECT_REASONS, WA_IQ_TYPES, WA_LOGOUT_REASONS, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_MEDIA_HKDF_INFO, WA_NODE_TAGS, WA_PAIRING_KDF_INFO, WA_PREVIEW_MEDIA_HKDF_INFO, WA_READY_STATES, WA_RETRYABLE_ACK_CODES, WA_SIGNALING, WA_STREAM_SIGNALING, WA_SUPPORTED_DIRTY_TYPES, WA_PRIVACY_CATEGORIES, WA_PRIVACY_CATEGORY_TO_SETTING, WA_PRIVACY_DISALLOWED_LIST_CATEGORIES, WA_PRIVACY_SETTING_TO_CATEGORY, WA_PRIVACY_TAGS, WA_PRIVACY_VALUES, WA_XMLNS } from './protocol/index.js';
@@ -1,5 +1,5 @@
1
1
  import { describeAckNode, isAckOrReceiptNode, isNegativeAckNode, isRetryableNegativeAck } from './primitives/ack.js';
2
- import { WA_DEFAULTS, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_NODE_TAGS } from '../protocol/constants.js';
2
+ import { WA_ADDRESSING_MODES, WA_DEFAULTS, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_NODE_TAGS } from '../protocol/constants.js';
3
3
  import { buildReceiptNode } from '../transport/node/builders/global.js';
4
4
  import { delay } from '../util/async.js';
5
5
  import { parseOptionalInt, toError } from '../util/primitives.js';
@@ -226,7 +226,8 @@ export class WaMessageClient {
226
226
  }
227
227
  extractAckMetadata(ackNode) {
228
228
  const addressingModeRaw = ackNode.attrs.addressing_mode;
229
- const addressingMode = addressingModeRaw === 'pn' || addressingModeRaw === 'lid'
229
+ const addressingMode = addressingModeRaw === WA_ADDRESSING_MODES.PN ||
230
+ addressingModeRaw === WA_ADDRESSING_MODES.LID
230
231
  ? addressingModeRaw
231
232
  : undefined;
232
233
  return {
@@ -85,6 +85,10 @@ function pickContextInfoTarget(message) {
85
85
  }
86
86
  return null;
87
87
  }
88
+ /** Reads the `contextInfo` off a message's first content submessage (null when absent). */
89
+ export function getContextInfo(message) {
90
+ return pickContextInfoTarget(message)?.contextInfo ?? null;
91
+ }
88
92
  export function resolveSendContextInfo(input) {
89
93
  const ctx = {
90
94
  ...(input.contentLevel ?? {}),
@@ -1,9 +1,10 @@
1
- export { getWaCompanionPlatformId, WA_BROWSERS, WA_COMPANION_PLATFORM_IDS } from './browser.js';
1
+ export { getWaBrowserDisplayName, getWaCompanionPlatformId, WA_BROWSERS, WA_COMPANION_PLATFORM_IDS } from './browser.js';
2
+ export { resolveWaDeviceIdentity } from './device-identity.js';
2
3
  export { WA_SIGNALING, WA_PAIRING_KDF_INFO } from './auth.js';
3
4
  export { WA_CONNECTION_REASONS, WA_DISCONNECT_REASONS, WA_FAILURE_REASONS, WA_LOGOUT_REASONS, WA_READY_STATES, WA_STREAM_SIGNALING } from './stream.js';
4
5
  export { WA_IQ_TYPES, WA_NODE_TAGS, WA_XMLNS } from './nodes.js';
5
6
  export { WA_CALL_CHILD_TAGS, WA_CALL_NODE_ATTRS, WA_CALL_PAYLOAD_TAGS, WA_CALL_RECEIPT_PAYLOAD_TAGS } from './call.js';
6
- export { WA_EDIT_ATTRS, WA_ENC_MEDIA_TYPES, WA_EVENT_META_TYPES, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_NACK_REASONS, WA_POLL_META_TYPES, WA_RETRYABLE_ACK_CODES, WA_STANZA_MSG_TYPES } from './message.js';
7
+ export { WA_ADDRESSING_MODES, WA_EDIT_ATTRS, WA_ENC_CIPHERTEXT_TYPES, WA_ENC_MEDIA_TYPES, WA_EVENT_META_TYPES, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_NACK_REASONS, WA_POLL_META_TYPES, WA_RETRYABLE_ACK_CODES, WA_STANZA_MSG_TYPES } from './message.js';
7
8
  export { WA_APP_STATE_COLLECTIONS, WA_APP_STATE_COLLECTION_STATES, WA_APP_STATE_ERROR_CODES, WA_APP_STATE_KDF_INFO, WA_APP_STATE_KEY_TYPES, WA_APP_STATE_SYNC_DATA_TYPE } from './appstate.js';
8
9
  export { getWaMediaHkdfInfo, WA_MEDIA_HKDF_INFO, WA_PREVIEW_MEDIA_HKDF_INFO } from './media.js';
9
10
  export { WA_ACCOUNT_SYNC_PROTOCOLS, WA_DIRTY_PROTOCOLS, WA_DIRTY_TYPES, WA_SUPPORTED_DIRTY_TYPES } from './dirty.js';
@@ -17,6 +18,6 @@ export { WA_BIZ_BOT_TYPES, WA_BOT_DEFAULT_CAPABILITIES, WA_BOT_HKDF_INFO, WA_BOT
17
18
  export { WA_STATUS_DISTRIBUTION_SETTINGS } from './status.js';
18
19
  export { WA_EMAIL_CONTEXTS, WA_EMAIL_ERROR_CODES, WA_EMAIL_LIMITS, WA_EMAIL_TAGS, WA_EMAIL_XMLNS } from './email.js';
19
20
  export { AB_PROP_CONFIGS, resolveAbPropNameByCode, WA_ABPROPS_PROTOCOL_VERSION, WA_ABPROPS_REFRESH_BOUNDS } from './abprops.js';
20
- export { WA_GROUP_PARTICIPANT_TYPES } from './group.js';
21
+ export { WA_GROUP_MEMBERSHIP_ACTION_TAGS, WA_GROUP_PARTICIPANT_TYPES } from './group.js';
21
22
  export { WA_USYNC_CONTEXTS, WA_USYNC_DEFAULTS, WA_USYNC_MODES } from './usync.js';
22
23
  export { WA_NEWSLETTER_FETCH_KEY_TYPES, WA_NEWSLETTER_MUTE_TYPES, WA_NEWSLETTER_MUTE_VALUES, WA_NEWSLETTER_PICTURE_TYPES, WA_NEWSLETTER_RECEIVE_TYPES, WA_NEWSLETTER_ROLES, WA_NEWSLETTER_SEND_TYPES, WA_NEWSLETTER_STATE_TYPES, WA_NEWSLETTER_VERIFICATION_STATES, WA_NEWSLETTER_VIEW_ROLES } from './newsletter.js';
@@ -0,0 +1,18 @@
1
+ import { getWaCompanionPlatformId } from './browser.js';
2
+ import { WA_DEFAULTS } from './defaults.js';
3
+ import { getRuntimeOsDisplayName } from '../util/runtime.js';
4
+ /**
5
+ * Resolves the device identity from raw options, applying the shared defaults:
6
+ * browser falls back to {@link WA_DEFAULTS}.DEVICE_BROWSER, OS name to the
7
+ * detected runtime OS, and platform id is inferred from the browser. This is
8
+ * the single source of truth used by both the client factory and the auth
9
+ * client so the pairing payload and any analytics/telemetry agree.
10
+ */
11
+ export function resolveWaDeviceIdentity(options) {
12
+ const browser = options.deviceBrowser ?? WA_DEFAULTS.DEVICE_BROWSER;
13
+ return Object.freeze({
14
+ browser,
15
+ osDisplayName: options.deviceOsDisplayName ?? getRuntimeOsDisplayName(),
16
+ platform: options.devicePlatform ?? getWaCompanionPlatformId(browser)
17
+ });
18
+ }
@@ -3,3 +3,9 @@ export const WA_GROUP_PARTICIPANT_TYPES = Object.freeze({
3
3
  ADMIN: 'admin',
4
4
  REGULAR: 'participant'
5
5
  });
6
+ /** `<membership_requests_action>` child tags for approving/rejecting group join requests. */
7
+ export const WA_GROUP_MEMBERSHIP_ACTION_TAGS = Object.freeze({
8
+ REQUESTS_ACTION: 'membership_requests_action',
9
+ APPROVE: 'approve',
10
+ REJECT: 'reject'
11
+ });
@@ -5,6 +5,18 @@ export const WA_MESSAGE_TAGS = Object.freeze({
5
5
  ACK: 'ack',
6
6
  ERROR: 'error'
7
7
  });
8
+ /** `<enc type>` ciphertext kinds (Signal `msg`/`pkmsg`, group `skmsg`, message-secret `msmsg`). */
9
+ export const WA_ENC_CIPHERTEXT_TYPES = Object.freeze({
10
+ MESSAGE: 'msg',
11
+ PREKEY: 'pkmsg',
12
+ SENDER_KEY: 'skmsg',
13
+ MESSAGE_SECRET: 'msmsg'
14
+ });
15
+ /** `addressing_mode` stanza attr: phone-number vs LID addressing. */
16
+ export const WA_ADDRESSING_MODES = Object.freeze({
17
+ PN: 'pn',
18
+ LID: 'lid'
19
+ });
8
20
  export const WA_MESSAGE_TYPES = Object.freeze({
9
21
  ENC_VERSION: '2',
10
22
  MEDIA_NOTIFY: 'medianotify',
@@ -10,6 +10,7 @@ export const WA_NODE_TAGS = Object.freeze({
10
10
  NOTIFICATION: 'notification',
11
11
  SUCCESS: 'success',
12
12
  INFO_BULLETIN: 'ib',
13
+ OFFLINE: 'offline',
13
14
  DIRTY: 'dirty',
14
15
  EDGE_ROUTING: 'edge_routing',
15
16
  ROUTING_INFO: 'routing_info',
@@ -80,7 +81,20 @@ export const WA_NODE_TAGS = Object.freeze({
80
81
  USERNAME: 'username',
81
82
  TEXT_STATUS: 'text_status',
82
83
  DISAPPEARING_MODE: 'disappearing_mode',
83
- BUSINESS: 'business'
84
+ BUSINESS: 'business',
85
+ PUB_KEY: 'pub-key',
86
+ KEY_INDEX_LIST: 'key-index-list',
87
+ CLIENT_PROPS: 'client-props',
88
+ PEM: 'pem',
89
+ TTL: 'ttl',
90
+ KEY_ID: 'key_id',
91
+ REMOVE_COMPANION_DEVICE: 'remove-companion-device',
92
+ PRIMARY_IDENTITY_PUB: 'primary_identity_pub',
93
+ COMPANION_IDENTITY_PUBLIC: 'companion_identity_public',
94
+ COMPANION_SERVER_AUTH_KEY_PUB: 'companion_server_auth_key_pub',
95
+ LINK_CODE_PAIRING_WRAPPED_PRIMARY_EPHEMERAL_PUB: 'link_code_pairing_wrapped_primary_ephemeral_pub',
96
+ LINK_CODE_PAIRING_WRAPPED_COMPANION_EPHEMERAL_PUB: 'link_code_pairing_wrapped_companion_ephemeral_pub',
97
+ LINK_CODE_PAIRING_WRAPPED_KEY_BUNDLE: 'link_code_pairing_wrapped_key_bundle'
84
98
  });
85
99
  export const WA_IQ_TYPES = Object.freeze({
86
100
  GET: 'get',
@@ -2,7 +2,7 @@ import { injectDeviceListMetadata } from '../message/crypto/icdc.js';
2
2
  import { wrapDeviceSentMessage } from '../message/encode/device-sent.js';
3
3
  import { unpadPkcs7, writeRandomPadMax16 } from '../message/encode/padding.js';
4
4
  import { proto } from '../proto.js';
5
- import { WA_DEFAULTS, WA_NODE_TAGS } from '../protocol/constants.js';
5
+ import { WA_ADDRESSING_MODES, WA_DEFAULTS, WA_NODE_TAGS } from '../protocol/constants.js';
6
6
  import { isGroupOrBroadcastJid, isHostedDeviceJid, isStatusBroadcastJid, normalizeDeviceJid, parseJidFull, toUserJid } from '../protocol/jid.js';
7
7
  import { decodeRetryReplayPayload } from './codec.js';
8
8
  import { decodeBinaryNode } from '../transport/binary/index.js';
@@ -197,8 +197,8 @@ export class WaRetryReplayService {
197
197
  addressingMode: isStatus
198
198
  ? undefined
199
199
  : requesterAddress.server === WA_DEFAULTS.LID_SERVER
200
- ? 'lid'
201
- : 'pn',
200
+ ? WA_ADDRESSING_MODES.LID
201
+ : WA_ADDRESSING_MODES.PN,
202
202
  encType: encrypted.type,
203
203
  ciphertext: encrypted.ciphertext,
204
204
  retryCount,
@@ -1,17 +1,82 @@
1
1
  import { hmacSha256Sign, toRawPubKey, xeddsaSign, xeddsaVerify } from '../../crypto/index.js';
2
- import { ADV_PREFIX_ACCOUNT_SIGNATURE, ADV_PREFIX_DEVICE_SIGNATURE, ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE, ADV_PREFIX_HOSTED_DEVICE_SIGNATURE } from '../attestation/constants.js';
2
+ import { ADV_PREFIX_ACCOUNT_KEY_INDEX, ADV_PREFIX_ACCOUNT_SIGNATURE, ADV_PREFIX_DEVICE_SIGNATURE, ADV_PREFIX_HOSTED_ACCOUNT_KEY_INDEX, ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE, ADV_PREFIX_HOSTED_DEVICE_SIGNATURE } from '../attestation/constants.js';
3
3
  import { concatBytes } from '../../util/bytes.js';
4
- export { ADV_PREFIX_ACCOUNT_SIGNATURE, ADV_PREFIX_DEVICE_SIGNATURE, ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE } from '../attestation/constants.js';
4
+ export { ADV_PREFIX_ACCOUNT_KEY_INDEX, ADV_PREFIX_ACCOUNT_SIGNATURE, ADV_PREFIX_DEVICE_SIGNATURE, ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE } from '../attestation/constants.js';
5
+ /**
6
+ * Companion-side check that the primary's account key signed a device identity.
7
+ * Verifies `accountSignature` over `prefix || details || identityPublicKey`
8
+ * against the account signature public key. Inverse of
9
+ * {@link generateDeviceIdentityAccountSignature}.
10
+ */
5
11
  export async function verifyDeviceIdentityAccountSignature(details, accountSignature, identityPublicKey, accountSignatureKey, isHosted = false) {
6
12
  const prefix = isHosted ? ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE : ADV_PREFIX_ACCOUNT_SIGNATURE;
7
13
  const message = concatBytes([prefix, details, identityPublicKey]);
8
14
  return xeddsaVerify(toRawPubKey(accountSignatureKey), message, accountSignature);
9
15
  }
16
+ /**
17
+ * Companion-side signature proving possession of the identity private key after
18
+ * adopting a primary-signed identity. Signs
19
+ * `prefix || details || identityKeyPair.pubKey || accountSignatureKey`, and is
20
+ * verified by the primary via {@link verifyDeviceSignature}.
21
+ */
10
22
  export async function generateDeviceSignature(details, identityKeyPair, accountSignatureKey, isHosted = false) {
11
23
  const prefix = isHosted ? ADV_PREFIX_HOSTED_DEVICE_SIGNATURE : ADV_PREFIX_DEVICE_SIGNATURE;
12
24
  const message = concatBytes([prefix, details, identityKeyPair.pubKey, accountSignatureKey]);
13
25
  return xeddsaSign(identityKeyPair.privKey, message);
14
26
  }
27
+ /**
28
+ * HMAC-SHA256 over an `ADVSignedDeviceIdentity` (or hosted-prefixed variant)
29
+ * keyed by the shared ADV secret, binding the signed identity to the pairing
30
+ * secret inside `ADVSignedDeviceIdentityHMAC`.
31
+ */
15
32
  export function computeAdvIdentityHmac(secretKey, details) {
16
33
  return hmacSha256Sign(secretKey, details);
17
34
  }
35
+ /**
36
+ * Primary-side counterpart to {@link verifyDeviceIdentityAccountSignature}.
37
+ * Signs a companion's `ADVDeviceIdentity` details with the account (primary)
38
+ * identity key so a linking companion can prove the primary authorized this
39
+ * device. The signed message is `prefix || details || companionIdentityPublicKey`.
40
+ *
41
+ * `accountIdentityKeyPair` is the primary's own Signal identity key pair; its
42
+ * public half is what ships as `ADVSignedDeviceIdentity.accountSignatureKey`.
43
+ * Pass `companionIdentityPublicKey` in the exact byte form the companion will
44
+ * present at verification (the value carried in its pairing QR / key bundle).
45
+ */
46
+ export async function generateDeviceIdentityAccountSignature(details, companionIdentityPublicKey, accountIdentityKeyPair, isHosted = false) {
47
+ const prefix = isHosted ? ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE : ADV_PREFIX_ACCOUNT_SIGNATURE;
48
+ const message = concatBytes([prefix, details, companionIdentityPublicKey]);
49
+ return xeddsaSign(accountIdentityKeyPair.privKey, message);
50
+ }
51
+ /**
52
+ * Primary-side counterpart to {@link generateDeviceSignature}. Verifies the
53
+ * device signature a companion returns after adopting a signed identity, which
54
+ * proves the companion holds the identity private key it registered. The signed
55
+ * message is `prefix || details || companionIdentityPublicKey || accountSignatureKey`.
56
+ */
57
+ export async function verifyDeviceSignature(details, deviceSignature, companionIdentityPublicKey, accountSignatureKey, isHosted = false) {
58
+ const prefix = isHosted ? ADV_PREFIX_HOSTED_DEVICE_SIGNATURE : ADV_PREFIX_DEVICE_SIGNATURE;
59
+ const message = concatBytes([prefix, details, companionIdentityPublicKey, accountSignatureKey]);
60
+ return xeddsaVerify(toRawPubKey(companionIdentityPublicKey), message, deviceSignature);
61
+ }
62
+ /**
63
+ * Signs an `ADVKeyIndexList` details blob with the account (primary) identity
64
+ * key. A primary republishes this list whenever its companion set changes so
65
+ * every device can validate the current set of key indexes. The signed message
66
+ * is `prefix || keyIndexListDetails`.
67
+ */
68
+ export async function signKeyIndexList(keyIndexListDetails, accountIdentityKeyPair, isHosted = false) {
69
+ const prefix = isHosted ? ADV_PREFIX_HOSTED_ACCOUNT_KEY_INDEX : ADV_PREFIX_ACCOUNT_KEY_INDEX;
70
+ const message = concatBytes([prefix, keyIndexListDetails]);
71
+ return xeddsaSign(accountIdentityKeyPair.privKey, message);
72
+ }
73
+ /**
74
+ * Verifies an `ADVSignedKeyIndexList` account signature against the primary's
75
+ * account signature key. Inverse of {@link signKeyIndexList}; used by a device
76
+ * to validate a key-index list published by the primary.
77
+ */
78
+ export async function verifyKeyIndexListSignature(keyIndexListDetails, accountSignature, accountSignatureKey, isHosted = false) {
79
+ const prefix = isHosted ? ADV_PREFIX_HOSTED_ACCOUNT_KEY_INDEX : ADV_PREFIX_ACCOUNT_KEY_INDEX;
80
+ const message = concatBytes([prefix, keyIndexListDetails]);
81
+ return xeddsaVerify(toRawPubKey(accountSignatureKey), message, accountSignature);
82
+ }
@@ -1,4 +1,6 @@
1
1
  export const ADV_PREFIX_ACCOUNT_SIGNATURE = new Uint8Array([6, 0]);
2
2
  export const ADV_PREFIX_DEVICE_SIGNATURE = new Uint8Array([6, 1]);
3
+ export const ADV_PREFIX_ACCOUNT_KEY_INDEX = new Uint8Array([6, 2]);
3
4
  export const ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE = new Uint8Array([6, 5]);
4
5
  export const ADV_PREFIX_HOSTED_DEVICE_SIGNATURE = new Uint8Array([6, 6]);
6
+ export const ADV_PREFIX_HOSTED_ACCOUNT_KEY_INDEX = new Uint8Array([6, 7]);
@@ -14,3 +14,4 @@ export { SenderKeyManager } from './group/SenderKeyManager.js';
14
14
  export { createAndStoreInitialKeys } from './registration/utils.js';
15
15
  export { SignalProtocol } from './session/SignalProtocol.js';
16
16
  export { createSignalSessionResolver } from './session/resolver.js';
17
+ export { ADV_PREFIX_ACCOUNT_KEY_INDEX, ADV_PREFIX_ACCOUNT_SIGNATURE, ADV_PREFIX_DEVICE_SIGNATURE, ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE, computeAdvIdentityHmac, generateDeviceIdentityAccountSignature, generateDeviceSignature, signKeyIndexList, verifyDeviceIdentityAccountSignature, verifyDeviceSignature, verifyKeyIndexListSignature } from './attestation/WaAdvSignature.js';
@@ -1,6 +1,6 @@
1
1
  export { decodeBinaryNode, decodeBinaryNodeStanza, encodeBinaryNode, encodeBinaryNodeStanza } from './binary/index.js';
2
2
  export { isProxyAgent, isProxyDispatcher, isProxyTransport, toProxyAgent, toProxyDispatcher } from './proxy.js';
3
- export { fetchLatestWaWebVersion } from './wa-web-version-fetcher.js';
3
+ export { fetchLatestWaWebVersion, fetchLatestWaMobileVersion } from './wa-version-fetcher.js';
4
4
  export { WaComms } from './WaComms.js';
5
5
  export { verifyNoiseCertificateChain } from './noise/WaNoiseCert.js';
6
6
  export { WaNoiseHandshake } from './noise/WaNoiseHandshake.js';
@@ -1,4 +1,5 @@
1
1
  import { WA_DEFAULTS } from '../../../protocol/defaults.js';
2
+ import { WA_GROUP_MEMBERSHIP_ACTION_TAGS } from '../../../protocol/group.js';
2
3
  import { WA_IQ_TYPES, WA_XMLNS } from '../../../protocol/nodes.js';
3
4
  import { buildIqNode } from '../../node/query.js';
4
5
  export function buildCreateGroupIq(input) {
@@ -76,20 +77,20 @@ export function buildMembershipRequestsActionIq(input) {
76
77
  const children = [];
77
78
  if (approve.length > 0) {
78
79
  children.push({
79
- tag: 'approve',
80
+ tag: WA_GROUP_MEMBERSHIP_ACTION_TAGS.APPROVE,
80
81
  attrs: {},
81
82
  content: approve.map((jid) => ({ tag: 'participant', attrs: { jid } }))
82
83
  });
83
84
  }
84
85
  if (reject.length > 0) {
85
86
  children.push({
86
- tag: 'reject',
87
+ tag: WA_GROUP_MEMBERSHIP_ACTION_TAGS.REJECT,
87
88
  attrs: {},
88
89
  content: reject.map((jid) => ({ tag: 'participant', attrs: { jid } }))
89
90
  });
90
91
  }
91
92
  return buildIqNode(WA_IQ_TYPES.SET, input.groupJid, WA_XMLNS.GROUPS, [
92
- { tag: 'membership_requests_action', attrs: {}, content: children }
93
+ { tag: WA_GROUP_MEMBERSHIP_ACTION_TAGS.REQUESTS_ACTION, attrs: {}, content: children }
93
94
  ]);
94
95
  }
95
96
  export function buildCancelMembershipRequestsIq(input) {