From e8ed9331772a14f174fab3421d291408ef60d59e Mon Sep 17 00:00:00 2001 From: Kenta Iwasaki Date: Fri, 12 Jun 2020 05:51:56 +0900 Subject: [PATCH] sdk/js: implement flatend protocol in javascript --- .gitignore | 3 +- README.md | 10 +- cmd/microservice/main.go | 10 +- ts/main.js | 447 +++++++++++++++++++++++++++++++++++++++ ts/package-lock.json | 106 ++++++++++ ts/package.json | 14 ++ ts/yarn.lock | 92 ++++++++ 7 files changed, 667 insertions(+), 15 deletions(-) create mode 100644 ts/main.js create mode 100644 ts/package-lock.json create mode 100644 ts/package.json create mode 100644 ts/yarn.lock diff --git a/.gitignore b/.gitignore index 62c8935..182f973 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea/ \ No newline at end of file +.idea/ +**/node_modules/ \ No newline at end of file diff --git a/README.md b/README.md index 7dd1dd2..1ec7eb5 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,9 @@ func handleGetTodos(ctx *flatend.Context) []byte { } func main() { - service := &flatend.Service{ - Addr: "127.0.0.1:9000", - Services: map[string]flatend.Handler{ - "all_todos": handleAllTodos, - "get_todos": handleGetTodos, - }, - } + service := &flatend.Service{Addr: "127.0.0.1:9000"} + service.Register("all_todos", handleAllTodos) + service.Register("get_todos", handleGetTodos) check(service.Start()) } ``` \ No newline at end of file diff --git a/cmd/microservice/main.go b/cmd/microservice/main.go index bedca02..fd18ed0 100644 --- a/cmd/microservice/main.go +++ b/cmd/microservice/main.go @@ -23,12 +23,8 @@ func handleGetTodos(ctx *flatend.Context) []byte { } func main() { - service := &flatend.Service{ - Addr: "127.0.0.1:9000", - Services: map[string]flatend.Handler{ - "all_todos": handleAllTodos, - "get_todos": handleGetTodos, - }, - } + service := &flatend.Service{Addr: "127.0.0.1:9000"} + service.Register("all_todos", handleAllTodos) + service.Register("get_todos", handleGetTodos) check(service.Start()) } diff --git a/ts/main.js b/ts/main.js new file mode 100644 index 0000000..371f5d8 --- /dev/null +++ b/ts/main.js @@ -0,0 +1,447 @@ +import nacl from "tweetnacl"; +import * as net from "net"; +import blake2b from 'blake2b'; +import * as crypto from "crypto"; +import {EventEmitter} from "events"; +import ip from "ip"; + +const OPCODE_HANDSHAKE = 0; +const OPCODE_REQUEST = 1; + +class RequestPacket { + constructor( + { + services = [], + data = Buffer.of() + } + ) { + this.services = services; + this.data = data; + } + + encode() { + const services = this.services.reduce( + (acc, service) => Buffer.concat([acc, Buffer.of(service.length), Buffer.from(service, "utf8")]), + Buffer.of(this.services.length), + ); + + const data = Buffer.alloc(4); + data.writeUInt32BE(this.data.byteLength); + + return Buffer.concat([services, data, this.data]) + } + + static decode(buf) { + let size = buf.readUInt8(); + buf = buf.slice(1); + + const services = [...Array(size)].map(() => { + const size = buf.readUInt8(); + buf = buf.slice(1); + + const service = buf.slice(0, size); + buf = buf.slice(size); + + return service.toString("utf8"); + }); + + size = buf.readUInt32BE(); + buf = buf.slice(4); + + const data = buf.slice(0, size); + + return new RequestPacket({services, data}); + } +} + + +class ID { + constructor( + { + publicKey = Buffer.alloc(nacl.sign.publicKeyLength), + host = "", + port = 0, + }, + ) { + this.publicKey = publicKey; + this.host = host; + this.port = port; + } + + /** + * + * @param {Buffer} buf + * @return {[ID, Buffer]} + */ + static decode(buf) { + const publicKey = buf.slice(0, nacl.sign.publicKeyLength); + buf = buf.slice(nacl.sign.publicKeyLength); + + const hostType = buf.readUInt8(); + buf = buf.slice(1); + + let host; + switch (hostType) { + case 0: + [host, buf] = [buf.slice(0, 4), buf.slice(4)]; + break; + case 1: + [host, buf] = [buf.slice(0, 16), buf.slice(16)] + break; + case 2: + [host, buf] = [Buffer.of(), buf] + break; + default: + throw new Error(`Unknown host type '${hostType}' specified in handshake packet.`) + } + + const port = buf.readUInt16BE(); + buf = buf.slice(2) + + return [new ID({publicKey, host, port}), buf]; + } + + /** + * + * @return {Buffer} + */ + encode() { + let host; + if (typeof this.host === "string") { + if (ip.isV4Format(this.host)) { + host = Buffer.concat([Buffer.of(0), ip.toBuffer(this.host)]); + } else if (ip.isV6Format(this.host)) { + host = Buffer.concat([Buffer.of(1), ip.toBuffer(this.host)]); + } else { + host = Buffer.of(2); + } + } else if (Buffer.isBuffer(this.host)) { + const type = this.host.byteLength === 4 ? 0 : this.host.byteLength === 16 ? 1 : 2; + host = Buffer.concat([Buffer.of(type), this.host]); + } + + const port = Buffer.alloc(2); + port.writeUInt16BE(this.port); + + return Buffer.concat([this.publicKey, host, port]); + } +} + +class HandshakePacket { + constructor({id = new ID({}), services = [], signature = Buffer.alloc(nacl.sign.signatureLength)}) { + this.id = id; + this.services = services; + this.signature = signature; + } + + get payload() { + return Buffer.concat([ + this.id.encode(), + ...this.services.map(service => Buffer.from(service, "utf8")), + ]) + } + + sign(secretKey) { + this.signature = Buffer.from(nacl.sign.detached(this.payload, secretKey)); + return this; + } + + encode() { + const services = this.services.reduce( + (acc, service) => Buffer.concat([acc, Buffer.of(service.length), Buffer.from(service, "utf8")]), + Buffer.of(this.services.length), + ); + + return Buffer.concat([ + this.id.encode(), + services, + this.signature, + ]); + } + + static decode(buf) { + let decoded = ID.decode(buf); + + let id = decoded[0]; + buf = decoded[1]; + + const size = buf.readUInt8(); + buf = buf.slice(1); + + const services = [...Array(size)].map(() => { + const size = buf.readUInt8(); + const service = buf.slice(1, 1 + size); + buf = buf.slice(1 + size); + return service.toString("utf8"); + }); + + const signature = buf.slice(0, nacl.sign.signatureLength); + buf = buf.slice(nacl.sign.signatureLength); + + const packet = new HandshakePacket({id, services, signature}); + + if (!nacl.sign.detached.verify(packet.payload, packet.signature, packet.id.publicKey)) { + throw new Error(`Signature specified in handshake packet is invalid.`) + } + + return packet; + } +} + +const sized = data => { + const buf = Buffer.alloc(4); + buf.writeUInt32BE(data.byteLength); + return Buffer.concat([buf, data]); +} + +/** + * + * @param {Socket} sock + * @param {number} n + * @param {number} timeout + * @return {Promise} + */ +const readExactly = (sock, n, timeout = 3000) => new Promise((resolve, reject) => { + const doTimeout = () => { + const err = new Error(`Timed out after ${timeout} millisecond(s) trying to read ${n} byte(s).`); + sock.destroy(err); + reject(err); + } + + const handle = timeout > 0 && setTimeout(doTimeout, timeout); + + const tryRead = () => { + const buf = sock.read(n); + if (!buf) { + sock.once('readable', tryRead); + } else { + if (handle) clearTimeout(handle); + resolve(buf); + } + } + + sock.once('readable', tryRead); +}); + +class Client { + /** + * + * @param {net.Socket} conn + * @param {Buffer} secret + */ + constructor(conn, secret) { + this.readNonce = 0n; + this.writeNonce = 0n; + + this.pending = new EventEmitter(); + this.counter = 0; + + this.conn = conn; + this.secret = secret; + + this.conn.on('readable', this._read.bind(this)); + } + + async request(req, timeout = 3000) { + const seq = this.counter === 0 ? 1 : this.counter++; + if (this.counter === 2 ** 32) this.counter = 0; + + const header = Buffer.alloc(4); + header.writeUInt32BE(seq); + + const response = new Promise((resolve, reject) => { + const doTimeout = () => { + this.pending.removeAllListeners(`${seq}`); + reject(new Error(`Timed out waiting for response to request.`)); + } + + const handle = timeout > 0 && setTimeout(doTimeout, timeout); + + this.pending.once(`${seq}`, data => { + if (handle) clearTimeout(handle); + resolve(data); + }); + }); + + req = Buffer.concat([header, Buffer.from(req)]); + this.conn.write(sized(this.encrypt(req))); + + return await response; + } + + send(buf) { + buf = Buffer.concat([Buffer.alloc(4), Buffer.from(buf)]); + this.conn.write(sized(this.encrypt(buf))); + } + + reply(seq, buf) { + const header = Buffer.alloc(4); + header.writeUInt32BE(seq); + + this.conn.write(sized(this.encrypt(Buffer.concat([header, buf])))); + } + + /** + * + * @param {crypto.BinaryLike} buf + * @returns {Buffer} + */ + encrypt(buf) { + const nonce = Buffer.alloc(12); + nonce.writeBigUInt64BE(this.writeNonce); + this.writeNonce++ + + const cipher = crypto.createCipheriv("aes-256-gcm", this.secret, nonce, {authTagLength: 16}); + const ciphered = cipher.update(buf); + cipher.final(); + + return Buffer.concat([ciphered, cipher.getAuthTag()]); + } + + /** + * + * @param {crypto.BinaryLike} buf + * @returns {Buffer} + */ + decrypt(buf) { + if (buf.byteLength < 16) { + throw new Error("Data to be decrypted must be at least 16 bytes."); + } + + const nonce = Buffer.alloc(12); + nonce.writeBigUInt64BE(this.readNonce); + this.readNonce++ + + const decipher = crypto.createDecipheriv("aes-256-gcm", this.secret, nonce, {authTagLength: 16}); + decipher.setAuthTag(buf.slice(buf.byteLength - 16, buf.byteLength)); + + const deciphered = decipher.update(buf.slice(0, buf.byteLength - 16)); + decipher.final(); + + return deciphered; + } + + _read() { + while (true) { + const header = this.conn.read(4); + if (!header) return; + + const length = header.readUInt32BE(); + + let frame = this.conn.read(length); + if (!frame) { + this.conn.unshift(header); + return; + } + + frame = this.decrypt(frame); + if (frame.byteLength <= 5) { // seq, opcode + this.conn.destroy(new Error(`Unexpected EOF reading message.`)); + return; + } + + const seq = frame.readUInt32BE(); + frame = frame.slice(4); + + if (seq === 0 || this.pending.listeners(`${seq}`).length === 0) { + const opcode = frame.readUInt8(); + frame = frame.slice(1); + + if (opcode !== OPCODE_REQUEST) { + throw new Error(`Got unexpected opcode ${opcode}.`); + } + + const packet = RequestPacket.decode(frame); + const body = JSON.parse(packet.data.toString("utf8")); + + console.log(body); + + this.reply(seq, packet.data); + } else { + this.pending.emit(`${seq}`, frame); + } + } + } +} + +class Node { + constructor({keys = nacl.sign.keyPair(), host = "", port = 0}) { + this.clients = new Map(); + + this.id = new ID({publicKey: keys.publicKey, host: host, port: port}); + this.publicKey = keys.publicKey; + this.secretKey = keys.secretKey; + this.host = host; + this.port = port; + } + + listen() { + this.srv = new net.Server(); + this.srv.listen(this.port, this.host); + + return new Promise((resolve, reject) => { + this.srv.on('error', reject); + this.srv.on('listening', () => { + const info = this.srv.address(); + switch (info.family) { + case "IPv6": + console.log(`Listening for connections on [${info.address}]:${info.port}.`); + break; + default: + console.log(`Listening for connections on ${info.address}:${info.port}.`); + break; + } + + this.id.host = this.host = info.address; + this.id.port = this.port = info.port; + + resolve(); + }); + }); + } + + async dial(opts) { + if (this.clients.has(opts)) { + return this.clients.get(opts); + } + + const conn = new net.Socket(); + conn.connect(opts); + + const {publicKey, secretKey} = nacl.box.keyPair(); + conn.write(publicKey); + + const peerPublicKey = await readExactly(conn, nacl.box.publicKeyLength); + + let sessionKey = nacl.scalarMult(secretKey, Buffer.from(peerPublicKey.buffer)); + sessionKey = blake2b(32).update(sessionKey).digest(); + sessionKey = Buffer.from(sessionKey); + + const client = new Client(conn, sessionKey); + this.clients.set(opts, client); + + return client; + } +} + +async function main() { + const node = new Node({host: "127.0.0.1"}); + await node.listen(); + + console.log(`Node ID:`, node.id); + + const client = await node.dial({port: 9000, host: "127.0.0.1"}); + + console.log(`Session Key: ${client.secret.toString("hex")}`); + + let packet = new HandshakePacket({id: node.id, services: ["get_todos"]}).sign(node.secretKey); + packet = HandshakePacket.decode(await client.request(Buffer.concat([Buffer.of(OPCODE_HANDSHAKE), packet.encode()]))); + + console.log(packet); + + // for (let i = 0; i < 100; i++) { + // client.send(`[${i}] Hello from NodeJS!`); + // } +} + +main().catch(err => console.error(err)); \ No newline at end of file diff --git a/ts/package-lock.json b/ts/package-lock.json new file mode 100644 index 0000000..3958f68 --- /dev/null +++ b/ts/package-lock.json @@ -0,0 +1,106 @@ +{ + "name": "flatend", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "14.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz", + "integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "blake2b": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/blake2b/-/blake2b-2.1.3.tgz", + "integrity": "sha512-pkDss4xFVbMb4270aCyGD3qLv92314Et+FsKzilCLxDz5DuZ2/1g3w4nmBbu6nKApPspnjG7JcwTjGZnduB1yg==", + "requires": { + "blake2b-wasm": "^1.1.0", + "nanoassert": "^1.0.0" + } + }, + "blake2b-wasm": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-1.1.7.tgz", + "integrity": "sha512-oFIHvXhlz/DUgF0kq5B1CqxIDjIJwh9iDeUUGQUcvgiGz7Wdw03McEO7CfLBy7QKGdsydcMCgO9jFNBAFCtFcA==", + "requires": { + "nanoassert": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "nanoassert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", + "integrity": "sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "typescript": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", + "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/ts/package.json b/ts/package.json new file mode 100644 index 0000000..2efb881 --- /dev/null +++ b/ts/package.json @@ -0,0 +1,14 @@ +{ + "name": "flatend", + "version": "1.0.0", + "main": "index.js", + "repository": "https://github.com/lithdew/flatend", + "author": "Kenta Iwasaki ", + "license": "MIT", + "type": "module", + "dependencies": { + "blake2b": "^2.1.3", + "ip": "^1.1.5", + "tweetnacl": "^1.0.3" + } +} diff --git a/ts/yarn.lock b/ts/yarn.lock new file mode 100644 index 0000000..531e594 --- /dev/null +++ b/ts/yarn.lock @@ -0,0 +1,92 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@^14.0.13": + version "14.0.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9" + integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +blake2b-wasm@^1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/blake2b-wasm/-/blake2b-wasm-1.1.7.tgz#e4d075da10068e5d4c3ec1fb9accc4d186c55d81" + integrity sha512-oFIHvXhlz/DUgF0kq5B1CqxIDjIJwh9iDeUUGQUcvgiGz7Wdw03McEO7CfLBy7QKGdsydcMCgO9jFNBAFCtFcA== + dependencies: + nanoassert "^1.0.0" + +blake2b@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/blake2b/-/blake2b-2.1.3.tgz#f5388be424768e7c6327025dad0c3c6d83351bca" + integrity sha512-pkDss4xFVbMb4270aCyGD3qLv92314Et+FsKzilCLxDz5DuZ2/1g3w4nmBbu6nKApPspnjG7JcwTjGZnduB1yg== + dependencies: + blake2b-wasm "^1.1.0" + nanoassert "^1.0.0" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +nanoassert@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-1.1.0.tgz#4f3152e09540fde28c76f44b19bbcd1d5a42478d" + integrity sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40= + +source-map-support@^0.5.17: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +ts-node@^8.10.2: + version "8.10.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" + integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +typescript@^3.9.5: + version "3.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" + integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==