feat: native-mobile-api

This commit is contained in:
SamuelScheit
2023-04-20 13:01:11 +02:00
parent 28be45a9b4
commit ef673f62ca
17 changed files with 940 additions and 74 deletions

View File

@@ -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<string>((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)

View File

@@ -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",

View File

@@ -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 = /.*@.*/

View File

@@ -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
}

View File

@@ -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) => (

View File

@@ -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))
}
}

251
src/Socket/registration.ts Normal file
View File

@@ -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<string, string>, headers?: Record<string, string> } = {}) {
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
}

View File

@@ -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<void>(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<T>(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<Uint8Array>(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
}

31
src/Socket/web-socket.ts Normal file
View File

@@ -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<string, string>,
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
}
}

View File

@@ -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 = {

View File

@@ -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 */

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -8,26 +8,31 @@ import { Curve, hmacSign } from './crypto'
import { encodeBigEndian } from './generics'
import { createSignalIdentity } from './signal'
type ClientPayloadConfig = Pick<SocketConfig, 'version' | 'browser' | 'syncFullHistory'>
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

View File

@@ -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)}</${node.tag}>` : '/>'
return tag + content
}

127
yarn.lock
View File

@@ -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"