@ryuu-reinzz/baileys 3.0.0-beta.2 → 3.0.0-beta.21

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.
@@ -2,7 +2,7 @@ import { proto } from '../../WAProto/index.js';
2
2
  import { makeLibSignalRepository } from '../Signal/libsignal.js';
3
3
  import { Browsers } from '../Utils/browser-utils.js';
4
4
  import logger from '../Utils/logger.js';
5
- const version = [2, 3000, 1027934701];
5
+ const version = [2, 3000, 1033105955];
6
6
  export const UNAUTHORIZED_CODES = [401, 403, 419];
7
7
  export const DEFAULT_ORIGIN = 'https://web.whatsapp.com';
8
8
  export const CALL_VIDEO_PREFIX = 'https://call.whatsapp.com/video/';
@@ -15,15 +15,20 @@ export const WA_ADV_DEVICE_SIG_PREFIX = Buffer.from([6, 1]);
15
15
  export const WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX = Buffer.from([6, 5]);
16
16
  export const WA_ADV_HOSTED_DEVICE_SIG_PREFIX = Buffer.from([6, 6]);
17
17
  export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60;
18
+ /** Status messages older than 24 hours are considered expired */
19
+ export const STATUS_EXPIRY_SECONDS = 24 * 60 * 60;
20
+ /** WA Web enforces a 14-day maximum age for placeholder resend requests */
21
+ export const PLACEHOLDER_MAX_AGE_SECONDS = 14 * 24 * 60 * 60;
18
22
  export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0';
19
23
  export const DICT_VERSION = 3;
20
24
  export const KEY_BUNDLE_TYPE = Buffer.from([5]);
21
25
  export const NOISE_WA_HEADER = Buffer.from([87, 65, 6, DICT_VERSION]); // last is "DICT_VERSION"
22
26
  /** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
23
27
  export const URL_REGEX = /https:\/\/(?![^:@\/\s]+:[^:@\/\s]+@)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?/g;
24
- // TODO: Add WA root CA
25
28
  export const WA_CERT_DETAILS = {
26
- SERIAL: 0
29
+ SERIAL: 0,
30
+ ISSUER: 'WhatsAppLongTerm1',
31
+ PUBLIC_KEY: Buffer.from('142375574d0a587166aae71ebe516437c4a28b73e3695c6ce1f7f9545da8ee6b', 'hex')
27
32
  };
28
33
  export const PROCESSABLE_HISTORY_TYPES = [
29
34
  proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
@@ -51,7 +56,9 @@ export const DEFAULT_CONNECTION_CONFIG = {
51
56
  markOnlineOnConnect: true,
52
57
  syncFullHistory: true,
53
58
  patchMessageBeforeSending: msg => msg,
54
- shouldSyncHistoryMessage: () => true,
59
+ shouldSyncHistoryMessage: ({ syncType }) => {
60
+ return syncType !== proto.HistorySync.HistorySyncType.FULL;
61
+ },
55
62
  shouldIgnoreJid: () => false,
56
63
  linkPreviewImageThumbnailWidth: 192,
57
64
  transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
@@ -112,4 +119,10 @@ export const DEFAULT_CACHE_TTLS = {
112
119
  CALL_OFFER: 5 * 60, // 5 minutes
113
120
  USER_DEVICES: 5 * 60 // 5 minutes
114
121
  };
122
+ export const TimeMs = {
123
+ Minute: 60 * 1000,
124
+ Hour: 60 * 60 * 1000,
125
+ Day: 24 * 60 * 60 * 1000,
126
+ Week: 7 * 24 * 60 * 60 * 1000
127
+ };
115
128
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,7 @@
1
- /* @ts-ignore */
1
+ // @ts-ignore
2
2
  import * as libsignal from 'libsignal';
3
+ // @ts-ignore
4
+ import { PreKeyWhisperMessage } from 'libsignal/src/protobufs.js';
3
5
  import { LRUCache } from 'lru-cache';
4
6
  import { generateSignalPubKey } from '../Utils/index.js';
5
7
  import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, transferDevice, WAJIDDomains } from '../WABinary/index.js';
@@ -7,6 +9,28 @@ import { SenderKeyName } from './Group/sender-key-name.js';
7
9
  import { SenderKeyRecord } from './Group/sender-key-record.js';
8
10
  import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage } from './Group/index.js';
