tests(core): added e2e and fuzzing tests
This commit is contained in:
parent
0a45f5e71a
commit
77022e29c9
21 changed files with 625 additions and 80 deletions
|
@ -43,7 +43,8 @@
|
||||||
"typescript": "^4.1.3",
|
"typescript": "^4.1.3",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"glob": "^7.1.7",
|
"glob": "^7.1.7",
|
||||||
"rimraf": "^3.0.2"
|
"rimraf": "^3.0.2",
|
||||||
|
"dotenv-flow": "^3.2.0"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
61
packages/client/tests/buffer-utils.spec.ts
Normal file
61
packages/client/tests/buffer-utils.spec.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { describe, it } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { isProbablyPlainText } from '../src/utils/file-utils'
|
||||||
|
|
||||||
|
describe('isProbablyPlainText', () => {
|
||||||
|
it('should return true for buffers only containing printable ascii', () => {
|
||||||
|
expect(
|
||||||
|
isProbablyPlainText(Buffer.from('hello this is some ascii text'))
|
||||||
|
).to.be.true
|
||||||
|
expect(
|
||||||
|
isProbablyPlainText(
|
||||||
|
Buffer.from(
|
||||||
|
'hello this is some ascii text\nwith unix new lines'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).to.be.true
|
||||||
|
expect(
|
||||||
|
isProbablyPlainText(
|
||||||
|
Buffer.from(
|
||||||
|
'hello this is some ascii text\r\nwith windows new lines'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).to.be.true
|
||||||
|
expect(
|
||||||
|
isProbablyPlainText(
|
||||||
|
Buffer.from(
|
||||||
|
'hello this is some ascii text\n\twith unix new lines and tabs'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).to.be.true
|
||||||
|
expect(
|
||||||
|
isProbablyPlainText(
|
||||||
|
Buffer.from(
|
||||||
|
'hello this is some ascii text\r\n\twith windows new lines and tabs'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for buffers containing some binary data', () => {
|
||||||
|
expect(isProbablyPlainText(Buffer.from('hello this is cedilla: ç'))).to
|
||||||
|
.be.false
|
||||||
|
expect(
|
||||||
|
isProbablyPlainText(
|
||||||
|
Buffer.from('hello this is some ascii text with emojis 🌸')
|
||||||
|
)
|
||||||
|
).to.be.false
|
||||||
|
|
||||||
|
// random strings of 16 bytes
|
||||||
|
expect(
|
||||||
|
isProbablyPlainText(
|
||||||
|
Buffer.from('717f80f08eb9d88c3931712c0e2be32f', 'hex')
|
||||||
|
)
|
||||||
|
).to.be.false
|
||||||
|
expect(
|
||||||
|
isProbablyPlainText(
|
||||||
|
Buffer.from('20e8e218e54254c813b261432b0330d7', 'hex')
|
||||||
|
)
|
||||||
|
).to.be.false
|
||||||
|
})
|
||||||
|
})
|
16
packages/core/.env
Normal file
16
packages/core/.env
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# These env variables are only used in e2e tests
|
||||||
|
|
||||||
|
# API ID and Hash (obviously)
|
||||||
|
API_ID=
|
||||||
|
API_HASH=
|
||||||
|
|
||||||
|
# User (test DC 2) username and session string
|
||||||
|
USER_USERNAME=
|
||||||
|
USER_SESSION=
|
||||||
|
# User (test DC != 2) username and session string
|
||||||
|
USER_OTHER_DC_USERNAME=
|
||||||
|
USER_OTHER_DC_SESSION=
|
||||||
|
|
||||||
|
# Bot token (from test DC!)
|
||||||
|
BOT_USERNAME=
|
||||||
|
BOT_TOKEN=
|
1
packages/core/.gitignore
vendored
Normal file
1
packages/core/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.env.local
|
61
packages/core/scripts/get-user-token-for-e2e.ts
Normal file
61
packages/core/scripts/get-user-token-for-e2e.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { BaseTelegramClient, defaultDcs } from '../src'
|
||||||
|
|
||||||
|
require('dotenv-flow').config({ path: __dirname + '/../' })
|
||||||
|
require('debug').enable('mtcute:*')
|
||||||
|
|
||||||
|
if (!process.env.API_ID || !process.env.API_HASH) {
|
||||||
|
console.warn('Set API_ID and API_HASH env variables')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dcId = process.argv[2] ?? '2'
|
||||||
|
|
||||||
|
const tg = new BaseTelegramClient({
|
||||||
|
apiId: process.env.API_ID,
|
||||||
|
apiHash: process.env.API_HASH,
|
||||||
|
primaryDc: defaultDcs.defaultTestDc,
|
||||||
|
})
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
await tg.connect()
|
||||||
|
|
||||||
|
let numbers = Math.floor(Math.random() * 9999).toString()
|
||||||
|
while (numbers.length !== 4) numbers += '0'
|
||||||
|
|
||||||
|
const phone = `99966${dcId}${numbers}`
|
||||||
|
const code = `${dcId}${dcId}${dcId}${dcId}${dcId}${dcId}`
|
||||||
|
|
||||||
|
const res = await tg.call({
|
||||||
|
_: 'auth.sendCode',
|
||||||
|
phoneNumber: phone,
|
||||||
|
apiId: tg['_initConnectionParams'].apiId,
|
||||||
|
apiHash: tg['_apiHash'],
|
||||||
|
settings: { _: 'codeSettings' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const res1 = await tg.call({
|
||||||
|
_: 'auth.signIn',
|
||||||
|
phoneNumber: phone,
|
||||||
|
phoneCodeHash: res.phoneCodeHash,
|
||||||
|
phoneCode: code,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res1._ === 'auth.authorizationSignUpRequired') {
|
||||||
|
await tg.call({
|
||||||
|
_: 'auth.signUp',
|
||||||
|
phoneNumber: phone,
|
||||||
|
phoneCodeHash: res.phoneCodeHash,
|
||||||
|
firstName: 'MTCute E2E',
|
||||||
|
lastName: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = `mtcute_e2e_${numbers}`
|
||||||
|
|
||||||
|
await tg.call({
|
||||||
|
_: 'account.updateUsername',
|
||||||
|
username,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('User %s, DC %d: %s', username, dcId, await tg.exportSession())
|
||||||
|
})().catch(console.error)
|
|
@ -185,15 +185,15 @@ export class TelegramConnection extends PersistentConnection {
|
||||||
protected onConnectionUsable(): void {
|
protected onConnectionUsable(): void {
|
||||||
super.onConnectionUsable()
|
super.onConnectionUsable()
|
||||||
|
|
||||||
|
Object.entries(this._pendingRpcCalls).forEach(([id, it]) =>
|
||||||
|
this._resend(it, id)
|
||||||
|
)
|
||||||
|
|
||||||
const sendOnceUsable = this._sendOnceUsable
|
const sendOnceUsable = this._sendOnceUsable
|
||||||
// this way in case connection is still invalid (somehow??) messages aren't lost
|
// this way in case connection is still invalid (somehow??) messages aren't lost
|
||||||
this._sendOnceUsable = []
|
this._sendOnceUsable = []
|
||||||
sendOnceUsable.forEach((it) => this._resend(it))
|
sendOnceUsable.forEach((it) => this._resend(it))
|
||||||
|
|
||||||
Object.entries(this._pendingRpcCalls).forEach(([id, it]) =>
|
|
||||||
this._resend(it, id)
|
|
||||||
)
|
|
||||||
|
|
||||||
this._pingInterval = setInterval(() => {
|
this._pingInterval = setInterval(() => {
|
||||||
if (this._pendingPing === null) {
|
if (this._pendingPing === null) {
|
||||||
this._pendingPing = ulongToLong(
|
this._pendingPing = ulongToLong(
|
||||||
|
|
|
@ -64,7 +64,7 @@ export type TransportFactory = () => ICuteTransport
|
||||||
* When receiving a packet, its content is sent to feed(),
|
* When receiving a packet, its content is sent to feed(),
|
||||||
* and codec is supposed to emit `packet` or `error` event when packet is parsed.
|
* and codec is supposed to emit `packet` or `error` event when packet is parsed.
|
||||||
*/
|
*/
|
||||||
export interface PacketCodec {
|
export interface IPacketCodec {
|
||||||
/** Initial tag of the codec. Will be sent immediately once connected. */
|
/** Initial tag of the codec. Will be sent immediately once connected. */
|
||||||
tag(): MaybeAsync<Buffer>
|
tag(): MaybeAsync<Buffer>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PacketCodec, TransportError } from './abstract'
|
import { IPacketCodec, TransportError } from './abstract'
|
||||||
import { StreamedCodec } from './streamed'
|
import { StreamedCodec } from './streamed'
|
||||||
import { randomBytes } from '../../utils/buffer-utils'
|
import { randomBytes } from '../../utils/buffer-utils'
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ const PADDED_TAG = Buffer.from([0xdd, 0xdd, 0xdd, 0xdd])
|
||||||
*/
|
*/
|
||||||
export class IntermediatePacketCodec
|
export class IntermediatePacketCodec
|
||||||
extends StreamedCodec
|
extends StreamedCodec
|
||||||
implements PacketCodec {
|
implements IPacketCodec {
|
||||||
tag(): Buffer {
|
tag(): Buffer {
|
||||||
return TAG
|
return TAG
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PacketCodec } from './abstract'
|
import { IPacketCodec } from './abstract'
|
||||||
import { IEncryptionScheme } from '../../utils/crypto'
|
import { IEncryptionScheme } from '../../utils/crypto'
|
||||||
import { buffersEqual, randomBytes } from '../../utils/buffer-utils'
|
import { buffersEqual, randomBytes } from '../../utils/buffer-utils'
|
||||||
import { WrappedCodec } from './wrapped'
|
import { WrappedCodec } from './wrapped'
|
||||||
|
@ -19,13 +19,13 @@ interface MtProxyInfo {
|
||||||
media: boolean
|
media: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ObfuscatedPacketCodec extends WrappedCodec implements PacketCodec {
|
export class ObfuscatedPacketCodec extends WrappedCodec implements IPacketCodec {
|
||||||
private _encryptor?: IEncryptionScheme
|
private _encryptor?: IEncryptionScheme
|
||||||
private _decryptor?: IEncryptionScheme
|
private _decryptor?: IEncryptionScheme
|
||||||
|
|
||||||
private _proxy?: MtProxyInfo
|
private _proxy?: MtProxyInfo
|
||||||
|
|
||||||
constructor(inner: PacketCodec, proxy?: MtProxyInfo) {
|
constructor(inner: IPacketCodec, proxy?: MtProxyInfo) {
|
||||||
super(inner)
|
super(inner)
|
||||||
this._proxy = proxy
|
this._proxy = proxy
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ICuteTransport, PacketCodec, TransportState } from './abstract'
|
import { ICuteTransport, IPacketCodec, TransportState } from './abstract'
|
||||||
import { tl } from '@mtcute/tl'
|
import { tl } from '@mtcute/tl'
|
||||||
import { Socket, connect } from 'net'
|
import { Socket, connect } from 'net'
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
|
@ -18,7 +18,7 @@ export abstract class TcpTransport
|
||||||
protected _state: TransportState = TransportState.Idle
|
protected _state: TransportState = TransportState.Idle
|
||||||
protected _socket: Socket | null = null
|
protected _socket: Socket | null = null
|
||||||
|
|
||||||
abstract _packetCodec: PacketCodec
|
abstract _packetCodec: IPacketCodec
|
||||||
protected _crypto: ICryptoProvider
|
protected _crypto: ICryptoProvider
|
||||||
|
|
||||||
packetCodecInitialized = false
|
packetCodecInitialized = false
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ICuteTransport, PacketCodec, TransportState } from './abstract'
|
import { ICuteTransport, IPacketCodec, TransportState } from './abstract'
|
||||||
import { tl } from '@mtcute/tl'
|
import { tl } from '@mtcute/tl'
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
import { typedArrayToBuffer } from '../../utils/buffer-utils'
|
import { typedArrayToBuffer } from '../../utils/buffer-utils'
|
||||||
|
@ -42,7 +42,7 @@ export abstract class WebSocketTransport
|
||||||
private _socket: WebSocket | null = null
|
private _socket: WebSocket | null = null
|
||||||
private _crypto: ICryptoProvider
|
private _crypto: ICryptoProvider
|
||||||
|
|
||||||
abstract _packetCodec: PacketCodec
|
abstract _packetCodec: IPacketCodec
|
||||||
packetCodecInitialized = false
|
packetCodecInitialized = false
|
||||||
|
|
||||||
private _baseDomain: string
|
private _baseDomain: string
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
import { PacketCodec } from './abstract'
|
import { IPacketCodec } from './abstract'
|
||||||
import { ICryptoProvider } from '../../utils/crypto'
|
import { ICryptoProvider } from '../../utils/crypto'
|
||||||
|
|
||||||
export abstract class WrappedCodec extends EventEmitter {
|
export abstract class WrappedCodec extends EventEmitter {
|
||||||
protected _crypto: ICryptoProvider
|
protected _crypto: ICryptoProvider
|
||||||
protected _inner: PacketCodec
|
protected _inner: IPacketCodec
|
||||||
|
|
||||||
constructor(inner: PacketCodec) {
|
constructor(inner: IPacketCodec) {
|
||||||
super()
|
super()
|
||||||
this._inner = inner
|
this._inner = inner
|
||||||
this._inner.on('error', (err) => this.emit('error', err))
|
this._inner.on('error', (err) => this.emit('error', err))
|
||||||
|
|
62
packages/core/tests/e2e/idle-connection.spec.ts
Normal file
62
packages/core/tests/e2e/idle-connection.spec.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { describe, it } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { BaseTelegramClient, defaultDcs, TransportState } from '../../src'
|
||||||
|
import { sleep } from '../../src/utils/misc-utils'
|
||||||
|
|
||||||
|
require('dotenv-flow').config()
|
||||||
|
|
||||||
|
describe('e2e : idle connection', function () {
|
||||||
|
if (!process.env.API_ID || !process.env.API_HASH) {
|
||||||
|
console.warn('Warning: skipping e2e idle connection test (no API_ID or API_HASH)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
// 75s is to make sure ping is sent
|
||||||
|
|
||||||
|
it('75s idle to test dc', async () => {
|
||||||
|
const client = new BaseTelegramClient({
|
||||||
|
apiId: process.env.API_ID!,
|
||||||
|
apiHash: process.env.API_HASH!,
|
||||||
|
primaryDc: defaultDcs.defaultTestDc
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
await sleep(75000)
|
||||||
|
|
||||||
|
expect(client.primaryConnection['_transport'].state()).eq(TransportState.Ready)
|
||||||
|
|
||||||
|
const config = await client.call({ _: 'help.getConfig' })
|
||||||
|
expect(config._).eql('config')
|
||||||
|
|
||||||
|
await client.close()
|
||||||
|
expect(client.primaryConnection['_transport'].state()).eq(TransportState.Idle)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!process.env.USER_SESSION) {
|
||||||
|
console.warn('Warning: skipping e2e idle connection test with auth (no USER_SESSION)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
it('75s idle to test dc with auth', async () => {
|
||||||
|
const client = new BaseTelegramClient({
|
||||||
|
apiId: process.env.API_ID!,
|
||||||
|
apiHash: process.env.API_HASH!,
|
||||||
|
primaryDc: defaultDcs.defaultTestDc
|
||||||
|
})
|
||||||
|
|
||||||
|
client.importSession(process.env.USER_SESSION!)
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
await sleep(75000)
|
||||||
|
|
||||||
|
expect(client.primaryConnection['_transport'].state()).eq(TransportState.Ready)
|
||||||
|
|
||||||
|
const config = await client.call({ _: 'help.getConfig' })
|
||||||
|
expect(config._).eql('config')
|
||||||
|
|
||||||
|
await client.close()
|
||||||
|
expect(client.primaryConnection['_transport'].state()).eq(TransportState.Idle)
|
||||||
|
})
|
||||||
|
})
|
173
packages/core/tests/e2e/receiving-updates.spec.ts
Normal file
173
packages/core/tests/e2e/receiving-updates.spec.ts
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
import { describe, it } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { BaseTelegramClient, bufferToBigInt, defaultDcs, randomBytes, tl } from '../../src'
|
||||||
|
import { sleep } from '../../src/utils/misc-utils'
|
||||||
|
|
||||||
|
require('dotenv-flow').config()
|
||||||
|
|
||||||
|
async function createClientPair(
|
||||||
|
session1: string,
|
||||||
|
session2: string
|
||||||
|
): Promise<[BaseTelegramClient, BaseTelegramClient]> {
|
||||||
|
const client1 = new BaseTelegramClient({
|
||||||
|
apiId: process.env.API_ID!,
|
||||||
|
apiHash: process.env.API_HASH!,
|
||||||
|
primaryDc: defaultDcs.defaultTestDc,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (session1.indexOf(':') > -1) {
|
||||||
|
// bot token
|
||||||
|
await client1.call({
|
||||||
|
_: 'auth.importBotAuthorization',
|
||||||
|
apiId: parseInt(process.env.API_ID!),
|
||||||
|
apiHash: process.env.API_HASH!,
|
||||||
|
botAuthToken: session1,
|
||||||
|
flags: 0
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
client1.importSession(session1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const client2 = new BaseTelegramClient({
|
||||||
|
apiId: process.env.API_ID!,
|
||||||
|
apiHash: process.env.API_HASH!,
|
||||||
|
primaryDc: defaultDcs.defaultTestDc,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (session2.indexOf(':') > -1) {
|
||||||
|
// bot token
|
||||||
|
await client2.call({
|
||||||
|
_: 'auth.importBotAuthorization',
|
||||||
|
apiId: parseInt(process.env.API_ID!),
|
||||||
|
apiHash: process.env.API_HASH!,
|
||||||
|
botAuthToken: session2,
|
||||||
|
flags: 0
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
client2.importSession(session2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [client1, client2]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('e2e : receiving updates', function () {
|
||||||
|
if (!process.env.API_ID || !process.env.API_HASH) {
|
||||||
|
console.warn('Warning: skipping e2e updates test (no API_ID or API_HASH)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!process.env.BOT_TOKEN || !process.env.USER_SESSION) {
|
||||||
|
console.warn('Warning: skipping e2e updates test (no API_ID or API_HASH)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
it('receiving from user by bot', async () => {
|
||||||
|
const [actor, observer] = await createClientPair(
|
||||||
|
process.env.USER_SESSION!,
|
||||||
|
process.env.BOT_TOKEN!
|
||||||
|
)
|
||||||
|
|
||||||
|
await observer.connect()
|
||||||
|
await observer.waitUntilUsable()
|
||||||
|
|
||||||
|
let updatesCount = 0
|
||||||
|
|
||||||
|
observer['_handleUpdate'] = function() {
|
||||||
|
updatesCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = await actor.call({
|
||||||
|
_: 'contacts.resolveUsername',
|
||||||
|
username: process.env.BOT_USERNAME!
|
||||||
|
}).then((res) => res.users[0])
|
||||||
|
|
||||||
|
expect(bot._).eq('user')
|
||||||
|
|
||||||
|
const inputPeer: tl.TypeInputPeer = {
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: bot.id,
|
||||||
|
accessHash: (bot as tl.RawUser).accessHash!
|
||||||
|
}
|
||||||
|
|
||||||
|
await actor.call({
|
||||||
|
_: 'messages.startBot',
|
||||||
|
bot: {
|
||||||
|
_: 'inputUser',
|
||||||
|
userId: bot.id,
|
||||||
|
accessHash: (bot as tl.RawUser).accessHash!
|
||||||
|
},
|
||||||
|
peer: inputPeer,
|
||||||
|
randomId: bufferToBigInt(randomBytes(8)),
|
||||||
|
startParam: '123'
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await actor.call({
|
||||||
|
_: 'messages.sendMessage',
|
||||||
|
peer: inputPeer,
|
||||||
|
randomId: bufferToBigInt(randomBytes(8)),
|
||||||
|
message: `Test ${i}`
|
||||||
|
})
|
||||||
|
await sleep(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// to make sure the updates were delivered
|
||||||
|
await sleep(5000)
|
||||||
|
|
||||||
|
// 5 messages + /start = 6
|
||||||
|
// it is assumed that there were no gaps
|
||||||
|
expect(updatesCount).gte(6)
|
||||||
|
|
||||||
|
await actor.close()
|
||||||
|
await observer.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('receiving from user by user', async () => {
|
||||||
|
const [actor, observer] = await createClientPair(
|
||||||
|
process.env.USER_SESSION!,
|
||||||
|
process.env.USER_OTHER_DC_SESSION!
|
||||||
|
)
|
||||||
|
|
||||||
|
await observer.connect()
|
||||||
|
await observer.waitUntilUsable()
|
||||||
|
|
||||||
|
let updatesCount = 0
|
||||||
|
|
||||||
|
observer['_handleUpdate'] = function() {
|
||||||
|
updatesCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await actor.call({
|
||||||
|
_: 'contacts.resolveUsername',
|
||||||
|
username: process.env.USER_OTHER_DC_USERNAME!
|
||||||
|
}).then((res) => res.users[0])
|
||||||
|
|
||||||
|
expect(user._).eq('user')
|
||||||
|
|
||||||
|
const inputPeer: tl.TypeInputPeer = {
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: user.id,
|
||||||
|
accessHash: (user as tl.RawUser).accessHash!
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await sleep(1000)
|
||||||
|
await actor.call({
|
||||||
|
_: 'messages.sendMessage',
|
||||||
|
peer: inputPeer,
|
||||||
|
randomId: bufferToBigInt(randomBytes(8)),
|
||||||
|
message: `Test ${i}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// to make sure the updates were delivered
|
||||||
|
await sleep(5000)
|
||||||
|
|
||||||
|
// 5 messages
|
||||||
|
// it is assumed that there were no gaps
|
||||||
|
expect(updatesCount).gte(5)
|
||||||
|
|
||||||
|
await actor.close()
|
||||||
|
await observer.close()
|
||||||
|
})
|
||||||
|
})
|
101
packages/core/tests/fuzz/fuzz-packet.spec.ts
Normal file
101
packages/core/tests/fuzz/fuzz-packet.spec.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { describe, after, it } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import {
|
||||||
|
BaseTelegramClient, BinaryReader,
|
||||||
|
BinaryWriter,
|
||||||
|
defaultDcs,
|
||||||
|
randomBytes,
|
||||||
|
} from '../../src'
|
||||||
|
import { sleep } from '../../src/utils/misc-utils'
|
||||||
|
import { createAesIgeForMessage } from '../../src/utils/crypto/mtproto'
|
||||||
|
|
||||||
|
require('dotenv-flow').config()
|
||||||
|
|
||||||
|
describe('fuzz : packet', async function () {
|
||||||
|
if (!process.env.API_ID || !process.env.API_HASH) {
|
||||||
|
console.warn(
|
||||||
|
'Warning: skipping fuzz packet test (no API_ID or API_HASH)'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout(45000)
|
||||||
|
|
||||||
|
it('random packet', async () => {
|
||||||
|
const client = new BaseTelegramClient({
|
||||||
|
apiId: process.env.API_ID!,
|
||||||
|
apiHash: process.env.API_HASH!,
|
||||||
|
primaryDc: defaultDcs.defaultTestDc,
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
await client.waitUntilUsable()
|
||||||
|
|
||||||
|
const errors: Error[] = []
|
||||||
|
|
||||||
|
const errorHandler = (err: Error) => {
|
||||||
|
errors.push(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.onError(errorHandler)
|
||||||
|
|
||||||
|
const conn = client.primaryConnection
|
||||||
|
const mtproto = conn['_mtproto']
|
||||||
|
|
||||||
|
const createFakeMtprotoPacket = async (payload: Buffer): Promise<Buffer> => {
|
||||||
|
// create a fake mtproto packet
|
||||||
|
const messageId = conn['_getMessageId']().minus(1) // must be odd
|
||||||
|
|
||||||
|
const innerWriter = BinaryWriter.alloc(payload.length + 32)
|
||||||
|
innerWriter.raw(mtproto.serverSalt)
|
||||||
|
innerWriter.raw(mtproto._sessionId)
|
||||||
|
innerWriter.long(messageId)
|
||||||
|
innerWriter.int32(0) // seqno
|
||||||
|
innerWriter.int32(payload.length)
|
||||||
|
innerWriter.raw(payload)
|
||||||
|
|
||||||
|
const innerData = innerWriter.result()
|
||||||
|
|
||||||
|
const messageKey = (
|
||||||
|
await mtproto._crypto.sha256(
|
||||||
|
Buffer.concat([mtproto._authKeyServerSalt!, innerData])
|
||||||
|
)
|
||||||
|
).slice(8, 24)
|
||||||
|
const ige = await createAesIgeForMessage(
|
||||||
|
mtproto._crypto,
|
||||||
|
mtproto._authKey!,
|
||||||
|
messageKey,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
const encryptedData = await ige.encrypt(innerData)
|
||||||
|
|
||||||
|
const writer = BinaryWriter.alloc(24 + encryptedData.length)
|
||||||
|
writer.raw(mtproto._authKeyId!)
|
||||||
|
writer.raw(messageKey)
|
||||||
|
|
||||||
|
return Buffer.concat([mtproto._authKeyId!, messageKey, encryptedData])
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const payload = randomBytes(Math.round(Math.random() * 16) * 16)
|
||||||
|
|
||||||
|
await conn['onMessage'](await createFakeMtprotoPacket(payload))
|
||||||
|
await sleep(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// similar test, but this time only using object ids that do exist
|
||||||
|
const objectIds = Object.keys(new BinaryReader(Buffer.alloc(0))._objectsMap)
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const payload = randomBytes((Math.round(Math.random() * 16) + 1) * 16)
|
||||||
|
const objectId = parseInt(objectIds[Math.round(Math.random() * objectIds.length)])
|
||||||
|
payload.writeUInt32LE(objectId, 0)
|
||||||
|
|
||||||
|
await conn['onMessage'](await createFakeMtprotoPacket(payload))
|
||||||
|
await sleep(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
expect(errors.length).gt(0)
|
||||||
|
})
|
||||||
|
})
|
113
packages/core/tests/fuzz/fuzz-transport.spec.ts
Normal file
113
packages/core/tests/fuzz/fuzz-transport.spec.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { describe, it } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import {
|
||||||
|
BaseTelegramClient,
|
||||||
|
defaultDcs,
|
||||||
|
ICuteTransport,
|
||||||
|
randomBytes,
|
||||||
|
tl,
|
||||||
|
TransportState,
|
||||||
|
} from '../../src'
|
||||||
|
import { EventEmitter } from 'events'
|
||||||
|
import { sleep } from '../../src/utils/misc-utils'
|
||||||
|
|
||||||
|
require('dotenv-flow').config()
|
||||||
|
|
||||||
|
class RandomBytesTransport extends EventEmitter implements ICuteTransport {
|
||||||
|
dc: tl.RawDcOption
|
||||||
|
|
||||||
|
interval: NodeJS.Timeout | null
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
clearInterval(this.interval!)
|
||||||
|
this.emit('close')
|
||||||
|
this.interval = null
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(dc: tl.RawDcOption): void {
|
||||||
|
this.dc = dc
|
||||||
|
|
||||||
|
setTimeout(() => this.emit('ready'), 0)
|
||||||
|
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.emit('message', randomBytes(64))
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDc(): tl.RawDcOption | null {
|
||||||
|
return this.dc
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: Buffer): Promise<void> {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
state(): TransportState {
|
||||||
|
return this.interval ? TransportState.Ready : TransportState.Idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('fuzz : transport', function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
it('RandomBytesTransport (no auth)', async () => {
|
||||||
|
const client = new BaseTelegramClient({
|
||||||
|
transport: () => new RandomBytesTransport(),
|
||||||
|
apiId: 0,
|
||||||
|
apiHash: '',
|
||||||
|
primaryDc: defaultDcs.defaultTestDc,
|
||||||
|
})
|
||||||
|
|
||||||
|
const errors: Error[] = []
|
||||||
|
|
||||||
|
client.onError((err) => {
|
||||||
|
errors.push(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
await sleep(15000)
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
expect(errors.length).gt(0)
|
||||||
|
errors.forEach((err) => {
|
||||||
|
expect(err.message).match(/unknown object id/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('RandomBytesTransport (with auth)', async () => {
|
||||||
|
const client = new BaseTelegramClient({
|
||||||
|
transport: () => new RandomBytesTransport(),
|
||||||
|
apiId: 0,
|
||||||
|
apiHash: '',
|
||||||
|
primaryDc: defaultDcs.defaultTestDc,
|
||||||
|
})
|
||||||
|
// random key just to make it think it already has one
|
||||||
|
await client.storage.setAuthKeyFor(2, randomBytes(256))
|
||||||
|
|
||||||
|
// in this case, there will be no actual errors, only
|
||||||
|
// warnings like 'received message with unknown authKey'
|
||||||
|
//
|
||||||
|
// to test for that, we hook into `decryptMessage` and make
|
||||||
|
// sure that it returns `null`
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
|
||||||
|
let hadNonNull = false
|
||||||
|
|
||||||
|
const decryptMessage =
|
||||||
|
client.primaryConnection['_mtproto'].decryptMessage
|
||||||
|
client.primaryConnection[
|
||||||
|
'_mtproto'
|
||||||
|
].decryptMessage = async function () {
|
||||||
|
const res = await decryptMessage.apply(this, arguments)
|
||||||
|
if (res !== null) hadNonNull = true
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(15000)
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
expect(hadNonNull).false
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it } from 'mocha'
|
import { describe, it } from 'mocha'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { IntermediatePacketCodec } from '../../src/network/transports/tcp-intermediate'
|
import { IntermediatePacketCodec } from '../../src/network/transports/intermediate'
|
||||||
import { TransportError } from '../../src/network/transports/abstract'
|
import { TransportError } from '../../src/network/transports/abstract'
|
||||||
|
|
||||||
describe('IntermediatePacketCodec', () => {
|
describe('IntermediatePacketCodec', () => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
bigIntToBuffer,
|
bigIntToBuffer,
|
||||||
bufferToBigInt,
|
bufferToBigInt,
|
||||||
ICryptoProvider,
|
ICryptoProvider,
|
||||||
PacketCodec,
|
IPacketCodec,
|
||||||
WrappedCodec,
|
WrappedCodec,
|
||||||
randomBytes,
|
randomBytes,
|
||||||
} from '@mtcute/core'
|
} from '@mtcute/core'
|
||||||
|
@ -285,7 +285,7 @@ export async function generateFakeTlsHeader(
|
||||||
* Must only be used inside {@link MtProxyTcpTransport}
|
* Must only be used inside {@link MtProxyTcpTransport}
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export class FakeTlsPacketCodec extends WrappedCodec implements PacketCodec {
|
export class FakeTlsPacketCodec extends WrappedCodec implements IPacketCodec {
|
||||||
protected _stream = Buffer.alloc(0)
|
protected _stream = Buffer.alloc(0)
|
||||||
|
|
||||||
private _header: Buffer
|
private _header: Buffer
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
IntermediatePacketCodec,
|
IntermediatePacketCodec,
|
||||||
ObfuscatedPacketCodec,
|
ObfuscatedPacketCodec,
|
||||||
PacketCodec,
|
IPacketCodec,
|
||||||
PaddedIntermediatePacketCodec,
|
PaddedIntermediatePacketCodec,
|
||||||
parseUrlSafeBase64,
|
parseUrlSafeBase64,
|
||||||
TcpTransport,
|
TcpTransport,
|
||||||
|
@ -86,7 +86,7 @@ export class MtProxyTcpTransport extends TcpTransport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_packetCodec!: PacketCodec
|
_packetCodec!: IPacketCodec
|
||||||
|
|
||||||
connect(dc: tl.RawDcOption): void {
|
connect(dc: tl.RawDcOption): void {
|
||||||
if (this._state !== TransportState.Idle)
|
if (this._state !== TransportState.Idle)
|
||||||
|
@ -110,7 +110,7 @@ export class MtProxyTcpTransport extends TcpTransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._fakeTlsDomain) {
|
if (!this._fakeTlsDomain) {
|
||||||
let inner: PacketCodec
|
let inner: IPacketCodec
|
||||||
if (this._randomPadding) {
|
if (this._randomPadding) {
|
||||||
inner = new PaddedIntermediatePacketCodec()
|
inner = new PaddedIntermediatePacketCodec()
|
||||||
} else {
|
} else {
|
||||||
|
|
68
yarn.lock
68
yarn.lock
|
@ -2161,14 +2161,6 @@ currently-unhandled@^0.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-find-index "^1.0.1"
|
array-find-index "^1.0.1"
|
||||||
|
|
||||||
d@1, d@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
|
|
||||||
integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
|
|
||||||
dependencies:
|
|
||||||
es5-ext "^0.10.50"
|
|
||||||
type "^1.0.1"
|
|
||||||
|
|
||||||
dargs@^7.0.0:
|
dargs@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc"
|
resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc"
|
||||||
|
@ -2384,6 +2376,18 @@ dot-prop@^6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-obj "^2.0.0"
|
is-obj "^2.0.0"
|
||||||
|
|
||||||
|
dotenv-flow@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "http://localhost:4873/dotenv-flow/-/dotenv-flow-3.2.0.tgz#a5d79dd60ddb6843d457a4874aaf122cf659a8b7"
|
||||||
|
integrity sha512-GEB6RrR4AbqDJvNSFrYHqZ33IKKbzkvLYiD5eo4+9aFXr4Y4G+QaFrB/fNp0y6McWBmvaPn3ZNjIufnj8irCtg==
|
||||||
|
dependencies:
|
||||||
|
dotenv "^8.0.0"
|
||||||
|
|
||||||
|
dotenv@^8.0.0:
|
||||||
|
version "8.6.0"
|
||||||
|
resolved "http://localhost:4873/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
|
||||||
|
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
|
||||||
|
|
||||||
duplexer@^0.1.1:
|
duplexer@^0.1.1:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
|
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
|
||||||
|
@ -2501,37 +2505,11 @@ es-to-primitive@^1.2.1:
|
||||||
is-date-object "^1.0.1"
|
is-date-object "^1.0.1"
|
||||||
is-symbol "^1.0.2"
|
is-symbol "^1.0.2"
|
||||||
|
|
||||||
es5-ext@^0.10.35, es5-ext@^0.10.50:
|
|
||||||
version "0.10.53"
|
|
||||||
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
|
|
||||||
integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
|
|
||||||
dependencies:
|
|
||||||
es6-iterator "~2.0.3"
|
|
||||||
es6-symbol "~3.1.3"
|
|
||||||
next-tick "~1.0.0"
|
|
||||||
|
|
||||||
es6-error@^4.0.1:
|
es6-error@^4.0.1:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
|
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
|
||||||
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
|
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
|
||||||
|
|
||||||
es6-iterator@~2.0.3:
|
|
||||||
version "2.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
|
|
||||||
integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
|
|
||||||
dependencies:
|
|
||||||
d "1"
|
|
||||||
es5-ext "^0.10.35"
|
|
||||||
es6-symbol "^3.1.1"
|
|
||||||
|
|
||||||
es6-symbol@^3.1.1, es6-symbol@^3.1.3, es6-symbol@~3.1.3:
|
|
||||||
version "3.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
|
|
||||||
integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
|
|
||||||
dependencies:
|
|
||||||
d "^1.0.1"
|
|
||||||
ext "^1.1.2"
|
|
||||||
|
|
||||||
escalade@^3.1.1:
|
escalade@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||||
|
@ -2693,13 +2671,6 @@ expand-template@^2.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||||
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
|
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
|
||||||
|
|
||||||
ext@^1.1.2:
|
|
||||||
version "1.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
|
|
||||||
integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
|
|
||||||
dependencies:
|
|
||||||
type "^2.0.0"
|
|
||||||
|
|
||||||
extend@~3.0.2:
|
extend@~3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||||
|
@ -4369,11 +4340,6 @@ neo-async@^2.6.0:
|
||||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||||
|
|
||||||
next-tick@~1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
|
||||||
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
|
|
||||||
|
|
||||||
node-abi@^2.21.0:
|
node-abi@^2.21.0:
|
||||||
version "2.26.0"
|
version "2.26.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.26.0.tgz#355d5d4bc603e856f74197adbf3f5117a396ba40"
|
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.26.0.tgz#355d5d4bc603e856f74197adbf3f5117a396ba40"
|
||||||
|
@ -6222,16 +6188,6 @@ type-fest@^0.8.0, type-fest@^0.8.1:
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||||
|
|
||||||
type@^1.0.1:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
|
|
||||||
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
|
|
||||||
|
|
||||||
type@^2.0.0:
|
|
||||||
version "2.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
|
|
||||||
integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
|
|
||||||
|
|
||||||
typedarray-to-buffer@^3.1.5:
|
typedarray-to-buffer@^3.1.5:
|
||||||
version "3.1.5"
|
version "3.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||||
|
|
Loading…
Reference in a new issue