Comparing version 7.5.2 to 7.6.0
@@ -40,3 +40,4 @@ declare module 'tuyapi' { | ||
dps: number; | ||
set: string|number|boolean | ||
set: string|number|boolean; | ||
cid?: string; | ||
multiple?: boolean; | ||
@@ -43,0 +44,0 @@ shouldWaitForResponse?: boolean; |
112
index.js
@@ -150,3 +150,3 @@ // Import packages | ||
const commandByte = this.device.version === '3.4' ? CommandType.DP_QUERY_NEW : CommandType.DP_QUERY; | ||
const commandByte = this.device.version === '3.4' || this.device.version === '3.5' ? CommandType.DP_QUERY_NEW : CommandType.DP_QUERY; | ||
@@ -371,3 +371,3 @@ // Create byte buffer | ||
if (this.device.version === '3.4') { | ||
if (this.device.version === '3.4' || this.device.version === '3.5') { | ||
/* | ||
@@ -404,3 +404,3 @@ { | ||
const commandByte = this.device.version === '3.4' ? CommandType.CONTROL_NEW : CommandType.CONTROL; | ||
const commandByte = this.device.version === '3.4' || this.device.version === '3.5' ? CommandType.CONTROL_NEW : CommandType.CONTROL; | ||
const sequenceN = ++this._currentSequenceN; | ||
@@ -417,13 +417,31 @@ // Encode into packet | ||
return this._setQueue.add(() => pTimeout(new Promise((resolve, reject) => { | ||
// Make sure we only resolve or reject once | ||
let resolvedOrRejected = false; | ||
// Send request and wait for response | ||
try { | ||
if (this.device.version === '3.5') { | ||
this._currentSequenceN++; | ||
} | ||
// Send request | ||
this._send(buffer); | ||
this._send(buffer).catch(error => { | ||
if (options.shouldWaitForResponse && !resolvedOrRejected) { | ||
reject(error); | ||
} | ||
}); | ||
if (options.shouldWaitForResponse) { | ||
this._setResolver = resolve; | ||
this._setResolver = data => { | ||
if (!resolvedOrRejected) { | ||
resolve(data); | ||
} | ||
}; | ||
this._setResolveAllowGet = options.isSetCallToGetData; | ||
} else { | ||
resolvedOrRejected = true; | ||
resolve(); | ||
} | ||
} catch (error) { | ||
resolvedOrRejected = true; | ||
reject(error); | ||
@@ -494,7 +512,12 @@ } | ||
this._pingPongTimeout = setTimeout(() => { | ||
if (this._lastPingAt < now) { | ||
this.disconnect(); | ||
} | ||
}, this._responseTimeout * 1000); | ||
if (this._pingPongTimeout === null) { | ||
// If we do not expect a pong from a former ping, we need to set a timeout | ||
this._pingPongTimeout = setTimeout(() => { | ||
if (this._lastPingAt < now) { | ||
this.disconnect(); | ||
} | ||
}, this._responseTimeout * 1000); | ||
} else { | ||
debug('There was no response to the last ping.'); | ||
} | ||
@@ -679,3 +702,3 @@ // Send ping | ||
if (this.device.version === '3.4') { | ||
if (this.device.version === '3.4' || this.device.version === '3.5') { | ||
// Negotiate session key then emit 'connected' | ||
@@ -692,6 +715,6 @@ // 16 bytes random + 32 bytes hmac | ||
debug('Protocol 3.4: Negotiate Session Key - Send Msg 0x03'); | ||
debug('Protocol 3.4, 3.5: Negotiate Session Key - Send Msg 0x03'); | ||
this.client.write(buffer); | ||
} catch (error) { | ||
debug('Error binding key for protocol 3.4: ' + error); | ||
debug('Error binding key for protocol 3.4, 3.5: ' + error); | ||
} | ||
@@ -712,9 +735,6 @@ | ||
_packetHandler(packet) { | ||
// Response was received, so stop waiting | ||
clearTimeout(this._sendTimeout); | ||
// Protocol 3.4 - Response to Msg 0x03 | ||
// Protocol 3.4, 3.5 - Response to Msg 0x03 | ||
if (packet.commandByte === CommandType.SESS_KEY_NEG_RES) { | ||
if (!this.connectPromise) { | ||
debug('Protocol 3.4: Ignore Key exchange message because no connection in progress.'); | ||
debug('Protocol 3.4, 3.5: Ignore Key exchange message because no connection in progress.'); | ||
return; | ||
@@ -725,5 +745,9 @@ } | ||
this._tmpRemoteKey = packet.payload.subarray(0, 16); | ||
debug('Protocol 3.4: Local Random Key: ' + this._tmpLocalKey.toString('hex')); | ||
debug('Protocol 3.4: Remote Random Key: ' + this._tmpRemoteKey.toString('hex')); | ||
debug('Protocol 3.4, 3.5: Local Random Key: ' + this._tmpLocalKey.toString('hex')); | ||
debug('Protocol 3.4, 3.5: Remote Random Key: ' + this._tmpRemoteKey.toString('hex')); | ||
if (this.device.version === '3.4' || this.device.version === '3.5') { | ||
this._currentSequenceN = packet.sequenceN - 1; | ||
} | ||
const calcLocalHmac = this.device.parser.cipher.hmac(this._tmpLocalKey).toString('hex'); | ||
@@ -758,6 +782,11 @@ const expLocalHmac = packet.payload.slice(16, 16 + 32).toString('hex'); | ||
this.sessionKey = this.device.parser.cipher._encrypt34({data: this.sessionKey}); | ||
debug('Protocol 3.4: Session Key: ' + this.sessionKey.toString('hex')); | ||
debug('Protocol 3.4: Initialization done'); | ||
if (this.device.version === '3.4') { | ||
this.sessionKey = this.device.parser.cipher._encrypt34({data: this.sessionKey}); | ||
} else if (this.device.version === '3.5') { | ||
this.sessionKey = this.device.parser.cipher._encrypt35({data: this.sessionKey, iv: this._tmpLocalKey}); | ||
} | ||
debug('Protocol 3.4, 3.5: Session Key: ' + this.sessionKey.toString('hex')); | ||
debug('Protocol 3.4, 3.5: Initialization done'); | ||
this.device.parser.cipher.setSessionKey(this.sessionKey); | ||
@@ -777,2 +806,4 @@ this.device.key = this.sessionKey; | ||
clearTimeout(this._pingPongTimeout); | ||
this._pingPongTimeout = null; | ||
this._lastPingAt = new Date(); | ||
@@ -804,22 +835,22 @@ | ||
this._expectRefreshResponseForSequenceN = undefined; | ||
} else { | ||
} else if (packet.sequenceN in this._resolvers) { | ||
// Call data resolver for sequence number | ||
if (packet.sequenceN in this._resolvers) { | ||
debug('Received DP_REFRESH response packet - resolve'); | ||
this._resolvers[packet.sequenceN](packet.payload); | ||
// Remove resolver | ||
delete this._resolvers[packet.sequenceN]; | ||
this._expectRefreshResponseForSequenceN = undefined; | ||
} else if (this._expectRefreshResponseForSequenceN && this._expectRefreshResponseForSequenceN in this._resolvers) { | ||
debug('Received DP_REFRESH response packet without data - resolve'); | ||
this._resolvers[this._expectRefreshResponseForSequenceN](packet.payload); | ||
debug('Received DP_REFRESH response packet - resolve'); | ||
this._resolvers[packet.sequenceN](packet.payload); | ||
// Remove resolver | ||
delete this._resolvers[this._expectRefreshResponseForSequenceN]; | ||
this._expectRefreshResponseForSequenceN = undefined; | ||
} else { | ||
debug('Received DP_REFRESH response packet - no resolver found for sequence number' + packet.sequenceN); | ||
} | ||
// Remove resolver | ||
delete this._resolvers[packet.sequenceN]; | ||
this._expectRefreshResponseForSequenceN = undefined; | ||
} else if (this._expectRefreshResponseForSequenceN && this._expectRefreshResponseForSequenceN in this._resolvers) { | ||
debug('Received DP_REFRESH response packet without data - resolve'); | ||
this._resolvers[this._expectRefreshResponseForSequenceN](packet.payload); | ||
// Remove resolver | ||
delete this._resolvers[this._expectRefreshResponseForSequenceN]; | ||
this._expectRefreshResponseForSequenceN = undefined; | ||
} else { | ||
debug('Received DP_REFRESH response packet - no resolver found for sequence number' + packet.sequenceN); | ||
} | ||
return; | ||
@@ -911,5 +942,2 @@ } | ||
// Clear timeouts | ||
clearTimeout(this._sendTimeout); | ||
clearTimeout(this._connectTimeout); | ||
clearTimeout(this._responseTimeout); | ||
clearInterval(this._pingPongInterval); | ||
@@ -916,0 +944,0 @@ clearTimeout(this._pingPongTimeout); |
@@ -19,3 +19,3 @@ const crypto = require('crypto'); | ||
/** | ||
* Sets the session key used for Protocol 3.4 | ||
* Sets the session key used for Protocol 3.4, 3.5 | ||
* @param {Buffer} sessionKey Session key | ||
@@ -41,2 +41,6 @@ */ | ||
if (this.version === '3.5') { | ||
return this._encrypt35(options); | ||
} | ||
return this._encryptPre34(options); | ||
@@ -88,2 +92,27 @@ } | ||
/** | ||
* Encrypt data for protocol 3.5 | ||
* @param {Object} options Options for encryption | ||
* @param {String} options.data data to encrypt | ||
* @param {Boolean} [options.base64=true] `true` to return result in Base64 | ||
* @returns {Buffer|String} returns Buffer unless options.base64 is true | ||
*/ | ||
_encrypt35(options) { | ||
let encrypted; | ||
let localIV = Buffer.from((Date.now() * 10).toString().slice(0, 12)); | ||
if (options.iv !== undefined) { | ||
localIV = options.iv.slice(0, 12); | ||
} | ||
const cipher = crypto.createCipheriv('aes-128-gcm', this.getKey(), localIV); | ||
if (options.aad === undefined) { | ||
encrypted = Buffer.concat([cipher.update(options.data), cipher.final()]); | ||
} else { | ||
cipher.setAAD(options.aad); | ||
encrypted = Buffer.concat([localIV, cipher.update(options.data), cipher.final(), cipher.getAuthTag(), Buffer.from([0x00, 0x00, 0x99, 0x66])]); | ||
} | ||
return encrypted; | ||
} | ||
/** | ||
* Decrypts data. | ||
@@ -99,2 +128,6 @@ * @param {String|Buffer} data to decrypt | ||
if (this.version === '3.5') { | ||
return this._decrypt35(data); | ||
} | ||
return this._decryptPre34(data); | ||
@@ -188,2 +221,47 @@ } | ||
/** | ||
* Decrypts data for protocol 3.5 | ||
* @param {String|Buffer} data to decrypt | ||
* @returns {Object|String} | ||
* returns object if data is JSON, else returns string | ||
*/ | ||
_decrypt35(data) { | ||
let result; | ||
const header = data.slice(0, 14); | ||
const iv = data.slice(14, 26); | ||
const tag = data.slice(data.length - 16); | ||
data = data.slice(26, data.length - 16); | ||
try { | ||
const decipher = crypto.createDecipheriv('aes-128-gcm', this.getKey(), iv); | ||
decipher.setAuthTag(tag); | ||
decipher.setAAD(header); | ||
result = Buffer.concat([decipher.update(data), decipher.final()]); | ||
result = result.slice(4); // Remove 32bit return code | ||
} catch (_) { | ||
throw new Error('Decrypt failed'); | ||
} | ||
// Try to parse data as JSON, otherwise return as string. | ||
// 3.5 protocol | ||
// {"protocol":4,"t":1632405905,"data":{"dps":{"101":true},"cid":"00123456789abcde"}} | ||
try { | ||
if (result.indexOf(this.version) === 0) { | ||
result = result.slice(15); | ||
} | ||
const res = JSON.parse(result); | ||
if ('data' in res) { | ||
const resData = res.data; | ||
resData.t = res.t; | ||
return resData; // Or res.data // for compatibility with tuya-mqtt | ||
} | ||
return res; | ||
} catch (_) { | ||
return result; | ||
} | ||
} | ||
/** | ||
* Calculates a MD5 hash. | ||
@@ -200,3 +278,3 @@ * @param {String} data to hash | ||
* Gets the key used for encryption/decryption | ||
* @returns {String} sessionKey (if set for protocol 3.4) or key | ||
* @returns {String} sessionKey (if set for protocol 3.4, 3.5) or key | ||
*/ | ||
@@ -208,3 +286,3 @@ getKey() { | ||
/** | ||
* Returns the HMAC for the current key (sessionKey if set for protocol 3.4 or key) | ||
* Returns the HMAC for the current key (sessionKey if set for protocol 3.4, 3.5 or key) | ||
* @param {Buffer} data data to hash | ||
@@ -211,0 +289,0 @@ * @returns {Buffer} HMAC |
@@ -5,2 +5,3 @@ const Cipher = require('./cipher'); | ||
const HEADER_SIZE = 16; | ||
const HEADER_SIZE_3_5 = 4; | ||
@@ -113,3 +114,4 @@ /** | ||
if (prefix !== 0x000055AA) { | ||
// Only for 3.4 and 3.5 packets | ||
if (prefix !== 0x000055AA && prefix !== 0x00006699) { | ||
throw new TypeError(`Prefix does not match: ${buffer.toString('hex')}`); | ||
@@ -121,3 +123,6 @@ } | ||
const suffixLocation = buffer.indexOf('0000AA55', 0, 'hex'); | ||
let suffixLocation = buffer.indexOf('0000AA55', 0, 'hex'); | ||
if (suffixLocation === -1) {// Couldn't find 0000AA55 during parse | ||
suffixLocation = buffer.indexOf('00009966', 0, 'hex'); | ||
} | ||
@@ -132,18 +137,38 @@ if (suffixLocation !== buffer.length - 4) { | ||
if (suffix !== 0x0000AA55) { | ||
if (suffix !== 0x0000AA55 && suffix !== 0x00009966) { | ||
throw new TypeError(`Suffix does not match: ${buffer.toString('hex')}`); | ||
} | ||
// Get sequence number | ||
const sequenceN = buffer.readUInt32BE(4); | ||
let sequenceN; | ||
let commandByte; | ||
let payloadSize; | ||
// Get command byte | ||
const commandByte = buffer.readUInt32BE(8); | ||
if (suffix === 0x0000AA55) { | ||
// Get sequence number | ||
sequenceN = buffer.readUInt32BE(4); | ||
// Get payload size | ||
const payloadSize = buffer.readUInt32BE(12); | ||
// Get command byte | ||
commandByte = buffer.readUInt32BE(8); | ||
// Check for payload | ||
if (buffer.length - 8 < payloadSize) { | ||
throw new TypeError(`Packet missing payload: payload has length ${payloadSize}.`); | ||
// Get payload size | ||
payloadSize = buffer.readUInt32BE(12); | ||
// Check for payload | ||
if (buffer.length - 8 < payloadSize) { | ||
throw new TypeError(`Packet missing payload: payload has length ${payloadSize}.`); | ||
} | ||
} else if (suffix === 0x00009966) { | ||
// Get sequence number | ||
sequenceN = buffer.readUInt32BE(6); | ||
// Get command byte | ||
commandByte = buffer.readUInt32BE(10); | ||
// Get payload size | ||
payloadSize = buffer.readUInt32BE(14) + 14; // Add additional bytes for extras | ||
// Check for payload | ||
if (buffer.length - 8 < payloadSize) { | ||
throw new TypeError(`Packet missing payload: payload has length ${payloadSize}.`); | ||
} | ||
} | ||
@@ -165,28 +190,38 @@ | ||
let payload; | ||
if (returnCode & 0xFFFFFF00) { | ||
if (this.version === '3.4' && !packageFromDiscovery) { | ||
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 0x24); | ||
if (this.version === '3.5') { | ||
payload = buffer.slice(HEADER_SIZE_3_5, HEADER_SIZE_3_5 + payloadSize); | ||
sequenceN = buffer.slice(6, 10).readUInt32BE(); | ||
commandByte = buffer.slice(10, 14).readUInt32BE(); | ||
} else { | ||
if (returnCode & 0xFFFFFF00) { | ||
if (this.version === '3.4' && !packageFromDiscovery) { | ||
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 0x24); | ||
} else if (this.version === '3.5' && !packageFromDiscovery) { | ||
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 0x24); | ||
} else { | ||
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 8); | ||
} | ||
} else if (this.version === '3.4' && !packageFromDiscovery) { | ||
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 0x24); | ||
} else if (this.version === '3.5' && !packageFromDiscovery) { | ||
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 0x24); | ||
} else { | ||
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 8); | ||
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 8); | ||
} | ||
} else if (this.version === '3.4' && !packageFromDiscovery) { | ||
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 0x24); | ||
} else { | ||
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 8); | ||
} | ||
// Check CRC | ||
if (this.version === '3.4' && !packageFromDiscovery) { | ||
const expectedCrc = buffer.slice(HEADER_SIZE + payloadSize - 0x24, buffer.length - 4).toString('hex'); | ||
const computedCrc = this.cipher.hmac(buffer.slice(0, HEADER_SIZE + payloadSize - 0x24)).toString('hex'); | ||
// Check CRC | ||
if (this.version === '3.4' && !packageFromDiscovery) { | ||
const expectedCrc = buffer.slice(HEADER_SIZE + payloadSize - 0x24, buffer.length - 4).toString('hex'); | ||
const computedCrc = this.cipher.hmac(buffer.slice(0, HEADER_SIZE + payloadSize - 0x24)).toString('hex'); | ||
if (expectedCrc !== computedCrc) { | ||
throw new Error(`HMAC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`); | ||
} | ||
} else { | ||
const expectedCrc = buffer.readInt32BE(HEADER_SIZE + payloadSize - 8); | ||
const computedCrc = crc(buffer.slice(0, payloadSize + 8)); | ||
if (expectedCrc !== computedCrc) { | ||
throw new Error(`HMAC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`); | ||
} | ||
} else if (this.version !== '3.5') { | ||
const expectedCrc = buffer.readInt32BE(HEADER_SIZE + payloadSize - 8); | ||
const computedCrc = crc(buffer.slice(0, payloadSize + 8)); | ||
if (expectedCrc !== computedCrc) { | ||
throw new Error(`CRC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`); | ||
if (expectedCrc !== computedCrc) { | ||
throw new Error(`CRC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`); | ||
} | ||
} | ||
@@ -221,2 +256,9 @@ } | ||
// Incoming 3.5 data isn't 0 because of iv and tag so check size after | ||
if (this.version === '3.5') { | ||
if (data.length === 0) { | ||
return false; | ||
} | ||
} | ||
// Try to parse data as JSON. | ||
@@ -297,2 +339,6 @@ // If error, return as string. | ||
if (this.version === '3.5') { | ||
return this._encode35(options); | ||
} | ||
return this._encodePre34(options); | ||
@@ -431,4 +477,51 @@ } | ||
} | ||
/** | ||
* Encodes a payload into a Tuya-protocol-complient packet for protocol version 3.5 | ||
* @param {Object} options Options for encoding | ||
* @param {Buffer|String|Object} options.data data to encode | ||
* @param {Boolean} options.encrypted whether or not to encrypt the data | ||
* @param {Number} options.commandByte | ||
* command byte of packet (use CommandType definitions) | ||
* @param {Number} [options.sequenceN] optional, sequence number | ||
* @returns {Buffer} Encoded Buffer | ||
*/ | ||
_encode35(options) { | ||
let payload = options.data; | ||
if (options.commandByte !== CommandType.DP_QUERY && | ||
options.commandByte !== CommandType.HEART_BEAT && | ||
options.commandByte !== CommandType.DP_QUERY_NEW && | ||
options.commandByte !== CommandType.SESS_KEY_NEG_START && | ||
options.commandByte !== CommandType.SESS_KEY_NEG_FINISH && | ||
options.commandByte !== CommandType.DP_REFRESH) { | ||
// Add 3.5 header | ||
const buffer = Buffer.alloc(payload.length + 15); | ||
Buffer.from('3.5').copy(buffer, 0); | ||
payload.copy(buffer, 15); | ||
payload = buffer; | ||
// OO options.data = '3.5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + options.data; | ||
} | ||
// Allocate buffer for prefix, unknown, sequence, command, length | ||
let buffer = Buffer.alloc(18); | ||
// Add prefix, command, and length | ||
buffer.writeUInt32BE(0x00006699, 0); // Prefix | ||
buffer.writeUInt16BE(0x0, 4); // Unknown | ||
buffer.writeUInt32BE(options.sequenceN, 6); // Sequence | ||
buffer.writeUInt32BE(options.commandByte, 10); // Command | ||
buffer.writeUInt32BE(payload.length + 28 /* 0x1c */, 14); // Length | ||
const encrypted = this.cipher.encrypt({ | ||
data: payload, | ||
aad: buffer.slice(4, 18) | ||
}); | ||
buffer = Buffer.concat([buffer, encrypted]); | ||
return buffer; | ||
} | ||
} | ||
module.exports = {MessageParser, CommandType}; |
{ | ||
"name": "tuyapi", | ||
"version": "7.5.2", | ||
"version": "7.6.0", | ||
"description": "An easy-to-use API for devices that use Tuya's cloud services", | ||
@@ -41,3 +41,3 @@ "main": "index.js", | ||
"dependencies": { | ||
"debug": "^4.3.4", | ||
"debug": "^4.3.7", | ||
"p-queue": "6.6.2", | ||
@@ -53,3 +53,3 @@ "p-retry": "4.6.2", | ||
"delay": "4.4.1", | ||
"documentation": "^12.3.0", | ||
"documentation": "^14.0.0", | ||
"nyc": "15.1.0", | ||
@@ -56,0 +56,0 @@ "xo": "0.25.4" |
@@ -199,3 +199,3 @@ # TuyAPI 🌧 🔌 | ||
### Ports | ||
- [python-tuya](https://github.com/clach04/python-tuya) a Python port by [clach04](https://github.com/clach04) | ||
- [TinyTuya](https://github.com/jasonacox/tinytuya) a Python port by [jasonacox](https://github.com/jasonacox) and [uzlonewolf](https://github.com/uzlonewolf) | ||
- [aiotuya](https://github.com/frawau/aiotuya) a Python port by [frawau](https://github.com/frawau) | ||
@@ -206,2 +206,3 @@ - [m4rcus.TuyaCore](https://github.com/Marcus-L/m4rcus.TuyaCore) a .NET port by [Marcus-L](https://github.com/Marcus-L) | ||
- [rust-tuyapi](https://github.com/EmilSodergren/rust-tuyapi) a Rust port by [EmilSodergren](https://github.com/EmilSodergren) | ||
- [GoTuya](https://github.com/Binozo/GoTuya) a Go port by [Binozo](https://github.com/Binozo) | ||
@@ -223,2 +224,4 @@ ### Clients for Tuya's Cloud | ||
- [smart-home-panel](https://github.com/MadeleineSmith/smart-home-panel-fe) A website for controlling a smart light bulb | ||
- [GoTuya](https://github.com/Binozo/GoTuya) An easy-to-use api to control Tuya devices on the local network | ||
- [luminea2mqtt](https://github.com/dennis9819/luminea2mqtt/tree/master) An expandable luminea2mqtt bridge with HA Autodiscover | ||
@@ -225,0 +228,0 @@ |
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
79408
1829
230
3
Updateddebug@^4.3.7