9
11
  import { LIDMappingStore } from './lid-mapping.js';
12
+ /** Extract identity key from PreKeyWhisperMessage for identity change detection */
13
+ function extractIdentityFromPkmsg(ciphertext) {
14
+ try {
15
+ if (!ciphertext || ciphertext.length < 2) {
16
+ return undefined;
17
+ }
18
+ // Version byte check (version 3)
19
+ const version = ciphertext[0];
20
+ if ((version & 0xf) !== 3) {
21
+ return undefined;
22
+ }
23
+ // Parse protobuf (skip version byte)
24
+ const preKeyProto = PreKeyWhisperMessage.decode(ciphertext.slice(1));
25
+ if (preKeyProto.identityKey?.length === 33) {
26
+ return new Uint8Array(preKeyProto.identityKey);
27
+ }
28
+ return undefined;
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
10
34
  export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
11
35
  const lidMapping = new LIDMappingStore(auth.keys, logger, pnToLIDFunc);
12
36
  const storage = signalStorage(auth, lidMapping);
@@ -48,6 +72,17 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
48
72
  async decryptMessage({ jid, type, ciphertext }) {
49
73
  const addr = jidToSignalProtocolAddress(jid);
50
74
  const session = new libsignal.SessionCipher(storage, addr);
75
+ // Extract and save sender's identity key before decryption for identity change detection
76
+ if (type === 'pkmsg') {
77
+ const identityKey = extractIdentityFromPkmsg(ciphertext);
78
+ if (identityKey) {
79
+ const addrStr = addr.toString();
80
+ const identityChanged = await storage.saveIdentity(addrStr, identityKey);
81
+ if (identityChanged) {
82
+ logger.info({ jid, addr: addrStr }, 'identity key changed or new contact, session will be re-established');
83
+ }
84
+ }
85
+ }
51
86
  async function doDecrypt() {
52
87
  let result;
53
88
  switch (type) {
@@ -296,7 +331,33 @@ function signalStorage({ creds, keys }, lidMapping) {
296
331
  await keys.set({ session: { [wireJid]: session.serialize() } });
297
332
  },
298
333
  isTrustedIdentity: () => {
299
- return true; // todo: implement
334
+ return true; // TOFU - Trust on First Use (same as WhatsApp Web)
335
+ },
336
+ loadIdentityKey: async (id) => {
337
+ const wireJid = await resolveLIDSignalAddress(id);
338
+ const { [wireJid]: key } = await keys.get('identity-key', [wireJid]);
339
+ return key || undefined;
340
+ },
341
+ saveIdentity: async (id, identityKey) => {
342
+ const wireJid = await resolveLIDSignalAddress(id);
343
+ const { [wireJid]: existingKey } = await keys.get('identity-key', [wireJid]);
344
+ const keysMatch = existingKey &&
345
+ existingKey.length === identityKey.length &&
346
+ existingKey.every((byte, i) => byte === identityKey[i]);
347
+ if (existingKey && !keysMatch) {
348
+ // Identity changed - clear session and update key
349
+ await keys.set({
350
+ session: { [wireJid]: null },
351
+ 'identity-key': { [wireJid]: identityKey }
352
+ });
353
+ return true;
354
+ }
355
+ if (!existingKey) {
356
+ // New contact - Trust on First Use (TOFU)
357
+ await keys.set({ 'identity-key': { [wireJid]: identityKey } });
358
+ return true;
359
+ }
360
+ return false;
300
361
  },
301
362
  loadPreKey: async (id) => {
302
363
  const keyId = id.toString();
@@ -7,16 +7,16 @@ export class LIDMappingStore {
7
7
  ttlAutopurge: true,
8
8
  updateAgeOnGet: true
9
9
  });
10
+ this.inflightLIDLookups = new Map();
11
+ this.inflightPNLookups = new Map();
10
12
  this.keys = keys;
11
13
  this.pnToLIDFunc = pnToLIDFunc;
12
14
  this.logger = logger;
13
15
  }
14
- /**
15
- * Store LID-PN mapping - USER LEVEL
16
- */
17
16
  async storeLIDPNMappings(pairs) {
18
- // Validate inputs
19
- const pairMap = {};
17
+ if (pairs.length === 0)
18
+ return;
19
+ const validatedPairs = [];
20
20
  for (const { lid, pn } of pairs) {
21
21
  if (!((isLidUser(lid) && isPnUser(pn)) || (isPnUser(lid) && isLidUser(pn)))) {
22
22
  this.logger.warn(`Invalid LID-PN mapping: ${lid}, ${pn}`);
@@ -25,67 +25,135 @@ export class LIDMappingStore {
25
25
  const lidDecoded = jidDecode(lid);
26
26
  const pnDecoded = jidDecode(pn);
27
27
  if (!lidDecoded || !pnDecoded)
28
- return;
29
- const pnUser = pnDecoded.user;
30
- const lidUser = lidDecoded.user;
31
- let existingLidUser = this.mappingCache.get(`pn:${pnUser}`);
32
- if (!existingLidUser) {
33
- this.logger.trace(`Cache miss for PN user ${pnUser}; checking database`);
34
- const stored = await this.keys.get('lid-mapping', [pnUser]);
35
- existingLidUser = stored[pnUser];
28
+ continue;
29
+ validatedPairs.push({ pnUser: pnDecoded.user, lidUser: lidDecoded.user });
30
+ }
31
+ if (validatedPairs.length === 0)
32
+ return;
33
+ const cacheMissSet = new Set();
34
+ const existingMappings = new Map();
35
+ for (const { pnUser } of validatedPairs) {
36
+ const cached = this.mappingCache.get(`pn:${pnUser}`);
37
+ if (cached) {
38
+ existingMappings.set(pnUser, cached);
39
+ }
40
+ else {
41
+ cacheMissSet.add(pnUser);
42
+ }
43
+ }
44
+ if (cacheMissSet.size > 0) {
45
+ const cacheMisses = [...cacheMissSet];
46
+ this.logger.trace(`Batch fetching ${cacheMisses.length} LID mappings from database`);
47
+ const stored = await this.keys.get('lid-mapping', cacheMisses);
48
+ for (const pnUser of cacheMisses) {
49
+ const existingLidUser = stored[pnUser];
36
50
  if (existingLidUser) {
37
- // Update cache with database value
51
+ existingMappings.set(pnUser, existingLidUser);
38
52
  this.mappingCache.set(`pn:${pnUser}`, existingLidUser);
39
53
  this.mappingCache.set(`lid:${existingLidUser}`, pnUser);
40
54
  }
41
55
  }
56
+ }
57
+ const pairMap = {};
58
+ for (const { pnUser, lidUser } of validatedPairs) {
59
+ const existingLidUser = existingMappings.get(pnUser);
42
60
  if (existingLidUser === lidUser) {
43
61
  this.logger.debug({ pnUser, lidUser }, 'LID mapping already exists, skipping');
44
62
  continue;
45
63
  }
46
64
  pairMap[pnUser] = lidUser;
47
65
  }
66
+ if (Object.keys(pairMap).length === 0)
67
+ return;
48
68
  this.logger.trace({ pairMap }, `Storing ${Object.keys(pairMap).length} pn mappings`);
69
+ const batchData = {};
70
+ for (const [pnUser, lidUser] of Object.entries(pairMap)) {
71
+ batchData[pnUser] = lidUser;
72
+ batchData[`${lidUser}_reverse`] = pnUser;
73
+ }
49
74
  await this.keys.transaction(async () => {
50
- for (const [pnUser, lidUser] of Object.entries(pairMap)) {
51
- await this.keys.set({
52
- 'lid-mapping': {
53
- [pnUser]: lidUser,
54
- [`${lidUser}_reverse`]: pnUser
55
- }
56
- });
57
- this.mappingCache.set(`pn:${pnUser}`, lidUser);
58
- this.mappingCache.set(`lid:${lidUser}`, pnUser);
59
- }
75
+ await this.keys.set({ 'lid-mapping': batchData });
60
76
  }, 'lid-mapping');
77
+ // Update cache after successful DB write
78
+ for (const [pnUser, lidUser] of Object.entries(pairMap)) {
79
+ this.mappingCache.set(`pn:${pnUser}`, lidUser);
80
+ this.mappingCache.set(`lid:${lidUser}`, pnUser);
81
+ }
61
82
  }
62
- /**
63
- * Get LID for PN - Returns device-specific LID based on user mapping
64
- */
65
83
  async getLIDForPN(pn) {
66
84
  return (await this.getLIDsForPNs([pn]))?.[0]?.lid || null;
67
85
  }
68
86
  async getLIDsForPNs(pns) {
87
+ if (pns.length === 0)
88
+ return null;
89
+ const sortedPns = [...new Set(pns)].sort();
90
+ const cacheKey = sortedPns.join(',');
91
+ const inflight = this.inflightLIDLookups.get(cacheKey);
92
+ if (inflight) {
93
+ this.logger.trace(`Coalescing getLIDsForPNs request for ${sortedPns.length} PNs`);
94
+ return inflight;
95
+ }
96
+ const promise = this._getLIDsForPNsImpl(pns);
97
+ this.inflightLIDLookups.set(cacheKey, promise);
98
+ try {
99
+ return await promise;
100
+ }
101
+ finally {
102
+ this.inflightLIDLookups.delete(cacheKey);
103
+ }
104
+ }
105
+ async _getLIDsForPNsImpl(pns) {
69
106
  const usyncFetch = {};
70
- // mapped from pn to lid mapping to prevent duplication in results later
71
107
  const successfulPairs = {};
108
+ const pending = [];
109
+ const addResolvedPair = (pn, decoded, lidUser) => {
110
+ const normalizedLidUser = lidUser.toString();
111
+ if (!normalizedLidUser) {
112
+ this.logger.warn(`Invalid or empty LID user for PN ${pn}: lidUser = "${lidUser}"`);
113
+ return false;
114
+ }
115
+ // Push the PN device ID to the LID to maintain device separation
116
+ const pnDevice = decoded.device !== undefined ? decoded.device : 0;
117
+ const deviceSpecificLid = `${normalizedLidUser}${!!pnDevice ? `:${pnDevice}` : ``}@${decoded.server === 'hosted' ? 'hosted.lid' : 'lid'}`;
118
+ this.logger.trace(`getLIDForPN: ${pn} → ${deviceSpecificLid} (user mapping with device ${pnDevice})`);
119
+ successfulPairs[pn] = { lid: deviceSpecificLid, pn };
120
+ return true;
121
+ };
72
122
  for (const pn of pns) {
73
123
  if (!isPnUser(pn) && !isHostedPnUser(pn))
74
124
  continue;
75
125
  const decoded = jidDecode(pn);
76
126
  if (!decoded)
77
127
  continue;
78
- // Check cache first for PN → LID mapping
79
128
  const pnUser = decoded.user;
80
- let lidUser = this.mappingCache.get(`pn:${pnUser}`);
81
- if (!lidUser) {
82
- // Cache miss - check database
83
- const stored = await this.keys.get('lid-mapping', [pnUser]);
84
- lidUser = stored[pnUser];
85
- if (lidUser) {
129
+ const cached = this.mappingCache.get(`pn:${pnUser}`);
130
+ if (cached && typeof cached === 'string') {
131
+ if (!addResolvedPair(pn, decoded, cached)) {
132
+ this.logger.warn(`Invalid entry for ${pn} (pair not resolved)`);
133
+ continue;
134
+ }
135
+ continue;
136
+ }
137
+ pending.push({ pn, pnUser, decoded });
138
+ }
139
+ if (pending.length) {
140
+ const pnUsers = [...new Set(pending.map(item => item.pnUser))];
141
+ const stored = await this.keys.get('lid-mapping', pnUsers);
142
+ for (const pnUser of pnUsers) {
143
+ const lidUser = stored[pnUser];
144
+ if (lidUser && typeof lidUser === 'string') {
86
145
  this.mappingCache.set(`pn:${pnUser}`, lidUser);
87
146
  this.mappingCache.set(`lid:${lidUser}`, pnUser);
88
147
  }
148
+ }
149
+ for (const { pn, pnUser, decoded } of pending) {
150
+ const cached = this.mappingCache.get(`pn:${pnUser}`);
151
+ if (cached && typeof cached === 'string') {
152
+ if (!addResolvedPair(pn, decoded, cached)) {
153
+ this.logger.warn(`Invalid entry for ${pn} (pair not resolved)`);
154
+ continue;
155
+ }
156
+ }
89
157
  else {
90
158
  this.logger.trace(`No LID mapping found for PN user ${pnUser}; batch getting from USync`);
91
159
  const device = decoded.device || 0;
@@ -99,19 +167,8 @@ export class LIDMappingStore {
99
167
  else {
100
168
  usyncFetch[normalizedPn]?.push(device);
101
169
  }
102
- continue;
103
170
  }
104
171
  }
105
- lidUser = lidUser.toString();
106
- if (!lidUser) {
107
- this.logger.warn(`Invalid or empty LID user for PN ${pn}: lidUser = "${lidUser}"`);
108
- return null;
109
- }
110
- // Push the PN device ID to the LID to maintain device separation
111
- const pnDevice = decoded.device !== undefined ? decoded.device : 0;
112
- const deviceSpecificLid = `${lidUser}${!!pnDevice ? `:${pnDevice}` : ``}@${decoded.server === 'hosted' ? 'hosted.lid' : 'lid'}`;
113
- this.logger.trace(`getLIDForPN: ${pn} → ${deviceSpecificLid} (user mapping with device ${pnDevice})`);
114
- successfulPairs[pn] = { lid: deviceSpecificLid, pn };
115
172
  }
116
173
  if (Object.keys(usyncFetch).length > 0) {
117
174
  const result = await this.pnToLIDFunc?.(Object.keys(usyncFetch)); // this function already adds LIDs to mapping
@@ -134,38 +191,81 @@ export class LIDMappingStore {
134
191
  }
135
192
  }
136
193
  else {
137
- return null;
194
+ this.logger.warn('USync fetch yielded no results for pending PNs');
138
195
  }
139
196
  }
140
- return Object.values(successfulPairs);
197
+ return Object.values(successfulPairs).length > 0 ? Object.values(successfulPairs) : null;
141
198
  }
142
- /**
143
- * Get PN for LID - USER LEVEL with device construction
144
- */
145
199
  async getPNForLID(lid) {
146
- if (!isLidUser(lid))
147
- return null;
148
- const decoded = jidDecode(lid);
149
- if (!decoded)
200
+ return (await this.getPNsForLIDs([lid]))?.[0]?.pn || null;
201
+ }
202
+ async getPNsForLIDs(lids) {
203
+ if (lids.length === 0)
150
204
  return null;
151
- // Check cache first for LID → PN mapping
152
- const lidUser = decoded.user;
153
- let pnUser = this.mappingCache.get(`lid:${lidUser}`);
154
- if (!pnUser || typeof pnUser !== 'string') {
155
- // Cache miss - check database
156
- const stored = await this.keys.get('lid-mapping', [`${lidUser}_reverse`]);
157
- pnUser = stored[`${lidUser}_reverse`];
205
+ const sortedLids = [...new Set(lids)].sort();
206
+ const cacheKey = sortedLids.join(',');
207
+ const inflight = this.inflightPNLookups.get(cacheKey);
208
+ if (inflight) {
209
+ this.logger.trace(`Coalescing getPNsForLIDs request for ${sortedLids.length} LIDs`);
210
+ return inflight;
211
+ }
212
+ const promise = this._getPNsForLIDsImpl(lids);
213
+ this.inflightPNLookups.set(cacheKey, promise);
214
+ try {
215
+ return await promise;
216
+ }
217
+ finally {
218
+ this.inflightPNLookups.delete(cacheKey);
219
+ }
220
+ }
221
+ async _getPNsForLIDsImpl(lids) {
222
+ const successfulPairs = {};
223
+ const pending = [];
224
+ const addResolvedPair = (lid, decoded, pnUser) => {
158
225
  if (!pnUser || typeof pnUser !== 'string') {
159
- this.logger.trace(`No reverse mapping found for LID user: ${lidUser}`);
160
- return null;
226
+ return false;
227
+ }
228
+ const lidDevice = decoded.device !== undefined ? decoded.device : 0;
229
+ const pnJid = `${pnUser}:${lidDevice}@${decoded.domainType === WAJIDDomains.HOSTED_LID ? 'hosted' : 's.whatsapp.net'}`;
230
+ this.logger.trace(`Found reverse mapping: ${lid} → ${pnJid}`);
231
+ successfulPairs[lid] = { lid, pn: pnJid };
232
+ return true;
233
+ };
234
+ for (const lid of lids) {
235
+ if (!isLidUser(lid))
236
+ continue;
237
+ const decoded = jidDecode(lid);
238
+ if (!decoded)
239
+ continue;
240
+ const lidUser = decoded.user;
241
+ const cached = this.mappingCache.get(`lid:${lidUser}`);
242
+ if (cached && typeof cached === 'string') {
243
+ addResolvedPair(lid, decoded, cached);
244
+ continue;
245
+ }
246
+ pending.push({ lid, lidUser, decoded });
247
+ }
248
+ if (pending.length) {
249
+ const reverseKeys = [...new Set(pending.map(item => `${item.lidUser}_reverse`))];
250
+ const stored = await this.keys.get('lid-mapping', reverseKeys);
251
+ for (const { lid, lidUser, decoded } of pending) {
252
+ let pnUser = this.mappingCache.get(`lid:${lidUser}`);
253
+ if (!pnUser || typeof pnUser !== 'string') {
254
+ pnUser = stored[`${lidUser}_reverse`];
255
+ if (pnUser && typeof pnUser === 'string') {
256
+ this.mappingCache.set(`lid:${lidUser}`, pnUser);
257
+ this.mappingCache.set(`pn:${pnUser}`, lidUser);
258
+ }
259
+ }
260
+ if (pnUser && typeof pnUser === 'string') {
261
+ addResolvedPair(lid, decoded, pnUser);
262
+ }
263
+ else {
264
+ this.logger.trace(`No reverse mapping found for LID user: ${lidUser}`);
265
+ }
161
266
  }
162
- this.mappingCache.set(`lid:${lidUser}`, pnUser);
163
267
  }
164
- // Construct device-specific PN JID
165
- const lidDevice = decoded.device !== undefined ? decoded.device : 0;
166
- const pnJid = `${pnUser}:${lidDevice}@${decoded.domainType === WAJIDDomains.HOSTED_LID ? 'hosted' : 's.whatsapp.net'}`;
167
- this.logger.trace(`Found reverse mapping: ${lid} → ${pnJid}`);
168
- return pnJid;
268
+ return Object.values(successfulPairs).length ? Object.values(successfulPairs) : null;
169
269
  }
170
270
  }
171
271
  //# sourceMappingURL=lid-mapping.js.map
@@ -35,11 +35,15 @@ export class WebSocketClient extends AbstractSocketClient {
35
35
  this.socket?.on(event, (...args) => this.emit(event, ...args));
36
36
  }
37
37
  }
38
- close() {
38
+ async close() {
39
39
  if (!this.socket) {
40
40
  return;
41
41
  }
42
+ const closePromise = new Promise(resolve => {
43
+ this.socket?.once('close', resolve);
44
+ });
42
45
  this.socket.close();
46
+ await closePromise;
43
47
  this.socket = null;
44
48
  }
45
49
  send(str, cb) {
@@ -10,35 +10,38 @@ export const makeBusinessSocket = (config) => {
10
10
  const node = [];
11
11
  const simpleFields = ['address', 'email', 'description'];
12
12
  node.push(...simpleFields
13
- .filter(key => args[key])
13
+ .filter(key => args[key] !== undefined && args[key] !== null)
14
14
  .map(key => ({
15
15
  tag: key,
16
16
  attrs: {},
17
17
  content: args[key]
18
18
  })));
19
- if (args.websites) {
19
+ if (args.websites !== undefined) {
20
20
  node.push(...args.websites.map(website => ({
21
21
  tag: 'website',
22
22
  attrs: {},
23
23
  content: website
24
24
  })));
25
25
  }
26
- if (args.hours) {
26
+ if (args.hours !== undefined) {
27
27
  node.push({
28
28
  tag: 'business_hours',
29
29
  attrs: { timezone: args.hours.timezone },
30
- content: args.hours.days.map(config => {
30
+ content: args.hours.days.map(dayConfig => {
31
31
  const base = {
32
32
  tag: 'business_hours_config',
33
- attrs: { day_of_week: config.day, mode: config.mode }
33
+ attrs: {
34
+ day_of_week: dayConfig.day,
35
+ mode: dayConfig.mode
36
+ }
34
37
  };
35
- if (config.mode === 'specific_hours') {
38
+ if (dayConfig.mode === 'specific_hours') {
36
39
  return {
37
40
  ...base,
38
41
  attrs: {
39
42
  ...base.attrs,
40
- open_time: config.openTimeInMinutes,
41
- close_time: config.closeTimeInMinutes
43
+ open_time: dayConfig.openTimeInMinutes,
44
+ close_time: dayConfig.closeTimeInMinutes
42
45
  }
43
46
  };
44
47
  }