From ef673f62ca29c4bf3f2b8a60ba16a0bd13d86862 Mon Sep 17 00:00:00 2001 From: SamuelScheit Date: Thu, 20 Apr 2023 13:01:11 +0200 Subject: [PATCH] feat: native-mobile-api --- Example/example.ts | 79 +++++++++- package.json | 4 + src/Defaults/index.ts | 14 ++ src/Defaults/phonenumber-mcc.json | 223 ++++++++++++++++++++++++++ src/Socket/index.ts | 2 +- src/Socket/mobile-socket.ts | 47 ++++++ src/Socket/registration.ts | 251 ++++++++++++++++++++++++++++++ src/Socket/socket.ts | 86 +++++----- src/Socket/web-socket.ts | 31 ++++ src/Types/Auth.ts | 8 + src/Types/Socket.ts | 2 + src/Utils/auth-utils.ts | 10 +- src/Utils/crypto.ts | 4 + src/Utils/noise-handler.ts | 35 +++-- src/Utils/validate-connection.ts | 60 +++++-- src/WABinary/generic-utils.ts | 31 ++++ yarn.lock | 127 +++++++++++++++ 17 files changed, 940 insertions(+), 74 deletions(-) create mode 100644 src/Defaults/phonenumber-mcc.json create mode 100644 src/Socket/mobile-socket.ts create mode 100644 src/Socket/registration.ts create mode 100644 src/Socket/web-socket.ts diff --git a/Example/example.ts b/Example/example.ts index eb6fd15..12c2340 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -1,13 +1,20 @@ import { Boom } from '@hapi/boom' +import parsePhoneNumber from 'libphonenumber-js' import NodeCache from 'node-cache' -import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, getAggregateVotesInPollMessage, makeCacheableSignalKeyStore, makeInMemoryStore, proto, useMultiFileAuthState, WAMessageContent, WAMessageKey } from '../src' -import MAIN_LOGGER from '../src/Utils/logger' +import P from 'pino' +import readline from 'readline' +import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, getAggregateVotesInPollMessage, makeCacheableSignalKeyStore, makeInMemoryStore, PHONENUMBER_MCC, proto, useMultiFileAuthState, WAMessageContent, WAMessageKey } from '../src' -const logger = MAIN_LOGGER.child({ }) -logger.level = 'trace' +const logger = P({ + transport: { + target: 'pino-pretty' + }, + level: 'trace' +}) const useStore = !process.argv.includes('--no-store') const doReplies = !process.argv.includes('--no-reply') +const useMobile = process.argv.includes('--mobile') // external map to store retry counts of messages when decryption/encryption fails // keep this out of the socket itself, so as to prevent a message decryption/encryption loop across socket restarts @@ -33,6 +40,7 @@ const startSock = async() => { version, logger, printQRInTerminal: true, + mobile: useMobile, auth: { creds: state.creds, /** caching makes the store faster to send/recv messages */ @@ -49,6 +57,69 @@ const startSock = async() => { store?.bind(sock.ev) + // If mobile was chosen, ask for the code + if(useMobile && !sock.authState.creds.registered) { + const question = (text: string) => new Promise((resolve) => rl.question(text, resolve)) + + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + const { registration } = sock.authState.creds || { registration: {} } + + if(!registration.phoneNumber) { + registration.phoneNumber = await question('Please enter your mobile phone number:\n') + } else { + console.log('Your mobile phone number is not registered.') + } + + const phoneNumber = parsePhoneNumber(registration!.phoneNumber) + if(!phoneNumber?.isValid()) { + throw new Error('Invalid phone number: ' + registration!.phoneNumber) + } + + registration.phoneNumber = phoneNumber.format('E.164') + registration.phoneNumberCountryCode = phoneNumber.countryCallingCode + registration.phoneNumberNationalNumber = phoneNumber.nationalNumber + const mcc = PHONENUMBER_MCC[phoneNumber.countryCallingCode] + if(!mcc) { + throw new Error('Could not find MCC for phone number: ' + registration!.phoneNumber + '\nPlease specify the MCC manually.') + } + + registration.phoneNumberMobileCountryCode = mcc + + async function enterCode() { + try { + const code = await question('Please enter the one time code:\n') + const response = await sock.register(code.replace(/["']/g, '').trim().toLowerCase()) + console.log('Successfully registered your phone number.') + console.log(response) + rl.close() + } catch(error) { + console.error('Failed to register your phone number. Please try again.\n', error) + await askForOTP() + } + } + + async function askForOTP() { + let code = await question('How would you like to receive the one time code for registration? "sms" or "voice"\n') + code = code.replace(/["']/g, '').trim().toLowerCase() + + if(code !== 'sms' && code !== 'voice') { + return await askForOTP() + } + + registration.method = code + + try { + await sock.requestRegistrationCode(registration) + await enterCode() + } catch(error) { + console.error('Failed to request registration code. Please try again.\n', error) + await askForOTP() + } + } + + askForOTP() + } + const sendMessageWTyping = async(msg: AnyMessageContent, jid: string) => { await sock.presenceSubscribe(jid) await delay(500) diff --git a/package.json b/package.json index 88ade39..681da97 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,13 @@ "@hapi/boom": "^9.1.3", "axios": "^1.3.3", "futoin-hkdf": "^1.5.1", + "libphonenumber-js": "^1.10.20", "libsignal": "git+https://github.com/adiwajshing/libsignal-node", "music-metadata": "^7.12.3", "node-cache": "^5.1.2", "pino": "^7.0.0", "protobufjs": "^6.11.3", + "uuid": "^9.0.0", "ws": "^8.0.0" }, "peerDependencies": { @@ -79,11 +81,13 @@ "@types/jest": "^27.5.1", "@types/node": "^16.0.0", "@types/sharp": "^0.29.4", + "@types/uuid": "^9.0.0", "@types/ws": "^8.0.0", "eslint": "^8.0.0", "jest": "^27.0.6", "jimp": "^0.16.1", "link-preview-js": "^3.0.0", + "pino-pretty": "^9.4.0", "qrcode-terminal": "^0.12.0", "sharp": "^0.30.5", "ts-jest": "^27.0.3", diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index 6491a8c..9a150ed 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -4,22 +4,36 @@ import type { AuthenticationState, MediaType, SocketConfig, WAVersion } from '.. import { Browsers } from '../Utils' import logger from '../Utils/logger' import { version } from './baileys-version.json' +import phoneNumberMCC from './phonenumber-mcc.json' export const UNAUTHORIZED_CODES = [401, 403, 419] +export const PHONENUMBER_MCC = phoneNumberMCC + export const DEFAULT_ORIGIN = 'https://web.whatsapp.com' +export const MOBILE_ENDPOINT = 'g.whatsapp.net' +export const MOBILE_PORT = 443 export const DEF_CALLBACK_PREFIX = 'CB:' export const DEF_TAG_PREFIX = 'TAG:' export const PHONE_CONNECTION_CB = 'CB:Pong' export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60 +export const MOBILE_TOKEN = Buffer.from('0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM4174c0243f5277a5d7720ce842cc4ae6') +export const MOBILE_REGISTRATION_ENDPOINT = 'https://v.whatsapp.net/v2' +export const MOBILE_USERAGENT = 'WhatsApp/2.22.24.81 iOS/15.3.1 Device/Apple-iPhone_7' +export const REGISTRATION_PUBLIC_KEY = Buffer.from([ + 5, 142, 140, 15, 116, 195, 235, 197, 215, 166, 134, 92, 108, 60, 132, 56, 86, 176, 97, 33, 204, 232, 234, 119, 77, + 34, 251, 111, 18, 37, 18, 48, 45, +]) export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0' export const DICT_VERSION = 2 export const KEY_BUNDLE_TYPE = Buffer.from([5]) export const NOISE_WA_HEADER = Buffer.from( [ 87, 65, 6, DICT_VERSION ] ) // last is "DICT_VERSION" +export const PROTOCOL_VERSION = [5, 2] +export const MOBILE_NOISE_HEADER = Buffer.concat([Buffer.from('WA'), Buffer.from(PROTOCOL_VERSION)]) /** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */ export const URL_REGEX = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/ export const URL_EXCLUDE_REGEX = /.*@.*/ diff --git a/src/Defaults/phonenumber-mcc.json b/src/Defaults/phonenumber-mcc.json new file mode 100644 index 0000000..2fd7c9f --- /dev/null +++ b/src/Defaults/phonenumber-mcc.json @@ -0,0 +1,223 @@ +{ + "93": 412, + "355": 276, + "213": 603, + "1-684": 544, + "376": 213, + "244": 631, + "1-264": 365, + "1-268": 344, + "54": 722, + "374": 283, + "297": 363, + "61": 505, + "43": 232, + "994": 400, + "1-242": 364, + "973": 426, + "880": 470, + "1-246": 342, + "375": 257, + "32": 206, + "501": 702, + "229": 616, + "1-441": 350, + "975": 402, + "591": 736, + "387": 218, + "267": 652, + "55": 724, + "1-284": 348, + "673": 528, + "359": 284, + "226": 613, + "257": 642, + "855": 456, + "237": 624, + "238": 625, + "1-345": 346, + "236": 623, + "235": 622, + "56": 730, + "86": 454, + "57": 732, + "269": 654, + "682": 548, + "506": 712, + "385": 219, + "53": 368, + "357": 280, + "420": 230, + "243": 630, + "45": 238, + "253": 638, + "1-767": 366, + "1-809": 370, + "1-849": 370, + "1-829": 370, + "593": 740, + "20": 602, + "503": 706, + "240": 627, + "291": 657, + "372": 248, + "251": 636, + "500": 750, + "298": 288, + "679": 542, + "358": 244, + "33": 208, + "689": 547, + "241": 628, + "220": 607, + "995": 282, + "49": 262, + "233": 620, + "350": 266, + "30": 202, + "299": 290, + "1-473": 352, + "1-671": 535, + "502": 704, + "224": 537, + "592": 738, + "509": 372, + "504": 708, + "852": 454, + "36": 216, + "354": 274, + "91": 404, + "62": 510, + "98": 432, + "964": 418, + "353": 234, + "972": 425, + "39": 222, + "225": 612, + "1-876": 338, + "81": 440, + "962": 416, + "254": 639, + "686": 545, + "383": 221, + "965": 419, + "371": 247, + "961": 415, + "266": 651, + "231": 618, + "218": 606, + "423": 295, + "370": 246, + "352": 270, + "389": 294, + "261": 646, + "265": 650, + "60": 502, + "960": 472, + "223": 610, + "356": 278, + "692": 551, + "222": 609, + "230": 617, + "52": 334, + "691": 550, + "373": 259, + "377": 212, + "976": 428, + "382": 297, + "1-664": 354, + "212": 604, + "258": 643, + "95": 414, + "264": 649, + "674": 536, + "977": 429, + "31": 204, + "687": 546, + "64": 530, + "505": 710, + "227": 614, + "234": 621, + "683": 555, + "1-670": 534, + "47": 242, + "968": 226, + "92": 410, + "680": 552, + "970": 423, + "507": 714, + "675": 537, + "595": 744, + "51": 716, + "63": 515, + "48": 260, + "351": 268, + "1-787, 1-939": 330, + "974": 427, + "242": 630, + "40": 226, + "7": 250, + "250": 635, + "290": 658, + "1-869": 356, + "1-758": 358, + "508": 308, + "1-784": 360, + "685": 544, + "378": 292, + "239": 626, + "966": 420, + "221": 608, + "381": 220, + "248": 633, + "232": 619, + "65": 525, + "386": 293, + "677": 540, + "27": 655, + "211": 659, + "34": 214, + "94": 413, + "249": 634, + "597": 746, + "268": 653, + "46": 240, + "41": 228, + "963": 417, + "886": 466, + "992": 436, + "255": 640, + "66": 520, + "228": 615, + "690": 554, + "676": 539, + "1-868": 374, + "216": 605, + "90": 286, + "993": 438, + "1-649": 376, + "688": 553, + "1-340": 332, + "256": 641, + "380": 255, + "971": 424, + "44": 234, + "1": 310, + "598": 748, + "998": 434, + "678": 541, + "379": 225, + "58": 734, + "681": 543, + "967": 421, + "260": 645, + "263": 648, + "670": 514, + "245": 632, + "856": 457, + "599": 362, + "850": 467, + "262": 647, + "82": 450, + "84": 452 +} diff --git a/src/Socket/index.ts b/src/Socket/index.ts index 7efebf0..7338c9c 100644 --- a/src/Socket/index.ts +++ b/src/Socket/index.ts @@ -1,6 +1,6 @@ import { DEFAULT_CONNECTION_CONFIG } from '../Defaults' import { UserFacingSocketConfig } from '../Types' -import { makeBusinessSocket as _makeSocket } from './business' +import { makeRegistrationSocket as _makeSocket } from './registration' // export the last socket layer const makeWASocket = (config: UserFacingSocketConfig) => ( diff --git a/src/Socket/mobile-socket.ts b/src/Socket/mobile-socket.ts new file mode 100644 index 0000000..edcdd5d --- /dev/null +++ b/src/Socket/mobile-socket.ts @@ -0,0 +1,47 @@ +import { Socket } from 'net' +import { MOBILE_ENDPOINT, MOBILE_PORT } from '../Defaults' +import { SocketConfig } from '../Types' + +export class MobileSocket extends Socket { + constructor(public config: SocketConfig) { + super() + + if(config.auth.creds.registered) { + this.connect() + } + + this.on('data', (d) => { + this.emit('message', d) + }) + } + + override connect() { + return super.connect(MOBILE_PORT, MOBILE_ENDPOINT, () => { + this.emit('open') + }) + } + + get isOpen(): boolean { + return this.readyState === 'open' + } + + get isClosed(): boolean { + return this.readyState === 'closed' + } + + get isClosing(): boolean { + return this.isClosed + } + + get isConnecting(): boolean { + return this.readyState === 'opening' + } + + close(): void { + this.end() + } + + send(data: unknown, cb?: ((err?: Error | undefined) => void) | undefined) { + return super.write(data as Uint8Array | string, cb as ((err?: Error | undefined) => void)) + } +} \ No newline at end of file diff --git a/src/Socket/registration.ts b/src/Socket/registration.ts new file mode 100644 index 0000000..3f3672e --- /dev/null +++ b/src/Socket/registration.ts @@ -0,0 +1,251 @@ +/* eslint-disable camelcase */ +import { MOBILE_REGISTRATION_ENDPOINT, MOBILE_TOKEN, MOBILE_USERAGENT, REGISTRATION_PUBLIC_KEY } from '../Defaults' +import { KeyPair, SignedKeyPair, SocketConfig } from '../Types' +import { aesEncryptGCM, Curve, md5 } from '../Utils/crypto' +import { jidEncode } from '../WABinary' +import { makeBusinessSocket } from './business' +import { MobileSocket } from './mobile-socket' + +function urlencode(str: string) { + return str.replace(/-/g, '%2d').replace(/_/g, '%5f').replace(/~/g, '%7e') +} + +const validRegistrationOptions = (config: RegistrationOptions) => config?.phoneNumberCountryCode && + config.phoneNumberNationalNumber && + config.phoneNumberMobileCountryCode + +export const makeRegistrationSocket = (config: SocketConfig) => { + const sock = makeBusinessSocket(config) + + const register = async(code: string) => { + if(!validRegistrationOptions(config.auth.creds.registration)) { + throw new Error('please specify the registration options') + } + + const result = await mobileRegister({ ...sock.authState.creds, ...sock.authState.creds.registration as RegistrationOptions, code }) + + sock.authState.creds.me = { + id: jidEncode(result.login!, 's.whatsapp.net'), + name: '~' + } + + sock.authState.creds.registered = true + sock.ev.emit('creds.update', sock.authState.creds) + + if(sock.ws instanceof MobileSocket) { + sock.ws.connect() + } + + return result + } + + const requestRegistrationCode = async(registrationOptions?: RegistrationOptions) => { + registrationOptions = registrationOptions || config.auth.creds.registration + if(!validRegistrationOptions(registrationOptions)) { + throw new Error('Invalid registration options') + } + + sock.authState.creds.registration = registrationOptions + + sock.ev.emit('creds.update', sock.authState.creds) + + return mobileRegisterCode({ ...config.auth.creds, ...registrationOptions }) + } + + return { + ...sock, + register, + requestRegistrationCode, + } +} + +// Backup_token: Base64.getEncoder().encodeToString(Arrays.copyOfRange(Base64.getDecoder().decode(UUID.randomUUID().toString().replace('-','')),0,15)) + +export interface RegistrationData { + registrationId: number + signedPreKey: SignedKeyPair + noiseKey: KeyPair + signedIdentityKey: KeyPair + identityId: Buffer + phoneId: string + deviceId: string + backupToken: Buffer +} + +export interface RegistrationOptions { + /** your phone number */ + phoneNumber?: string + /** the country code of your phone number */ + phoneNumberCountryCode: string + /** your phone number without country code */ + phoneNumberNationalNumber: string + /** the country code of your mobile network + * @see {@link https://de.wikipedia.org/wiki/Mobile_Country_Code} + */ + phoneNumberMobileCountryCode: string + /** the network code of your mobile network + * @see {@link https://de.wikipedia.org/wiki/Mobile_Network_Code} + */ + phoneNumberMobileNetworkCode: string + /** + * How to send the one time code + */ + method?: 'sms' | 'voice' +} + +export type RegistrationParams = RegistrationData & RegistrationOptions + +function convertBufferToUrlHex(buffer: Buffer) { + var id = '' + + buffer.forEach((x) => { + // encode random identity_id buffer as percentage url encoding + id += `%${x.toString(16).padStart(2, '0').toLowerCase()}` + }) + + return id +} + +export function registrationParams(params: RegistrationParams) { + const e_regid = Buffer.alloc(4) + e_regid.writeInt32BE(params.registrationId) + + const e_skey_id = Buffer.alloc(3) + e_skey_id.writeInt16BE(params.signedPreKey.keyId) + + params.phoneNumberCountryCode = params.phoneNumberCountryCode.replace('+', '').trim() + params.phoneNumberNationalNumber = params.phoneNumberNationalNumber.replace(/[/-\s)(]/g, '').trim() + + return { + cc: params.phoneNumberCountryCode, + in: params.phoneNumberNationalNumber, + Rc: '0', + lg: 'en', + lc: 'GB', + mistyped: '6', + authkey: Buffer.from(params.noiseKey.public).toString('base64url'), + e_regid: e_regid.toString('base64url'), + e_keytype: 'BQ', + e_ident: Buffer.from(params.signedIdentityKey.public).toString('base64url'), + // e_skey_id: e_skey_id.toString('base64url'), + e_skey_id: 'AAAA', + e_skey_val: Buffer.from(params.signedPreKey.keyPair.public).toString('base64url'), + e_skey_sig: Buffer.from(params.signedPreKey.signature).toString('base64url'), + fdid: params.phoneId, + network_ratio_type: '1', + expid: params.deviceId, + simnum: '1', + hasinrc: '1', + pid: Math.floor(Math.random() * 1000).toString(), + id: convertBufferToUrlHex(params.identityId), + backup_token: convertBufferToUrlHex(params.backupToken), + token: md5(Buffer.concat([MOBILE_TOKEN, Buffer.from(params.phoneNumberNationalNumber)])).toString('hex'), + } +} + +/** + * Requests a registration code for the given phone number. + */ +export function mobileRegisterCode(params: RegistrationParams) { + return mobileRegisterFetch('/code', { + params: { + ...registrationParams(params), + mcc: `${params.phoneNumberMobileCountryCode}`.padStart(3, '0'), + mnc: `${params.phoneNumberMobileNetworkCode || '001'}`.padStart(3, '0'), + sim_mcc: '000', + sim_mnc: '000', + method: params?.method || 'sms', + reason: '', + hasav: '1' + }, + }) +} + +export function mobileRegisterExists(params: RegistrationParams) { + return mobileRegisterFetch('/exist', { + params: registrationParams(params) + }) +} + +/** + * Registers the phone number on whatsapp with the received OTP code. + */ +export async function mobileRegister(params: RegistrationParams & { code: string }) { + //const result = await mobileRegisterFetch(`/reg_onboard_abprop?cc=${params.phoneNumberCountryCode}&in=${params.phoneNumberNationalNumber}&rc=0`) + + return mobileRegisterFetch('/register', { + params: { ...registrationParams(params), code: params.code.replace('-', '') }, + }) +} + +/** + * Encrypts the given string as AEAD aes-256-gcm with the public whatsapp key and a random keypair. + */ +export function mobileRegisterEncrypt(data: string) { + const keypair = Curve.generateKeyPair() + const key = Curve.sharedKey(keypair.private, REGISTRATION_PUBLIC_KEY) + + const buffer = aesEncryptGCM(Buffer.from(data), new Uint8Array(key), Buffer.alloc(12), Buffer.alloc(0)) + + return Buffer.concat([Buffer.from(keypair.public), buffer]).toString('base64url') +} + +export async function mobileRegisterFetch(path: string, opts: { params?: Record, headers?: Record } = {}) { + let url = `${MOBILE_REGISTRATION_ENDPOINT}${path}` + + if(opts.params) { + const parameter = [] as string[] + + for(const param in opts.params) { + parameter.push(param + '=' + urlencode(opts.params[param])) + } + + console.log('parameter', opts.params, parameter) + + // const params = urlencode(mobileRegisterEncrypt(parameter.join('&'))) + // url += `?ENC=${params}` + url += `?${parameter.join('&')}` + } + + if(!opts.headers) { + opts.headers = {} + } + + opts.headers['User-Agent'] = MOBILE_USERAGENT + + const response = await fetch(url, opts) + + const text = await response.text() + + try { + var json = JSON.parse(text) + } catch(error) { + throw text + } + + if(!response.ok || json.reason) { + throw json + } + + if(json.status && !['ok', 'sent'].includes(json.status)) { + throw json + } + + return json as ExistsResponse +} + + +export interface ExistsResponse { + status: 'fail' + voice_length?: number + voice_wait?: number + sms_length?: number + sms_wait?: number + reason?: 'incorrect' | 'missing_param' + login?: string + flash_type?: number + ab_hash?: string + ab_key?: string + exp_cfg?: string + lid?: string +} diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index b91a3a3..eb9d772 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -1,12 +1,14 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Boom } from '@hapi/boom' import { promisify } from 'util' -import WebSocket from 'ws' import { proto } from '../../WAProto' -import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, DEFAULT_ORIGIN, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT } from '../Defaults' +import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MOBILE_NOISE_HEADER, NOISE_WA_HEADER } from '../Defaults' import { DisconnectReason, SocketConfig } from '../Types' -import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout } from '../Utils' +import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, generateLoginNode, generateMdTagPrefix, generateMobileNode, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout } from '../Utils' import { makeEventBuffer } from '../Utils/event-buffer' -import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary' +import { assertNodeErrorFree, BinaryNode, binaryNodeToString, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary' +import { MobileSocket } from './mobile-socket' +import { WebSocket } from './web-socket' /** * Connects to WA servers and performs: @@ -14,37 +16,35 @@ import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild, * - listen to messages and emit events * - query phone connection */ -export const makeSocket = ({ - waWebSocketUrl, - connectTimeoutMs, - logger, - agent, - keepAliveIntervalMs, - version, - browser, - auth: authState, - printQRInTerminal, - defaultQueryTimeoutMs, - syncFullHistory, - transactionOpts, - qrTimeout, - options, - makeSignalRepository -}: SocketConfig) => { - const ws = new WebSocket(waWebSocketUrl, undefined, { - origin: DEFAULT_ORIGIN, - headers: options.headers as {}, - handshakeTimeout: connectTimeoutMs, - timeout: connectTimeoutMs, - agent - }) + +export const makeSocket = (config: SocketConfig) => { + const { + connectTimeoutMs, + logger, + keepAliveIntervalMs, + browser, + auth: authState, + printQRInTerminal, + defaultQueryTimeoutMs, + transactionOpts, + qrTimeout, + makeSignalRepository, + } = config + + config.mobile = config.mobile || config.auth.creds.registered + const ws = config.mobile ? new MobileSocket(config) : new WebSocket(config) ws.setMaxListeners(0) const ev = makeEventBuffer(logger) /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */ const ephemeralKeyPair = Curve.generateKeyPair() /** WA noise protocol wrapper */ - const noise = makeNoiseHandler(ephemeralKeyPair, logger) + const noise = makeNoiseHandler({ + keyPair: ephemeralKeyPair, + NOISE_HEADER: config.mobile ? MOBILE_NOISE_HEADER : NOISE_WA_HEADER, + mobile: config.mobile, + logger + }) const { creds } = authState // add transaction capability @@ -63,7 +63,7 @@ export const makeSocket = ({ const sendPromise = promisify(ws.send) /** send a raw buffer */ const sendRawMessage = async(data: Uint8Array | Buffer) => { - if(ws.readyState !== ws.OPEN) { + if(!ws.isOpen) { throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) } @@ -84,7 +84,7 @@ export const makeSocket = ({ /** send a binary node */ const sendNode = (frame: BinaryNode) => { if(logger.level === 'trace') { - logger.trace({ msgId: frame.attrs.id, fromMe: true, frame }, 'communication') + logger.trace(binaryNodeToString(frame), 'xml send') } const buff = encodeBinaryNode(frame) @@ -101,7 +101,7 @@ export const makeSocket = ({ /** await the next incoming message */ const awaitNextMessage = async(sendMsg?: Uint8Array) => { - if(ws.readyState !== ws.OPEN) { + if(!ws.isOpen) { throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) @@ -186,21 +186,21 @@ export const makeSocket = ({ } helloMsg = proto.HandshakeMessage.fromObject(helloMsg) - logger.info({ browser, helloMsg }, 'connected to WA Web') + logger.info({ browser, helloMsg }, 'connected to WA') const init = proto.HandshakeMessage.encode(helloMsg).finish() const result = await awaitNextMessage(init) const handshake = proto.HandshakeMessage.decode(result) - logger.trace({ handshake }, 'handshake recv from WA Web') + logger.trace({ handshake }, 'handshake recv from WA') const keyEnc = noise.processHandshake(handshake, creds.noiseKey) - const config = { version, browser, syncFullHistory } - let node: proto.IClientPayload - if(!creds.me) { + if(config.mobile) { + node = generateMobileNode(config) + } else if(!creds.me) { node = generateRegistrationNode(creds, config) logger.info({ node }, 'not logged in, attempting registration...') } else { @@ -276,7 +276,7 @@ export const makeSocket = ({ const msgId = frame.attrs.id if(logger.level === 'trace') { - logger.trace({ msgId, fromMe: false, frame }, 'communication') + logger.trace(binaryNodeToString(frame), 'recv xml') } /* Check if this is a response to a message we sent */ @@ -321,7 +321,7 @@ export const makeSocket = ({ ws.removeAllListeners('open') ws.removeAllListeners('message') - if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) { + if(!ws.isClosed && !ws.isClosing) { try { ws.close() } catch{ } @@ -338,11 +338,11 @@ export const makeSocket = ({ } const waitForSocketOpen = async() => { - if(ws.readyState === ws.OPEN) { + if(ws.isOpen) { return } - if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { + if(ws.isClosed || ws.isClosing) { throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) } @@ -375,7 +375,7 @@ export const makeSocket = ({ */ if(diff > keepAliveIntervalMs + 5000) { end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost })) - } else if(ws.readyState === ws.OPEN) { + } else if(ws.isOpen) { // if its all good, send a keep alive request query( { @@ -472,7 +472,7 @@ export const makeSocket = ({ let qrMs = qrTimeout || 60_000 // time to let a QR live const genPairQR = () => { - if(ws.readyState !== ws.OPEN) { + if(!ws.isOpen) { return } diff --git a/src/Socket/web-socket.ts b/src/Socket/web-socket.ts new file mode 100644 index 0000000..ece9264 --- /dev/null +++ b/src/Socket/web-socket.ts @@ -0,0 +1,31 @@ +import { WebSocket as WS } from 'ws' +import { DEFAULT_ORIGIN } from '../Defaults' +import { SocketConfig } from '../Types' + +export class WebSocket extends WS { + constructor(public config: SocketConfig) { + super(config.waWebSocketUrl, undefined, { + origin: DEFAULT_ORIGIN, + headers: config.options.headers as Record, + handshakeTimeout: config.connectTimeoutMs, + timeout: config.connectTimeoutMs, + agent: config.agent, + }) + } + + get isOpen() { + return this.readyState === WS.OPEN + } + + get isClosed() { + return this.readyState === WS.CLOSED + } + + get isClosing() { + return this.readyState === WS.CLOSING + } + + get isConnecting() { + return this.readyState === WS.CONNECTING + } +} diff --git a/src/Types/Auth.ts b/src/Types/Auth.ts index 72bde7c..717b94a 100644 --- a/src/Types/Auth.ts +++ b/src/Types/Auth.ts @@ -1,4 +1,5 @@ import type { proto } from '../../WAProto' +import { RegistrationOptions } from '../Socket/registration' import type { Contact } from './Contact' import type { MinimalMessage } from './Message' @@ -58,6 +59,13 @@ export type AuthenticationCreds = SignalCreds & { /** number of times history & app state has been synced */ accountSyncCounter: number accountSettings: AccountSettings + // mobile creds + deviceId: string + phoneId: string + identityId: Buffer + registered: boolean + backupToken: Buffer + registration: RegistrationOptions } export type SignalDataTypeMap = { diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index 42edaa7..51dd6f7 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -31,6 +31,8 @@ export type SocketConfig = { defaultQueryTimeoutMs: number | undefined /** ping-pong interval for WS connection */ keepAliveIntervalMs: number + /** should baileys use the mobile api instead of the multi device api */ + mobile?: boolean /** proxy agent */ agent?: Agent /** pino logger */ diff --git a/src/Utils/auth-utils.ts b/src/Utils/auth-utils.ts index a4f2e73..7cc830c 100644 --- a/src/Utils/auth-utils.ts +++ b/src/Utils/auth-utils.ts @@ -1,6 +1,7 @@ import { randomBytes } from 'crypto' import NodeCache from 'node-cache' import type { Logger } from 'pino' +import { v4 as uuidv4 } from 'uuid' import { DEFAULT_CACHE_TTLS } from '../Defaults' import type { AuthenticationCreds, CacheStore, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types' import { Curve, signedKeyPair } from './crypto' @@ -202,6 +203,13 @@ export const initAuthCreds = (): AuthenticationCreds => { accountSyncCounter: 0, accountSettings: { unarchiveChats: false - } + }, + // mobile creds + deviceId: Buffer.from(uuidv4().replace(/-/g, ''), 'hex').toString('base64url'), + phoneId: uuidv4(), + identityId: randomBytes(20), + registered: false, + backupToken: randomBytes(20), + registration: {} as RegistrationOptions } } \ No newline at end of file diff --git a/src/Utils/crypto.ts b/src/Utils/crypto.ts index 7855eb5..46c020f 100644 --- a/src/Utils/crypto.ts +++ b/src/Utils/crypto.ts @@ -107,6 +107,10 @@ export function sha256(buffer: Buffer) { return createHash('sha256').update(buffer).digest() } +export function md5(buffer: Buffer) { + return createHash('md5').update(buffer).digest() +} + // HKDF key expansion export function hkdf(buffer: Uint8Array | Buffer, expandedLength: number, info: { salt?: Buffer, info?: string }) { return HKDF(!Buffer.isBuffer(buffer) ? Buffer.from(buffer) : buffer, expandedLength, info) diff --git a/src/Utils/noise-handler.ts b/src/Utils/noise-handler.ts index bd61ddf..2de65e6 100644 --- a/src/Utils/noise-handler.ts +++ b/src/Utils/noise-handler.ts @@ -1,7 +1,7 @@ import { Boom } from '@hapi/boom' import { Logger } from 'pino' import { proto } from '../../WAProto' -import { NOISE_MODE, NOISE_WA_HEADER, WA_CERT_DETAILS } from '../Defaults' +import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults' import { KeyPair } from '../Types' import { BinaryNode, decodeBinaryNode } from '../WABinary' import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto' @@ -13,10 +13,17 @@ const generateIV = (counter: number) => { return new Uint8Array(iv) } -export const makeNoiseHandler = ( - { public: publicKey, private: privateKey }: KeyPair, +export const makeNoiseHandler = ({ + keyPair: { private: privateKey, public: publicKey }, + NOISE_HEADER, + mobile, + logger, +}: { + keyPair: KeyPair + NOISE_HEADER: Uint8Array + mobile: boolean logger: Logger -) => { +}) => { logger = logger.child({ class: 'ns' }) const authenticate = (data: Uint8Array) => { @@ -86,7 +93,7 @@ export const makeNoiseHandler = ( let inBytes = Buffer.alloc(0) - authenticate(NOISE_WA_HEADER) + authenticate(NOISE_HEADER) authenticate(publicKey) return { @@ -103,12 +110,18 @@ export const makeNoiseHandler = ( mixIntoKey(Curve.sharedKey(privateKey, decStaticContent)) const certDecoded = decrypt(serverHello!.payload!) - const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded) - const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!) + if(mobile) { + const cert = proto.CertChain.NoiseCertificate.decode(certDecoded) + logger.debug(cert) + } else { + const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded) - if(issuerSerial !== WA_CERT_DETAILS.SERIAL) { - throw new Boom('certification match failed', { statusCode: 400 }) + const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!) + + if(issuerSerial !== WA_CERT_DETAILS.SERIAL) { + throw new Boom('certification match failed', { statusCode: 400 }) + } } const keyEnc = encrypt(noiseKey.public) @@ -121,11 +134,11 @@ export const makeNoiseHandler = ( data = encrypt(data) } - const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length + const introSize = sentIntro ? 0 : NOISE_HEADER.length const frame = Buffer.alloc(introSize + 3 + data.byteLength) if(!sentIntro) { - frame.set(NOISE_WA_HEADER) + frame.set(NOISE_HEADER) sentIntro = true } diff --git a/src/Utils/validate-connection.ts b/src/Utils/validate-connection.ts index 444634d..29b6cab 100644 --- a/src/Utils/validate-connection.ts +++ b/src/Utils/validate-connection.ts @@ -8,26 +8,31 @@ import { Curve, hmacSign } from './crypto' import { encodeBigEndian } from './generics' import { createSignalIdentity } from './signal' -type ClientPayloadConfig = Pick +const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => { + const osVersion = config.mobile ? '15.3.1' : '0.1' + const version = config.mobile ? [2, 22, 24] : config.version + const device = config.mobile ? 'iPhone_7' : 'Desktop' + const manufacturer = config.mobile ? 'Apple' : '' + const platform = config.mobile ? proto.ClientPayload.UserAgent.Platform.IOS : proto.ClientPayload.UserAgent.Platform.WEB + const phoneId = config.mobile ? { phoneId: config.auth.creds.phoneId } : {} -const getUserAgent = ({ version }: ClientPayloadConfig): proto.ClientPayload.IUserAgent => { - const osVersion = '0.1' return { appVersion: { primary: version[0], secondary: version[1], tertiary: version[2], }, - platform: proto.ClientPayload.UserAgent.Platform.WEB, + platform, releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE, - mcc: '000', - mnc: '000', + mcc: config.auth.creds.registration?.phoneNumberMobileCountryCode || '000', + mnc: config.auth.creds.registration?.phoneNumberMobileNetworkCode || '000', osVersion: osVersion, - manufacturer: '', - device: 'Desktop', + manufacturer, + device, osBuildNumber: osVersion, localeLanguageIso6391: 'en', localeCountryIso31661Alpha2: 'US', + ...phoneId } } @@ -36,7 +41,7 @@ const PLATFORM_MAP = { 'Windows': proto.ClientPayload.WebInfo.WebSubPlatform.WIN32 } -const getWebInfo = (config: ClientPayloadConfig): proto.ClientPayload.IWebInfo => { +const getWebInfo = (config: SocketConfig): proto.ClientPayload.IWebInfo => { let webSubPlatform = proto.ClientPayload.WebInfo.WebSubPlatform.WEB_BROWSER if(config.syncFullHistory && PLATFORM_MAP[config.browser[0]]) { webSubPlatform = PLATFORM_MAP[config.browser[0]] @@ -45,16 +50,43 @@ const getWebInfo = (config: ClientPayloadConfig): proto.ClientPayload.IWebInfo = return { webSubPlatform } } -const getClientPayload = (config: ClientPayloadConfig): proto.IClientPayload => { - return { +const getClientPayload = (config: SocketConfig) => { + const payload: proto.IClientPayload = { connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN, connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED, userAgent: getUserAgent(config), - webInfo: getWebInfo(config), } + + if(!config.mobile) { + payload.webInfo = getWebInfo(config) + } + + return payload } -export const generateLoginNode = (userJid: string, config: ClientPayloadConfig): proto.IClientPayload => { +export const generateMobileNode = (config: SocketConfig): proto.IClientPayload => { + if(!config.auth.creds) { + throw new Boom('No registration data found', { data: config }) + } + + const payload: proto.IClientPayload = { + ...getClientPayload(config), + sessionId: Math.floor(Math.random() * 999999999 + 1), + shortConnect: true, + connectAttemptCount: 0, + device: 0, + dnsSource: { + appCached: false, + dnsMethod: proto.ClientPayload.DNSSource.DNSResolutionMethod.SYSTEM, + }, + passive: false, // XMPP heartbeat setting (false: server actively pings) (true: client actively pings) + pushName: 'test', + username: Number(`${config.auth.creds.registration.phoneNumberCountryCode}${config.auth.creds.registration.phoneNumberNationalNumber}`), + } + return proto.ClientPayload.fromObject(payload) +} + +export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => { const { user, device } = jidDecode(userJid)! const payload: proto.IClientPayload = { ...getClientPayload(config), @@ -67,7 +99,7 @@ export const generateLoginNode = (userJid: string, config: ClientPayloadConfig): export const generateRegistrationNode = ( { registrationId, signedPreKey, signedIdentityKey }: SignalCreds, - config: ClientPayloadConfig + config: SocketConfig ) => { // the app version needs to be md5 hashed // and passed in diff --git a/src/WABinary/generic-utils.ts b/src/WABinary/generic-utils.ts index dd38620..48b323c 100644 --- a/src/WABinary/generic-utils.ts +++ b/src/WABinary/generic-utils.ts @@ -87,4 +87,35 @@ function bufferToUInt(e: Uint8Array | Buffer, t: number) { } return a +} + +const tabs = (n: number) => '\t'.repeat(n) + +export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i = 0) { + if(!node) { + return node + } + + if(typeof node === 'string') { + return tabs(i) + node + } + + if(node instanceof Uint8Array) { + return tabs(i) + Buffer.from(node).toString('hex') + } + + if(Array.isArray(node)) { + return node.map((x) => tabs(i + 1) + binaryNodeToString(x, i + 1)).join('\n') + } + + const children = binaryNodeToString(node.content, i + 1) + + const tag = `<${node.tag} ${Object.entries(node.attrs || {}) + .filter(([, v]) => v !== undefined) + .map(([k, v]) => `${k}='${v}'`) + .join(' ')}` + + const content: string = children ? `>\n${children}\n${tabs(i)}` : '/>' + + return tag + content } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 43a983d..3bb1ad9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1127,6 +1127,11 @@ resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz" integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== +"@types/uuid@^9.0.0": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6" + integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA== + "@types/ws@^8.0.0": version "8.5.3" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz" @@ -1576,6 +1581,14 @@ buffer@^5.2.0, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" @@ -1736,6 +1749,11 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" +colorette@^2.0.7: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + combined-stream@^1.0.6, combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1838,6 +1856,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dateformat@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" + integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== + debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -2246,6 +2269,11 @@ event-target-shim@^5.0.0: resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + execa@^5.0.0: version "5.1.1" resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" @@ -2286,6 +2314,11 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" +fast-copy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa" + integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -2317,6 +2350,11 @@ fast-redact@^3.0.0: resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.1.tgz" integrity sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fastq@^1.6.0: version "1.15.0" resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz" @@ -2547,6 +2585,17 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + global@~4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz" @@ -2635,6 +2684,14 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +help-me@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563" + integrity sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA== + dependencies: + glob "^8.0.0" + readable-stream "^3.6.0" + html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz" @@ -3372,6 +3429,11 @@ jimp@^0.16.1: "@jimp/types" "^0.16.1" regenerator-runtime "^0.13.3" +joycon@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + jpeg-js@0.4.2: version "0.4.2" resolved "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.2.tgz" @@ -3494,6 +3556,11 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libphonenumber-js@^1.10.20: + version "1.10.28" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.28.tgz#cae7e929cad96cee5ecc9449027192ecba39ee72" + integrity sha512-1eAgjLrZA0+2Wgw4hs+4Q/kEBycxQo8ZLYnmOvZ3AlM8ImAVAJgDPlZtISLEzD1vunc2q8s2Pn7XwB7I8U3Kzw== + "libsignal@git+https://github.com/adiwajshing/libsignal-node": version "2.0.1" resolved "git+ssh://git@github.com/adiwajshing/libsignal-node.git#11dbd962ea108187c79a7c46fe4d6f790e23da97" @@ -3866,6 +3933,11 @@ on-exit-leak-free@^0.2.0: resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz" integrity sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg== +on-exit-leak-free@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" + integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -4033,6 +4105,14 @@ picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pino-abstract-transport@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" + integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + pino-abstract-transport@v0.5.0: version "0.5.0" resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz" @@ -4041,6 +4121,26 @@ pino-abstract-transport@v0.5.0: duplexify "^4.1.2" split2 "^4.0.0" +pino-pretty@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-9.4.0.tgz#fc4026e83c87272cbdfb7afed121770e6000940c" + integrity sha512-NIudkNLxnl7MGj1XkvsqVyRgo6meFP82ECXF2PlOI+9ghmbGuBUUqKJ7IZPIxpJw4vhhSva0IuiDSAuGh6TV9g== + dependencies: + colorette "^2.0.7" + dateformat "^4.6.3" + fast-copy "^3.0.0" + fast-safe-stringify "^2.1.1" + help-me "^4.0.1" + joycon "^3.1.1" + minimist "^1.2.6" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.0.0" + pump "^3.0.0" + readable-stream "^4.0.0" + secure-json-parse "^2.4.0" + sonic-boom "^3.0.0" + strip-json-comments "^3.1.1" + pino-std-serializers@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz" @@ -4285,6 +4385,16 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.3.0.tgz#0914d0c72db03b316c9733bb3461d64a3cc50cba" + integrity sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + readable-web-to-node-stream@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz" @@ -4412,6 +4522,11 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" +secure-json-parse@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" + integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== + semver@7.x, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7: version "7.3.7" resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" @@ -4516,6 +4631,13 @@ sonic-boom@^2.2.1: dependencies: atomic-sleep "^1.0.0" +sonic-boom@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c" + integrity sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g== + dependencies: + atomic-sleep "^1.0.0" + source-map-support@^0.5.6: version "0.5.21" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" @@ -4971,6 +5093,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz"