diff --git a/deno/README.md b/deno/README.md new file mode 100644 index 0000000..20fdd9d --- /dev/null +++ b/deno/README.md @@ -0,0 +1,578 @@ +# flatend + +[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](LICENSE) +[![Discord Chat](https://img.shields.io/discord/697002823123992617)](https://discord.gg/HZEbkeQ) +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/lithdew/flatend) +[![npm version](https://img.shields.io/npm/v/flatend.svg?style=flat)](https://www.npmjs.com/package/flatend) +[![npm downloads](https://img.shields.io/npm/dm/flatend.svg?style=flat)](https://www.npmjs.com/package/flatend) +[![Security Responsible Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow.svg)](https://github.com/nodejs/security-wg/blob/master/processes/responsible_disclosure_template.md) + + + +**flatend** is an experimental framework and protocol to make microservices more modular, simpler, safer, cheaper, and faster to build using [p2p networking](https://github.com/lithdew/monte). + +**flatend** aims to provide the benefits low-code tools try to bring to increase developer productivity, but with [zero vendor lock-in](https://news.ycombinator.com/item?id=20985429), [strong performance](https://projectricochet.com/blog/top-10-meteor-performance-problems), and [zero bias towards certain coding styles/patterns](https://news.ycombinator.com/item?id=12166666). + +## Features + +- Fully agnostic and compatible with any type of language, database, tool, library, or framework. +- P2P-based service discovery, load balancing, routing, and PKI via [Kademlia](https://en.wikipedia.org/wiki/Kademlia). +- Fully-encrypted, end-to-end, bidirectional streaming RPC via [Monte](https://github.com/lithdew/monte). +- Automatic reconnect/retry upon crashes or connection loss. +- Zero-hassle serverless: every function is a microservice. +- Stream multiple gigabytes of data across microservices. + +## Gateways + +**flatend** additionally comes with scalable, high-performance, production-ready, easily-deployable API gateways that are bundled into a [small, single executable binary](https://github.com/lithdew/flatend/releases) to help you quickly deploy your microservices. + +- Written in [Go](https://golang.org/). +- HTTP/1.1, HTTP/2 support. +- Automatic HTTPS via [LetsEncrypt](https://letsencrypt.org/). +- Expose/load-balance across microservices. +- Serve static files and directories. +- REPL for real-time management (_coming soon!_). +- Prometheus metrics (_coming soon!_). +- WebSocket support (_coming soon!_). +- gRPC support (_coming soon!_). + +All gateways have been extensively tested on [Rackspace](https://www.rackspace.com/), [Scaleway](https://www.scaleway.com/en/), [AWS](https://aws.amazon.com/), [Google Cloud](https://cloud.google.com/), and [DigitalOcean](https://www.digitalocean.com/). + +## Requirements + +Although **flatend** at its core is a protocol, and hence agnostic to whichever programming langauge you use, there are currently only two reference implementations in NodeJS and Go. + +- NodeJS v12.18.1+ (Windows, Linux, Mac) +- Go v1.14.1 (Windows, Linux Mac) + +The rationale for starting with NodeJS and Go is so that, for any new product/service, you may: + +1. Quickly prototype and deploy in NodeJS with SQLite using a 2USD/month bare-metal server. +2. Once you start scaling up, split up your microservice and rewrite the performance-critical parts in Go. +3. Run a red/blue deployment easily to gradually deploy your new microservices and experience zero downtime. + +Support is planned for the following runtimes/languages: + +1. [Zig v0.7+](https://ziglang.org/) +2. [Deno v1.0+](https://deno.land/) +3. [Python v3.8+](https://www.python.org/) + +Have any questions? Come chat with us on [Discord](https://discord.gg/HZEbkeQ). + +## Usage + +To get started quickly, download the API gateway binary for your platform [here](https://github.com/lithdew/flatend/releases). Otherwise, build the binary from source by following the instructions [here](#build-from-source). + +Create a new `config.toml`, and paste in: + +```toml +addr = "127.0.0.1:9000" + +[[http]] +addr = ":3000" + +[[http.routes]] +path = "GET /hello" +service = "hello_world" +``` + +Run: + +```shell +$ ./flatend +2020/06/18 04:07:07 Listening for Flatend nodes on '127.0.0.1:9000'. +2020/06/18 04:07:07 Listening for HTTP requests on '[::]:3000'. +``` + +Now, let's build your first microservice in [Go](#go)/[NodeJS](#nodejs). + +### Go + +Add [`flatend`](https://pkg.go.dev/github.com/lithdew/flatend) to a new Go modules project. + +```shell +$ go mod init github.com/lithdew/flatend-testbed +go: creating new go.mod: module github.com/lithdew/flatend-testbed + +$ go get github.com/lithdew/flatend +go: downloading github.com/lithdew/flatend vX.X.X +go: github.com/lithdew/flatend upgrade => vX.X.X +``` + +Write a function that describes how to handle requests for the service `hello_world` in `main.go`. + +```go +package main + +import "github.com/lithdew/flatend" + +func helloWorld(ctx *flatend.Context) { + ctx.WriteHeader("Content-Type", "text/plain; charset=utf-8") + ctx.Write([]byte("Hello world!")) +} +``` + +Register the function as a handler for the service `hello_world`. + +```go +func main() { + _ = &flatend.Node{ + Services: map[string]flatend.Handler{ + "hello_world": helloWorld, + }, + } +} +``` + +Start the node and have it connect to Flatend's API gateway. + +```go +func main() { + node := &flatend.Node{ + Services: map[string]flatend.Handler{ + "hello_world": helloWorld, + }, + } + node.Start("127.0.0.1:9000") + + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + <-ch + + node.Shutdown() +} +``` + +Run it. + +```shell +$ go run main.go +2020/06/18 04:09:25 Listening for Flatend nodes on '[::]:41581'. +2020/06/18 04:09:25 You are now connected to 127.0.0.1:9000. Services: [] +2020/06/18 04:09:25 Re-probed 127.0.0.1:9000. Services: [] +2020/06/18 04:09:25 Discovered 0 peer(s). +``` + +Visit [localhost:3000/hello](http://localhost:3000/hello). + +```shell +$ curl http://localhost:3000/hello +Hello world! +``` + +Try restart your API gateway and watch your service re-discover it. + +```shell +$ go run main.go +2020/06/18 04:11:06 Listening for Flatend nodes on '[::]:39313'. +2020/06/18 04:11:06 You are now connected to 127.0.0.1:9000. Services: [] +2020/06/18 04:11:06 Re-probed 127.0.0.1:9000. Services: [] +2020/06/18 04:11:06 Discovered 0 peer(s). +2020/06/18 04:11:07 127.0.0.1:9000 has disconnected from you. Services: [] +2020/06/18 04:11:07 Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +2020/06/18 04:11:08 Trying to reconnect to 127.0.0.1:9000. Sleeping for 617.563636ms. +2020/06/18 04:11:08 Trying to reconnect to 127.0.0.1:9000. Sleeping for 686.907514ms. +2020/06/18 04:11:09 You are now connected to 127.0.0.1:9000. Services: [] +``` + +

+ +

+ +Check out more examples [here](https://github.com/lithdew/flatend/tree/master/examples/go). I recommend checking out the [Todo List](https://github.com/lithdew/flatend/tree/master/examples/go/todo) one which stores data in [SQLite](http://sqlite.org/). + +### NodeJS + +Add [`flatend`](https://www.npmjs.com/package/flatend) to a new npm/yarn project. + +```shell +$ yarn init -y +yarn init vX.X.X +success Saved package.json + +$ yarn add flatend +yarn add vX.X.X +info No lockfile found. +[1/4] Resolving packages... +[2/4] Fetching packages... +[3/4] Linking dependencies... +[4/4] Building fresh packages... + +success Saved lockfile. +success Saved X new dependencies. +``` + +Write a function that describes how to handle requests for the service `hello_world` in `index.js`. + +```js +const { Node, Context } = require("flatend"); + +const helloWorld = (ctx) => ctx.send("Hello world!"); +``` + +Register the function as a handler for the service `hello_world`. Start the node and have it connect to Flatend's API gateway. + +```js +const { Node, Context } = require("flatend"); + +const helloWorld = (ctx) => ctx.send("Hello world!"); + +async function main() { + await Node.start({ + addrs: ["127.0.0.1:9000"], + services: { + hello_world: helloWorld, + }, + }); +} + +main().catch((err) => console.error(err)); +``` + +Run it. + +```shell +$ DEBUG=* node index.js + flatend You are now connected to 127.0.0.1:9000. Services: [] +0ms + flatend Discovered 0 peer(s). +19ms +``` + +Visit [localhost:3000/hello](http://localhost:3000/hello). + +```shell +$ curl http://localhost:3000/hello +Hello world! +``` + +Try restart your API gateway and watch your service re-discover it. + +```shell +$ DEBUG=* node index.js + flatend You are now connected to 127.0.0.1:9000. Services: [] +0ms + flatend Discovered 0 peer(s). +19ms + flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +41s + flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +504ms + flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +503ms + flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +503ms + flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +503ms + flatend You are now connected to 127.0.0.1:9000. Services: [] +21ms +``` + +

+ +

+ +Check out more examples [here](https://github.com/lithdew/flatend/tree/master/examples/nodejs). I recommend checking out the [Todo List](https://github.com/lithdew/flatend/tree/master/examples/nodejs/todo) one which stores data in [SQLite](http://sqlite.org/). + +## Options + +### Go SDK + +```go +package flatend + +import "github.com/lithdew/kademlia" + +type Node struct { + // A reachable, public address which peers may reach you on. + // The format of the address must be [host]:[port]. + PublicAddr string + + // A 32-byte Ed25519 private key. A secret key must be provided + // to allow for peers to reach you. A secret key may be generated + // by calling `flatend.GenerateSecretKey()`. + SecretKey kademlia.PrivateKey + + // A list of addresses and ports assembled using: + // 1. flatend.BindAny() (bind to all hosts and any available port) + // 2. flatend.BindTCP(string) (binds to a [host]:[port]) + // 3. flatend.BindTCPv4(string) (binds to an [IPv4 host]:[port]) + // 4. flatend.BindTCPv6(string) (binds to an [IPv6 host]:[port]) + // which your Flatend node will listen for other nodes from. + BindAddrs []BindFunc + + // A mapping of service names to their respective handlers. + Services map[string]Handler + + // .... +} + +// Start takes in 'addrs', which is list of addresses to nodes to +// initially reach out for/bootstrap from first. +(*Node).Start(addrs string) + +import "io" +import "io/ioutil" + +func helloWorld(ctx *flatend.Context) { + // The ID of the requester may be accessed via `ctx.ID`. + _ = ctx.ID + + // All headers must be written before writing any response body data. + + // Headers are used to send small amounts of metadata to a requester. + + // For example, the HTTP API gateway directly sets headers provided + // as a response as the headers of a HTTP response to a HTTP request + // which has been transcribed to a Flatend service request that is + // handled by some given node. + + ctx.WriteHeader("header key", "header val") + + // The first response body write call will send all set headers to the + // requester. Any other headers set after the first call are ignored. + ctx.Write([]byte("Hello world!")) + + + // All request headers may be accessed via `ctx.Headers`. Headers + // are represented as map[string]string. + header, exists := ctx.Headers["params.id"] + _, _ = header, exists + + // The body of a request may be accessed via `ctx.Body`. Request bodies + // are unbounded in size, and represented as a `io.ReadCloser`. + + // It is advised to wrap the body under an `io.LimitReader` to limit + // the size of the bodies of requests. + + buf, err := ioutil.ReadAll(io.LimitReader(ctx.Body, 65536)) + _, _ = buf, err + + // If no 'ctx.Write' calls are made by the end of the handler, an + // empty response body is provided. +} +``` + +### NodeJS SDK + +```js +const { Node } = require("flatend"); + +export interface NodeOptions { + // A reachable, public address which peers may reach you on. + // The format of the address must be [host]:[port]. + publicAddr?: string; + + // A list of [host]:[port] addresses which this node will bind a listener + // against to accept new Flatend nodes. + bindAddrs?: string[]; + + // A list of addresses to nodes to initially reach out + // for/bootstrap from first. + addrs?: string[]; + + // An Ed25519 secret key. A secret key must be provided to allow for + // peers to reach you. A secret key may be generated by calling + // 'flatend.generateSecretKey()'. + secretKey?: Uint8Array; + + // A mapping of service names to their respective handlers. + services?: { [key: string]: Handler }; +} + +await Node.start((opts: NodeOpts)); + +const { Context } = require("flatend"); + +// Handlers may optionally be declared as async, and may optionally +// return promises. + +const helloWorld = async (ctx) => { + // 'ctx' is a NodeJS Duplex stream. Writing to it writes a response + // body, and reading from it reads a request body. + + _ = ctx.id; // The ID of the requester. + + ctx.pipe(ctx); // This would pipe all request data as response data. + + // Headers are used to send small amounts of metadata to a requester. + + // For example, the HTTP API gateway directly sets headers provided + // as a response as the headers of a HTTP response to a HTTP request + // which has been transcribed to a Flatend service request that is + // handled by some given node. + + ctx.header("header key", "header val"); + + // All request headers may be accessed via 'ctx.headers'. Headers + // are represented as an object. + + // The line below closes the response with the body being a + // JSON-encoded version of the request headers provided. + + ctx.json(ctx.headers); + + // Arbitrary streams may be piped into 'ctx', like the contents of + // a file for example. + + const fs = require("fs"); + fs.createFileStream("index.js").pipe(ctx); + + // Any errors thrown in a handler are caught and sent as a JSON + // response. + + throw new Error("This shouldn't happen!"); + + // The 'ctx' stream must be closed, either manually via 'ctx.end()' or + // via a function. Not closing 'ctx' will cause the handler to deadlock. + + // DO NOT DO THIS! + // ctx.write("hello world!"); + + // DO THIS! + ctx.write("hello world!"); + ctx.end(); + + // OR THIS! + ctx.send("hello world!"); + + // The line below reads the request body into a buffer up to 65536 bytes. + // If the body exceeds 65536 bytes, an error will be thrown. + + const body = await ctx.read({ limit: 65536 }); + console.log("I got this message:", body.toString("utf8")); +}; +``` + +### API Gateway + +The configuration file for the API gateway is written in [TOML](https://github.com/toml-lang/toml). + +```toml +# Address to listen for other Flatend nodes on. +addr = "127.0.0.1:9000" + +[[http]] +https = true # Enable/disable HTTPS support. Default is false. + +# Domain(s) for HTTPS support. Ignored if https = false. +domain = "lithdew.net" +domains = ["a.lithdew.net", "b.lithdew.net"] + +# Addresses to serve HTTP requests on. +# Default is :80 if https = false, and :443 if https = true. + +addr = ":3000" +addrs = [":3000", ":4000", "127.0.0.1:9000"] + +# Remove trailing slashes in HTTP route path? Default is true. +redirect_trailing_slash = true + +# Redirect to the exact configured HTTP route path? Default is true. +redirect_fixed_path = true + +[http.timeout] +read = "10s" # HTTP request read timeout. Default is 10s. +read_header = "10s" # HTTP request header read timeout. Default is 10s. +idle = "10s" # Idle connection timeout. Default is 10s. +write = "10s" # HTTP response write timeout. Default is 10s. +shutdown = "10s" # Graceful shutdown timeout. Default is 10s. + +[http.min] +body_size = 1048576 # Min HTTP request body size in bytes. + +[http.max] +header_size = 1048576 # Max HTTP request header size in bytes. +body_size = 1048576 # Max HTTP request body size in bytes. + +# The route below serves the contents of the file 'config.toml' upon +# recipient of a 'GET' request at path '/'. The contents of the file +# are instructed to not be cached to the requester. + +# By default, caching for static files that are served is enabled. +# Instead of a file, a directory may be statically served as well. + +[[http.routes]] +path = "GET /" +static = "config.toml" +nocache = true + +# The route below takes an URL route parameter ':id', and includes it +# in a request sent to any Flatend node we know that advertises +# themselves of handling the service 'a', 'b', or 'c'. The HTTP +# request body, query parameters, and headers are additionally +# sent to the node. + +[[http.routes]] +path = "POST /:id" +services = ["a", "b", "c"] +``` + +## Build from source + +```shell +$ git clone https://github.com/lithdew/flatend.git && cd flatend +Cloning into 'flatend'... +remote: Enumerating objects: 290, done. +remote: Counting objects: 100% (290/290), done. +remote: Compressing objects: 100% (186/186), done. +remote: Total 1063 (delta 144), reused 231 (delta 97), pack-reused 773 +Receiving objects: 100% (1063/1063), 419.83 KiB | 796.00 KiB/s, done. +Resolving deltas: 100% (571/571), done. + +$ go version +go version go1.14.4 linux/amd64 + +$ go build ./cmd/flatend +``` + +## Showcase + + + +[**Mask Demand Calculator**](https://wars-mask.surge.sh/en) - Helps you quickly calculate the amount of masks your household needs. Serving scraped RSS feeds with Flatend to more than 200K+ site visitors. + +## Help + +Got a question? Either: + +1. Create an [issue](https://github.com/lithdew/flatend/issues/new). +2. Chat with us on [Discord](https://discord.gg/HZEbkeQ). + +## FAQ + +#### Is flatend production-ready? Who uses flatend today? + +_flatend is still a heavy work-in-progress_. That being said, it is being field tested with a few enterprise projects related to energy and IoT right now. + +Deployments of flatend have also been made with a few hundred thousand visitors. + +#### Will I be able to run flatend myself? + +It was built from the start to allow for self-hosting on the cloud, on bare-metal servers, in Docker containers, on Kubernetes, etc. The cloud is your limit (see the pun I did there?). + +#### I'm worried about vendor lock-in - what happens if flatend goes out of business? + +flatend's code is completely open in this single Github repository: there's no funny business going on here. + +The mission of flatend is to eliminate vendor lock-in and be agnostic to any kinds of hosting environments starting from day one. Also to be somewhat of a breath of fresh air to the existing low-code tools out there. + +#### How does flatend compare to `XXX`? + +flatend gives me enough flexibility as a developer to use the tools and deployment patterns I want, gives me the scalability/performance I need, and at the same time lets me be very productive in building products/services quick. + +flatend amalgamates a lot of what I sort of wish I had while building roughly tens of hackathon projects and startup projects. + +For example, in many cases I just want to spend two bucks a month knowing that the things I build can easily handle a load of thousands of request per second. + +Using the API gateways pre-provided with flatend, I can easily build a system that supports that and rapidly prototype its business logic in NodeJS. + +#### Who owns the code that I write in flatend, and the data that I and my users save in flatend? + +You own the data and the code. All the code is MIT licensed, and strongly compliant with GDPR/CCPA as well. + +All communication across microservices are fully-encrypted end-to-end using AES-256 Galois Counter Mode (GCM). Encryption keys are ephemeral and established per-session, and are established using a X25519 Diffie-Hellman handshake followed by a single pass of BLAKE-2b 256-bit. + +Y'know, basically just a hyper-specific standard configuration setting of the [Noise Protocol](http://www.noiseprotocol.org/). + +#### I have a 3rd party/legacy system that I need to use with my backend. Can I still use flatend? + +flatend from the start was made to be agnostic to whichever databases, programming languages, tools, or hosting environments you choose to put it through. + +At the end of the day, flatend is just a protocol. That being said, to use flatend with your system would require writing a sort of shim or SDK for it. + +Reach out to us on Discord, maybe the system you are looking to support may be an integration point well worth providing a reference implementation for. + +## License + +**flatend**, and all of its source code is released under the [MIT License](LICENSE). diff --git a/deno/package.json b/deno/package.json new file mode 100644 index 0000000..fde9f11 --- /dev/null +++ b/deno/package.json @@ -0,0 +1,35 @@ +{ + "name": "flatend", + "description": "Production-ready microservice mesh networks with just a few lines of code.", + "author": "Kenta Iwasaki", + "license": "MIT", + "version": "0.0.8", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "prepare": "yarn tsc", + "test": "TS_NODE_PROJECT=tests/tsconfig.json mocha -r ts-node/register tests/**/*.spec.ts" + }, + "dependencies": { + "blake2b": "^2.1.3", + "debug": "^4.1.1", + "ipaddr.js": "^1.9.1", + "object-hash": "^2.0.3", + "tweetnacl": "^1.0.3" + }, + "devDependencies": { + "@types/chai": "^4.2.11", + "@types/debug": "^4.1.5", + "@types/ip": "^1.1.0", + "@types/mocha": "^7.0.2", + "@types/node": "^14.0.13", + "@types/object-hash": "^1.3.3", + "chai": "^4.2.0", + "chai-bytes": "^0.1.2", + "core-js": "^3.6.5", + "mocha": "^8.0.1", + "prettier": "^2.0.5", + "ts-node": "^8.10.2", + "typescript": "^3.9.5" + } +} diff --git a/deno/src/context.ts b/deno/src/context.ts new file mode 100644 index 0000000..8a6481b --- /dev/null +++ b/deno/src/context.ts @@ -0,0 +1,141 @@ +import { Buffer } from "https://deno.land/std/node/buffer.ts"; +import { Duplex, finished } from "./std-node-stream.ts"; +import { Stream, STREAM_CHUNK_SIZE } from "./stream.ts"; +import { ID } from "./kademlia.ts"; +import * as util from 'https://deno.land/std/node/util.ts' +import { DataPacket, Opcode, ServiceResponsePacket } from "./packet.ts"; +import { Provider } from "./provider.ts"; +import { chunkBuffer } from "./node.ts"; + +export type BufferEncoding = string + +export type Handler = (ctx: Context) => void; + +export class Context extends Duplex { + _provider: Provider; + _stream: Stream; + _headersWritten = false; + _headers: { [key: string]: string } = {}; + + headers: { [key: string]: string }; + + get id(): ID { + return this._provider.id!; + } + + constructor( + provider: Provider, + stream: Stream, + headers: { [key: string]: string } + ) { + super(); + + this._provider = provider; + this._stream = stream; + this.headers = headers; + + // pipe stream body to context + + setTimeout(async () => { + for await (const frame of this._stream.body) { + this.push(frame); + } + this.push(null); + }); + + // write stream eof when stream writable is closed + + setTimeout(async () => { + await util.promisify(finished)(this, { readable: false }); + + await this._writeHeader(); + + const payload = new DataPacket(this._stream.id, Buffer.from([])).encode(); + await this._provider.write( + this._provider.rpc.message( + 0, + Buffer.concat([Buffer.of(Opcode.Data), payload]) + ) + ); + }); + } + + header(key: string, val: string): Context { + this._headers[key] = val; + return this; + } + + send(data: string | Buffer | Uint8Array) { + this.write(data); + if (!this.writableEnded) this.end(); + } + + json(data: any) { + this.header("content-type", "application/json"); + this.send(JSON.stringify(data)); + } + + _read(size: number) { + this._stream.body._read(size); + } + + async body(opts?: { limit?: number }): Promise { + const limit = opts?.limit ?? 2 ** 16; + + let buf = Buffer.from([]); + for await (const chunk of this) { + buf = Buffer.concat([buf, chunk]); + if (buf.byteLength > limit) { + throw new Error( + `Exceeded max allowed body size limit of ${limit} byte(s).` + ); + } + } + + return buf; + } + + async _writeHeader() { + if (!this._headersWritten) { + this._headersWritten = true; + + const payload = new ServiceResponsePacket( + this._stream.id, + true, + this._headers + ).encode(); + await this._provider.write( + this._provider.rpc.message( + 0, + Buffer.concat([Buffer.of(Opcode.ServiceResponse), payload]) + ) + ); + } + } + + _write( + chunk: any, + encoding: BufferEncoding, + callback: (error?: Error | null) => void + ) { + const write = async () => { + await this._writeHeader(); + + const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding); + + for (const chunk of chunkBuffer(buf, STREAM_CHUNK_SIZE)) { + const payload = new DataPacket(this._stream.id, chunk).encode(); + await this._provider.write( + this._provider.rpc.message( + 0, + Buffer.concat([Buffer.of(Opcode.Data), payload]) + ) + ); + } + }; + + write() + .then(() => callback()) + .catch((error) => callback(error)); + } +} diff --git a/deno/src/kademlia.ts b/deno/src/kademlia.ts new file mode 100644 index 0000000..6cd83bf --- /dev/null +++ b/deno/src/kademlia.ts @@ -0,0 +1,192 @@ +import { Buffer } from "https://deno.land/std/node/buffer.ts"; +import * as nacl from "https://deno.land/x/tweetnacl_deno/src/nacl.ts"; +import ipaddr from "https://jspm.dev/ipaddr.js"; +import { assert } from "https://deno.land/std/testing/asserts.ts"; + +function BufferCompare(a: Buffer | Uint8Array, b: Buffer|Uint8Array) { + //if (typeof a.compare === 'function') return a.compare(b) + if (a === b) return 0 + + var x = a.length + var y = b.length + + var i = 0 + var len = Math.min(x, y) + while (i < len) { + if (a[i] !== b[i]) break + + ++i + } + + if (i !== len) { + x = a[i] + y = b[i] + } + + if (x < y) return -1 + if (y < x) return 1 + return 0 +} + +type IPv4 = ipaddr.IPv4; +type IPv6 = ipaddr.IPv6; + +export enum UpdateResult { + New, + Ok, + Full, + Fail, +} + +const leadingZeros = (buf: Uint8Array): number => { + const i = buf.findIndex((b) => b != 0); + if (i === -1) return buf.byteLength * 8; + + let b = buf[i] >>> 0; + if (b === 0) return i * 8 + 8; + return i * 8 + ((7 - ((Math.log(b) / Math.LN2) | 0)) | 0); +}; + +const xor = (a: Uint8Array, b: Uint8Array): Uint8Array => { + const c = Buffer.alloc(Math.min(a.byteLength, b.byteLength)); + for (let i = 0; i < c.byteLength; i++) c[i] = a[i] ^ b[i]; + return c; +}; + +export class ID { + publicKey: Uint8Array = Buffer.alloc(nacl.SignLength.PublicKey); + host: IPv4 | IPv6; + port: number = 0; + + constructor(publicKey: Uint8Array, host: IPv4 | IPv6, port: number) { + this.publicKey = publicKey; + this.host = host; + this.port = port; + } + + get addr(): string { + let host = this.host; + if (host.kind() === "ipv6" && (host).isIPv4MappedAddress()) { + host = (host).toIPv4Address(); + } + return host.toString() + ":" + this.port; + } + + public encode(): Buffer { + let host = Buffer.of(...this.host.toByteArray()); + host = Buffer.concat([Buffer.of(host.byteLength === 4 ? 0 : 1), host]); + + const port = Buffer.alloc(2); + port.writeUInt16BE(this.port); + + return Buffer.concat([this.publicKey, host, port]); + } + + public static decode(buf: Buffer): [ID, Buffer] { + const publicKey = Uint8Array.from(buf.slice(0, nacl.SignLength.PublicKey)); + buf = buf.slice(nacl.SignLength.PublicKey); + + const hostHeader = buf.readUInt8(); + buf = buf.slice(1); + + assert(hostHeader === 0 || hostHeader === 1); + + const hostLen = hostHeader === 0 ? 4 : 16; + const host = ipaddr.fromByteArray([...buf.slice(0, hostLen)]); + buf = buf.slice(hostLen); + + const port = buf.readUInt16BE(); + buf = buf.slice(2); + + return [new ID(publicKey, host, port), buf]; + } +} + +export class Table { + buckets: Array> = [ + ...Array(nacl.SignLength.PublicKey * 8), + ].map(() => []); + + pub: Uint8Array; + cap: number = 16; + length: number = 0; + + public constructor( + pub: Uint8Array = Buffer.alloc(nacl.SignLength.PublicKey) + ) { + this.pub = pub; + } + + private bucketIndex(pub: Uint8Array): number { + if (BufferCompare(pub, this.pub) === 0) return 0; + return leadingZeros(xor(pub, this.pub)); + } + + public update(id: ID): UpdateResult { + if (BufferCompare(id.publicKey, this.pub) === 0) return UpdateResult.Fail; + + const bucket = this.buckets[this.bucketIndex(id.publicKey)]; + + const i = bucket.findIndex( + (item) => BufferCompare(item.publicKey, id.publicKey) === 0 + ); + if (i >= 0) { + bucket.unshift(...bucket.splice(i, 1)); + return UpdateResult.Ok; + } + + if (bucket.length < this.cap) { + bucket.unshift(id); + this.length++; + return UpdateResult.New; + } + return UpdateResult.Full; + } + + public delete(pub: Uint8Array): boolean { + const bucket = this.buckets[this.bucketIndex(pub)]; + const i = bucket.findIndex((id) => BufferCompare(id.publicKey, pub) === 0); + if (i >= 0) { + bucket.splice(i, 1); + this.length--; + return true; + } + return false; + } + + public has(pub: Uint8Array): boolean { + const bucket = this.buckets[this.bucketIndex(pub)]; + return !!bucket.find((id) => BufferCompare(id.publicKey, pub) === 0); + } + + public closestTo(pub: Uint8Array, k = this.cap): ID[] { + const closest: ID[] = []; + + const fill = (i: number) => { + const bucket = this.buckets[i]; + for (let i = 0; closest.length < k && i < bucket.length; i++) { + if (BufferCompare(bucket[i].publicKey, pub) != 0) + closest.push(bucket[i]); + } + }; + + const m = this.bucketIndex(pub); + + fill(m); + + for ( + let i = 1; + closest.length < k && (m - i >= 0 || m + i < this.buckets.length); + i++ + ) { + if (m - i >= 0) fill(m - i); + if (m + i < this.buckets.length) fill(m + i); + } + + closest.sort((a: ID, b: ID) => + BufferCompare(xor(a.publicKey, pub), xor(b.publicKey, pub)) + ); + + return closest.length > k ? closest.slice(0, k) : closest; + } +} diff --git a/deno/src/mod.ts b/deno/src/mod.ts new file mode 100644 index 0000000..8521f61 --- /dev/null +++ b/deno/src/mod.ts @@ -0,0 +1,14 @@ +export { Node, generateSecretKey } from "./node.ts"; +export { Context } from "./context.ts"; +export { ID, Table, UpdateResult } from "./kademlia.ts"; +export { getAvailableAddress, splitHostPort } from "./net.ts"; +export { Provider } from "./provider.ts"; +export { x25519, serverHandshake, clientHandshake, Session } from "./session.ts"; +export { + drain, + lengthPrefixed, + prefixLength, + RPC, + Stream, + Streams, +} from "./stream.ts"; diff --git a/deno/src/net.ts b/deno/src/net.ts new file mode 100644 index 0000000..36da440 --- /dev/null +++ b/deno/src/net.ts @@ -0,0 +1,50 @@ +import { IPv4, IPv6 } from "https://jspm.dev/ipaddr.js"; +import * as net from "./std-node-net.ts"; +import * as events from "https://deno.land/std/node/events.ts"; +import ipaddr from "https://jspm.dev/ipaddr.js"; + +/** + * Returns an available TCP host/port that may be listened to. + */ +export async function getAvailableAddress(): Promise<{ + family: string; + host: IPv4 | IPv6; + port: number; +}> { + const server = net.createServer(); + server.unref(); + server.listen(); + + await events.once(server, "listening"); + + const info = (server.address())!; + + let host = ipaddr.parse(info.address.length === 0 ? "0.0.0.0" : info.address); + if (host.kind() === "ipv6" && (host).isIPv4MappedAddress()) { + host = (host).toIPv4Address(); + } + + server.close(); + + await events.once(server, "close"); + + return { family: info.family, host, port: info.port }; +} + +export function splitHostPort( + addr: string +): { host: IPv4 | IPv6; port: number } { + const fields = addr.split(":").filter((field) => field.length > 0); + if (fields.length === 0) + throw new Error("Unable to split host:port from address."); + + const port = parseInt(fields.pop()!); + if (port < 0 || port > 2 ** 16) throw new Error(`Port ${port} is invalid.`); + + let host = ipaddr.parse(fields.length === 0 ? "0.0.0.0" : fields.join(":")); + if (host.kind() === "ipv6" && (host).isIPv4MappedAddress()) { + host = (host).toIPv4Address(); + } + + return { host, port }; +} diff --git a/deno/src/node.ts b/deno/src/node.ts new file mode 100644 index 0000000..f0057d8 --- /dev/null +++ b/deno/src/node.ts @@ -0,0 +1,620 @@ +import { Buffer } from "https://deno.land/std/node/buffer.ts"; +import { debug } from "https://deno.land/std/log/mod.ts"; +import { Context, Handler } from "./context.ts"; +import * as net from "./std-node-net.ts"; +import { ID, Table } from "./kademlia.ts"; +import * as nacl from 'https://deno.land/x/tweetnacl_deno/src/nacl.ts' +import { getAvailableAddress, splitHostPort } from "./net.ts"; +// import ipaddr from "https://jspm.dev/ipaddr.js"; +import { + DataPacket, + FindNodeRequest, + FindNodeResponse, + HandshakePacket, + Opcode, + ServiceRequestPacket, + ServiceResponsePacket, +} from "./packet.ts"; +import * as events from "https://deno.land/std/node/events.ts"; +import { clientHandshake, serverHandshake, Session } from "./session.ts"; +import { Provider } from "./provider.ts"; +import hash from "https://jspm.dev/object-hash"; + +export interface NodeOptions { + // A reachable, public address which peers may reach you on. + // The format of the address must be [host]:[port]. + publicAddr?: string; + + // A list of [host]:[port] addresses which this node will bind a listener + // against to accept new Flatend nodes. + bindAddrs?: string[]; + + // A list of addresses to nodes to initially reach out + // for/bootstrap from first. + addrs?: string[]; + + // An Ed25519 secret key. A secret key must be provided to allow for + // peers to reach you. A secret key may be generated by calling + // 'flatend.generateSecretKey()'. + secretKey?: Uint8Array; + + // A mapping of service names to their respective handlers. + services?: { [key: string]: Handler }; +} + +export class Node { + services = new Map>(); + clients = new Map(); + servers = new Set(); + conns = new Set(); + table = new Table(); + + id?: ID; + keys?: nacl.SignKeyPair; + handlers: { [key: string]: Handler } = {}; + _shutdown = false; + + public static async start(opts: NodeOptions): Promise { + const node = new Node(); + + if (opts.services) node.handlers = opts.services; + + if (opts.secretKey) { + node.keys = nacl.sign_keyPair_fromSecretKey(opts.secretKey); + + debug(`Public Key: ${Buffer.from(node.keys.publicKey).toString("hex")}`); + + const bindAddrs = opts.bindAddrs ?? []; + if (bindAddrs.length === 0) { + if (opts.publicAddr) { + bindAddrs.push(opts.publicAddr); + } else { + const { host, port } = await getAvailableAddress(); + bindAddrs.push(host + ":" + port); + } + } + + // TODO: import from ipaddr + // let publicHost: ipaddr.IPv4 | ipaddr.IPv6; + let publicHost: any; + let publicPort: number; + + if (opts.publicAddr) { + const { host, port } = splitHostPort(opts.publicAddr); + publicHost = host; + publicPort = port; + } else { + const { host, port } = splitHostPort(bindAddrs[0]); + publicHost = host; + publicPort = port; + } + + node.id = new ID(node.keys.publicKey, publicHost, publicPort); + node.table = new Table(node.id.publicKey); + + const promises = []; + + for (const bindAddr of bindAddrs) { + const { host, port } = splitHostPort(bindAddr); + promises.push(node.listen({ host: host.toString(), port })); + } + + await Promise.all(promises); + } + + if (opts.addrs) { + const promises = []; + + for (const addr of opts.addrs) { + const { host, port } = splitHostPort(addr); + promises.push(node.connect({ host: host.toString(), port: port })); + } + + await Promise.all(promises); + await node.bootstrap(); + } + + return node; + } + + async bootstrap() { + const pub = this.id?.publicKey ?? Buffer.alloc(nacl.SignLength.PublicKey); + const visited = new Set(); + + let queue: ID[] = this.table.closestTo(pub, this.table.cap); + if (queue.length === 0) return; + + for (const id of queue) { + visited.add(Buffer.from(id.publicKey).toString("hex")); + } + + const closest: ID[] = []; + + while (queue.length > 0) { + const next: ID[] = []; + + await Promise.all( + queue.map(async (id) => { + const { host, port } = splitHostPort(id.addr); + + try { + const client = await this.connect({ host: host.toString(), port }); + + const res = FindNodeResponse.decode( + await client.request( + Buffer.concat([ + Buffer.of(Opcode.FindNodeRequest), + new FindNodeRequest(pub).encode(), + ]) + ) + )[0]; + + res.closest = res.closest.filter((id) => { + return !visited.has(Buffer.from(id.publicKey).toString("hex")); + }); + + closest.push(...res.closest); + next.push(...res.closest); + } catch (err) { + // ignore + } + }) + ); + + queue = next; + } + + debug(`Discovered ${closest.length} peer(s).`); + } + + /** + * Shuts down all active connections and listeners on this node. After shutting + * down a node, it may not be reused. \ + */ + async shutdown() { + if (this._shutdown) throw new Error("Node is shut down."); + + this._shutdown = true; + + const promises = []; + + for (const conn of this.conns) { + promises.push(events.once(conn, "close")); + conn.end(); + } + + for (const server of this.servers) { + promises.push(events.once(server, "close")); + server.close(); + } + + await Promise.all(promises); + } + + /** + * Provides a list of nodes that provide either one of the many specified services. + * + * @param services List of services. + */ + providersFor(services: string[]): Provider[] { + const map = this._providers(services).reduce( + (map: Map, provider: Provider) => + provider.id?.publicKey + ? map.set(hash(provider.id.publicKey), provider) + : map, + new Map() + ); + + return [...map.values()]; + } + + _providers(services: string[]): Provider[] { + const providers: Provider[] = []; + for (const service of services) { + const entries = this.services.get(service); + if (!entries) continue; + providers.push(...entries); + } + return providers; + } + + /** + * Request one of any available nodes to provide one of the many specified services. A request header + * may be attached to the request sent out to a designated node, along with a body. + * + * @param services List of services. + * @param headers Request headers. + * @param body The request body. Must not be null/undefined. + */ + async push( + services: string[], + headers: { [key: string]: string }, + body: AsyncIterable + ) { + if (this._shutdown) throw new Error("Node is shut down."); + + const providers = this._providers(services); + + for (const provider of providers) { + return await provider.push(services, headers, body); + } + + throw new Error( + `No nodes were able to process your request for service(s): [${services.join( + ", " + )}]` + ); + } + + /** + * Start listening for Flatend nodes at a specified IP family/host/port. + * + * @param opts IP family/host/port. + */ + async listen(opts: net.ListenOptions) { + if (this._shutdown) throw new Error("Node is shut down."); + + const server = net.createServer(async (conn) => { + this.conns.add(conn); + + setTimeout(async () => { + await events.once(conn, "close"); + this.conns.delete(conn); + }); + + try { + const secret = await serverHandshake(conn); + const session = new Session(secret); + + const provider = new Provider(conn, session, false); + setTimeout(() => this.read(provider)); + } catch (err) { + debug("Error from incoming node:", err); + conn.end(); + } + }); + + server.listen(opts); + + await events.once(server, "listening"); + + this.servers.add(server); + + setTimeout(async () => { + await events.once(server, "close"); + this.servers.delete(server); + }); + + const info = (server.address())!; + + debug(`Listening for Flatend nodes on '${info.address}:${info.port}'.`); + } + + /** + * Connect to a Flatend node and ask and keep track of the services it provides. + * + * @param opts Flatend node IP family/host/port. + */ + async connect(opts: net.NetConnectOpts) { + if (this._shutdown) throw new Error("Node is shut down."); + + let provider = this.clients.get(hash(opts)); + + if (!provider) { + const conn = net.connect(opts); + await events.once(conn, "connect"); + this.conns.add(conn); + + setTimeout(async () => { + await events.once(conn, "close"); + this.clients.delete(hash(opts)); + this.conns.delete(conn); + }); + + try { +console.log('connect beforehad') + const secret = await clientHandshake(conn); +console.log('connect aftershake') + const session = new Session(secret); +console.log('connect aftersess') + + provider = new Provider(conn, session, true); + this.clients.set(hash(opts), provider); + + setTimeout(() => this.read(provider!)); + + const handshake = new HandshakePacket( + this.id, + [...Object.keys(this.handlers)], + undefined + ); + if (this.keys) + handshake.signature = nacl.sign_detached( + handshake.payload, + this.keys.secretKey + ); + + const response = await provider.request( + Buffer.concat([Buffer.of(Opcode.Handshake), handshake.encode()]) + ); + const packet = HandshakePacket.decode(response)[0]; + + provider.handshaked = true; + + if (packet.id && packet.signature) { + if ( + !nacl.sign_detached_verify( + packet.payload, + packet.signature, + packet.id.publicKey + ) + ) { + throw new Error(`Handshake packet signature is malformed.`); + } + provider.id = packet.id; + this.table.update(provider.id); + } + + debug( + `You have connected to '${ + provider.addr + }'. Services: [${packet.services.join(", ")}]` + ); + + for (const service of packet.services) { + provider.services.add(service); + + let providers = this.services.get(service); + if (!providers) { + providers = new Set(); + this.services.set(service, providers); + } + providers.add(provider); + } + + setTimeout(async () => { + await events.once(provider!.sock, "end"); + + debug( + `'${ + provider!.addr + }' has disconnected from you. Services: [${packet.services.join( + ", " + )}]` + ); + + if (provider!.id) { + this.table.delete(provider!.id.publicKey); + } + + for (const service of packet.services) { + let providers = this.services.get(service)!; + if (!providers) continue; + + providers.delete(provider!); + if (providers.size === 0) this.services.delete(service); + } + }); + + setTimeout(async () => { + await events.once(provider!.sock, "end"); + + if (this._shutdown) return; + + let count = 8; + + const reconnect = async () => { + if (this._shutdown) return; + + if (count-- === 0) { + debug( + `Tried 8 times reconnecting to ${provider!.addr}. Giving up.` + ); + return; + } + + debug( + `Trying to reconnect to '${provider!.addr}'. Sleeping for 500ms.` + ); + + try { + await this.connect(opts); + } catch (err) { + setTimeout(reconnect, 500); + } + }; + + setTimeout(reconnect, 500); + }); + } catch (err) { + conn.end(); + throw err; + } + } + + return provider; + } + + async read(provider: Provider) { + try { + await this._read(provider); + } catch (err) { + debug("Provider had shut down with an error:", err); + } + + provider.sock.end(); + } + + async _read(provider: Provider) { + for await (const { seq, opcode, frame } of provider.read()) { + await this._handle(provider, seq, opcode, frame); + } + } + + async _handle( + provider: Provider, + seq: number, + opcode: number, + frame: Buffer + ) { + switch (opcode) { + case Opcode.Handshake: { + if (provider.handshaked) { + throw new Error("Provider attempted to handshake twice."); + } + provider.handshaked = true; + + const packet = HandshakePacket.decode(frame)[0]; + if (packet.id && packet.signature) { + if ( + !nacl.sign_detached_verify( + packet.payload, + packet.signature, + packet.id.publicKey + ) + ) { + throw new Error(`Handshake packet signature is malformed.`); + } + provider.id = packet.id; + this.table.update(provider.id); + } + + debug( + `'${ + provider.addr + }' has connected to you. Services: [${packet.services.join(", ")}]` + ); + + for (const service of packet.services) { + provider.services.add(service); + + let providers = this.services.get(service); + if (!providers) { + providers = new Set(); + this.services.set(service, providers); + } + providers.add(provider); + } + + setTimeout(async () => { + await events.once(provider.sock, "end"); + + debug( + `'${ + provider.addr + }' has disconnected from you. Services: [${packet.services.join( + ", " + )}]` + ); + + if (provider.id) { + this.table.delete(provider.id.publicKey); + } + + for (const service of packet.services) { + let providers = this.services.get(service)!; + if (!providers) continue; + + providers.delete(provider); + if (providers.size === 0) this.services.delete(service); + } + }); + + const response = new HandshakePacket( + this.id, + [...Object.keys(this.handlers)], + undefined + ); + if (this.keys) + response.signature = nacl.sign_detached( + response.payload, + this.keys.secretKey + ); + + await provider.write(provider.rpc.message(seq, response.encode())); + + return; + } + case Opcode.ServiceRequest: { + const packet = ServiceRequestPacket.decode(frame)[0]; + const stream = provider.streams.register(packet.id); + + const service = packet.services.find( + (service) => service in this.handlers + ); + if (!service) { + const payload = new ServiceResponsePacket( + packet.id, + false, + {} + ).encode(); + await provider.write( + provider.rpc.message( + 0, + Buffer.concat([Buffer.of(Opcode.ServiceResponse), payload]) + ) + ); + } else { + const ctx = new Context(provider, stream, packet.headers); + const handler = this.handlers[service]; + + setTimeout(async () => { + try { + await handler(ctx); + } catch (err) { + if (!ctx.writableEnded) { + ctx.json({ error: err?.message ?? "Internal server error." }); + } + } + }); + } + + return; + } + case Opcode.ServiceResponse: { + const packet = ServiceResponsePacket.decode(frame)[0]; + const stream = provider.streams.get(packet.id); + if (!stream) { + throw new Error( + `Got response headers for stream ID ${packet.id} which is not registered.` + ); + } + provider.streams.pull(stream, packet.handled, packet.headers); + return; + } + case Opcode.Data: { + const packet = DataPacket.decode(frame)[0]; + const stream = provider.streams.get(packet.id); + if (!stream) { + throw new Error( + `Got data for stream ID ${packet.id} which is not registered, or has ended.` + ); + } + provider.streams.recv(stream, packet.data); + return; + } + case Opcode.FindNodeRequest: { + const packet = FindNodeRequest.decode(frame)[0]; + const response = new FindNodeResponse( + this.table.closestTo(packet.target, this.table.cap) + ); + await provider.write(provider.rpc.message(seq, response.encode())); + } + } + } +} + +/** + * Generates an Ed25519 secret key for a node. + */ +export function generateSecretKey(): Buffer { + return Buffer.from(nacl.sign_keyPair().secretKey); +} + +export function* chunkBuffer(buf: Buffer, size: number) { + while (buf.byteLength > 0) { + size = size > buf.byteLength ? buf.byteLength : size; + yield buf.slice(0, size); + buf = buf.slice(size); + } +} diff --git a/deno/src/packet.ts b/deno/src/packet.ts new file mode 100644 index 0000000..61efc61 --- /dev/null +++ b/deno/src/packet.ts @@ -0,0 +1,333 @@ +import { Buffer } from "https://deno.land/std/node/buffer.ts"; +import * as nacl from "https://deno.land/x/tweetnacl_deno/src/nacl.ts"; +import {assert} from "https://deno.land/std/testing/asserts.ts"; +import { ID } from "./kademlia.ts"; + +export enum Opcode { + Handshake, + ServiceRequest, + ServiceResponse, + Data, + FindNodeRequest, + FindNodeResponse, +} + +export class HandshakePacket { + id?: ID; + services: string[] = []; + signature?: Uint8Array; + + constructor( + id: ID | undefined, + services: string[], + signature: Uint8Array | undefined + ) { + 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")), + ]); + } + + public encode(): Buffer { + const id = this.id + ? Buffer.concat([Buffer.of(1), this.id.encode()]) + : Buffer.of(0); + const services = this.services.reduce( + (result, service) => + Buffer.concat([ + result, + Buffer.of(service.length), + Buffer.from(service, "utf8"), + ]), + Buffer.of(this.services.length) + ); + const signature = this.id && this.signature ? this.signature : Buffer.of(); + + return Buffer.concat([id, services, signature]); + } + + public static decode(buf: Buffer): [HandshakePacket, Buffer] { + const header = buf.readUInt8(); + buf = buf.slice(1); + + assert(header === 0 || header === 1); + + const result: [ID | undefined, Buffer] = + header === 1 ? ID.decode(buf) : [undefined, buf]; + + const id = result[0]; + buf = result[1]; + + const size = buf.readUInt8(); + buf = buf.slice(1); + + const services: string[] = [...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"); + }); + + let signature: Buffer | undefined; + if (id) { + signature = buf.slice(0, nacl.SignLength.Signature); + buf = buf.slice(nacl.SignLength.Signature); + } + + return [new HandshakePacket(id, services, signature), buf]; + } +} + +export class ServiceRequestPacket { + id: number; + services: string[] = []; + headers: { [key: string]: string }; + + public constructor( + id: number, + services: string[], + headers: { [key: string]: string } + ) { + this.id = id; + this.services = services; + this.headers = headers; + } + + public encode(): Buffer { + const id = Buffer.alloc(4); + id.writeUInt32BE(this.id); + + const services = this.services.reduce( + (result, service) => + Buffer.concat([ + result, + Buffer.of(service.length), + Buffer.from(service, "utf8"), + ]), + Buffer.of(this.services.length) + ); + + const headersLen = Buffer.alloc(2); + headersLen.writeUInt16BE(Object.keys(this.headers).length); + + const headers = Object.keys(this.headers).reduce((result, key) => { + const value = this.headers[key]; + + const keyBuf = Buffer.concat([ + Buffer.of(key.length), + Buffer.from(key, "utf8"), + ]); + const valueBuf = Buffer.concat([ + Buffer.alloc(2), + Buffer.from(value, "utf8"), + ]); + valueBuf.writeUInt16BE(value.length); + + return Buffer.concat([result, keyBuf, valueBuf]); + }, headersLen); + + return Buffer.concat([id, services, headers]); + } + + public static decode(buf: Buffer): [ServiceRequestPacket, Buffer] { + const id = buf.readUInt32BE(); + buf = buf.slice(4); + + const servicesLen = buf.readUInt8(); + buf = buf.slice(1); + + const services: string[] = [...Array(servicesLen)].map(() => { + const serviceLen = buf.readUInt8(); + buf = buf.slice(1); + + const service = buf.slice(0, serviceLen); + buf = buf.slice(serviceLen); + + return service.toString("utf8"); + }); + + const headersLen = buf.readUInt16BE(); + buf = buf.slice(2); + + const headers = [...Array(headersLen)].reduce((map, _) => { + const keyLen = buf.readUInt8(); + buf = buf.slice(1); + + const key = buf.slice(0, keyLen).toString("utf8"); + buf = buf.slice(keyLen); + + const valueLen = buf.readUInt16BE(); + buf = buf.slice(2); + + const value = buf.slice(0, valueLen).toString("utf8"); + buf = buf.slice(valueLen); + + map[key] = value; + return map; + }, {}); + + return [new ServiceRequestPacket(id, services, headers), buf]; + } +} + +export class ServiceResponsePacket { + id: number; + handled: boolean = false; + headers: { [key: string]: string }; + + public constructor( + id: number, + handled: boolean, + headers: { [key: string]: string } + ) { + this.id = id; + this.handled = handled; + this.headers = headers; + } + + public encode(): Buffer { + const id = Buffer.alloc(4); + id.writeUInt32BE(this.id); + + const handled = Buffer.of(this.handled ? 1 : 0); + + const headersLen = Buffer.alloc(2); + headersLen.writeUInt16BE(Object.keys(this.headers).length); + + const headers = Object.keys(this.headers).reduce((result, key) => { + const value = this.headers[key]; + + const keyBuf = Buffer.concat([ + Buffer.of(key.length), + Buffer.from(key, "utf8"), + ]); + const valueBuf = Buffer.concat([ + Buffer.alloc(2), + Buffer.from(value, "utf8"), + ]); + valueBuf.writeUInt16BE(value.length); + + return Buffer.concat([result, keyBuf, valueBuf]); + }, headersLen); + + return Buffer.concat([id, handled, headers]); + } + + public static decode(buf: Buffer): [ServiceResponsePacket, Buffer] { + const id = buf.readUInt32BE(); + buf = buf.slice(4); + + const handled = buf.readUInt8() === 1; + buf = buf.slice(1); + + const headersLen = buf.readUInt16BE(); + buf = buf.slice(2); + + const headers = [...Array(headersLen)].reduce((map, _) => { + const keyLen = buf.readUInt8(); + buf = buf.slice(1); + + const key = buf.slice(0, keyLen).toString("utf8"); + buf = buf.slice(keyLen); + + const valueLen = buf.readUInt16BE(); + buf = buf.slice(2); + + const value = buf.slice(0, valueLen).toString("utf8"); + buf = buf.slice(valueLen); + + map[key] = value; + return map; + }, {}); + + return [new ServiceResponsePacket(id, handled, headers), buf]; + } +} + +export class DataPacket { + id: number; + data: Buffer; + + public constructor(id: number, data: Buffer) { + this.id = id; + this.data = data; + } + + public encode(): Buffer { + const id = Buffer.alloc(4); + id.writeUInt32BE(this.id); + + const dataLen = Buffer.alloc(2); + dataLen.writeUInt16BE(this.data.byteLength); + + return Buffer.concat([id, dataLen, this.data]); + } + + public static decode(buf: Buffer): [DataPacket, Buffer] { + const id = buf.readUInt32BE(); + buf = buf.slice(4); + + const dataLen = buf.readUInt16BE(); + buf = buf.slice(2); + + const data = buf.slice(0, dataLen); + buf = buf.slice(dataLen); + + return [new DataPacket(id, data), buf]; + } +} + +export class FindNodeRequest { + target: Uint8Array; + + public constructor(target: Uint8Array) { + this.target = target; + } + + public encode(): Buffer { + return Buffer.from(this.target); + } + + public static decode(buf: Buffer): [FindNodeRequest, Buffer] { + const target = buf.slice(0, nacl.SignLength.PublicKey); + buf = buf.slice(nacl.SignLength.PublicKey); + return [new FindNodeRequest(target), buf]; + } +} + +export class FindNodeResponse { + closest: ID[]; + + public constructor(closest: ID[]) { + this.closest = closest; + } + + public encode(): Buffer { + return Buffer.concat([ + Buffer.of(this.closest.length), + ...this.closest.map((id) => id.encode()), + ]); + } + + public static decode(buf: Buffer): [FindNodeResponse, Buffer] { + const closestLen = buf.readUInt8(); + buf = buf.slice(1); + + const closest = [...Array(closestLen)].map(() => { + const [id, leftover] = ID.decode(buf); + buf = leftover; + return id; + }); + + return [new FindNodeResponse(closest), buf]; + } +} diff --git a/deno/src/provider.ts b/deno/src/provider.ts new file mode 100644 index 0000000..9152e65 --- /dev/null +++ b/deno/src/provider.ts @@ -0,0 +1,96 @@ +import { Buffer } from "https://deno.land/std/node/buffer.ts"; +import { ID } from "./kademlia.ts"; +import * as net from "./std-node-net.ts"; +import { Session } from "./session.ts"; +import { + drain, + lengthPrefixed, + prefixLength, + RPC, + Stream, + Streams, +} from "./stream.ts"; +import { Opcode } from "./packet.ts"; + +export class Provider { + id?: ID; + handshaked = false; + services = new Set(); + + sock: net.Socket; + session: Session; + rpc: RPC; + streams: Streams; + + get addr(): string { + if (this.id) return this.id.addr; + return ""; + } + + constructor(sock: net.Socket, session: Session, client: boolean) { + this.sock = sock; + this.session = session; + this.rpc = new RPC(client); + this.streams = new Streams(client); + } + + async write(buf: Buffer) { + buf = prefixLength(this.session.encrypt(buf)); + if (!this.sock.write(buf)) await drain(this.sock); + } + + async request(data: Buffer): Promise { + const [req, res] = this.rpc.request(data); + await this.write(req); + return await res; + } + + async *read() { + const stream = this.rpc.parse( + this.session.decrypted(lengthPrefixed(this.sock)) + ); + for await (let { seq, frame } of stream) { + if (frame.byteLength < 1) + throw new Error(`Frame must be prefixed with an opcode byte.`); + + const opcode = frame.readUInt8(); + frame = frame.slice(1); + + yield { seq, opcode, frame }; + } + } + + async push( + services: string[], + headers: { [key: string]: string }, + body: AsyncIterable + ): Promise { + const err = new Error( + `No nodes were able to process your request for service(s): [${services.join( + ", " + )}]` + ); + + const stream = this.streams.register(); + const [header, handled] = this.streams.push(stream, services, headers); + + await this.write( + this.rpc.message( + 0, + Buffer.concat([Buffer.of(Opcode.ServiceRequest), header]) + ) + ); + + for await (const chunk of this.streams.encoded(stream.id, body)) { + await this.write( + this.rpc.message(0, Buffer.concat([Buffer.of(Opcode.Data), chunk])) + ); + } + + if (!(await handled)) { + throw err; + } + + return stream; + } +} diff --git a/deno/src/session.ts b/deno/src/session.ts new file mode 100644 index 0000000..ac28626 --- /dev/null +++ b/deno/src/session.ts @@ -0,0 +1,88 @@ +import { Buffer } from "https://deno.land/std/node/buffer.ts"; +import * as net from "./std-node-net.ts"; +import * as nacl from "https://deno.land/x/tweetnacl_deno/src/nacl.ts"; +import * as events from "https://deno.land/std/node/events.ts"; +import crypto from "./std-node-crypto.ts"; + +import { blake2b } from "https://deno.land/x/blake2b/mod.ts"; + +export function x25519(privateKey: Uint8Array, publicKey: Uint8Array): Buffer { + return Buffer.from(blake2b(nacl.scalarMult(privateKey, publicKey), undefined, undefined, 32).toString()); +} + +export async function serverHandshake(conn: net.Socket): Promise { + const serverKeys = nacl.box_keyPair(); + + await events.once(conn, "readable"); + const clientPublicKey = conn.read(nacl.BoxLength.PublicKey); + + conn.write(serverKeys.publicKey); + + return x25519(serverKeys.secretKey, clientPublicKey); +} + +export async function clientHandshake(client: net.Socket): Promise { + const clientKeys = nacl.box_keyPair(); + + client.write(clientKeys.publicKey); +console.log('hanread') + await events.once(client, "readable"); + const serverPublicKey = client.read(nacl.BoxLength.PublicKey); + +console.log('handpuh', serverPublicKey) + return x25519(clientKeys.secretKey, serverPublicKey); +} + +export class Session { + secret: Uint8Array; + readNonce: bigint = BigInt(0); + writeNonce: bigint = BigInt(0); + + constructor(secret: Uint8Array) { + this.secret = secret; + } + + public async *decrypted(stream: AsyncIterable) { + for await (const frame of stream) { + yield this.decrypt(frame); + } + } + + public encrypt(src: string | ArrayBufferLike): Buffer { + // @ts-ignore + const buf = Buffer.isBuffer(src) ? src : Buffer.from(src); + + const nonce = Buffer.alloc(12); + nonce.writeBigUInt64BE(this.writeNonce); + this.writeNonce = this.writeNonce + BigInt(1); + + 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()]); + } + + public decrypt(buf: Buffer): Buffer { + if (buf.byteLength < 16) throw new Error("Missing authentication tag."); + + const nonce = Buffer.alloc(12); + nonce.writeBigUInt64BE(this.readNonce); + this.readNonce = this.readNonce + BigInt(1); + + 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; + } +} diff --git a/deno/src/std-node-crypto.ts b/deno/src/std-node-crypto.ts new file mode 100644 index 0000000..873362f --- /dev/null +++ b/deno/src/std-node-crypto.ts @@ -0,0 +1,122 @@ +import { Buffer } from "https://deno.land/std/node/buffer.ts"; + +export default {createCipheriv, createDecipheriv} + +// export function createCipheriv( +// algorithm: CipherCCMTypes, +// key: CipherKey, +// iv: BinaryLike | null, +// options: CipherCCMOptions +// ): CipherCCM +// export function createCipheriv( +// algorithm: CipherGCMTypes, +// key: CipherKey, +// iv: BinaryLike | null, +// options?: CipherGCMOptions +// ): CipherGCM +export function createCipheriv( + algorithm: string, + key: CipherKey, + iv: BinaryLike | null, + options?: any//stream.TransformOptions +): Cipher{ + return new Cipher() +} + +class Cipher /*extends stream.Transform */{ + // private constructor() + update(data: BinaryLike): Buffer{ + console.log('todo: crypto.Cipher.update') + return Buffer.from([]) + } + // update(data: string, input_encoding: Utf8AsciiBinaryEncoding): Buffer + // update( + // data: NodeJS.ArrayBufferView, + // input_encoding: undefined, + // output_encoding: HexBase64BinaryEncoding + // ): string + // update( + // data: string, + // input_encoding: Utf8AsciiBinaryEncoding | undefined, + // output_encoding: HexBase64BinaryEncoding + // ): string + final(): Buffer { + console.log('todo: crypto.Cipher.update') + return Buffer.from([]) + } + // final(output_encoding: BufferEncoding): string + // setAutoPadding(auto_padding?: boolean): this + getAuthTag(): Buffer { + console.log('todo: crypto.Cipher.update') + return Buffer.from([]) + } + // setAAD(buffer: Buffer): this; // docs only say buffer +} + + +export function createDecipheriv(algorithm: string, key: CipherKey, iv: BinaryLike | null, options?:any/* stream.TransformOptions*/): Decipher{ + return new Decipher() +} + + + class Decipher /* extends stream.Transform*/ { + // private constructor(); + update(data: BinaryLike): Buffer{ + console.log('todo: crypto.Decipher.update') + return Buffer.from([]) + } + // update(data: NodeJS.ArrayBufferView): Buffer; + // update(data: string, input_encoding: HexBase64BinaryEncoding): Buffer; + // update(data: NodeJS.ArrayBufferView, input_encoding: HexBase64BinaryEncoding | undefined, output_encoding: Utf8AsciiBinaryEncoding): string; + // update(data: string, input_encoding: HexBase64BinaryEncoding | undefined, output_encoding: Utf8AsciiBinaryEncoding): string; + final(): Buffer { + console.log('todo: crypto.Decipher.final') + return Buffer.from([]) + } + // final(output_encoding: BufferEncoding): string; + // setAutoPadding(auto_padding?: boolean): this; + setAuthTag(tag: Buffer): this { + console.log('todo: crypto.Decipher.setAuthTag') + return this + } + // setAAD(buffer: NodeJS.ArrayBufferView): this; + } + +// Types + + // type KeyObjectType = 'secret' | 'public' | 'private'; + + // interface KeyExportOptions { + // type: 'pkcs1' | 'spki' | 'pkcs8' | 'sec1'; + // format: T; + // cipher?: string; + // passphrase?: string | Buffer; + // } + + // class KeyObject { + // private constructor(); + // asymmetricKeyType?: KeyType; + // * + // * For asymmetric keys, this property represents the size of the embedded key in + // * bytes. This property is `undefined` for symmetric keys. + + // asymmetricKeySize?: number; + // export(options: KeyExportOptions<'pem'>): string | Buffer; + // export(options?: KeyExportOptions<'der'>): Buffer; + // symmetricKeySize?: number; + // type: KeyObjectType; + // } + + type CipherCCMTypes = 'aes-128-ccm' | 'aes-192-ccm' | 'aes-256-ccm' | 'chacha20-poly1305'; + type CipherGCMTypes = 'aes-128-gcm' | 'aes-192-gcm' | 'aes-256-gcm'; + + type BinaryLike = string | Uint8Array// | NodeJS.ArrayBufferView; + + type CipherKey = BinaryLike// | KeyObject; + + interface CipherCCMOptions /*extends stream.TransformOptions*/ { + authTagLength: number; + } + interface CipherGCMOptions /*extends stream.TransformOptions*/ { + authTagLength?: number; + } \ No newline at end of file diff --git a/deno/src/std-node-net.ts b/deno/src/std-node-net.ts new file mode 100644 index 0000000..aa7b684 --- /dev/null +++ b/deno/src/std-node-net.ts @@ -0,0 +1,325 @@ +import { Buffer } from 'https://deno.land/std/node/buffer.ts' +import { EventEmitter } from 'https://deno.land/std/node/events.ts' +import { Duplex } from './std-node-stream.ts' + +export class Socket extends Duplex { + #connection?: Deno.Conn + + constructor(options?: SocketConstructorOpts) { + super() + console.log('todo: net.Socket', options) + // setTimeout(() => {}, 3000) + } + // // Extended base methods + // write(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean; + // write(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean; + + // connect(port: number, host: string, connectionListener?: () => void): this; + // connect(port: number, connectionListener?: () => void): this; + // connect(path: string, connectionListener?: () => void): this; + + connect(options: SocketConnectOpts, connectionListener?: () => void): this { + if (!('host' in options)) { + throw new Error('host missing') + } + + console.log('todo: net.Socket.connect', options) + + if (this.write !== Socket.prototype.write) + this.write = Socket.prototype.write + + // if (this.destroyed) { + // this._handle = null; + // this._peername = null; + // this._sockname =s null; + // } + if (typeof connectionListener === 'function') { + this.once('connect', connectionListener) + } + this.connecting = true + this.writable = true + + setTimeout(() => + Deno.connect({ + hostname: options?.host, + port: Number(options?.port), + }).then(async conn => { + this.#connection = conn + this.connecting = false + this.emit('connect') + this.emit('ready') + + const i = setInterval(() => { + const ok = new Uint8Array(32) + conn.read(ok).then(len => { + console.log('read', ok) + }) + // console.log('reading', ok) + }, 1000) + + console.log('todo: net.connect.connected', conn.read(new Uint8Array(0))) + // await Promise.all([Deno.copy(conn, this), Deno.copy(this, conn)]); + }) + ) + + return this + } + + // setEncoding(encoding?: BufferEncoding): this; + // pause(): this; + // resume(): this; + // setTimeout(timeout: number, callback?: () => void): this; + // setNoDelay(noDelay?: boolean): this; + // setKeepAlive(enable?: boolean, initialDelay?: number): this; + // address(): AddressInfo | string; + // unref(): this; + // ref(): this; + + // readonly bufferSize: number; + // readonly bytesRead: number; + // readonly bytesWritten: number; + /*readonly*/ connecting: boolean = false + // readonly destroyed: boolean; + // readonly localAddress: string; + // readonly localPort: number; + // readonly remoteAddress?: string; + // readonly remoteFamily?: string; + // readonly remotePort?: number; + /* override because readonly*/ writable: boolean = false + + read(len: number): Uint8Array { + console.log('todo: net.Socket.read', len) + const buf = new Uint8Array(len) + this.#connection?.read(buf) || buf + return buf + // return super.read(len) + // return new Uint8Array(8) + } + // write(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean; + // write(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean; + write(str: Uint8Array): boolean { + console.log(' ok: net.Socket.write', str) + this.#connection?.write(str) + return super.write(str) + } + end(): boolean { + console.log('todo: net.Socket.end') + return false + } + [Symbol.asyncIterator](): AsyncIterableIterator { + console.log('todo: net.Socket.asyncIterator') + return false as any + } +} + +export interface AddressInfo { + address: string + family: string + port: number +} + +export class Server extends EventEmitter { + #listener?: (socket: Socket) => void + + constructor(connectionListener?: (socket: Socket) => void) { + super() + console.log('todo: net.Server') + this.#listener = connectionListener + } + + unref() { + console.log('todo: net.Server.unref') + } + + close() { + console.log('todo: net.Server.close') + } + address(): AddressInfo { + console.log('todo: net.Server.address') + return ({} as unknown) as AddressInfo + } + + // listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this; + // listen(port?: number, hostname?: string, listeningListener?: () => void): this; + // listen(port?: number, backlog?: number, listeningListener?: () => void): this; + // listen(port?: number, listeningListener?: () => void): this; + // listen(path: string, backlog?: number, listeningListener?: () => void): this; + // listen(path: string, listeningListener?: () => void): this; + listen(options?: ListenOptions, listeningListener?: () => void): this { + console.log('todo: net.Server.listen') + return this + } + // listen(handle: any, backlog?: number, listeningListener?: () => void): this; + // listen(handle: any, listeningListener?: () => void): this; + // close(callback?: (err?: Error) => void): this; + // address(): AddressInfo | string | null; + // getConnections(cb: (error: Error | null, count: number) => void): void; + // ref(): this; + // unref(): this; + // maxConnections: number; + // connections: number; + // listening: boolean; + + // /** + // * events.EventEmitter + // * 1. close + // * 2. connection + // * 3. error + // * 4. listening + // */ + // addListener(event: string, listener: (...args: any[]) => void): this; + // addListener(event: "close", listener: () => void): this; + // addListener(event: "connection", listener: (socket: Socket) => void): this; + // addListener(event: "error", listener: (err: Error) => void): this; + // addListener(event: "listening", listener: () => void): this; + + // emit(event: string | symbol, ...args: any[]): boolean; + // emit(event: "close"): boolean; + // emit(event: "connection", socket: Socket): boolean; + // emit(event: "error", err: Error): boolean; + // emit(event: "listening"): boolean; + + // on(event: string, listener: (...args: any[]) => void): this; + // on(event: "close", listener: () => void): this; + // on(event: "connection", listener: (socket: Socket) => void): this; + // on(event: "error", listener: (err: Error) => void): this; + // on(event: "listening", listener: () => void): this; + + // once(event: string, listener: (...args: any[]) => void): this; + // once(event: "close", listener: () => void): this; + // once(event: "connection", listener: (socket: Socket) => void): this; + // once(event: "error", listener: (err: Error) => void): this; + // once(event: "listening", listener: () => void): this; + + // prependListener(event: string, listener: (...args: any[]) => void): this; + // prependListener(event: "close", listener: () => void): this; + // prependListener(event: "connection", listener: (socket: Socket) => void): this; + // prependListener(event: "error", listener: (err: Error) => void): this; + // prependListener(event: "listening", listener: () => void): this; + + // prependOnceListener(event: string, listener: (...args: any[]) => void): this; + // prependOnceListener(event: "close", listener: () => void): this; + // prependOnceListener(event: "connection", listener: (socket: Socket) => void): this; + // prependOnceListener(event: "error", listener: (err: Error) => void): this; + // prependOnceListener(event: "listening", listener: () => void): this; +} + +// Types + +export type LookupFunction = ( + hostname: string, + options: any /*dns.LookupOneOptions*/, + callback: ( + err: any /*NodeJS.ErrnoException | null*/, + address: string, + family: number + ) => void +) => void + +export interface AddressInfo { + address: string + family: string + port: number +} + +export interface SocketConstructorOpts { + fd?: number + allowHalfOpen?: boolean + readable?: boolean + writable?: boolean +} + +export interface OnReadOpts { + buffer: Uint8Array | (() => Uint8Array) + /** + * This function is called for every chunk of incoming data. + * Two arguments are passed to it: the number of bytes written to buffer and a reference to buffer. + * Return false from this function to implicitly pause() the socket. + */ + callback(bytesWritten: number, buf: Uint8Array): boolean +} + +export interface ConnectOpts { + /** + * If specified, incoming data is stored in a single buffer and passed to the supplied callback when data arrives on the socket. + * Note: this will cause the streaming functionality to not provide any data, however events like 'error', 'end', and 'close' will + * still be emitted as normal and methods like pause() and resume() will also behave as expected. + */ + onread?: OnReadOpts +} + +export interface TcpSocketConnectOpts extends ConnectOpts { + port: number + host?: string + localAddress?: string + localPort?: number + hints?: number + family?: number + lookup?: LookupFunction +} + +export interface IpcSocketConnectOpts extends ConnectOpts { + path: string +} + +export type SocketConnectOpts = TcpSocketConnectOpts | IpcSocketConnectOpts + +export interface ListenOptions { + port?: number + host?: string + backlog?: number + path?: string + exclusive?: boolean + readableAll?: boolean + writableAll?: boolean + /** + * @default false + */ + ipv6Only?: boolean +} + +export interface TcpNetConnectOpts + extends TcpSocketConnectOpts, + SocketConstructorOpts { + timeout?: number +} + +export interface IpcNetConnectOpts + extends IpcSocketConnectOpts, + SocketConstructorOpts { + timeout?: number +} + +export type NetConnectOpts = TcpNetConnectOpts | IpcNetConnectOpts + +export function createServer( + connectionListener?: (socket: Socket) => void +): Server { + console.log('todo: net.createServer') + return new Server(console.log) +} +// export function createServer(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean }, connectionListener?: (socket: Socket) => void): Server; +export function connect( + options: NetConnectOpts, + connectionListener?: () => void +): Socket { + const socket = new Socket(options) + + if (options.timeout) { + // socket.setTimeout(options.timeout); + } + + return socket.connect(options) +} +// export function connect(port: number, host?: string, connectionListener?: () => void): Socket; +// export function connect(path: string, connectionListener?: () => void): Socket; +// export function createConnection(options: NetConnectOpts, connectionListener?: () => void): Socket; +// export function createConnection(port: number, host?: string, connectionListener?: () => void): Socket; +// export function createConnection(path: string, connectionListener?: () => void): Socket; +// export function isIP(input: string): number; +export function isIPv4(input: string): boolean { + return true +} +export function isIPv6(input: string): boolean { + return false +} diff --git a/deno/src/std-node-stream.ts b/deno/src/std-node-stream.ts new file mode 100644 index 0000000..4177b93 --- /dev/null +++ b/deno/src/std-node-stream.ts @@ -0,0 +1,442 @@ +import * as events from "https://deno.land/std/node/events.ts" + +export class Stream extends events.EventEmitter { + constructor(opts?: ReadableOptions) { + super() + console.log('todo: stream.Stream') + } + pipe(destination: T, options?: { end?: boolean }): T { + return destination + } + on(event:any, listener: any){ + console.log('todo: stream.Stream.on', event) + return super.on(event, listener) + } + emit(event:any, data?: any){ + console.log('todo: stream.Stream.emit', event, data) + return super.emit(event, data) + } + once(event:any, listener: any){ + // console.log('todo: stream.Stream.once', event) + return super.once(event, listener) + } +} + + +interface ReadableOptions { + highWaterMark?: number; + encoding?: BufferEncoding; + objectMode?: boolean; + read?(this: Readable, size: number): void; + destroy?(this: Readable, error: Error | null, callback: (error: Error | null) => void): void; + autoDestroy?: boolean; +} + +export class Readable extends Stream { + #needReadable = false; + #emittedReadable = false + + /** + * A utility method for creating Readable Streams out of iterators. + */ + static from( + iterable: Iterable | AsyncIterable, + options?: ReadableOptions + ): Readable{ + console.log('todo: stream.Readable') + return new Readable()} + + // readable: boolean + // readonly readableEncoding: BufferEncoding | null + // readonly readableEnded: boolean + // readonly readableHighWaterMark: number + // readonly readableLength: number + // readonly readableObjectMode: boolean + destroyed: boolean=false + // constructor(opts?: ReadableOptions) + _read(size: number): void{ console.log('TODO _read') } + read(size?: number): any{ + console.log('todo: stream.Readable.read', size) + + } + // setEncoding(encoding: BufferEncoding): this + // pause(): this + // resume(): this + // isPaused(): boolean + // unpipe(destination?: NodeJS.WritableStream): this + // unshift(chunk: any, encoding?: BufferEncoding): void + // wrap(oldStream: NodeJS.ReadableStream): this + push(chunk: any, encoding?: BufferEncoding): boolean{ + console.log('todo: stream.Readable.push') + return true + } + _destroy(error: Error | null, callback: (error?: Error | null) => void): void{ console.log('TODO _destroy') } + destroy(error?: Error): void{ console.log('TODO destroy') } + + + on(event:any, listener: any){ + console.log('todo: stream.Readable.on', event) + const ok = super.on(event, listener) + if (event ==='readable'&&!this.#emittedReadable) { + this.#emittedReadable = true + super.emit('readable', this) + } + return ok + } + emit(event:any, data?: any){ + console.log('todo: stream.Readable.emit', event, data) + return super.emit(event, data) + } + once(event:any, listener: any){ + // console.log('todo: stream.Readable.once', event) + return super.once(event, listener) + } + + /** + * Event emitter + * The defined events on documents including: + * 1. close + * 2. data + * 3. end + * 4. error + * 5. pause + * 6. readable + * 7. resume + */ + // addListener(event: 'close', listener: () => void): this + // addListener(event: 'data', listener: (chunk: any) => void): this + // addListener(event: 'end', listener: () => void): this + // addListener(event: 'error', listener: (err: Error) => void): this + // addListener(event: 'pause', listener: () => void): this + // addListener(event: 'readable', listener: () => void): this + // addListener(event: 'resume', listener: () => void): this + // addListener(event: string | symbol, listener: (...args: any[]) => void): this { + // console.log('todo: stream.Readable.addListener', event) + // if (!this.#listeners[event]) { + // this.#listeners[event] =[] + // } + // this.#listeners[event].push(listener) + // super.addListener(event, listener) + // return this + // } + + // emit(event: 'close'): boolean + // emit(event: 'data', chunk: any): boolean + // emit(event: 'end'): boolean + // emit(event: 'error', err: Error): boolean + // emit(event: 'pause'): boolean + // emit(event: 'readable'): boolean + // emit(event: 'resume'): boolean + // emit(event: string | symbol, ...args: any[]): boolean { + // console.log('todo: stream.Readable.emit', event) + // return true + + // } + + // on(event: 'close', listener: () => void): this + // on(event: 'data', listener: (chunk: any) => void): this + // on(event: 'end', listener: () => void): this + // on(event: 'error', listener: (err: Error) => void): this + // on(event: 'pause', listener: () => void): this + // on(event: 'readable', listener: () => void): this + // on(event: 'resume', listener: () => void): this + // on(event: string | symbol, listener: (...args: any[]) => void): this { + // console.log('todo: stream.Readable.on', event) + // if (!this.#listeners[event]) { + // this.#listeners[event] =[] + // } + // this.#listeners[event].push(listener) + // return this + + // } + + // once(event: 'close', listener: () => void): this + // once(event: 'data', listener: (chunk: any) => void): this + // once(event: 'end', listener: () => void): this + // once(event: 'error', listener: (err: Error) => void): this + // once(event: 'pause', listener: () => void): this + // once(event: 'readable', listener: () => void): this + // once(event: 'resume', listener: () => void): this + // once(event: string | symbol, listener: (...args: any[]) => void): this { + // if (!this.#listeners[event]) { + // this.#listeners[event] =[] + // } + // this.#listeners[event].push(listener) + // console.log('todo: stream.Readable.once', event) + // return this + + // } + + // prependListener(event: 'close', listener: () => void): this + // prependListener(event: 'data', listener: (chunk: any) => void): this + // prependListener(event: 'end', listener: () => void): this + // prependListener(event: 'error', listener: (err: Error) => void): this + // prependListener(event: 'pause', listener: () => void): this + // prependListener(event: 'readable', listener: () => void): this + // prependListener(event: 'resume', listener: () => void): this + // prependListener( + // event: string | symbol, + // listener: (...args: any[]) => void + // ): this { + // console.log('todo: stream.Readable.prependListener') + // return this + + // } + + // prependOnceListener(event: 'close', listener: () => void): this + // prependOnceListener(event: 'data', listener: (chunk: any) => void): this + // prependOnceListener(event: 'end', listener: () => void): this + // prependOnceListener(event: 'error', listener: (err: Error) => void): this + // prependOnceListener(event: 'pause', listener: () => void): this + // prependOnceListener(event: 'readable', listener: () => void): this + // prependOnceListener(event: 'resume', listener: () => void): this + // prependOnceListener( + // event: string | symbol, + // listener: (...args: any[]) => void + // ): this { + // console.log('todo: stream.Readable.prependOnceListener') + // return this + + // } + + // removeListener(event: 'close', listener: () => void): this + // removeListener(event: 'data', listener: (chunk: any) => void): this + // removeListener(event: 'end', listener: () => void): this + // removeListener(event: 'error', listener: (err: Error) => void): this + // removeListener(event: 'pause', listener: () => void): this + // removeListener(event: 'readable', listener: () => void): this + // removeListener(event: 'resume', listener: () => void): this + // removeListener( + // event: string | symbol, + // listener: (...args: any[]) => void + // ): this { + // console.log('todo: stream.Readable.removeListener') + // return this + // } + + [Symbol.asyncIterator](): AsyncIterableIterator { + console.log('todo: stream.Readable.asyncIterator') + return false as any + } +} + +interface WritableOptions { + highWaterMark?: number; + decodeStrings?: boolean; + defaultEncoding?: BufferEncoding; + objectMode?: boolean; + emitClose?: boolean; + write?(this: Writable, chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void; + writev?(this: Writable, chunks: Array<{ chunk: any, encoding: BufferEncoding }>, callback: (error?: Error | null) => void): void; + destroy?(this: Writable, error: Error | null, callback: (error: Error | null) => void): void; + final?(this: Writable, callback: (error?: Error | null) => void): void; + autoDestroy?: boolean; +} + +export class Writable extends Stream /* implements NodeJS.WritableStream */ { + readonly writable: boolean = true; + readonly writableEnded: boolean = false; + readonly writableFinished: boolean = false; + // readonly writableHighWaterMark: number; + // readonly writableLength: number; + // readonly writableObjectMode: boolean; + // readonly writableCorked: number; + destroyed: boolean = false + // constructor(opts?: WritableOptions); + _write(chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void{ console.log('TODO _write') } + _writev?(chunks: Array<{ chunk: any, encoding: BufferEncoding }>, callback: (error?: Error | null) => void): void{ console.log('TODO _writev') } + _destroy(error: Error | null, callback: (error?: Error | null) => void): void{ console.log('TODO _destroy') } + _final(callback: (error?: Error | null) => void): void{ console.log('TODO _final') } + write(chunk: any, cb?: (error: Error | null | undefined) => void): boolean{ + console.log(' ok: stream.Writable.write') + return super.push(chunk, cb) + } + // write(chunk: any, encoding: BufferEncoding, cb?: (error: Error | null | undefined) => void): boolean + // setDefaultEncoding(encoding: BufferEncoding): this; + end(cb?: () => void): void{ console.log('TODO end') } + // end(chunk: any, cb?: () => void): void + // end(chunk: any, encoding: BufferEncoding, cb?: () => void): void{ console.log('TODO //') } + cork(): void{ console.log('TODO cork') } + uncork(): void{ console.log('TODO uncork') } + destroy(error?: Error): void{ + console.log('todo: stream.Writable.destroy') + + } + + /** + * Event emitter + * The defined events on documents including: + * 1. close + * 2. drain + * 3. error + * 4. finish + * 5. pipe + * 6. unpipe + */ + // addListener(event: "close", listener: () => void): this; + // addListener(event: "drain", listener: () => void): this; + // addListener(event: "error", listener: (err: Error) => void): this; + // addListener(event: "finish", listener: () => void): this; + // addListener(event: "pipe", listener: (src: Readable) => void): this; + // addListener(event: "unpipe", listener: (src: Readable) => void): this; + // addListener(event: string | symbol, listener: (...args: any[]) => void): this { + // console.log('todo: stream.Writable.addListener') + // return this + // } + + // emit(event: "close"): boolean; + // emit(event: "drain"): boolean; + // emit(event: "error", err: Error): boolean; + // emit(event: "finish"): boolean; + // emit(event: "pipe", src: Readable): boolean; + // emit(event: "unpipe", src: Readable): boolean; + // emit(event: string | symbol, ...args: any[]): boolean { + // console.log('todo: stream.Writable.emit') + // return true + // } + + // on(event: "close", listener: () => void): this; + // on(event: "drain", listener: () => void): this; + // on(event: "error", listener: (err: Error) => void): this; + // on(event: "finish", listener: () => void): this; + // on(event: "pipe", listener: (src: Readable) => void): this; + // on(event: "unpipe", listener: (src: Readable) => void): this; + // on(event: string | symbol, listener: (...args: any[]) => void): this { + // console.log('todo: stream.Writable.on') + // return this + // } + + // once(event: "close", listener: () => void): this; + // once(event: "drain", listener: () => void): this; + // once(event: "error", listener: (err: Error) => void): this; + // once(event: "finish", listener: () => void): this; + // once(event: "pipe", listener: (src: Readable) => void): this; + // once(event: "unpipe", listener: (src: Readable) => void): this; + // once(event: string | symbol, listener: (...args: any[]) => void): this { + // console.log('todo: stream.Writable.once') + // return this + // } + + // prependListener(event: "close", listener: () => void): this; + // prependListener(event: "drain", listener: () => void): this; + // prependListener(event: "error", listener: (err: Error) => void): this; + // prependListener(event: "finish", listener: () => void): this; + // prependListener(event: "pipe", listener: (src: Readable) => void): this; + // prependListener(event: "unpipe", listener: (src: Readable) => void): this; + // prependListener(event: string | symbol, listener: (...args: any[]) => void): this { + // console.log('todo: stream.Writable.prependListener') + // return this + // } + + // prependOnceListener(event: "close", listener: () => void): this; + // prependOnceListener(event: "drain", listener: () => void): this; + // prependOnceListener(event: "error", listener: (err: Error) => void): this; + // prependOnceListener(event: "finish", listener: () => void): this; + // prependOnceListener(event: "pipe", listener: (src: Readable) => void): this; + // prependOnceListener(event: "unpipe", listener: (src: Readable) => void): this; + // prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this { + // console.log('todo: stream.Writable.prependOnceListener') + // return this + // } + + // removeListener(event: "close", listener: () => void): this; + // removeListener(event: "drain", listener: () => void): this; + // removeListener(event: "error", listener: (err: Error) => void): this; + // removeListener(event: "finish", listener: () => void): this; + // removeListener(event: "pipe", listener: (src: Readable) => void): this; + // removeListener(event: "unpipe", listener: (src: Readable) => void): this; + // removeListener(event: string | symbol, listener: (...args: any[]) => void): this { + // console.log('todo: stream.Writable.removeListener') + // return this + // } +} + + +interface SocketConstructorOpts { + fd?: number + allowHalfOpen?: boolean + readable?: boolean + writable?: boolean +} + +type BufferEncoding = string + +export function finished(dup: Duplex, options: any) {} + + +// @ts-ignore +interface DuplexOptions extends ReadableOptions, WritableOptions { + // allowHalfOpen?: boolean; + // readableObjectMode?: boolean; + // writableObjectMode?: boolean; + // readableHighWaterMark?: number; + // writableHighWaterMark?: number; + // writableCorked?: number; + read?(this: Duplex, size: number): void; + write?(this: Duplex, chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void; + // writev?(this: Duplex, chunks: Array<{ chunk: any, encoding: BufferEncoding }>, callback: (error?: Error | null) => void): void; + // final?(this: Duplex, callback: (error?: Error | null) => void): void; + destroy?(this: Duplex, error: Error | null, callback: (error: Error | null) => void): void; +} + +// Note: Duplex extends both Readable and Writable. +// @ts-ignore +export class Duplex extends Readable implements Writable { + readonly writable: boolean = true; + readonly writableEnded: boolean = false; + readonly writableFinished: boolean = false; + // readonly writableHighWaterMark: number = 1; + // readonly writableLength: number = 1; + // readonly writableObjectMode: boolean = false; + // readonly writableCorked: number = 1; + constructor(opts?: DuplexOptions) { + super() + } + _write(chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void { + console.log('todo: stream.duplex._write') + } + _writev?(chunks: Array<{ chunk: any, encoding: BufferEncoding }>, callback: (error?: Error | null) => void): void { + console.log('todo: stream.duplex._writev') + } + _destroy(error: Error | null, callback: (error: Error | null) => void): void { + console.log('todo: stream.duplex._destroy') + } + _final(callback: (error?: Error | null) => void): void { + console.log('todo: stream.duplex._final') + } + write(chunk: any, cb?: (error: Error | null | undefined) => void): boolean { + console.log('todo: stream.duplex.write') + return true + } + setDefaultEncoding(encoding: BufferEncoding): this { + console.log('todo: stream.duplex.setDefaultEncoding') + return this + } + end(cb?: () => void): void { + console.log('todo: stream.duplex.end') + + } + cork(): void {} + uncork(): void {} + + + // Wirtable? + // [Symbol.asyncIterator]() { + // return { + // next() { + // return new Promise<{ value: any }>(res => {}) + // }, + // } + // } +} +applyMixins (Duplex, [Writable]); + +function applyMixins(derivedCtor: any, baseCtors: any[]) { + baseCtors.forEach(baseCtor => { + Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { + if (name !== 'constructor') { + derivedCtor.prototype[name] = baseCtor.prototype[name]; + } + }); + }); +} \ No newline at end of file diff --git a/deno/src/stream.ts b/deno/src/stream.ts new file mode 100644 index 0000000..7f95fe8 --- /dev/null +++ b/deno/src/stream.ts @@ -0,0 +1,204 @@ +import { Buffer } from 'https://deno.land/std/node/buffer.ts' +import { Readable, Writable } from "./std-node-stream.ts" +import * as events from "https://deno.land/std/node/events.ts"; +import { DataPacket, ServiceRequestPacket } from "./packet.ts"; + +export async function drain(writable: Writable) { + if (writable.destroyed) throw new Error(`premature close`); + + await Promise.race([ + events.once(writable, "drain"), + events.once(writable, "close").then(() => { + throw new Error(`premature close`); + }), + ]); +} + +export async function* lengthPrefixed(stream: AsyncIterable) { + // @ts-ignore + let buf: Buffer = Buffer.of(); + let size: number | undefined; + + for await (const data of stream) { + buf = Buffer.concat([buf, data]); + + while (true) { + if (!size) { + if (buf.byteLength < 4) break; + size = buf.readUInt32BE(0); + buf = buf.slice(4); + } + + if (buf.byteLength < size) break; + + const frame = buf.slice(0, size); + buf = buf.slice(size); + size = undefined; + + yield frame; + } + } +} + +export function prefixLength(src: string | ArrayBufferLike): Buffer { + // @ts-ignore + const data = Buffer.isBuffer(src) ? src : Buffer.from(src); + + const header = Buffer.alloc(4); + header.writeUInt32BE(data.byteLength); + return Buffer.concat([header, data]); +} + +export class RPC { + pending: events.EventEmitter = new events.EventEmitter(); + initial: number; + counter: number; + + constructor(client: boolean) { + this.counter = this.initial = client ? 1 : 2; + } + + next(): number { + const seq = this.counter; + if ((this.counter += 2) === 0) { + this.counter = this.initial; + } + return seq; + } + + message(seq: number, src: string | ArrayBufferLike): Buffer { + // @ts-ignore + const buf = Buffer.isBuffer(src) ? src : Buffer.from(src); + const header = Buffer.alloc(4); + header.writeUInt32BE(seq); + return Buffer.concat([header, buf]); + } + + request(src: string | ArrayBufferLike): [Buffer, Promise] { + const seq = this.next(); + const buf = this.message(seq, src); + return [buf, this.wait(seq)]; + } + + async wait(seq: number): Promise { + return (<[Buffer]>await events.once(this.pending, `${seq}`))[0]; + } + + async *parse(stream: AsyncIterable) { + for await (let frame of stream) { + if (frame.byteLength < 4) + throw new Error( + `Frame must be prefixed with an unsigned 32-bit sequence number.` + ); + + const seq = frame.readUInt32BE(); + frame = frame.slice(4); + + if (seq !== 0 && seq % 2 === this.initial % 2) { + this.pending.emit(`${seq}`, frame); + continue; + } + + yield { seq, frame }; + } + } +} + +export const STREAM_CHUNK_SIZE = 2048; + +export class Stream extends events.EventEmitter { + id: number; + body: Readable; + headers?: { [key: string]: string }; + + constructor(id: number) { + super(); + + this.id = id; + + this.body = new Readable(); + this.body._read = () => { + return; + }; + } +} + +export class Streams { + active = new Map(); + initial: number; + counter: number; + + constructor(client: boolean) { + this.counter = this.initial = client ? 0 : 1; + } + + register(id?: number): Stream { + if (id === undefined) { + id = this.counter; + if ((this.counter += 2) === 0) { + this.counter = this.initial; + } + } + + if (this.active.has(id)) { + throw new Error( + `Attempted to register stream with ID ${id} which already exists.` + ); + } + + const stream = new Stream(id); + this.active.set(id, stream); + + return stream; + } + + get(id: number): Stream | undefined { + return this.active.get(id); + } + + push( + stream: Stream, + services: string[], + headers: { [key: string]: string } + ): [Buffer, Promise] { + return [ + new ServiceRequestPacket(stream.id, services, headers).encode(), + this.wait(stream), + ]; + } + + pull(stream: Stream, handled: boolean, headers: { [key: string]: string }) { + stream.headers = headers; + // TODO: fix events.EventEmitter type + // @ts-ignore + stream.emit("ready", handled); + } + + recv(stream: Stream, data: Buffer) { + if (data.byteLength === 0) { + this.active.delete(stream.id); + stream.body.push(null); + } else { + stream.body.push(data); + } + } + + async *encoded(id: number, body: AsyncIterable) { + let buf: Buffer = Buffer.from([]); + for await (const chunk of body) { + buf = Buffer.concat([buf, Buffer.from(chunk)]); + while (buf.byteLength >= STREAM_CHUNK_SIZE) { + yield new DataPacket(id, buf.slice(0, STREAM_CHUNK_SIZE)).encode(); + buf = buf.slice(STREAM_CHUNK_SIZE); + } + } + if (buf.byteLength > 0) { + yield new DataPacket(id, buf).encode(); + } + yield new DataPacket(id, Buffer.from([])).encode(); + } + + async wait(stream: events.EventEmitter): Promise { + return (<[Boolean]>await events.once(stream, "ready"))[0]; + } +} diff --git a/deno/tests/globals.d.spec.ts b/deno/tests/globals.d.spec.ts new file mode 100644 index 0000000..5b3ab1b --- /dev/null +++ b/deno/tests/globals.d.spec.ts @@ -0,0 +1,5 @@ +declare module Chai { + interface Assertion { + equalBytes(expected: ArrayBufferLike): Chai.Equal; + } +} diff --git a/deno/tests/sock.spec.ts b/deno/tests/sock.spec.ts new file mode 100644 index 0000000..9ea30d3 --- /dev/null +++ b/deno/tests/sock.spec.ts @@ -0,0 +1,257 @@ +import "mocha"; +import * as net from "net"; +import * as events from "events"; +import { Node } from "../src"; +import chai, { expect } from "chai"; +import { Readable } from "stream"; +import { clientHandshake, serverHandshake, Session } from "../src/session"; +import * as nacl from "tweetnacl"; +import { ID } from "../src/kademlia"; +import { lengthPrefixed, prefixLength, RPC } from "../src/stream"; +import { Context } from "../src/context"; +import { getAvailableAddress } from "../src/net"; + +chai.use(require("chai-bytes")); + +const createEndToEnd = async (): Promise< + [net.Server, net.Socket, net.Socket] +> => { + const server = net.createServer(); + server.listen(); + + await events.once(server, "listening"); + + const info = (server.address())!; + const client = net.createConnection(info.port, info.address); + + const [[conn]] = <[[net.Socket], any[]]>( + await Promise.all([ + events.once(server, "connection"), + events.once(client, "connect"), + ]) + ); + + return [server, client, conn]; +}; + +describe("length prefix", function () { + it("should work end-to-end", async () => { + const expected = [...Array(100)].map(() => Math.random().toString(16)); + + const [server, client, conn] = await createEndToEnd(); + + setImmediate(async () => { + for (const data of expected) { + expect(client.write(prefixLength(data))).to.equal(true); + } + client.end(); + await events.once(client, "close"); + }); + + const stream = lengthPrefixed(conn); + for (const data of expected) { + expect((await stream.next()).value).to.equalBytes(Buffer.from(data)); + } + await events.once(conn, "close"); + + server.close(); + await events.once(server, "close"); + }); +}); + +const endToEndHandshake = async ( + client: net.Socket, + conn: net.Socket +): Promise<[Uint8Array, Uint8Array]> => { + return await Promise.all([clientHandshake(client), serverHandshake(conn)]); +}; + +describe("session", function () { + it("should work end-to-end", async () => { + const expected = [...Array(100)].map(() => Math.random().toString(16)); + + const [server, client, conn] = await createEndToEnd(); + const [clientSecret, serverSecret] = await endToEndHandshake(client, conn); + + expect(clientSecret).to.equalBytes(serverSecret); + + setImmediate(async () => { + const session = new Session(clientSecret); + const stream = session.decrypted(lengthPrefixed(client)); + + for (const data of expected) { + client.write(prefixLength(session.encrypt(data))); + } + + for (const data of expected) { + expect((await stream.next()).value).to.equalBytes(Buffer.from(data)); + } + + client.end(); + await events.once(client, "close"); + }); + + const session = new Session(serverSecret); + const stream = session.decrypted(lengthPrefixed(conn)); + + for (const data of expected) { + expect((await stream.next()).value).to.equalBytes(Buffer.from(data)); + } + + for (const data of expected) { + conn.write(prefixLength(session.encrypt(data))); + } + + await events.once(conn, "close"); + + server.close(); + await events.once(server, "close"); + }); +}); + +describe("encrypted rpc", function () { + it("should work end-to-end", async () => { + const expected = [...Array(100)].map(() => Math.random().toString(16)); + + const [server, client, conn] = await createEndToEnd(); + const [clientSecret, serverSecret] = await endToEndHandshake(client, conn); + + expect(clientSecret).to.equalBytes(serverSecret); + + setImmediate(async () => { + const rpc = new RPC(true); + const session = new Session(clientSecret); + + const stream = rpc.parse(session.decrypted(lengthPrefixed(client))); + + setImmediate(async () => { + while (true) { + const item = await stream.next(); + if (item.done) { + break; + } + + const { seq, frame } = item.value; + + client.write( + prefixLength( + session.encrypt( + rpc.message( + seq, + Buffer.concat([Buffer.from("FROM CLIENT: "), frame]) + ) + ) + ) + ); + } + }); + + for (const data of expected) { + const [req, res] = rpc.request(data); + client.write(prefixLength(session.encrypt(req))); + expect(await res).to.equalBytes(Buffer.from("FROM SERVER: " + data)); + } + + client.end(); + await events.once(client, "close"); + }); + + const rpc = new RPC(false); + const session = new Session(serverSecret); + + const stream = rpc.parse(session.decrypted(lengthPrefixed(conn))); + + setImmediate(async () => { + while (true) { + const item = await stream.next(); + if (item.done) { + break; + } + + const { seq, frame } = item.value; + + conn.write( + prefixLength( + session.encrypt( + rpc.message( + seq, + Buffer.concat([Buffer.from("FROM SERVER: "), frame]) + ) + ) + ) + ); + } + }); + + for (const data of expected) { + const [req, res] = rpc.request(data); + conn.write(prefixLength(session.encrypt(req))); + expect(await res).to.equalBytes(Buffer.from("FROM CLIENT: " + data)); + } + + await events.once(conn, "close"); + + server.close(); + await events.once(server, "close"); + }); +}); + +describe("node", function () { + it("should work end-to-end", async () => { + const aliceAddr = await getAvailableAddress(); + const bobAddr = await getAvailableAddress(); + + const alice = new Node(); + const bob = new Node(); + + alice.keys = nacl.sign.keyPair(); + bob.keys = nacl.sign.keyPair(); + + alice.id = new ID(alice.keys.publicKey, aliceAddr.host, aliceAddr.port); + bob.id = new ID(bob.keys.publicKey, bobAddr.host, bobAddr.port); + + alice.handlers["hello_world"] = async (ctx: Context) => { + expect(ctx.id.publicKey).to.equalBytes(bob.id!.publicKey!); + expect(await ctx.body()).to.equalBytes(Buffer.from("Bob says hi!")); + ctx.send("Alice says hi!"); + }; + + bob.handlers["hello_world"] = async (ctx: Context) => { + expect(ctx.id.publicKey).to.equalBytes(alice.id!.publicKey!); + expect(await ctx.body()).to.equalBytes(Buffer.from("Alice says hi!")); + ctx.send("Bob says hi!"); + }; + + await bob.listen({ port: bobAddr.port }); + await alice.connect({ host: bobAddr.host.toString(), port: bobAddr.port }); + + const aliceToBob = async () => { + for (let i = 0; i < 10; i++) { + const res = await alice.push( + ["hello_world"], + {}, + Readable.from("Alice says hi!") + ); + for await (const chunk of res.body) { + expect(chunk).to.equalBytes(Buffer.from("Bob says hi!")); + } + } + }; + + const bobToAlice = async () => { + for (let i = 0; i < 10; i++) { + const res = await bob.push( + ["hello_world"], + {}, + Readable.from("Bob says hi!") + ); + for await (const chunk of res.body) { + expect(chunk).to.equalBytes(Buffer.from("Alice says hi!")); + } + } + }; + + await Promise.all([aliceToBob(), bobToAlice()]); + await Promise.all([alice.shutdown(), bob.shutdown()]); + }); +}); diff --git a/deno/tests/tsconfig.json b/deno/tests/tsconfig.json new file mode 100644 index 0000000..4475e5a --- /dev/null +++ b/deno/tests/tsconfig.json @@ -0,0 +1,76 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es2015", + /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", + /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, + /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, + /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "dist", + /* Redirect output structure to the directory. */ + "rootDirs": [ + "src", + "tests" + ] /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, + /* Enable all strict type-checking options. */ + "noImplicitAny": true, + /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, + /* Enable strict null checks. */ + "strictFunctionTypes": true, + /* Enable strict checking of function types. */ + "strictBindCallApply": true, + /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": true, + /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true, + /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, + /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", + /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, + + /* Advanced Options */ + "skipLibCheck": true, + /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true + /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/deno/tsconfig.json b/deno/tsconfig.json new file mode 100644 index 0000000..4bdf2c0 --- /dev/null +++ b/deno/tsconfig.json @@ -0,0 +1,88 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es2015", + /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", + /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, + /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, + /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "dist", + /* Redirect output structure to the directory. */ + // "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, + /* Enable all strict type-checking options. */ + "noImplicitAny": false, + /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, + /* Enable strict null checks. */ + "strictFunctionTypes": true, + /* Enable strict checking of function types. */ + "strictBindCallApply": true, + /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": true, + /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": false, + /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, + /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", + /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + "rootDirs": ["src", "tests"], + /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, + /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, + /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true + /* Disallow inconsistently-cased references to the same file. */ + }, + "exclude": ["tests", "dist", "examples", "node_modules"] +} diff --git a/deno/yarn.lock b/deno/yarn.lock new file mode 100644 index 0000000..f591574 --- /dev/null +++ b/deno/yarn.lock @@ -0,0 +1,962 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/chai@^4.2.11": + version "4.2.11" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" + integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== + +"@types/debug@^4.1.5": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" + integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== + +"@types/ip@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/ip/-/ip-1.1.0.tgz#aec4f5bfd49e4a4c53b590d88c36eb078827a7c0" + integrity sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ== + dependencies: + "@types/node" "*" + +"@types/mocha@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" + integrity sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w== + +"@types/node@*", "@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== + +"@types/object-hash@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.3.tgz#624ed28222bd5af0f936b162589c06a2b0550161" + integrity sha512-75t+H8u2IU1zJPPqezkGLP4YxDlj8tx7H9SgYOT1G61NjJUUEELu1Lp7RKQKXhW+FL8nV7XyD/cNFAtrKGViYQ== + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array.prototype.map@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.2.tgz#9a4159f416458a23e9483078de1106b2ef68f8ec" + integrity sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.4" + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + +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" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +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== + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chai-bytes@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/chai-bytes/-/chai-bytes-0.1.2.tgz#c297e81d47eb3106af0676ded5bb5e0c9f981db3" + integrity sha512-0ol6oJS0y1ozj6AZK8n1pyv1/G+l44nqUJygAkK1UrYl+IOGie5vcrEdrAlwmLYGIA9NVvtHWosPYwWWIXf/XA== + +chai@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + pathval "^1.1.0" + type-detect "^4.0.5" + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + +chokidar@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" + integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.3.0" + optionalDependencies: + fsevents "~2.1.2" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +core-js@^3.6.5: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" + integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== + +debug@3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== + dependencies: + type-detect "^4.0.0" + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +diff@4.0.2, 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== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: + version "1.17.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.0" + is-regex "^1.1.0" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-get-iterator@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" + integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== + dependencies: + es-abstract "^1.17.4" + has-symbols "^1.0.1" + is-arguments "^1.0.4" + is-map "^2.0.1" + is-set "^2.0.1" + is-string "^1.0.5" + isarray "^2.0.5" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" + integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== + dependencies: + is-buffer "~2.0.3" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + +glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.0, has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-buffer@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== + +is-callable@^1.1.4, is-callable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" + integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" + integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" + integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== + dependencies: + has-symbols "^1.0.1" + +is-set@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" + integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== + +is-string@^1.0.4, is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +iterate-iterator@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6" + integrity sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw== + +iterate-value@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" + integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== + dependencies: + es-get-iterator "^1.0.2" + iterate-iterator "^1.0.1" + +js-yaml@3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +log-symbols@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== + dependencies: + chalk "^2.4.2" + +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== + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +mocha@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.0.1.tgz#fe01f0530362df271aa8f99510447bc38b88d8ed" + integrity sha512-vefaXfdYI8+Yo8nPZQQi0QO2o+5q9UIMX1jZ1XMmK3+4+CQjc7+B0hPdUeglXiTlr8IHMVRo63IhO9Mzt6fxOg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.3.1" + debug "3.2.6" + diff "4.0.2" + escape-string-regexp "1.0.5" + find-up "4.1.0" + glob "7.1.6" + growl "1.10.5" + he "1.2.0" + js-yaml "3.13.1" + log-symbols "3.0.0" + minimatch "3.0.4" + ms "2.1.2" + object.assign "4.1.0" + promise.allsettled "1.0.2" + serialize-javascript "3.0.0" + strip-json-comments "3.0.1" + supports-color "7.1.0" + which "2.0.2" + wide-align "1.1.3" + workerpool "6.0.0" + yargs "13.3.2" + yargs-parser "13.1.2" + yargs-unparser "1.6.0" + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoassert@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-1.1.0.tgz#4f3152e09540fde28c76f44b19bbcd1d5a42478d" + integrity sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40= + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-hash@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" + integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== + +object-inspect@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@4.1.0, object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pathval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= + +picomatch@^2.0.4, picomatch@^2.0.7: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +prettier@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== + +promise.allsettled@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" + integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg== + dependencies: + array.prototype.map "^1.0.1" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + iterate-value "^1.0.0" + +readdirp@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" + integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== + dependencies: + picomatch "^2.0.7" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +serialize-javascript@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.0.0.tgz#492e489a2d77b7b804ad391a5f5d97870952548e" + integrity sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw== + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +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== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-json-comments@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +supports-color@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +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== + +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +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== + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +workerpool@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" + integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yargs-parser@13.1.2, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-unparser@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" + integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== + dependencies: + flat "^4.1.0" + lodash "^4.17.15" + yargs "^13.3.0" + +yargs@13.3.2, yargs@^13.3.0: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==