From 955bbf9fafd43f069c3f7e85b234e63c81871965 Mon Sep 17 00:00:00 2001 From: Lily Anne Hall Date: Thu, 7 Nov 2024 22:47:34 -0800 Subject: [PATCH] fresh start to preserve privacy --- .eslintignore | 5 + .eslintrc | 79 + .gitignore | 1 + CODE_OF_CONDUCT.md | 46 + CONTRIBUTING.md | 48 + Dockerfile | 21 + Dockerfile.dev | 20 + LICENSE | 662 ++ README.md | 147 + bin/config.js | 98 + bin/kadence.js | 434 ++ doc/config.md | 230 + doc/identities.md | 54 + doc/index.json | 32 + doc/install.md | 78 + doc/messengers.md | 47 + doc/middleware.md | 105 + doc/nat.md | 63 + doc/plugins.md | 50 + doc/protocol.md | 288 + doc/quickstart.md | 273 + doc/transport-adapters.md | 82 + docker-compose.yml | 81 + example/expanded.js | 133 + example/minimal.js | 46 + index.js | 88 + lib/bucket.js | 126 + lib/constants.js | 83 + lib/contact-list.js | 95 + lib/control.js | 143 + lib/messenger.js | 214 + lib/node-abstract.js | 468 ++ lib/node-kademlia.js | 653 ++ lib/plugin-churnfilter.js | 210 + lib/plugin-contentaddress.js | 97 + lib/plugin-eclipse.js | 148 + lib/plugin-hashcash.js | 301 + lib/plugin-hashcash.worker.js | 22 + lib/plugin-hibernate.js | 155 + lib/plugin-logger.js | 110 + lib/plugin-onion.js | 232 + lib/plugin-quasar.js | 447 ++ lib/plugin-rolodex.js | 222 + lib/plugin-spartacus.js | 220 + lib/plugin-traverse.js | 364 + lib/plugin-trust.js | 196 + lib/routing-table.js | 158 + lib/rules-errors.js | 47 + lib/rules-kademlia.js | 115 + lib/transport-http.js | 193 + lib/transport-https.js | 47 + lib/transport-udp.js | 79 + lib/utils.js | 447 ++ lib/version.js | 33 + package-lock.json | 5862 +++++++++++++++++ package.json | 108 + test/.eslintrc | 24 + test/bucket.unit.js | 165 + test/contact-list.unit.js | 130 + test/control.unit.js | 54 + test/fixtures/node-generator.js | 39 + test/fixtures/transport-fake.js | 38 + test/messenger.unit.js | 108 + test/node-abstract.unit.js | 603 ++ ...ode-kademlia+transport-http.integration.js | 68 + ...node-kademlia+transport-udp.integration.js | 56 + test/node-kademlia.e2e.js | 198 + test/node-kademlia.unit.js | 792 +++ test/plugin-churnfilter.e2e.js | 71 + test/plugin-churnfilter.unit.js | 99 + test/plugin-contentaddress.e2e.js | 48 + test/plugin-contentaddress.unit.js | 48 + test/plugin-eclipse.unit.js | 71 + test/plugin-hashcash.e2e.js | 34 + test/plugin-hibernate.e2e.js | 49 + test/plugin-hibernate.unit.js | 145 + test/plugin-logger.unit.js | 95 + test/plugin-onion.unit.js | 97 + test/plugin-quasar.e2e.js | 93 + test/plugin-quasar.unit.js | 686 ++ test/plugin-rolodex.unit.js | 96 + test/plugin-spartacus.e2e.js | 69 + test/plugin-spartacus.unit.js | 162 + test/plugin-traverse.unit.js | 371 ++ test/plugin-trust.e2e.js | 155 + test/routing-table.unit.js | 170 + test/rules-errors.unit.js | 62 + test/rules-kademlia.unit.js | 295 + test/transport-http.unit.js | 352 + test/transport-https.unit.js | 46 + test/transport-udp.unit.js | 110 + test/utils.unit.js | 348 + test/version.unit.js | 19 + test/wallet.unit.js | 0 94 files changed, 20872 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 Dockerfile.dev create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bin/config.js create mode 100755 bin/kadence.js create mode 100644 doc/config.md create mode 100644 doc/identities.md create mode 100644 doc/index.json create mode 100644 doc/install.md create mode 100644 doc/messengers.md create mode 100644 doc/middleware.md create mode 100644 doc/nat.md create mode 100644 doc/plugins.md create mode 100644 doc/protocol.md create mode 100644 doc/quickstart.md create mode 100644 doc/transport-adapters.md create mode 100644 docker-compose.yml create mode 100644 example/expanded.js create mode 100644 example/minimal.js create mode 100644 index.js create mode 100644 lib/bucket.js create mode 100644 lib/constants.js create mode 100644 lib/contact-list.js create mode 100644 lib/control.js create mode 100644 lib/messenger.js create mode 100644 lib/node-abstract.js create mode 100644 lib/node-kademlia.js create mode 100644 lib/plugin-churnfilter.js create mode 100644 lib/plugin-contentaddress.js create mode 100644 lib/plugin-eclipse.js create mode 100644 lib/plugin-hashcash.js create mode 100644 lib/plugin-hashcash.worker.js create mode 100644 lib/plugin-hibernate.js create mode 100644 lib/plugin-logger.js create mode 100644 lib/plugin-onion.js create mode 100644 lib/plugin-quasar.js create mode 100644 lib/plugin-rolodex.js create mode 100644 lib/plugin-spartacus.js create mode 100644 lib/plugin-traverse.js create mode 100644 lib/plugin-trust.js create mode 100644 lib/routing-table.js create mode 100644 lib/rules-errors.js create mode 100644 lib/rules-kademlia.js create mode 100644 lib/transport-http.js create mode 100644 lib/transport-https.js create mode 100644 lib/transport-udp.js create mode 100644 lib/utils.js create mode 100644 lib/version.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 test/.eslintrc create mode 100644 test/bucket.unit.js create mode 100644 test/contact-list.unit.js create mode 100644 test/control.unit.js create mode 100644 test/fixtures/node-generator.js create mode 100644 test/fixtures/transport-fake.js create mode 100644 test/messenger.unit.js create mode 100644 test/node-abstract.unit.js create mode 100644 test/node-kademlia+transport-http.integration.js create mode 100644 test/node-kademlia+transport-udp.integration.js create mode 100644 test/node-kademlia.e2e.js create mode 100644 test/node-kademlia.unit.js create mode 100644 test/plugin-churnfilter.e2e.js create mode 100644 test/plugin-churnfilter.unit.js create mode 100644 test/plugin-contentaddress.e2e.js create mode 100644 test/plugin-contentaddress.unit.js create mode 100644 test/plugin-eclipse.unit.js create mode 100644 test/plugin-hashcash.e2e.js create mode 100644 test/plugin-hibernate.e2e.js create mode 100644 test/plugin-hibernate.unit.js create mode 100644 test/plugin-logger.unit.js create mode 100644 test/plugin-onion.unit.js create mode 100644 test/plugin-quasar.e2e.js create mode 100644 test/plugin-quasar.unit.js create mode 100644 test/plugin-rolodex.unit.js create mode 100644 test/plugin-spartacus.e2e.js create mode 100644 test/plugin-spartacus.unit.js create mode 100644 test/plugin-traverse.unit.js create mode 100644 test/plugin-trust.e2e.js create mode 100644 test/routing-table.unit.js create mode 100644 test/rules-errors.unit.js create mode 100644 test/rules-kademlia.unit.js create mode 100644 test/transport-http.unit.js create mode 100644 test/transport-https.unit.js create mode 100644 test/transport-udp.unit.js create mode 100644 test/utils.unit.js create mode 100644 test/version.unit.js create mode 100644 test/wallet.unit.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e6625f8 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +doc/**/*.js +doc/*.js +node_modules/**/*.js +test/* +plugin-onion.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..a9d32f5 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,79 @@ +{ + "env": { + "node": true, + "es6": true + }, + "globals": {}, + "rules": { + "no-bitwise": 0, + "camelcase": 0, + "curly": 2, + "eqeqeq": 2, + "no-extend-native": 2, + "wrap-iife": [ + 2, + "any" + ], + "indent": [ + 2, + 2, + { + "SwitchCase": 1 + } + ], + "no-use-before-define": [ + 2, + { + "functions": false + } + ], + "new-cap": 0, + "no-caller": 2, + "no-empty": 2, + "no-new": 2, + "quotes": [ + 2, + "single" + ], + "strict": [ + 2, + "global" + ], + "no-undef": 2, + "no-unused-vars": 2, + "max-params": [ + 2, + 4 + ], + "max-statements": [ + 2, + 20 + ], + "complexity": [ + 2, + 8 + ], + "max-depth": [ + 2, + 3 + ], + "max-len": [ + 2, + { + "code": 80, + "ignoreComments": true + } + ], + "no-multi-str": 2, + "keyword-spacing": [2, { + "after": true, + "before": true, + "overrides": { + "function": { + "after": false + } + } + }], + "brace-style": [2, "1tbs"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7ccfa31 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at gordonh@member.fsf.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0b964b8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +Contributing +============ + +This document outlines general patterns and conventions for contributing +to the project. **For in-depth documentation on Kadence, +[read the documentation](doc).** + +Coding Style +------------ + +Kadence adheres to +[Felix's Node.js Style Guide](https://github.com/felixge/node-style-guide). + +Test Coverage +------------- + +At the time of writing, Kad has near complete code coverage through +its test suite. It is important to never decrease coverage, so be sure to +include tests for any new code. + +You can run the coverage report with: + +``` +npm run coverage +``` + +Linting +------- + +To help maintain consistent expectations for code quality and enforcing these +conventions, there is an included `.eslintrc` file. Most editors support using +this to alert you of offending code in real time but, if your editor does not, +you can run the linter with: + +``` +npm run linter +``` + +Alternatively, the linter will run as part of the test suite as well, which can +be executed with: + +``` +npm test +``` + +--- + +Have fun and be excellent! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7d414f3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM debian:buster +LABEL maintainer "lily@tactcicalchihuahua.lol" +RUN apt-get update +RUN DEBIAN_FRONTEND=noninteractive apt-get -yq upgrade +RUN DEBIAN_FRONTEND=noninteractive apt-get -yq install wget apt-transport-https gnupg curl libssl-dev git python build-essential tor +RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - +RUN DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs +ENV GRANAX_USE_SYSTEM_TOR="1" +RUN git clone https://github.com/tactcicalchihuahua/kadence /root/kadence; \ + cd /root/kadence; \ + git fetch --tags; \ + git checkout $(git describe --tags `git rev-list --tags --max-count=1`); \ + cd /root/kadence && npm install --unsafe-perm --production +VOLUME ["/root/.config/kadence"] +EXPOSE 5274 +EXPOSE 5275 +ENV kadence_NodeListenAddress="0.0.0.0" +ENV kadence_ControlSockEnabled="0" +ENV kadence_ControlPortEnabled="1" +ENTRYPOINT ["/root/kadence/bin/kadence.js"] +CMD [] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..0e05b9d --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,20 @@ +FROM debian:buster +LABEL maintainer "lily@tacticalchihuahua.lol" +RUN apt-get update +RUN DEBIAN_FRONTEND=noninteractive apt-get -yq upgrade +RUN DEBIAN_FRONTEND=noninteractive apt-get -yq install wget apt-transport-https gnupg curl libssl-dev git python build-essential tor +RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - +RUN DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs +ENV GRANAX_USE_SYSTEM_TOR="1" +RUN git clone https://github.com/tacticalchihuahua/kadence /root/kadence; \ + cd /root/kadence && npm install --unsafe-perm --production +VOLUME ["/root/.config/kadence"] +EXPOSE 5274 +EXPOSE 5275 +ENV kadence_NodeListenAddress="0.0.0.0" +ENV kadence_ControlSockEnabled="0" +ENV kadence_ControlPortEnabled="1" +ENV kadence_TestNetworkEnabled="1" +ENV kadence_DaemonPidFilePath="/tmp/kadence.pid" +ENTRYPOINT ["/root/kadence/bin/kadence.js"] +CMD [] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a871fcf --- /dev/null +++ b/LICENSE @@ -0,0 +1,662 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2afddb1 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +

Kadence: An Extensible, Hardened, and Secure Distributed Systems Framework

+ +-- + +The Kadence Project is a complete implementation of the +[Kademlia](http://www.scs.stanford.edu/%7Edm/home/papers/kpos.pdf) distributed +hash table that aims to effectively mitigate *all vulnerabilities* described in +the [S/Kademlia](https://gnunet.org/sites/default/files/SKademlia2007.pdf) +paper and then some! Kadence provides developers of distributed systems a +complete framework for inventing new protocols on a rock solid base as well as +providing a complete reference implementation of a Kadence network. + +Ready to get started? + +``` +$ npm install -g @tacticalchihuahua/kadence +$ kadence --help +``` + +If you're new to Kadence, check out our tutorial for {@tutorial quickstart}! + +Features +-------- + +### Publish & Subscribe + +Kadence implements a completely decentralized publish/subscribe protocol based +on [Quasar](http://research.microsoft.com/en-us/um/people/saikat/pub/iptps08-quasar.pdf), +allowing you to build anything from peer-to-peer social networks to real time +sensor networks for the internet of things. + +### DDoS & Spam Protection + +Kadence enforces a [proof of work system](https://en.wikipedia.org/wiki/Proof-of-work_system) +called [Hashcash](https://en.wikipedia.org/wiki/Hash_cash) for relaying +messages to prevent abuse and make large scale denial of service and spam +attacks cost prohibitive. + +### Churn Impact Reduction + +Kadence proactively evicts offline or misbehaving peers from its routing table +and uses an exponential cooldown time for allowing them back in to prevent +unreliable contacts from propagating through the network. + +### Bandwidth Metering + +Kadence monitors bandwidth and enables end users to configure their maximum +bandwidth usage within a timeframe to suit their individual needs or prevent +overages with internet services providers that enforce +[bandwidth caps](https://en.wikipedia.org/wiki/Bandwidth_cap). + +### End-to-End Encryption + +Kadence can automatically generate SSL certificates and supports full +end-to-end encryption via TLS using it's built in HTTPS transport adapter to +prevent eavesdropping and [man in the middle attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attack). + +### Cryptographic Identities + +Kadence extends Kademlia's node identity selection with the same cryptography +bitcoin uses for securing funds. Node identities are derived from the hash of +the public portion of an [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) +key pair and each message is signed to ensure it hasn't been tampered with in +transit. + +### Sybil & Eclipse Mitigation + +Kadence employs a [proof of work system](https://en.wikipedia.org/wiki/Proof-of-work_system) +using [Equihash](https://en.wikipedia.org/wiki/Equihash) for generating valid +node identities and subsequent acceptance into the overlay network. This +forces nodes into sufficiently random sectors of the key space and makes +[Sybil](https://en.wikipedia.org/wiki/Sybil_attack) and +[Eclipse](http://www.eecs.harvard.edu/~mema/courses/cs264/papers/eclipse-infocom06.pdf) +attacks computationally very difficult and ultimately ineffective. + +### Automatic NAT Traversal + +Kadence supports multiple strategies for punching through +[network address translation](https://en.wikipedia.org/wiki/Network_address_translation). +This enables peers behind even the strictest of firewalls to become addressable +and join the network. Fallback to secure reverse tunnels is supported through +the use of [Diglet](https://gitlab.com/em/diglet) servers. + +### Multiple Network Transports + +Kadence supports the use of multiple transport adapters and is agnostic to the +underlying network protocol. Support for UDP and HTTP/HTTPS ship by default. +Plugin your own custom transport layer using using a simple interface. + +### Persistent Routing Tables + +Kadence remembers peers between restarts so after you've joined the network once +subsequent joins are fast and automatically select the best initial peers for +bootstrapping. + +### Sender & Destination Anonymity + +Kadence ships with full support for +[Tor Hidden Services](https://en.wikipedia.org/wiki/Tor_hidden_service) out of +the box with no additional software installation or configuration required. +This enables fully anonymized structured networks and leverages the latest +version 3 hidden services protocol. + +### Configurable Trust Policies + +Kadence provides a flexible trust policy plugin allowing for fine-tuned, +per-identity, per-method trust policies. Blacklist misbehaving nodes on an +open network or whitelist identities on an explicit trust-based network. + +### Simple Plugin Interface + +Kadence exposes a simple interface for extending the protocol with your own +application logic. Users of [Express](https://expressjs.com/) will find it +comfortable and familiar. If you are new to building distributed systems, you +will find it easy to get started. + +Research +-------- + +Kadence is used in academic research on distributed systems. Here are some +notable papers! + +* [Secure and Trustable Distributed Aggregation based on Kademlia](https://arxiv.org/pdf/1709.03265.pdf) +* [Distributed Random Process for a large-scale Peer-to-Peer Lottery](https://hal.inria.fr/hal-01583824/document) +* [DHT-based collaborative Web Translation](https://etd.ohiolink.edu/!etd.send_file?accession=ucin1479821556144121&disposition=inline) +* [Kademlia with Consistency Checks as a Foundation of Borderless Collaboration in Open Science Services](https://www.sciencedirect.com/science/article/pii/S1877050916327041) + +License +------- + +Kadence - Extensible, Hardened, and Secure Distributed Systems Framework +Copyright (C) 2019 Lily Anne Hall. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + + diff --git a/bin/config.js b/bin/config.js new file mode 100644 index 0000000..66718a7 --- /dev/null +++ b/bin/config.js @@ -0,0 +1,98 @@ +'use strict'; + +const ini = require('ini'); +const { existsSync, writeFileSync } = require('fs'); +const mkdirp = require('mkdirp'); +const { tmpdir, homedir } = require('os'); +const { join } = require('path'); + +const DEFAULT_DATADIR = join(homedir(), '.config/kadence'); + +module.exports = function(datadir) { + + datadir = datadir || DEFAULT_DATADIR; + + const options = { + + // Process PID + DaemonPidFilePath: join(datadir, 'kadence.pid'), + + // Identity/Cryptography + PrivateKeyPath: join(datadir, 'kadence.prv'), + IdentityNoncePath: join(datadir, 'nonce'), + IdentityProofPath: join(datadir, 'proof'), + + // Database + EmbeddedDatabaseDirectory: join(datadir, 'kadence.dht'), + EmbeddedPeerCachePath: join(datadir, 'peercache'), + + // Node Options + NodePublicPort: '5274', + NodeListenPort: '5274', + NodePublicAddress: '127.0.0.1', + NodeListenAddress: '0.0.0.0', + + // Onion Plugin + OnionEnabled: '0', + OnionVirtualPort: '443', + OnionHiddenServiceDirectory: join(datadir, 'hidden_service'), + OnionLoggingVerbosity: 'notice', + OnionLoggingEnabled: '0', + + // Bandwidth Metering + BandwidthAccountingEnabled: '0', + BandwidthAccountingMax: '5GB', + BandwidthAccountingReset: '24HR', + + // NAT Traversal + TraverseNatEnabled: '1', + TraversePortForwardTTL: '0', + TraverseReverseTunnelHostname: 'tun.tacticalchihuahua.lol', + TraverseReverseTunnelPort: '8443', + + // Churn Filter + ChurnFilterEnabled: '0', + ChurnCoolDownBaseTimeout: '5M', + ChurnCoolDownMultiplier: '2', + ChurnCoolDownResetTime: '60M', + + // SSL Certificate + SSLEnabled: '0', + SSLCertificatePath: join(datadir, 'kadence.crt'), + SSLKeyPath: join(datadir, 'kadence.key'), + SSLAuthorityPaths: [ + + ], + + // Network Bootstrapping + NetworkBootstrapNodes: [ + + ], + + // Debugging/Developer + VerboseLoggingEnabled: '1', + LogFilePath: join(datadir, 'kadence.log'), + LogFileMaxBackCopies: '3', + + // Local Control Protocol + ControlPortEnabled: '0', + ControlPort: '5275', + ControlSockEnabled: '1', + ControlSock: join(datadir, 'kadence.sock'), + + // Enables the Test Mode (lowers difficulty) + TestNetworkEnabled: '0' + + }; + + if (!existsSync(join(datadir, 'config'))) { + mkdirp.sync(datadir); + writeFileSync(join(datadir, 'config'), ini.stringify(options)); + } + + if (!existsSync(join(datadir, 'kadence.dht'))) { + mkdirp.sync(join(datadir, 'kadence.dht')); + } + + return options; +}; diff --git a/bin/kadence.js b/bin/kadence.js new file mode 100755 index 0000000..a1dacb1 --- /dev/null +++ b/bin/kadence.js @@ -0,0 +1,434 @@ +#!/usr/bin/env sh +':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@" + +'use strict'; + +// NB: We use self-signed certificates, *however*, we perform our own +// NB: authentication/authorization via ECDSA, so this is fine. We don't +// NB: care about certificate authorities, just TLS, because our nodes +// NB: identified by public key hashes and verified by signatures. +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +const { homedir } = require('os'); +const assert = require('assert'); +const async = require('async'); +const program = require('commander'); +const kadence = require('../index'); +const bunyan = require('bunyan'); +const RotatingLogStream = require('bunyan-rotating-file-stream'); +const fs = require('fs'); +const path = require('path'); +const options = require('./config'); +const npid = require('npid'); +const daemon = require('daemon'); +const pem = require('pem'); +const levelup = require('levelup'); +const leveldown = require('leveldown'); +const boscar = require('boscar'); +const { fork } = require('child_process'); +const os = require('os'); +const ms = require('ms'); +const rc = require('rc'); +const ini = require('ini'); +const encoding = require('encoding-down'); +const secp256k1 = require('secp256k1'); + + +program.version(` + kadence ${kadence.version.software} + protocol ${kadence.version.protocol} +`); + +program.description(` + Copyright (c) 2019 Lily Anne Hall. + Licensed under the GNU Affero General Public License Version 3 +`); + +program.option('--config ', 'path to a kadence configuration file', + path.join(homedir(), '.config/kadence/config')); +program.option('--datadir ', 'path to the default data directory', + path.join(homedir(), '.config/kadence')); +program.option('--shutdown', 'sends the shutdown signal to the daemon'); +program.option('--testnet', 'runs with reduced identity difficulty'); +program.option('--daemon', 'sends the kadence daemon to the background'); +program.option('--rpc [params]', 'send a command to the daemon'); +program.parse(process.argv); + +let argv; + +if (program.datadir) { + argv = { config: path.join(program.datadir, 'config') }; + program.config = argv.config; +} + +if (program.testnet) { + process.env.kadence_TestNetworkEnabled = '1'; +} + +let config = rc('kadence', options(program.datadir), argv); +let privkey, identity, logger, controller, node, nonce, proof; + + +// Handle certificate generation +function _generateSelfSignedCertificate() { + return new Promise((resolve, reject) => { + pem.createCertificate({ + days: 365, + selfSigned: true + }, (err, keys) => { + if (err) { + return reject(err); + } + + fs.writeFileSync(config.SSLKeyPath, keys.serviceKey); + fs.writeFileSync(config.SSLCertificatePath, keys.certificate); + resolve(); + }); + }); +} + +// Initialize logging +logger = bunyan.createLogger({ + name: 'kadence', + streams: [ + { + stream: new RotatingLogStream({ + path: config.LogFilePath, + totalFiles: parseInt(config.LogFileMaxBackCopies), + rotateExisting: true, + gzip: false + }) + }, + { stream: process.stdout } + ], + level: parseInt(config.VerboseLoggingEnabled) ? 'debug' : 'info' +}); + +if (parseInt(config.TestNetworkEnabled)) { + logger.info('kadence is running in test mode, difficulties are reduced'); + process.env.kadence_TestNetworkEnabled = config.TestNetworkEnabled; + kadence.constants.IDENTITY_DIFFICULTY = kadence.constants.TESTNET_DIFFICULTY; +} + +if (parseInt(config.TraverseNatEnabled) && parseInt(config.OnionEnabled)) { + logger.error('refusing to start with both TraverseNatEnabled and ' + + 'OnionEnabled - this is a privacy risk'); + process.exit(1); +} + +async function _init() { + // Generate a private extended key if it does not exist + if (!fs.existsSync(config.PrivateKeyPath)) { + fs.writeFileSync(config.PrivateKeyPath, kadence.utils.generatePrivateKey()); + } + + if (fs.existsSync(config.IdentityProofPath)) { + proof = fs.readFileSync(config.IdentityProofPath); + } + + if (fs.existsSync(config.IdentityNoncePath)) { + nonce = parseInt(fs.readFileSync(config.IdentityNoncePath).toString()); + } + + if (program.shutdown) { + try { + process.kill(parseInt( + fs.readFileSync(config.DaemonPidFilePath).toString().trim() + ), 'SIGTERM'); + } catch (err) { + logger.error('failed to shutdown daemon, is it running?'); + process.exit(1); + } + process.exit(); + } + + if (parseInt(config.SSLEnabled) && !fs.existsSync(config.SSLKeyPath)) { + await _generateSelfSignedCertificate(); + } + + if (program.daemon) { + require('daemon')({ cwd: process.cwd() }); + } + + try { + npid.create(config.DaemonPidFilePath).removeOnExit(); + } catch (err) { + logger.error('Failed to create PID file, is kadence already running?'); + process.exit(1); + } + + // Shutdown children cleanly on exit + process.on('exit', killChildrenAndExit); + process.on('SIGTERM', killChildrenAndExit); + process.on('SIGINT', killChildrenAndExit); + process.on('uncaughtException', (err) => { + npid.remove(config.DaemonPidFilePath); + logger.error(err.message); + logger.debug(err.stack); + process.exit(1); + }); + process.on('unhandledRejection', (err) => { + npid.remove(config.DaemonPidFilePath); + logger.error(err.message); + logger.debug(err.stack); + process.exit(1); + }); + + // Initialize private extended key + privkey = fs.readFileSync(config.PrivateKeyPath); + identity = new kadence.eclipse.EclipseIdentity( + secp256k1.publicKeyCreate(privkey), + nonce, + proof + ); + + // If identity is not solved yet, start trying to solve it + if (!identity.validate()) { + logger.warn(`identity proof not yet solved, this can take a while`); + await identity.solve(); + fs.writeFileSync(config.IdentityNoncePath, identity.nonce.toString()); + fs.writeFileSync(config.IdentityProofPath, identity.proof); + } + + init(); +} + +function killChildrenAndExit() { + logger.info('exiting, killing child services, cleaning up'); + npid.remove(config.DaemonPidFilePath); + process.removeListener('exit', killChildrenAndExit); + + if (controller && parseInt(config.ControlSockEnabled)) { + controller.server.close(); + } + + process.exit(0); +} + +function registerControlInterface() { + assert(!(parseInt(config.ControlPortEnabled) && + parseInt(config.ControlSockEnabled)), + 'ControlSock and ControlPort cannot both be enabled'); + + controller = new boscar.Server(new kadence.Control(node)); + + if (parseInt(config.ControlPortEnabled)) { + logger.info('binding controller to port ' + config.ControlPort); + controller.listen(parseInt(config.ControlPort), '0.0.0.0'); + } + + if (parseInt(config.ControlSockEnabled)) { + logger.info('binding controller to path ' + config.ControlSock); + controller.listen(config.ControlSock); + } +} + +async function init() { + logger.info('initializing kadence'); + + // Initialize public contact data + const contact = { + hostname: config.NodePublicAddress, + protocol: parseInt(config.SSLEnabled) ? 'https:' : 'http:', + port: parseInt(config.NodePublicPort) + }; + + let transport; + + if (parseInt(config.SSLEnabled)) { + const key = fs.readFileSync(config.SSLKeyPath); + const cert = fs.readFileSync(config.SSLCertificatePath); + const ca = config.SSLAuthorityPaths.map(fs.readFileSync); + + transport = new kadence.HTTPSTransport({ key, cert, ca }); + } else { + transport = new kadence.HTTPTransport(); + } + + // Initialize protocol implementation + node = new kadence.KademliaNode({ + logger, + transport, + contact, + storage: levelup(encoding(leveldown(config.EmbeddedDatabaseDirectory))) + }); + + node.hashcash = node.plugin(kadence.hashcash({ + methods: ['PUBLISH', 'SUBSCRIBE'], + difficulty: 8 + })); + node.quasar = node.plugin(kadence.quasar()); + node.spartacus = node.plugin(kadence.spartacus(privkey, { + checkPublicKeyHash: false + })); + node.content = node.plugin(kadence.contentaddress({ + valueEncoding: 'hex' + })); + node.eclipse = node.plugin(kadence.eclipse(identity)); + node.rolodex = node.plugin(kadence.rolodex(config.EmbeddedPeerCachePath)); + + // Check if we need to enable the churn filter plugin (experimental) + if (parseInt(config.ChurnFilterEnabled)) { + node.blacklist = node.plugin(kadence.churnfilter({ + cooldownBaseTimeout: config.ChurnCoolDownBaseTimeout, + cooldownMultiplier: parseInt(config.ChurnCoolDownMultiplier), + cooldownResetTime: config.ChurnCoolDownResetTime + })); + } + + // Hibernate when bandwidth thresholds are reached + if (!!parseInt(config.BandwidthAccountingEnabled)) { + node.hibernate = node.plugin(kadence.hibernate({ + limit: config.BandwidthAccountingMax, + interval: config.BandwidthAccountingReset, + reject: ['FIND_VALUE', 'STORE'] + })); + } + + // Use Tor for an anonymous overlay + if (!!parseInt(config.OnionEnabled)) { + kadence.constants.T_RESPONSETIMEOUT = 20000; + node.onion = node.plugin(kadence.onion({ + dataDirectory: config.OnionHiddenServiceDirectory, + virtualPort: config.OnionVirtualPort, + localMapping: `127.0.0.1:${config.NodeListenPort}`, + torrcEntries: { + CircuitBuildTimeout: 10, + KeepalivePeriod: 60, + NewCircuitPeriod: 60, + NumEntryGuards: 8, + Log: `${config.OnionLoggingVerbosity} stdout` + }, + passthroughLoggingEnabled: !!parseInt(config.OnionLoggingEnabled) + })); + } + + // Punch through NATs + if (!!parseInt(config.TraverseNatEnabled)) { + node.traverse = node.plugin(kadence.traverse([ + new kadence.traverse.UPNPStrategy({ + mappingTtl: parseInt(config.TraversePortForwardTTL), + publicPort: parseInt(node.contact.port) + }), + new kadence.traverse.NATPMPStrategy({ + mappingTtl: parseInt(config.TraversePortForwardTTL), + publicPort: parseInt(node.contact.port) + }), + new kadence.traverse.ReverseTunnelStrategy({ + remoteAddress: config.TraverseReverseTunnelHostname, + remotePort: parseInt(config.TraverseReverseTunnelPort), + privateKey: node.spartacus.privateKey, + secureLocalConnection: parseInt(config.SSLEnabled), + verboseLogging: parseInt(config.VerboseLoggingEnabled) + }) + ])); + } + + // Handle any fatal errors + node.on('error', (err) => { + logger.error(err.message.toLowerCase()); + }); + + // Use verbose logging if enabled + if (!!parseInt(config.VerboseLoggingEnabled)) { + node.plugin(kadence.logger(logger)); + } + + // Cast network nodes to an array + if (typeof config.NetworkBootstrapNodes === 'string') { + config.NetworkBootstrapNodes = config.NetworkBootstrapNodes.trim().split(); + } + + async function joinNetwork(callback) { + let peers = config.NetworkBootstrapNodes.concat( + await node.rolodex.getBootstrapCandidates() + ); + + if (peers.length === 0) { + logger.info('no bootstrap seeds provided and no known profiles'); + logger.info('running in seed mode (waiting for connections)'); + + return node.router.events.once('add', (identity) => { + config.NetworkBootstrapNodes = [ + kadence.utils.getContactURL([ + identity, + node.router.getContactByNodeId(identity) + ]) + ]; + joinNetwork(callback) + }); + } + + logger.info(`joining network from ${peers.length} seeds`); + async.detectSeries(peers, (url, done) => { + const contact = kadence.utils.parseContactURL(url); + node.join(contact, (err) => { + done(null, (err ? false : true) && node.router.size > 1); + }); + }, (err, result) => { + if (!result) { + logger.error('failed to join network, will retry in 1 minute'); + callback(new Error('Failed to join network')); + } else { + callback(null, result); + } + }); + } + + node.listen(parseInt(config.NodeListenPort), () => { + logger.info( + `node listening on local port ${config.NodeListenPort} ` + + `and exposed at ${node.contact.protocol}//${node.contact.hostname}` + + `:${node.contact.port}` + ); + registerControlInterface(); + async.retry({ + times: Infinity, + interval: 60000 + }, done => joinNetwork(done), (err, entry) => { + if (err) { + logger.error(err.message); + process.exit(1); + } + + logger.info(`connected to network via ${entry}`); + logger.info(`discovered ${node.router.size} peers from seed`); + }); + }); +} + +// Check if we are sending a command to a running daemon's controller +if (program.rpc) { + assert(!(parseInt(config.ControlPortEnabled) && + parseInt(config.ControlSockEnabled)), + 'ControlSock and ControlPort cannot both be enabled'); + + const client = new boscar.Client(); + + if (parseInt(config.ControlPortEnabled)) { + client.connect(parseInt(config.ControlPort)); + } else if (parseInt(config.ControlSockEnabled)) { + client.connect(config.ControlSock); + } + + client.on('ready', () => { + const [method, ...params] = program.rpc.split(' '); + client.invoke(method, params, function(err, ...results) { + if (err) { + console.error(err); + process.exit(1); + } else { + console.info(results); + process.exit(0); + } + }); + }); + + client.on('error', err => { + console.error(err); + process.exit(1) + }); +} else { + // Otherwise, kick everything off + _init(); +} diff --git a/doc/config.md b/doc/config.md new file mode 100644 index 0000000..186aeca --- /dev/null +++ b/doc/config.md @@ -0,0 +1,230 @@ +This guide will show you how to get started with running `kadence`! A Kadence +node requires a configuration file to get up and running. The path to this +file is given to `kadence` when starting a node (or the defaults will be used). + +``` +kadence --config myconfig.ini +``` + +If a configuration file is not supplied, a minimal default configuration is +automatically created and used, which will generate a private extended key, +self-signed certificate, database, and other necessary files. All of this data +will be created and stored in `$HOME/.config/kadence`, unless a `--datadir` +option is supplied. Valid configuration files may be in either INI or JSON +format. + +#### DaemonPidFilePath + +##### Default: `$HOME/.config/kadence/kadence.pid` + +The location to write the PID file for the daemon. + +#### PrivateExtendedKeyPath + +##### Default: `$HOME/.config/kadence/kadence.prv` + +Path to private extended key file to use for master identity. + +#### ChildDerivationIndex + +##### Default: `0` + +The index for deriving this node's identity in accordance with the identity +difficulty. + +#### EmbeddedDatabaseDirectory + +##### Default: `$HOME/.config/kadence/kadence.dht` + +Sets the directory to store DHT entries. + +#### EmbeddedPeerCachePath + +##### Default: `$HOME/.config/kadence/peercache` + +File to store discovered peers for bootstrapping on subsequent restarts. + +#### EmbeddedWalletDirectory + +##### Default: `$HOME/.config/kadence/wallet.dat` + +Sets the directory to store solution files for storing entries in the DHT. + +#### NodePublicPort + +##### Default: `5274` + +Sets the port number to advertise to the network for reaching this node. + +#### NodeListenPort + +##### Default: `5274` + +Sets the local port to bind the node's RPC service. + +#### NodePublicAddress + +##### Default: `127.0.0.1` + +Sets the public address to advertise to the network for reaching this node. +If traversal strategies are enabled and succeed, this will be changed +automatically. If onion mode is enabled, then this should be left at it's +default. + +#### NodeListenAddress + +##### Default: `0.0.0.0` + +Sets the address to bind the RPC service. + +#### BandwidthAccountingEnabled + +##### Default: `0` + +Enables bandwidth metering and hibernation mode. When the property +BandwidthAccountingEnabled is `1`, we will enter low-bandwidth mode if the we +exceed `BandwidthAccountingMax` within the period defined by +`BandwidthAccountingReset` until the interval is finished. + +#### BandwidthAccountingMax + +##### Default: `5GB` + +Sets the maximum number of bandwidth to use per accounting interval for data +transfer. Low-bandwidth RPC messages will still be allowed. + +#### BandwidthAccountingReset + +##### Default: `24HR` + +Resets the bandwidth accounting on an interval defined by this property. + +#### VerboseLoggingEnabled + +##### Default: `1` + +More detailed logging of messages sent and received. Useful for debugging. + +#### LogFilePath + +##### Default: `$HEAD/.config/kadence.log` + +Path to write the daemon's log file. Log file will rotate either every 24 hours +or when it exceeds 10MB, whichever happens first. + +#### LogFileMaxBackCopies + +##### Default: `3` + +Maximum number of rotated log files to keep. + +#### NetworkBootstrapNodes[] + +##### Default: `(empty)` + +Add a map of network bootstrap nodes to this section to use for discovering +other peers. Default configuration should come with a list of known and +trusted contacts. + +#### OnionEnabled + +##### Default: `0` + +Places Kadence into anonymous mode, which establishes the node exclusively as +a Tor hidden services and forces all requests through the Tor network. + +#### OnionVirtualPort + +##### Default: `443` + +The virtual port to use for the hidden service. + +#### OnionHiddenServiceDirectory + +##### Default: `$HOME/.config/kadence/hidden_service` + +The directory to store hidden service keys and other information required by +the Tor process. + +#### OnionLoggingEnabled + +##### Default: `0` + +Redirects the Tor process log output through Kadence's logger for the purpose of +debugging. + +#### OnionLoggingVerbosity + +##### Default: `notice` + +Defines the verbosity level of the Tor process logging. Valid options are: +`debug`, `info`, `notice`. + +#### TraverseNatEnabled + +##### Default: `1` + +Enables UPnP and NAT-PMP traversal strategies for becoming addressable on the +public internet. + +#### TraversePortForwardTTL + +##### Default: `0` + +How long to keep the port mapping active on the router. The value `0` means +indefinitely (until revoked). + +#### SSLEnabled + +##### Default: `0` + +Flag to instruct the daemon to use SSL/TLS to secure communication. + +#### SSLCertificatePath + +##### Default: `$HOME/.config/kadence/kadence.crt` + +Path to the SSL certificate for our node. + +#### SSLKeyPath + +##### Default: `$HOME/.config/kadence/kadence.key` + +Path to the SSL private key for our node. + +#### SSLAuthorityPaths[] + +##### Default: `(emtpy)` + +Paths to intermediate certificate authority chains. + +#### ControlPortEnabled + +##### Default: `0` + +Enables the {@link Control} interface over a TCP socket. + +#### ControlPort + +##### Default: `5275` + +The TCP port to for the control interface to listen on. + +#### ControlSockEnabled + +##### Default: `1` + +Enables the {@link Control} interface over a UNIX domain socket. + +#### ControlSock + +##### Default: `$HOME/.config/kadence/kadence.sock` + +The path to the file to use for the control interface. + +#### TestNetworkEnabled + +##### Default: `0` + +Places Kadence into test mode, significantly lowering the identity solution +difficulty and the permission solution difficulty. diff --git a/doc/identities.md b/doc/identities.md new file mode 100644 index 0000000..9700388 --- /dev/null +++ b/doc/identities.md @@ -0,0 +1,54 @@ +Kadence represents other peers by using a {@link Bucket~contact} pair. Any +time an entry in a {@link Bucket} is retrieved or placed, it is in the format +of a tuple. The item at index 0 is *always* the string representation of the +{@link module:kadence/constants~B} size identity key in hexadecimal. The item +at index 1 can be any arbitrary JSON serializable object that the +{@link transport-adapters transport adapter} in use understands. + +For example, the {@link HTTPTransport} and the {@link UDPTransport} both accept +an object cotaining `hostname` and `port` properties. Other transports may +accept whatever they need. When constructing your {@link KademliaNode} +instance, these properties are set by you as `identity` and `contact`. If the +`identity` value is omitted, it will be randomly generated. + +> Take note that for a stable network, you will need to persist identities +> generated as nodes store data based on this key. + +```js +const node = new kadence.KademliaNode({ + // ... + identity: Buffer.from('059e5ce8d0d3ee0225ffe982e38f3f5f6f748328', 'hex'), + contact: { + hostname: 'my.reachable.hostname', + port: 1337 + } +}); +``` + +Since nodes may be using {@link module:kadence/traverse} to become addressable +on the internet, this {@link Bucket~contact} pair is included in every message +payload instead of relying on inferred return address information at the +transport layer. This makes every JSON-RPC message payload an array, containing +a request message at index 0 and a idenity notification at index 1. + +```json +[ + { + "jsonrpc": "2.0", + "id": "", + "method": "FIND_NODE", + "params": ["059e5ce8d0d3ee0225ffe982e38f3f5f6f748328"] + }, + { + "jsonrpc": "2.0", + "method": "IDENTITY", + "params": [ + "059e5ce8d0d3ee0225ffe982e38f3f5f6f748328", + { + "hostname": "", + "port": 1337 + } + ] + } +] +``` diff --git a/doc/index.json b/doc/index.json new file mode 100644 index 0000000..044b281 --- /dev/null +++ b/doc/index.json @@ -0,0 +1,32 @@ +{ + "config": { + "title": "Configuration Guide" + }, + "quickstart": { + "title": "Getting Started" + }, + "protocol": { + "title": "Protocol Specification" + }, + "install": { + "title": "Installing Kadence" + }, + "transport-adapters": { + "title": "Transport Adapters" + }, + "messengers": { + "title": "Message Format" + }, + "middleware": { + "title": "Middleware Stack" + }, + "plugins": { + "title": "Using and Authoring Plugins" + }, + "identities": { + "title": "Contacts and Identities" + }, + "nat": { + "title": "Traversing NATs and Firewalls" + } +} diff --git a/doc/install.md b/doc/install.md new file mode 100644 index 0000000..59d891d --- /dev/null +++ b/doc/install.md @@ -0,0 +1,78 @@ +Make sure you have the following prerequisites installed: + +* [Git](https://git-scm.org) +* [Node.js LTS + NPM (10.15.x)](https://nodejs.org) +* Python 2.7 +* GCC/G++/Make + +### Node.js + NPM + +#### GNU+Linux & Mac OSX + +``` +wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +``` + +Close your shell and open an new one. Now that you can call the `nvm` program, +install Node.js (which comes with NPM): + +``` +nvm install --lts +``` + +### Build Dependencies + +#### GNU+Linux + +Debian / Ubuntu / Mint / Trisquel / and Friends + +``` +apt install git python build-essential +``` + +Red Hat / Fedora / CentOS + +``` +yum groupinstall 'Development Tools' +``` + +You might also find yourself lacking a C++11 compiler - +[see this](http://hiltmon.com/blog/2015/08/09/c-plus-plus-11-on-centos-6-dot-6/). + +#### Mac OSX + +``` +xcode-select --install +``` + +#### Windows + +Run as administrator in PowerShell or cmd: + +``` +npm install -g windows-build-tools +``` + +### Daemon + +This package exposes the program `kadence`. To install, use the `--global` flag. + +``` +npm install -g @deadcanaries/kadence +``` + +### Core Library + +This package exposes a module providing a complete reference implementation +of the protocol. To use it in your project, from your project's root +directory, install as a dependency. + +``` +npm install @tacticalchihuahua/kadence --save +``` + +Then you can require the library with: + +``` +const kadence = require('@tacticalchihuahua/kadence'); +``` diff --git a/doc/messengers.md b/doc/messengers.md new file mode 100644 index 0000000..7b65c66 --- /dev/null +++ b/doc/messengers.md @@ -0,0 +1,47 @@ +Kadence implements a generic {@link Messenger} class that is used as the interface +between the application layer and the {@link AbstractNode~transport}. This +interface exposes 2 primary members: {@link Messenger~serializer} and +{@link Messenger~deserializer}. + +As you might expect, both of these objects are streams. Both are transform +streams. The transport adapter's readable end is piped through the +deserializer which is then processed by the middleware stack implemented by +the {@link AbstractNode}. The serializer is piped through the transport +adapter's writable end, which dispatches the message. + +The serializer and deserializer are +[metapipe](https://www.npmjs.com/package/metapipe) objects (transform streams +which are composed of a modifiable stack of transform streams). This means you +can extend the behavior of messages processing by simply prepending or +appending your own transforms. + +> By default, these metapipes use a built-in JSON-RPC serializer and +> deserializer. It is possible to completely change the message format sent +> over the network if desired by passing {@link KademliaNode} your own instance +> of {@link Messenger} using your own serializer and deserializer. + +Below is an example of extended the message processing pipeline. + +``` +const { Transform } = require('stream'); +const node = new kadence.KademliaNode(options); + +node.rpc.serializer.prepend(() => new Transform({ + transform function(data, encoding, callback) { + + }, + objectMode: true +})); + +node.rpc.deserializer.append(() => new Transform({ + transform: function(data, encoding, callback) { + + }, + objectMode: true +})); +``` + +> Note that the {@link KademliaRules} still expect the deserialized message to +> include `method` and `params` properties (conforming to +> {@link AbstractNode~request}). + diff --git a/doc/middleware.md b/doc/middleware.md new file mode 100644 index 0000000..150d678 --- /dev/null +++ b/doc/middleware.md @@ -0,0 +1,105 @@ +Kadence exposes an interface similar to [express](https://expressjs.com)'s +`use()` method to allow for extending the protocol via message processing +middleware. There are 4 distinct middleware stacks that process incoming +messages in a particular order: + +### Global Message Stack + +Global middleware is applied to any and all deserialized messages that are +provided to {@link AbstractNode} from the {@link Messenger~deserializer}. +Handlers added here are useful for any global logic that needs to be applied +such as validation of message format. + +Global middleware handlers follow the signature defined by +{@link AbstractNode~middleware} and are supplied as the only argument to +{@link AbstractNode#use}. The middleware handler receives, in order, +{@link AbstractNode~request}, {@link AbstractNode~response}, and +{@link AbstractNode~next} (which may be called with an error to exit the stack +and trigger the error handler stack). + +A simple example of a global middleware handler is a node blacklist. In the +example below, we define a set of node identities from which we wish to reject +all messages. + +```js +const blacklist = new Set([/* misbehaving node ids */]); + +node.use(function(request, response, next) { + let [identity] = request.contact; + + if (blacklist.includes(identity)) { + return next(new Error('Go away!')); // Exit this stack and enter error stack + } + + next(); // Continue to next handler in stack +}); +``` + +### Filtered Message Stack + +The primary function of the middleware stack is to enable developers to invent +new protocols by defining handlers for new methods. Similar to the Express +framework, if we wish to only apply a handler to certain types of messages, we +can define the method name as the first argument supplied to +{@link AbstractNode#use} and our handler as the second. + +This enables us to extend the base Kademlia protocol with new methods and +behaviors. If a message is received that invokes a method for which there is +not a handler defined, after being processed by the global stack, it will enter +the error handler stack with a "Method not found" error object. + +To demonstrate how this works, we provide an example of an `ECHO` handler - a +new protocol method that simply echoes the argument provided back to the +sender. + +```js +node.use('ECHO', function(request, response, next) { + let [message] = request.params; + + if (!message) { + return next(new Error('Nothing to echo')); // Exit to the error stack + } + + response.send([message]); // Respond back with the argument provided +}); +``` + +Like the global message stack, the filtered message stack can also have many +handlers defined. This is useful in the event that you want to provide +per-message-type validation without placing all of that logic into a single +handler. The same rules apply, call {@link AbstractNode~next} to move to the +next handler in the stack and call {@link AbstractNode~response#send} to halt +the stack and respond to the message. + +### Error Handler Stack + +Error handling middleware is applied to any message which previously resulting +in a call to {@link AbstractNode~next} with an error parameter. They are +defined by including an error argument in the first position to a +{@link AbstractNode~middleware} function. These can be scoped globally or by +protocol and will behave just like the global message stack and filtered +message stack. When a message enters the error handler stack, first it will +pass through the global error handlers then the filtered error handlers. If +there are no error handler middleware functions defined, the default handler, +which simply responds with the error message, is used. + +```js +node.use(function(err, request, response, next) { + if (!err) { + response.error('Method not found', -32602); + } else { + response.error(err.message, err.code); + } +}); + +node.use('ECHO', function(err, request, response, next) { + response.error(`ECHO error: ${err.message}`); +}); +``` + +Remember, the number of arguments supplied to {@link AbstractNode~middleware} +**matters**. Error handlers are registered if and only if there are 4 arguments +provided to {@link AbstractNode~middleware}: `error`, +{@link AbstractNode~request}, {@link AbstractNode~response}, and +{@link AbstractNode~next} *in order*. If there are less than four arguments +provided to the handler, it will not be inserted into the error handler stack. diff --git a/doc/nat.md b/doc/nat.md new file mode 100644 index 0000000..d61c9bc --- /dev/null +++ b/doc/nat.md @@ -0,0 +1,63 @@ +One of the most frustrating and daunting problems when deploying a distributed +network to users is dealing with NAT (or "[Network Address Translation](https://en.wikipedia.org/wiki/Network_address_translation)". +Kadence provides a plugin for traversing these systems with mulitple strategies +and is capable of breaking out of just about any network (albeit sometimes at +the expensive of performance). + +This functionality is encapsulated in the {@link module:kadence/traverse} +plugin, which initializes a {@link module:kadence/traverse~TraversePlugin}. +This plugin makes use of the {@link module:kadence/traverse~TraverseStrategy} +instances that are passed to it. At the time of writing, Kadence supports +UPnP, NAT-PMP, and a fallback reverse HTTPS tunneling mechanism (for use with +the {@link HTTPSTransport} and {@link HTTPTransport}). + +#### Using the Plugin + +Generally, you'll want to use all of the available strategies, but since some +strategies may only work with certain transports, you must explicity define +them when calling the plugin. + +```js +const node = new kadence.KademliaNode(options); // See "Getting Started" + +node.spartacus = node.plugin(kadence.spartacus()); // Optional, but recommended + +node.traverse = node.plugin(kadence.traverse([ // List in order of attempt + new kadence.traverse.UPNPStrategy({ + mappingTtl: 0, // Means keep this mapping until unmapped + publicPort: node.contact.port // The public port to map + }), + new kadence.traverse.NATPMPStrategy({ + mappingTtl: 0, // Means keep this mapping until unmapped + publicPort: node.contact.port // The public port to map + }), + new kadence.traverse.ReverseTunnelStrategy({ + remoteAddress: 'tun.tacticalchihuahua.lol', // Hostname of a Diglet server + remotePort: 8443, // Tunnel port of a Diglet server + verboseLogging: false, // If debuggin, set to `true` + secureLocalConnection: false, // Set to `true` if using HTTPSTransport + privateKey: node.spartacus.privateKey // Uses identity for tunnel routing + }) +])); +``` + +In the example above, we are assuming use of the {@link HTTPTransport}. When +the method {@link KademliaNode#listen} is called, this plugin will execute +each strategy in the order they are defined until one of them successfully +traverses the NAT and becomes public on the internet. If all of them fail, a +message will indicate that you are not addressable. + +The UPnP and NAT-PMP strategies attempt to instruct the router / NAT device +to forward a port to internet. Sometimes this is a feature that must be +enabled on the router itself and sometimes it is not supported at all - either +by the device or the ISP. + +The reverse tunnel strategy works by establishing an outbound connection to a +[Diglet server](https://github.com/tacticalchihuahua/diglet) which acts as a reverse +proxy back to your node on the outbound connection. By default, the plugin will +use a test server, but this may or may not be online or functioning at any +given time. The recommendation is to run your own Diglet server, which is very +simple to set up. Ideally, your users should each run their own Diglet server +to prevent dense centralization around a single tunnel, however this is +generally unreasonable to expect so it's good to bake in a rotating list of +public Diglet servers into your code. diff --git a/doc/plugins.md b/doc/plugins.md new file mode 100644 index 0000000..f7e2178 --- /dev/null +++ b/doc/plugins.md @@ -0,0 +1,50 @@ +Kadence plugins are a simple way to package additional features. A plugin is just +a function that receives an instance of {@link KademliaNode}. This function can +then apply any decorations desired. + +### Included Plugins + +* {@link module:kadence/eclipse~EclipsePlugin} +* {@link module:kadence/hashcash~HashCashPlugin} +* {@link module:kadence/hibernate~HibernatePlugin} +* {@link module:kadence/onion~OnionPlugin} +* {@link module:kadence/permission~PermissionPlugin} +* {@link module:kadence/quasar~QuasarPlugin} +* {@link module:kadence/rolodex~RolodexPlugin} +* {@link module:kadence/spartacus~SpartacusPlugin} +* {@link module:kadence/traverse~TraversePlugin} + +### Example: "Howdy, Neighbor" Plugin + +```js +/** + * Example "howdy, neighbor" plugin + * @function + * @param {KademliaNode} node + */ +module.exports = function(node) { + + const { identity } = node; + + /** + * Respond to HOWDY messages + */ + node.use('HOWDY', (req, res) => { + res.send(['howdy, neighbor']); + }); + + /** + * Say howdy to our nearest neighbor + */ + node.sayHowdy = function(callback) { + let neighbor = [ + ...node.router.getClosestContactsToKey(identity).entries() + ].shift(); + + node.send('HOWDY', ['howdy, neighbor'], neighbor, callback); + }; + +}; +``` + + diff --git a/doc/protocol.md b/doc/protocol.md new file mode 100644 index 0000000..a33fa65 --- /dev/null +++ b/doc/protocol.md @@ -0,0 +1,288 @@ +### Version 3.0 (March 3, 2018) + +Lily Anne Hall (lily@tacticalchihuahua.lol) + +--- + +### 0 License + +Copyright (C) 2018 Lily Anne Hall + +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.3 +or any later version published by the Free Software Foundation; +with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. +A copy of the license is included in the "LICENSE" file. + +### 1 Introduction + +This specification documents the Kadence protocol in its entirety for +the purpose of enabling its implementation in other languages. Described here, +is the protocol base - the minimum specification for compatibility with +Kadence. Additional optional extensions to this work may be defined in a +future specification. + +### 2 Identities + +Every node (host computer speaking the Kadence protocol) on the network possesses +a unique cryptographic identity. This identity is used to derive a special +160 bit identifier for the purpose of organizaing the overlay structure and +routing messages _(3.1: Kademlia)_. In order for a node to join the network it +must generate an identity. + +Identities are the RMD160 hash of an Equihash proof where the node's public +key is the proof input. Messages are signed with the corresponding private key. +This is designed to provide some resilience against sybil and, in particular, +eclipse attacks. An eclipse attack is a type of censorship by which an attacker +is able to manipulate the network's routing tables such that the attacker is +able to "surround" a target without their knowledge. + +In every message exchanged on the network, each party will include a tuple +structure which includes enough information to locate and authenticate each +party. + +``` +["", { /* */ }] +``` + +#### 2.1 Contact Hash Map + +The second entry in the identity tuple contains additional information specific +to addressing the node on the network. This includes: + +``` +{ + "hostname": "xxxxxxxx.onion", + "port": 80, + "protocol": "http:", + "pubkey": "...", + "proof": "..." +} +``` + +Additional properties may be included based on individual use cases within the +network, however the properties above are **required**. + +### 3 Network Structure + +Kadence employs a **structured** network, meaning that nodes are organized and +route messages based on a deterministic metric. The network uses a +[Kademlia](http://www.scs.stanford.edu/~dm/home/papers/kpos.pdf) distributed +hash table as the basis for the network overlay. In addition to Kademlia, +Kadence also employs other extensions to mitigate issues and attacks defined +by the work on [S/Kademlia](http://www.tm.uka.de/doc/SKademlia_2007.pdf). + +#### 3.1 Kademlia + +Once an Kadence node has completed generating its identity, it bootstraps its +routing table by following the Kademlia "join" procedure. This involves +querying a single known "seed" node for contact information about other nodes +that possess a Node ID that is close (XOR distance) to its own +_(`4.4 FIND_NODE`)_. This is done iteratively, sending the same query to the +`ALPHA` (3) results that are closest, until the further queries no longer +yield results that are closer or the routing table is sufficiently +bootstrapped. + +#### 3.2 Transport + +The Kadence network operates over HTTP and exclusively over +[Tor](https://torproject.org). + +Each Kadence node exposes a V3 hidden service to other nodes for receiving RPC +messages _(4. Remote Procedure Calls)_. Requests sent to the RPC endpoint +require a special HTTP header `x-kad-message-id` to be included that matches +the `id` parameter in the associated RPC message _(4.1 Structure and Authentication)_. + +### 4 Remote Procedure Calls + +* **Method:** `POST` +* **Path:** `/` +* **Content Type:** `application/json` +* **Headers:** `x-kad-message-id` + +#### 4.1 Structure and Authentication + +Each remote procedure call sent and received between nodes is composed in the +same structure. Messages are formatted as a +[JSON-RPC 2.0](http://www.jsonrpc.org/specification) *batch* payload containing +3 objects. These objects are positional, so ordering matters. The anatomy of a +message takes the form of: + +``` +[{ /* rpc */ },{ /* notification */ },{ /* notification */ }] +``` + +At position 0 is the RPC request/response object, which must follow the +JSON-RPC specification for such an object. It must contain the properties: +`jsonrpc`, `id`, `method`, and `params` if it is a request. It must contain the +properties: `jsonrpc`, `id`, and one of `result` or `error` if it is a +response. + +At positions 1 and 2 are a JSON-RPC notification object, meaning that it is not +required to contain an `id` property since no response is required. These two +notifications always assert methods `IDENTIFY` and `AUTHENTICATE` respectively. +Together, these objects provide the recipient with information regarding the +identity and addressing information of the sender as well as a cryptographic +signature to authenticate the payload. + +For `STORE` message, an additional `HASHCASH` message is included in the +payload to prevent spam. + +##### Example: Request + +``` +[ + { + "jsonrpc": "2.0", + "id": "", + "method": "", + "params": ["", ""] + }, + { + "jsonrpc": "2.0", + "method": "IDENTIFY", + "params": [ + "", + { + "hostname": "sender.onion", + "port": 80, + "protocol": "http:", + "pubkey": "...", + "proof": "..." + } + ] + }, + { + "jsonrpc": "2.0", + "method": "AUTHENTICATE", + "params": [ + "", + "" + ] + } +] +``` + +##### Example: Response + +``` +[ + { + "jsonrpc": "2.0", + "id": "", + "result": ["", ""] + }, + { + "jsonrpc": "2.0", + "method": "IDENTIFY", + "params": [ + "", + { + "hostname": "receiver.onion", + "port": 80, + "protocol": "http:", + "pubkey": "...", + "proof": "..." + } + ] + }, + { + "jsonrpc": "2.0", + "method": "AUTHENTICATE", + "params": [ + "", + "" + ] + } +] +``` + +In the examples above, `proof_hash` and `public_key` must be encoded +as hexidecimal string and `payload_signature` must be encoded as a +base64 string which is the concatenation of the public key recovery number with +the actual signature of the payload - excluding the object at index 2 +(`AUTHENTICATE`). This means that the message to be signed is +`[rpc, identify]`. + +> Note the exclusion of a timestamp or incrementing nonce in the payload means +> that a man-in-the-middle could carry out a replay attack. To combat this, it +> is urged that the `id` parameter of the RPC message (which is a universally +> unique identifier) be stored for a reasonable period of time and nodes should +> reject messages that attempt to use a duplicate UUID. + +The rest of this section describes each individual method in the base protocol +and defines the parameter and result signatures that are expected. If any RPC +message yields an error, then an `error` property including `code` and +`message` should be send in place of the `result` property. + +#### 4.2 `PING` + +This RPC involves one node sending a `PING` message to another, which +presumably replies. This has a two-fold effect: the recipient of the `PING` +must update the bucket corresponding to the sender; and, if there is a reply, +the sender must update the bucket appropriate to the recipient. + +Parameters: `[]` +Results: `[]` + +#### 4.3 `FIND_NODE` + +Basic kademlia lookup operation that builds a set of K contacts closest to the +the given key. The `FIND_NODE` RPC includes a 160-bit key. The recipient of the +RPC returns up to K contacts that it knows to be closest to the key. The +recipient must return K contacts if at all possible. It may only return fewer +than K if it is returning all of the contacts that it has knowledge of. + +Parameters: `[key_160_hex]` +Results: `[contact_0, contact_1, ...contactN]` + +#### 4.4 `FIND_VALUE` + +Kademlia search operation that is conducted as a node lookup and builds a list +of K closest contacts. If at any time during the lookup the value is returned, +the search is abandoned. If no value is found, the K closest contacts are +returned. Upon success, we must store the value at the nearest node seen during +the search that did not return the value. + +A `FIND_VALUE` RPC includes a B=160-bit key. If a corresponding value is +present on the recipient, the associated data is returned. Otherwise the RPC is +equivalent to a `FIND_NODE` and a set of K contacts is returned. + +If a value is returned, it must be in the form of an object with properties: +`timestamp` as a UNIX timestamp in milliseconds, `publisher` as a 160 bit +public key hash in hexidecimal of the original publisher, and `value` which may +be of mixed type that is valid JSON. + +Parameters: `[key_160_hex]` +Results: `{ timestamp, publisher, value }` or `[...contactN]` + +#### 4.5 `STORE` + +The sender of the `STORE` RPC provides a key and a block of data and requires +that the recipient store the data and make it available for later retrieval by +that key. Kadence **requires** that the key is the RMD160 hash of the supplied blob +and that the blob is *exactly* equal to 2MiB in size and encoded as base64. + +Parameters: `[key_160_hex, 2mib_value_base64]` +Results: `[key_160_hex, 2mib_value_base64]` + +An additional `HASHCASH` payload is appended to this message. + +``` +{ + "jsonrpc": "2.0", + "method": "HASHCASH", + "params": [""] +} +``` + +The stamp follows the hashcash specification. The resource segment of the stamp +is the sender identity, target identity, and method name concatenated. The +difficulty may be adjusted by community consensus to account for potential +attacks. + +### 9 References + +* Kademlia (`http://www.scs.stanford.edu/~dm/home/papers/kpos.pdf`) +* S/Kademlia (`http://www.tm.uka.de/doc/SKademlia_2007.pdf`) + diff --git a/doc/quickstart.md b/doc/quickstart.md new file mode 100644 index 0000000..7ff3edf --- /dev/null +++ b/doc/quickstart.md @@ -0,0 +1,273 @@ +Getting started with building distributed systems with Kadence is simple, but +requires a basic understanding of it's architecture in order to make the most +effective use of what it has to offer. There are two ways to build on top of +Kadence: using the complete reference implementation as a daemon and +communicating with it from *any language* using the {@link Control} interface +or using the core library directly using JavaScript. This tutorial will cover +both approaches. + +> Note that this guide assumes you already have Kadence installed. If you have +> not installed Kadence, follow our guide for {@tutorial install} and come +> back here when you're ready! + +### Contents + +1. [Using the Library](#library) +2. [Using the Daemon](#daemon) + + +### Using the Library + +Not all use-cases require the exact properties of a "proper" Kadence network. +Because of this, Kadence exposes it's core library as a complete framework for +building distributed systems. This guide will demonstrate how to use the +Kadence framework to invent new distributed protocols. + +Start by following the guide for {@tutorial install} and install Kadence +locally to a new project. First create your project. + +``` +$ mkdir myproject +$ cd myproject +$ npm init +``` + +Then install Kadence and save the dependency to your `package.json` file. + +``` +$ npm install @deadcanaries/kadence --save +``` + +#### Creating a Node + +Most of the framework revolves around the instantiation of a +{@link KademliaNode}, which exposes the primary interface for extending the +protocol. There are several required options to provide, notably: + +* {@link AbstractNode~transport} +* {@link AbstractNode~storage} +* {@link Bucket~contact} +* Identity buffer (160 bits) + +For this example we'll be using the {@link UDPTransport} and a LevelDB database +provided by the `levelup` and `leveldown` packages. + +```js +const kadence = require('@tacticalchihuahua/kadence'); +const levelup = require('levelup'); +const leveldown = require('leveldown'); +const encode = require('encoding-down'); + +const node = new kadence.KademliaNode({ + identity: kadence.utils.getRandomKeyBuffer(), + transport: new kadence.UDPTransport(), + storage: levelup(encode(leveldown('path/to/database'))), + contact: { + hostname: 'my.hostname', + port: 8080 + } +}); +``` + +The code above is the minimum setup for a complete Kademlia DHT. If this is all +you require, then all you need to do is listen on the port specified in the +`contact.port` option and join a known seed with {@link KademliaNode#join}. The +provided seed must be defined as a tuple (array) where the first item is the +hex encoded identity key of the seed and the second item is the +{@link Bucket~contact} object. You can read more about this structure in our +guide on {@tutorial identities}. + +If this node is the "first node" in the network, you don't need to call +{@link KademliaNode#join}, instead our node will just listen for connections +from others. + +```js +const seed = ['0000000000000000000000000000000000000000', { // (sample) + hostname: 'seed.hostname', + port: 8080 +}]; + +node.once('join', function() { + console.info(`connected to ${node.router.size} peers`); +}); + +node.once('error', function(err) { + console.error('failed to join the network', err); +}); + +node.listen(node.contact.port); +node.join(seed); +``` + +That's it, for a basic minimal Kademlia DHT, you're finished! Now you can use +the methods on {@link KademliaNode} to store and retrieve entries from the +network. To learn more about using plugins, extending the protocol with +middleware, custom transports, and the message pipelines, see: + +* {@tutorial plugins} +* {@tutorial middleware} +* {@tutorial transport-adapters} +* {@tutorial messengers} +* [Examples](https://github.com/tacticalchihuahua/kadence/tree/main/example) +* [Reference Implementation](https://github.com/tacticalchihuahua/kadence/blob/main/bin/kadence.js) + +> **Note!** If you are using Kadence to build a distributed network from scratch +> the best place to start is the [reference implementation](https://gitlab.com/kadence/kadence/blob/master/bin/kadence.js). +> This provides a complete working Kadence network that leverages all the +> features provided by the library as well as autogenerating keys, managing +> configuration, and more! + + +### Using the Daemon + +Kadence "proper" describes a Kademlia DHT with several notable protocol +extensions - specifically the extensions that are authored in the core library +as plugins: + +* {@link module:kadence/spartacus} +* {@link module:kadence/eclipse} +* {@link module:kadence/hashcash} +* {@link module:kadence/permission} +* {@link module:kadence/quasar} + +The daemon also leverages the following plugins that do not affect the protocol +itself, but rather provide features that improve the user experience or enable +other optional features. + +* {@link module:kadence/rolodex} +* {@link module:kadence/traverse} +* {@link module:kadence/onion} +* {@link module:kadence/hibernate} + +Together these plugins combined with the base implementation of the Kademlia +DHT form the Kadence protocol and a complete standalone program for running a +configurable Kadence node. If you installed Kadence using the `-g` or +`--global` flag, you now have access to the `kadence` command line program. +This program handles everything you need to interact with a Kadence network +from any programming language. + +#### Identity Generation + +Kadence mitigates eclipse attacks (a form of a sybil attack) by requiring node +identities to act as a proof-of-work solution. This means that Kadence expects +your node identity to be derived from a public key of which the Scrypt hash +contains a number of leading zero bits as defined by the value of +{@link module:kadence/constants~IDENTITY_DIFFICULTY}. This prevents adversaries +from quickly generating a large number of identities that are close enough to +each other to "surround" sections of the keyspace which could allow them to +poison the routing table, deny service, or otherwise manipulate portions of the +network. + +The first time you run `kadence`, it will automatically begin "mining" a valid +identity, which can take some time depending on your hardware. If you are just +getting started with testing Kadence, you'll probably want to set +`TestNetworkEnabled=1` in your `$HOME/.config/kadence/config` or set the +environment variable `kadence_TestNetworkEnabled=1` (see {@tutorial config}). +This will reduce the difficulty significantly and allow you to get started +quickly. In a live "production" network, you can pass `--solvers N` where `N` +is the number of CPU cores you'd like to dedicate to identity mining (and +solution mining as discussed later). + +In the example below, we are also setting `kadence_TraverseNatEnabled=0` +because for now we aren't interested in punching out and becoming addessable +on the internet. + +``` +$ export kadence_TestNetworkEnabled=1 kadence_TraverseNatEnabled=0 + +$ kadence +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"kadence is running in test mode, difficulties are reduced","time":"2018-03-16T15:28:05.188Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":40,"msg":"identity derivation not yet solved - 0 is invalid","time":"2018-03-16T15:28:05.357Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"solving identity derivation index with 1 solver processes, this can take a while","time":"2018-03-16T15:28:05.357Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"forking derivation process 0","time":"2018-03-16T15:28:05.377Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"solved identity derivation index 11 in 882ms","time":"2018-03-16T15:28:06.239Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"initializing kadence","time":"2018-03-16T15:28:06.244Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"validating solutions in wallet, this can take some time","time":"2018-03-16T15:28:06.257Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"node listening on local port 5274 and exposed at https://127.0.0.1:5274","time":"2018-03-16T15:28:06.262Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"binding controller to path /home/bookchin/.config/kadence/kadence.sock","time":"2018-03-16T15:28:06.262Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"forking solver process 0","time":"2018-03-16T15:28:06.263Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"no bootstrap seeds provided and no known profiles","time":"2018-03-16T15:28:06.269Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"running in seed mode (waiting for connections)","time":"2018-03-16T15:28:06.269Z","v":0} +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"derivation solver 0 exited normally","time":"2018-03-16T15:28:06.272Z","v":0} +``` + +Notice the log message `solved identity derivation index 11 in 882ms`. This +means that a new hierarchically deterministic private extended key was +generated and the child private key at index 11 yielded a public key that when +hashed with Scrypt satisfies the identity difficulty. Now you can join your +test Kadence network. + +#### Solution Mining + +Once your Kadence node has generated a valid identity, you'll begin seeing log +messages similar to the following: + +``` +{"name":"kadence","hostname":"librem","pid":23409,"level":30,"msg":"solver 0 found solution in 4 attempts (226ms)","time":"2018-03-16T15:28:06.804Z","v":0} +``` + +This is part of Kadence's permission protocol for storing entries in the DHT. +In a basic Kademlia network, entries can be stored and overwritten by any +party. Kadence employs a proof-of-work system that requires nodes attempting +to store an entry provide a "solution". Solutions are "mined" by a process +similar to how Kadence identities are generated, but instead are derived from +the identity solution. When a solution is found, it is stored in a "wallet" - +a directory of solution files. + +Solutions are then hashed and the resulting 160 bit key can be used to store +arbitrary data in the DHT and is keyed by the solution hash. In practice, this +means that your application must track any mapping from a key your application +understands to the solution hash that was used to store the entry in the +network. + +> While `TestNetworkEnabled=1`, these solutions will be found very quickly, so +> it's probably desirable to start the daemon with `--solvers 0` after you have +> mined enough solutions to use during development. + +#### Controlling the Daemon + +The Kadence daemon exposes a control interface to other applications by default +over a UNIX domain socket located at `$HOME/.config/kadence/kadence.sock`, but +may also be configured to listen on a TCP port instead. You may not enable both +types at once. + +The control interface speaks JSON-RPC 2.0 and it's {@link Control API is +documented here}. You can interact with the controller from any language that +can open a socket connection. For this example we'll use `telnet` and use the +TCP socket interface. + +``` +$ export kadence_ControlPortEnabled=1 kadence_ControlSockEnabled=0 + +$ kadence --solvers 0 +{"name":"kadence","hostname":"librem","pid":24893,"level":30,"msg":"kadence is running in test mode, difficulties are reduced","time":"2018-03-16T16:43:04.440Z","v":0} +{"name":"kadence","hostname":"librem","pid":24893,"level":30,"msg":"initializing kadence","time":"2018-03-16T16:43:04.503Z","v":0} +{"name":"kadence","hostname":"librem","pid":24893,"level":30,"msg":"validating solutions in wallet, this can take some time","time":"2018-03-16T16:43:04.519Z","v":0} +{"name":"kadence","hostname":"librem","pid":24893,"level":30,"msg":"node listening on local port 5274 and exposed at https://127.0.0.1:5274","time":"2018-03-16T16:43:04.576Z","v":0} +{"name":"kadence","hostname":"librem","pid":24893,"level":30,"msg":"binding controller to port 5275","time":"2018-03-16T16:43:04.577Z","v":0} +{"name":"kadence","hostname":"librem","pid":24893,"level":30,"msg":"there are no solver processes running","time":"2018-03-16T16:43:04.577Z","v":0} +{"name":"kadence","hostname":"librem","pid":24893,"level":30,"msg":"no bootstrap seeds provided and no known profiles","time":"2018-03-16T16:43:04.578Z","v":0} +{"name":"kadence","hostname":"librem","pid":24893,"level":30,"msg":"running in seed mode (waiting for connections)","time":"2018-03-16T16:43:04.578Z","v":0} +``` + +When starting Kadence with `ControlPortEnabled=1`, you'll notice a log message +`binding controller to port 5275`. Open a connection to this port and you can +start sending commands by typing a JSON-RPC payload and pressing return (which +terminates the command with a `\r\n`). The result of the command will be +written back to the socket. + +``` +$ telnet localhost 5275 +Trying ::1... +Trying 127.0.0.1... +Connected to localhost. +Escape character is '^]'. +{"jsonrpc":"2.0","id":"1234567890","method":"getProtocolInfo","params":[]} +{"jsonrpc":"2.0","id":"1234567890","result":[{"versions":{"protocol":"1.0.0","software":"3.1.2"},"identity":"27f06eba2be0a5f1399bfc0ebd477522118d1f69","contact":{"hostname":"127.0.0.1","protocol":"https:","port":5274,"xpub":"xpub69sEXvvUfWbQg8FSCPWPojcrUpkbtNkKDLSNSTx9GAsB1MVpeZ5eoCQTo4EViDnVn7pPpLbGq83aoD24vTGPnDKnXxqGJxxNbEJhizfFFQH","index":11,"agent":"1.0.0"},"peers":[]}]} +^] +telnet> quit +Connection closed. +``` + +Complete documentation on configuration properties and what they do can be +reviewed in the {@tutorial config}. diff --git a/doc/transport-adapters.md b/doc/transport-adapters.md new file mode 100644 index 0000000..bd47235 --- /dev/null +++ b/doc/transport-adapters.md @@ -0,0 +1,82 @@ +Kadence does not impose any particular transport layer, which makes it very +flexible for applying to many use cases. As far as Kadence is concerned, a valid +transport adapter is any `objectMode` +[`DuplexStream`](https://nodejs.org/dist/latest-v6.x/docs/api/stream.html) +that exposes a `listen()` method. + +Kadence ships with UDP and HTTP(S) transports so you don't need to implement a +transport adapter yourself to get started. If your network layer needs are not +met by these, check out the interface for {@link AbstractNode~transport}. + +### API for Transport Implementers + +The transport adapter interface has been designed to make implementing any +given networking or communication layer easy using JavaScript's inheritance +model. + +First, a developer would declare a new JavaScript class that extends the +[`DuplexStream`](https://nodejs.org/dist/latest-v6.x/docs/api/stream.html) +class, and implements the `_read`, `_write`, and `listen` methods. This +architecture makes it simple to implement any type of transport layer. + +When a consumer *reads* from the stream, they shall expect to receive a raw +buffer representing a received message which is processed by a +{@link Messenger} instance. When a consumer *writes* to the stream, they shall +expect the adapter to dispatch the message to the target. Calling `listen` on +the stream should perform any initialization needed, like binding to a port. + +Transport streams must be placed in `objectMode`. The `_read` method must push +the received messages as raw buffers to be parsed by the deserializer used by +the {@link Messenger} class (which by default is JSON-RPC). The `_write` method +receives an array object as it's first argument which contains the following: + +```js +[ + // String: unique identifier for the message, can be a request or a response + messageId, + // Buffer: raw payload to be delivered to the target + messagePayload, + [ + // String: target contact's identity key + identityKey, + // Object: target contact's address information (transport-specific) + contactInfo + ] +] +``` + +### Example: UDP Transport + +Implementing a UDP based transport adapter is very simple given that no state +must be maintained between requests and responses, so we will use it as a +simple example of how you might implement a transport. + +```js +const { Duplex: DuplexStream } = require('stream'); +const dgram = require('dgram'); + +class UDPTransport extends DuplexStream { + + constructor(options) { + super({ objectMode: true }); + this.socket = dgram.createSocket(); + } + + _write([id, buffer, target], encoding, callback) { + let [, contact] = target; + this.socket.send(buffer, 0, buffer.length, contact.port, contact.hostname, + callback); + } + + _read() { + this.socket.once('message', (buffer) => { + this.push(buffer); + }); + } + + listen() { + this.socket.bind(...arguments); + } + +} +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3d1e463 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,81 @@ +version: '3' +services: + kadence-1: + build: + context: . + dockerfile: ./Dockerfile.dev + environment: + - kadence_TestNetworkEnabled=1 + - kadence_ControlSockEnabled=0 + - kadence_ControlPortEnabled=1 + - kadence_TraverseNatEnabled=0 + - kadence_NodePublicAddress=kadence-1 + volumes: + - ./bin:/root/kadence/bin + - ./lib:/root/kadence/lib + - ./index.js:/root/kadence/index.js + - ./node_modules:/root/kadence/node_modules + - ./package.json:/root/kadence/package.json + ports: + - "6274:5274" + - "127.0.0.1:6275:5275" + kadence-2: + build: + context: . + dockerfile: ./Dockerfile.dev + environment: + - kadence_TestNetworkEnabled=1 + - kadence_TraverseNatEnabled=0 + - kadence_NodePublicAddress=kadence-2 + - kadence_NetworkBootstrapNodes=http://kadence-1:5274 + volumes: + - ./bin:/root/kadence/bin + - ./lib:/root/kadence/lib + - ./index.js:/root/kadence/index.js + - ./node_modules:/root/kadence/node_modules + - ./package.json:/root/kadence/package.json + kadence-3: + build: + context: . + dockerfile: ./Dockerfile.dev + environment: + - kadence_TestNetworkEnabled=1 + - kadence_TraverseNatEnabled=0 + - kadence_NodePublicAddress=kadence-3 + - kadence_NetworkBootstrapNodes=http://kadence-1:5274 + volumes: + - ./bin:/root/kadence/bin + - ./lib:/root/kadence/lib + - ./index.js:/root/kadence/index.js + - ./node_modules:/root/kadence/node_modules + - ./package.json:/root/kadence/package.json + kadence-4: + build: + context: . + dockerfile: ./Dockerfile.dev + environment: + - kadence_TestNetworkEnabled=1 + - kadence_TraverseNatEnabled=0 + - kadence_NodePublicAddress=kadence-4 + - kadence_NetworkBootstrapNodes=http://kadence-1:5274 + volumes: + - ./bin:/root/kadence/bin + - ./lib:/root/kadence/lib + - ./index.js:/root/kadence/index.js + - ./node_modules:/root/kadence/node_modules + - ./package.json:/root/kadence/package.json + kadence-5: + build: + context: . + dockerfile: ./Dockerfile.dev + environment: + - kadence_TestNetworkEnabled=1 + - kadence_TraverseNatEnabled=0 + - kadence_NodePublicAddress=kadence-5 + - kadence_NetworkBootstrapNodes=http://kadence-1:5274 + volumes: + - ./bin:/root/kadence/bin + - ./lib:/root/kadence/lib + - ./index.js:/root/kadence/index.js + - ./node_modules:/root/kadence/node_modules + - ./package.json:/root/kadence/package.json diff --git a/example/expanded.js b/example/expanded.js new file mode 100644 index 0000000..70d7e6a --- /dev/null +++ b/example/expanded.js @@ -0,0 +1,133 @@ +/** + * @example kadence/example/expanded + */ + +// NB: For a complete example of a full kadence implementation, see the daemon +// NB: implementation in bin/kadence.js + +'use strict'; + +// Import dependencies +const crypto = require('crypto'); +const bunyan = require('bunyan'); +const levelup = require('levelup'); +const encoding = require('encoding-down'); +const leveldown = require('leveldown'); +const kadence = require('@tacticalchihuahua/kadence'); + +// Prepare required options +const storage = levelup(encoding(leveldown('path/to/storage.db'))); +const logger = bunyan.createLogger({ name: 'kadence example' }); +const transport = new kadence.HTTPTransport(); + +// In production, persist identity to disk and load it +// Generating a new one every time will cause lookup problems +const identity = kadence.utils.getRandomKeyBuffer(); +const contact = { hostname: 'localhost', port: 1337 }; + +// Construct a kademlia node interface; the returned `Node` object exposes: +// - router +// - rpc +// - storage +// - identity +const node = new kadence.KademliaNode({ + transport, + storage, + logger, + identity, + contact +}); + +// Use rule "extensions" from plugins to add additional functionality. +// Plugins can also extend the `KademliaNode` object with additional methods +node.plugin(kadence.quasar()); + +// Use "global" rules for preprocessing *all* incoming messages +// This is useful for things like blacklisting certain nodes +node.use((request, response, next) => { + let [identityString] = request.contact + + if ([/* identity blacklist */].includes(identityString)) { + return next(new Error('You have been blacklisted')); + } + + next(); +}); + +// Use existing "base" rules to add additional logic to the base kad routes +// This is useful for things like validating key/value pairs +node.use('STORE', (request, response, next) => { + let [key, entry] = request.params; + let hash = crypto.createHash('rmd160').update(entry.value).digest('hex'); + + // Ensure values are content-addressable + if (key !== hash) { + return next(new Error('Key must be the RMD-160 hash of value')); + } + + next(); +}); + +// Use "userland" (that's you!) rules to create your own protocols +node.use('ECHO', (request, response, next) => { + if ([/* some naughty words */].includes(request.params.message)) { + return next(new Error( + `Oh goodness, I dare not say "${request.params.message}"` + )); + } + + response.send(request.params); +}); + +// Define a global custom error handler rule, simply by including the `err` +// argument in the handler +// Be sure you use response.error(message) if you want to respond +node.use((err, request, response, next) => { + response.error(err.message); +}); + +// Define error handlers for specific rules the same way, but including the +// rule name as the first argument +node.use('ECHO', (err, request, response, next) => { + response.error(err.message.replace(request.params.message, '[redacted]')); +}); + +// Extend the Node interface with your own plugins +// In many cases, you probably want parity with any userland message routes +// you have defined - in this case for the ECHO method +node.plugin(function(node) { + node.sendNeighborEcho = (text, callback) => { + const neighbor = [ + ...node.router.getClosestContactsToKey(node.identity).entries(), + ].shift(); + + node.send('ECHO', { message: text }, neighbor, callback); + }; +}); + +// When you are ready, start listening for messages and join the network +// The Node#listen method takes different arguments based on the transport +// adapter being used +node.listen(1337); + +node.join(['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', { + hostname: 'localhost', + port: 8080 +}], () => { + // Add 'join' callback which indicates peers were discovered and + // our node is now connected to the overlay network + logger.info(`Connected to ${node.router.size} peers!`) + + // Base protocol exposes: + // * node.iterativeFindNode(key, callback) + // * node.iterativeFindValue(key, callback) + // * node.iterativeStore(key, value, callback) + // + // Quasar plugin exposes: + // * node.quasarPublish(topic, content) + // * node.quasarSubscribe(topic, handler) + // * node.quasarUpdate(callback) + // + // Example plugin exposes: + // * node.sendNeighborEcho(text, callback) +}); diff --git a/example/minimal.js b/example/minimal.js new file mode 100644 index 0000000..2bb0ff9 --- /dev/null +++ b/example/minimal.js @@ -0,0 +1,46 @@ +/** + * @example kadence/example/minimal + */ + +'use strict'; + +// Import dependencies +const bunyan = require('bunyan'); +const levelup = require('levelup'); +const leveldown = require('leveldown'); +const encoding = require('encoding-down'); +const kadence = require('@tacticalchihuahua/kadence'); + +// Construct a kademlia node interface; the returned `KademliaNode` object +// exposes: +// - router +// - rpc +// - storage +// - identity +const node = new kadence.KademliaNode({ + transport: new kadence.HTTPTransport(), + storage: levelup(encoding(leveldown('path/to/storage.db'))), + contact: { hostname: 'localhost', port: 1337 } +}); + +// When you are ready, start listening for messages and join the network +// The Node#listen method takes different arguments based on the transport +// adapter being used +node.listen(1337); + +// Join a known peer by it's [identity, contact] +node.join(['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', { + hostname: 'localhost', + port: 8080 +}], () => { + // Add 'join' callback which indicates peers were discovered and + // our node is now connected to the overlay network + node.logger.info(`Connected to ${node.router.size} peers!`) + + // Base protocol exposes: + // * node.iterativeFindNode(key, callback) + // * node.iterativeFindValue(key, callback) + // * node.iterativeStore(key, value, callback) +}); + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..d2446cc --- /dev/null +++ b/index.js @@ -0,0 +1,88 @@ +'use strict'; + +/** + * Returns a new {@link KademliaNode} + */ +module.exports = function(options) { + return new module.exports.KademliaNode(options); +}; + +/** {@link KademliaNode} */ +module.exports.KademliaNode = require('./lib/node-kademlia'); + +/** {@link KademliaRules} */ +module.exports.KademliaRules = require('./lib/rules-kademlia'); + +/** {@link AbstractNode} */ +module.exports.AbstractNode = require('./lib/node-abstract'); + +/** {@link ErrorRules} */ +module.exports.ErrorRules = require('./lib/rules-errors'); + +/** {@link Bucket} */ +module.exports.Bucket = require('./lib/bucket'); + +/** {@link Control} */ +module.exports.Control = require('./lib/control'); + +/** {@link Messenger} */ +module.exports.Messenger = require('./lib/messenger'); + +/** {@link RoutingTable} */ +module.exports.RoutingTable = require('./lib/routing-table'); + +/** {@link UDPTransport} */ +module.exports.UDPTransport = require('./lib/transport-udp'); + +/** {@link HTTPTransport} */ +module.exports.HTTPTransport = require('./lib/transport-http'); + +/** {@link HTTPSTransport} */ +module.exports.HTTPSTransport = require('./lib/transport-https'); + +/** {@link module:kadence/hashcash} */ +module.exports.hashcash = require('./lib/plugin-hashcash'); + +/** {@link module:kadence/hibernate} */ +module.exports.hibernate = require('./lib/plugin-hibernate'); + +/** {@link module:kadence/onion} */ +module.exports.onion = require('./lib/plugin-onion'); + +/** {@link module:kadence/quasar} */ +module.exports.quasar = require('./lib/plugin-quasar'); + +/** {@link module:kadence/spartacus} */ +module.exports.spartacus = require('./lib/plugin-spartacus'); + +/** {@link module:kadence/traverse} */ +module.exports.traverse = require('./lib/plugin-traverse'); + +/** {@link module:kadence/eclipse} */ +module.exports.eclipse = require('./lib/plugin-eclipse'); + +/** {@link module:kadence/rolodex} */ +module.exports.rolodex = require('./lib/plugin-rolodex'); + +/** {@link module:kadence/contentaddress} */ +module.exports.contentaddress = require('./lib/plugin-contentaddress'); + +/** {@link module:kadence/trust} */ +module.exports.trust = require('./lib/plugin-trust'); + +/** {@link module:kadence/logger} */ +module.exports.logger = require('./lib/plugin-logger'); + +/** {@link module:kadence/churnfilter} */ +module.exports.churnfilter = require('./lib/plugin-churnfilter'); + +/** {@link module:kadence/constants} */ +module.exports.constants = require('./lib/constants'); + +/** {@link module:kadence/version} */ +module.exports.version = require('./lib/version'); + +/** {@link module:kadence/utils} */ +module.exports.utils = require('./lib/utils'); + + diff --git a/lib/bucket.js b/lib/bucket.js new file mode 100644 index 0000000..d31574d --- /dev/null +++ b/lib/bucket.js @@ -0,0 +1,126 @@ +'use strict'; + +const constants = require('./constants'); +const utils = require('./utils'); + + +/** + * @typedef {array} Bucket~contact + * @property {string} 0 - Node identity key + * @property {object} 1 - Contact information (varies by plugins) + */ + +/** + * Represents a column of the routing table holding up to K contacts + */ +class Bucket extends Map { + + /** + * @constructor + */ + constructor() { + super(); + } + + /** + * @property {number} length - The number of contacts in the bucket + */ + get length() { + return super.size; + } + + /** + * @property {object} head - The contact at the bucket head + */ + get head() { + return [...super.entries()].shift(); + } + + /** + * @property {object} tail - The contact at the bucket tail + */ + get tail() { + return [...super.entries()].pop(); + } + + /** + * Sets the contact to the node ID in the bucket if it is not full; if the + * bucket already contains the contact, move it to the tail - otherwise we + * place it at the head + * @param {string} nodeId - The identity key for the contact + * @param {object} contact - The address information for the contact + * @returns {number} index + */ + set(nodeId, contact) { + if (this.has(nodeId)) { + super.delete(nodeId); + super.set(nodeId, contact); + } else if (this.size < constants.K) { + let bucketEntries = [...this.entries()]; + + super.clear(); + super.set(nodeId, contact); + + for (let [nodeId, contact] of bucketEntries) { + super.set(nodeId, contact); + } + } + + return this.indexOf(nodeId); + } + + /** + * Returns the index of the given node id + * @param {string} key - Node identity key for getting index + * @returns {number} + */ + indexOf(key) { + let isMissing = -1; + let index = isMissing; + + for (let nodeId of this.keys()) { + index++; + + if (key !== nodeId) { + continue; + } + + return index; + } + + return isMissing; + } + + /** + * Returns an array of contacts in the bucket that are closest to the given + * key + * @param {string|buffer} key - Reference key for finding other contacts + * @param {number} [count=constants.K] - Max results to return + * @param {boolean} [exclusive=false] - Exclude result matching the key exactly + * @returns {array} + */ + getClosestToKey(key, count = constants.K, exclusive = false) { + let contacts = []; + + for (let [identity, contact] of this.entries()) { + contacts.push({ + contact, identity, distance: utils.getDistance(identity, key) + }); + } + + return new Map(contacts.sort((a, b) => { + return utils.compareKeyBuffers( + Buffer.from(a.distance, 'hex'), + Buffer.from(b.distance, 'hex') + ); + }).filter((result) => { + if (exclusive) { + return result.identity !== key.toString('hex'); + } else { + return true; + } + }).map((obj) => [obj.identity, obj.contact]).splice(0, count)); + } +} + +module.exports = Bucket; diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..431a36f --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,83 @@ +/** + * @module kadence/constants + */ + +'use strict'; + +/** + * @constant {number} ALPHA - Degree of parallelism + */ +exports.ALPHA = 3; + +/** + * @constant {number} B - Number of bits for nodeID creation + */ +exports.B = 160; + +/** + * @constant {number} K - Number of contacts held in a bucket + */ +exports.K = 20; + +/** + * @constant {number} T_REFRESH - Interval for performing router refresh + */ +exports.T_REFRESH = 3600000; + +/** + * @constant {number} T_REPLICATE - Interval for replicating local data + */ +exports.T_REPLICATE = 3600000; + +/** + * @constant {number} T_REPUBLISH - Interval for republishing data + */ +exports.T_REPUBLISH = 86400000; + +/** + * @constant {number} T_EXPIRE - Interval for expiring local data entries + */ +exports.T_EXPIRE = 86405000; + +/** + * @constant {number} T_RESPONSETIMEOUT - Time to wait for RPC response + */ +exports.T_RESPONSETIMEOUT = 10000; + +/** + * @constant {number} MAX_UNIMPROVED_REFRESHES - Quit refreshing no improvement + */ +exports.MAX_UNIMPROVED_REFRESHES = 3; + +/** + * @constant {number} IDENTITY_DIFFICULTY - Equihash params for identity proofs + */ +exports.IDENTITY_DIFFICULTY = { n: 126, k: 5 }; + +/** + * @constant {number} TESTNET_DIFFICULTY - Testnet difficulty override + */ +exports.TESTNET_DIFFICULTY = { n: 90, k: 5 }; + +/** + * @constant {number} LRU_CACHE_SIZE - Number of used hashcash stamps to track + */ +exports.LRU_CACHE_SIZE = 50; + +/** + * @constant {number} FILTER_DEPTH - Number of neighborhood hops to track + * subsrciptions for + */ +exports.FILTER_DEPTH = 3; + +/** + * @constant {number} MAX_RELAY_HOPS - Maximum times a message instance will be + * relayed when published + */ +exports.MAX_RELAY_HOPS = 6; + +/** + * @constant {number} SOFT_STATE_TIMEOUT - Time to wait before busting the + * subscription cache + */ +exports.SOFT_STATE_TIMEOUT = 3600000; diff --git a/lib/contact-list.js b/lib/contact-list.js new file mode 100644 index 0000000..010be72 --- /dev/null +++ b/lib/contact-list.js @@ -0,0 +1,95 @@ +'use strict'; + +const utils = require('./utils'); + +/** + * Manages contact lists returned from FIND_NODE queries + */ +class ContactList { + + /** + * @constructor + * @param {string} key - Lookup key for this operation + * @param {Bucket~contact[]} contacts - List of contacts to initialize with + */ + constructor(key, contacts = []) { + this.key = key; + this._contacts = []; + this._contacted = new Set(); + this._active = new Set(); + + this.add(contacts); + } + + /** + * @property {Bucket~contact} closest - The contact closest to the reference key + */ + get closest() { + return this._contacts[0]; + } + + /** + * @property {Bucket~contact[]} active - Contacts in the list that are active + */ + get active() { + return this._contacts.filter(contact => this._active.has(contact[0])); + } + + /** + * @property {Bucket~contact[]} uncontacted - Contacts in the list that have not been + * contacted + */ + get uncontacted() { + return this._contacts.filter(contact => !this._contacted.has(contact[0])); + } + + /** + * Adds the given contacts to the list + * @param {Bucket~contact[]} contacts + */ + add(contacts) { + let identities = this._contacts.map(c => c[0]); + let added = []; + + contacts.forEach(contact => { + if (identities.indexOf(contact[0]) === -1) { + this._contacts.push(contact); + identities.push(contact[0]); + added.push(contact); + } + }); + + this._contacts.sort(this._identitySort.bind(this)); + + return added; + } + + /** + * Marks the supplied contact as contacted + * @param {Bucket~contact} contact + */ + contacted(contact) { + this._contacted.add(contact[0]); + } + + /** + * Marks the supplied contact as active + * @param {Bucket~contact} contact + */ + responded(contact) { + this._active.add(contact[0]); + } + + /** + * @private + */ + _identitySort([aIdentity], [bIdentity]) { + return utils.compareKeyBuffers( + Buffer.from(utils.getDistance(aIdentity, this.key), 'hex'), + Buffer.from(utils.getDistance(bIdentity, this.key), 'hex') + ); + } + +} + +module.exports = ContactList; diff --git a/lib/control.js b/lib/control.js new file mode 100644 index 0000000..3467e51 --- /dev/null +++ b/lib/control.js @@ -0,0 +1,143 @@ +'use strict'; + +const constants = require('./constants'); +const version = require('./version'); +const utils = require('./utils'); + + +/** + * The Kadence daemon can be controlled by another process on the same host or + * remotely via socket connection. By default, the daemon is configured to + * listen on a UNIX domain socket located at $HOME/.config/kadence/kadence.sock. + * Once connected to the daemon, you may send it control commands to build + * networks in other languages. The controller understands newline terminated + * JSON-RPC 2.0 payloads. + */ +class Control { + + /** + * @constructor + * @param {KademliaNode} node + */ + constructor(node) { + this.node = node; + } + + /** + * @private + */ + _parseMethodSignature(name) { + const method = name; + const func = this[method].toString(); + const args = func.split(`${method}(`)[1].split(')')[0]; + const params = args.split(', ').map(s => s.trim()); + + params.pop(); + + return { method, params }; + } + + /** + * Returns a list of the support methods from the controller + * @param {Control~listMethodsCallback} callback + */ + listMethods(callback) { + callback(null, Object.getOwnPropertyNames(Object.getPrototypeOf(this)) + .filter(method => { + return method[0] !== '_' && method !== 'constructor' && + typeof this[method] === 'function'; + }) + .map(this._parseMethodSignature.bind(this)) + .sort((a, b) => b.method < a.method)); + } + /** + * @callback Control~listMethodsCallback + * @param {error|null} error + * @param {object[]} methods + * @param {string} methods.method + * @param {string[]} methods.params + */ + + /** + * Returns basic informations about the running node + * @param {Control~getProtocolInfoCallback} callback + */ + getProtocolInfo(callback) { + const peers = [], dump = this.node.router.getClosestContactsToKey( + this.node.identity, + constants.K * constants.B + ); + + for (let peer of dump) { + peers.push(peer); + } + + callback(null, { + versions: version, + identity: this.node.identity.toString('hex'), + contact: this.node.contact, + peers + }); + } + /** + * @callback Control~getProtocolInfoCallback + * @param {error|null} error + * @param {object} info + * @param {object} info.versions + * @param {string} info.versions.software + * @param {string} info.versions.protocol + * @param {string} info.identity + * @param {object} info.contact + * @param {array[]} info.peers + */ + + /** + * {@link KademliaNode#iterativeFindNode} + */ + /* istanbul ignore next */ + iterativeFindNode(hexKey, callback) { + this.node.iterativeFindNode(hexKey, callback); + } + + /** + * {@link KademliaNode#iterativeFindValue} + */ + /* istanbul ignore next */ + iterativeFindValue(hexKey, callback) { + this.node.iterativeFindValue(Buffer.from(hexKey, 'hex'), callback); + } + + /** + * {@link KademliaNode#iterativeStore} + */ + /* istanbul ignore next */ + iterativeStore(hexValue, callback) { + let hexKey = utils.hash160(Buffer.from(hexValue, 'hex')).toString('hex'); + this.node.iterativeStore(hexKey, hexValue, function(err, count) { + if (err) { + return callback(err); + } + + callback(null, count, hexKey); + }); + } + + /** + * {@link module:kadence/quasar~QuasarPlugin#quasarSubscribe} + */ + /* istanbul ignore next */ + quasarSubscribe(hexKey, callback) { + this.node.quasarSubscribe(hexKey, callback); + } + + /** + * {@link module:kadence/quasar~QuasarPlugin#quasarPublish} + */ + /* istanbul ignore next */ + quasarPublish(hexKey, contentValue, callback) { + this.node.quasarPublish(hexKey, contentValue, callback); + } + +} + +module.exports = Control; diff --git a/lib/messenger.js b/lib/messenger.js new file mode 100644 index 0000000..e427440 --- /dev/null +++ b/lib/messenger.js @@ -0,0 +1,214 @@ +'use strict'; + +const { EventEmitter } = require('events'); +const { Transform: TransformStream } = require('stream'); +const merge = require('merge'); +const jsonrpc = require('jsonrpc-lite'); +const uuid = require('uuid'); +const MetaPipe = require('metapipe'); + + +/** + * A factory class for creating metapipe instances from a "template" + */ +class MetaPipeFactory { + + /** + * @constructor + * @private + */ + constructor(options) { + this._options = options; + this._template = []; + } + + /** + * @private + */ + append(fn) { + this._template.push({ type: 'append', construct: fn }); + } + + /** + * @private + */ + prepend(fn) { + this._template.push({ type: 'prepend', construct: fn }); + } + + /** + * @private + */ + create() { + const metapipe = new MetaPipe(this._options); + + this._template.forEach(instruction => { + switch (instruction.type) { + case 'append': + metapipe.append(instruction.construct()); + break; + case 'prepend': + metapipe.prepend(instruction.construct()); + break; + default: + throw new Error(`Invalid instruction type "${instruction.type}"`); + } + }); + + return metapipe; + } + +} + +/** + * Represents and duplex stream for dispatching messages to a given transport + * adapter and receiving messages to process through middleware stacks + * @class + */ +class Messenger extends EventEmitter { + + static get DEFAULTS() { + return { + serializer: Messenger.JsonRpcSerializer, + deserializer: Messenger.JsonRpcDeserializer + }; + } + + /** + * @function + * @memberof Messenger + * @param {array} data - Object to transform + * @param {object} data.0 - JSON payload, parsed into an object + * @param {Bucket~contact} sender - Origin peer for message + * @param {Bucket~contact} receiver - Destination peer for message + * @param {function} callback - Transform stream callback(err, data) + */ + static get JsonRpcSerializer() { + return function([object, sender, receiver], callback) { + let message = jsonrpc.parseObject( + merge({ jsonrpc: '2.0', id: uuid() }, object) + ); + let notification = jsonrpc.notification('IDENTIFY', sender); + + switch (message.type) { + case 'request': + case 'error': + case 'success': + return callback(null, [ + message.payload.id, + Buffer.from(JSON.stringify([ + message.payload, + notification + ]), 'utf8'), + receiver + ]); + case 'invalid': + case 'notification': + default: + return callback(new Error(`Invalid message type "${message.type}"`)); + } + } + } + + + /** + * @function + * @memberof Messenger + * @param {buffer} rawMessage - Incoming message as buffer + * @param {function} callback - Transform stream callback(err, data) + */ + static get JsonRpcDeserializer() { + return function(buffer, callback) { + let [message, notification] = jsonrpc.parse(buffer.toString('utf8')); + + switch (message.type) { + case 'request': + case 'error': + case 'success': + return callback(null, [message, notification]); + case 'invalid': + case 'notification': + default: + return callback(new Error(`Invalid message type "${message.type}"`)); + } + } + } + + /** + * @interface Messenger~serializer + * @function + * @param {object|buffer} data - Outgoing message buffer or parsed JSON data + * @param {string|null} encoding - Encoding of incoming data + * @param {Messenger~serializerCallback} callback + */ + + /** + * @callback Messenger~serializerCallback + * @param {error|null} error + * @param {buffer|object} data - Serialized data to pass through middleware + */ + + /** + * @interface Messenger~deserializer + * @function + * @param {object|buffer} data - Incoming message buffer or parsed JSON data + * @param {string|null} encoding - Encoding of incoming data + * @param {Messenger~deserializerCallback} callback + */ + + /** + * @callback Messenger~deserializerCallback + * @param {error|null} error + * @param {buffer|object} data - Deserialized data to pass through middleware + */ + + /** + * @constructor + * @param {object} [options] + * @param {Messenger~serializer} [options.serializer] - Serializer function + * @param {Messenger~deserializer} [options.deserializer] - Deserializer function + */ + constructor(options=Messenger.DEFAULTS) { + super(); + + this._opts = merge(Messenger.DEFAULTS, options); + this.serializer = new MetaPipeFactory({ objectMode: true }); + this.deserializer = new MetaPipeFactory({ objectMode: true }); + + this.serializer.append(() => new TransformStream({ + objectMode: true, + transform: (object, enc, cb) => this._serialize(object, cb) + })); + this.deserializer.append(() => new TransformStream({ + objectMode: true, + transform: (object, enc, cb) => this._deserialize(object, cb) + })); + } + + /** + * Serializes a message to a buffer + * @private + */ + _serialize(object, callback) { + this._opts.serializer(object, (err, data) => { + callback(null, err ? undefined : data); + }); + } + + /** + * Deserializes a buffer into a message + * @private + */ + _deserialize(object, callback) { + if (!Buffer.isBuffer(object)) { + return callback(new Error('Cannot deserialize non-buffer chunk')); + } + + this._opts.deserializer(object, (err, data) => { + callback(null, err ? undefined : data); + }); + } + +} + +module.exports = Messenger; diff --git a/lib/node-abstract.js b/lib/node-abstract.js new file mode 100644 index 0000000..a113940 --- /dev/null +++ b/lib/node-abstract.js @@ -0,0 +1,468 @@ +'use strict'; + +const uuid = require('uuid'); +const async = require('async'); +const assert = require('assert'); +const bunyan = require('bunyan'); +const merge = require('merge'); +const constants = require('./constants'); +const utils = require('./utils'); +const { EventEmitter } = require('events'); +const RoutingTable = require('./routing-table'); +const Messenger = require('./messenger'); +const ErrorRules = require('./rules-errors'); + + +/** + * @typedef {object} AbstractNode~logger + * @property {function} debug - Passed string of debug information + * @property {function} info - Passed string of general information + * @property {function} warn - Passed string of warnings + * @property {function} error - Passed string of error message + */ + +/** + * @typedef {object} AbstractNode~transport + * @property {function} read - Returns raw message buffer if available + * @property {function} write - Passed raw message buffer + */ + +/** + * @typedef {object} AbstractNode~storage + * @description Implements a subset of the LevelUP interface + * @property {function} get + * @property {function} put + * @property {function} del + * @property {function} createReadStream + */ + +/** + * @typedef AbstractNode~request + * @property {array} contact - Peer who sent this request + * @property {string} contact.0 - Peer's node identity + * @property {object} contact.1 - Peer's contact information (varies by plugin) + * @property {array|object} params - Method parameters (varies by method) + * @property {string} method - Method name being called + */ + +/** + * @typedef AbstractNode~response + * @property {AbstractNode~responseSend} send + * @property {AbstractNode~responseError} error + */ + +/** + * @typedef {function} AbstractNode~next + * @param {error|null} error - Indicates to exit the middleware stack + */ + +/** + * @typedef AbstractNode~sendError + * @property {string} message - Error description + * @property {string} type - Error type + * @property {object} request - Request the error is from + * @property {string} request.id - Message id + * @property {array} request.params - Parameters sent + * @property {Bucket~contact} request.target - Contact message was for + * @property {string} request.method - RPC method in message + */ + +/** + * @method AbstractNode~responseSend + * @param {array|object} results - Result parameters to respond with + */ + +/** + * @method AbstractNode~responseError + * @param {string} errorMessage - Text describing the error encountered + * @param {number} [errorCode] - Error code + */ + +/** + * Represents a network node + */ +class AbstractNode extends EventEmitter { + + /** + * Join event is triggered when the routing table is no longer empty + * @event AbstractNode#join + */ + + /** + * Error event fires when a critical failure has occurred; if no handler is + * specified, then it will throw + * @event AbstractNode#error + * @type {Error} + */ + + static get DEFAULTS() { + return { + logger: bunyan.createLogger({ name: 'kadence' }), + identity: utils.getRandomKeyBuffer(), + transport: null, + storage: null, + messenger: new Messenger(), + contact: {} + }; + } + + static validate(options) { + if (typeof options.identity === 'string') { + options.identity = Buffer.from(options.identity, 'hex'); + } + + utils.validateStorageAdapter(options.storage); + utils.validateLogger(options.logger); + utils.validateTransport(options.transport); + assert.ok(utils.keyBufferIsValid(options.identity), 'Invalid identity'); + } + + /** + * Contructs the primary interface for a kad node + * @constructor + * @param {object} options + * @param {AbstractNode~transport} options.transport - See {@tutorial transport-adapters} + * @param {buffer} options.identity - See {@tutorial identities} + * @param {Bucket~contact} options.contact - See {@tutorial identities} + * @param {AbstractNode~storage} options.storage - See {@tutorial storage-adapters} + * @param {AbstractNode~logger} [options.logger] + * @param {Messenger} [options.messenger] - See {@tutorial messengers} + */ + constructor(options) { + AbstractNode.validate(options = merge(AbstractNode.DEFAULTS, options)); + super(); + + this._middlewares = { '*': [] }; + this._errors = { '*': [] }; + this._pending = new Map(); + + this.rpc = options.messenger; + this.transport = options.transport; + this.storage = options.storage; + this.identity = options.identity; + this.contact = options.contact; + this.logger = options.logger; + this.router = new RoutingTable(this.identity); + + this._init(); + } + + /** + * Establishes listeners and creates the message pipeline + * @private + */ + _init() { + this.transport.on('error', (err) => { + this.logger.warn(err.message.toLowerCase()); + if (err.dispose && this._pending.get(err.dispose)) { + const pending = this._pending.get(err.dispose); + err.type = 'TIMEOUT'; + pending.handler(err); + this._pending.delete(err.dispose); + } + }); + + this.transport.on('data', data => { + this.rpc.deserializer.create() + .once('error', err => this.logger.warn(err.message.toLowerCase())) + .once('data', data => this._process(data)) + .write(data); + }); + + setInterval(() => this._timeout(), constants.T_RESPONSETIMEOUT); + } + + /** + * Processes deserialized messages + * @private + */ + _process([message, contact]) { + /* eslint complexity: [2, 8] */ + this._updateContact(...contact.payload.params); + + // NB: If we are receiving a request, then pass it through the middleware + // NB: stacks to process it + if (message.type === 'request') { + return this.receive( + merge({}, message.payload, { contact: contact.payload.params }), + { + send: (data) => { + this.rpc.serializer.create() + .once('data', data => this.transport.write(data)) + .once('error', err => this.logger.warn(err.message.toLowerCase())) + .write([ + merge({ id: message.payload.id }, { result: data }), + [this.identity.toString('hex'), this.contact], + contact.payload.params + ]); + }, + error: (msg, code = -32000) => { + this.rpc.serializer.create() + .once('data', data => this.transport.write(data)) + .once('error', err => this.logger.warn(err.message.toLowerCase())) + .write([ + merge({ id: message.payload.id }, { + error: { message: msg, code } + }), + [this.identity.toString('hex'), this.contact], + contact.payload.params + ]); + } + } + ); + } + + // NB: If we aren't expecting this message, just throw it away + if (!this._pending.has(message.payload.id)) { + return this.logger.warn( + `received late or invalid response from ${contact.payload.params[0]}` + ); + } + + // NB: Check to make sure that the response comes from the identity + // NB: that the request was origninally intended for, unless the message + // NB: was sent to a null identity (such as during bootstrapping) + const { handler, fingerprint } = this._pending.get(message.payload.id); + const nullFingerprint = Buffer.alloc(constants.B / 8, 0).toString('hex'); + const msgSentToNullFingerprint = fingerprint === nullFingerprint; + const fingerprintsMatch = fingerprint === contact.payload.params[0]; + + if (!msgSentToNullFingerprint && !fingerprintsMatch) { + handler(new Error( + 'Response fingerprint differs from request destination' + ), null); + this._pending.delete(message.payload.id); + return; + } + + // NB: Otherwise, check if we are waiting on a response to a pending + // NB: message and fire the result handler + const handlerArgs = [ + (message.type === 'error' + ? new Error(message.payload.error.message) + : null), + (message.type === 'success' + ? message.payload.result + : null) + ]; + + handler(...handlerArgs); + this._pending.delete(message.payload.id); + } + + /** + * Enumerates all pending handlers and fires them with a timeout error if + * they have been pending too long + * @private + */ + _timeout() { + let now = Date.now(); + let err = new Error('Timed out waiting for response'); + + err.type = 'TIMEOUT'; + + for (let [id, entry] of this._pending.entries()) { + if (entry.timestamp + constants.T_RESPONSETIMEOUT >= now) { + continue; + } + + entry.handler(err); + this._pending.delete(id); + } + } + + /** + * Adds the given contact to the routing table + * @private + */ + _updateContact(identity, contact) { + if (identity === this.identity.toString('hex')) { + return null; + } else { + return this.router.addContactByNodeId(identity, contact); + } + } + + /** + * Validates the contact tuple + * @private + */ + _validateContact(target) { + return (Array.isArray(target) && target[0] && target[1]) + && (this.transport._validate ? this.transport._validate(target) : true); + } + + /** + * Sends the [method, params] to the contact and executes the handler on + * response or timeout + * @param {string} method - RPC method name + * @param {object|array} params - RPC parameters + * @param {Bucket~contact} contact - Destination address information + * @param {AbstractNode~sendCallback} [callback] + * @returns {Promise} + */ + send(method, params, target, handler) { + if (typeof handler === 'function') { + return this._send(method, params, target).then(function() { + handler(null, ...arguments); + }, handler); + } else { + return this._send(method, params, target); + } + } + /** + * @callback AbstractNode~sendCallback + * @param {null|AbstractNode~sendError} error + * @param {object|array|string|number} result + */ + + /** + * @private + */ + _send(method, params, target) { + return new Promise((resolve, reject) => { + const id = uuid(); + const timestamp = Date.now(); + + if (!this._validateContact(target)) { + return reject(new Error('Refusing to send message to invalid contact')); + } + + target[0] = target[0].toString('hex'); // NB: Allow identity to be a buffer + + function wrapped(err, ...params) { + if (err) { + err.request = { id, method, params, target }; + return reject(err); + } + + resolve(...params); + } + + this._pending.set(id, { + handler: wrapped, + timestamp, + fingerprint: target[0] + }); + this.rpc.serializer.create() + .once('error', err => reject(err)) + .once('data', data => this.transport.write(data)) + .write([ + { id, method, params }, + [this.identity.toString('hex'), this.contact], + target + ]); + }); + } + + /** + * Accepts an arbitrary function that receives this node as context + * for mounting protocol handlers and extending the node with other + * methods + * @param {function} plugin - {@tutorial plugins} + */ + plugin(func) { + assert(typeof func === 'function', 'Invalid plugin supplied'); + return func(this); + } + + /** + * Mounts a message handler route for processing incoming RPC messages + * @param {string} [method] - RPC method name to route through + * @param {AbstractNode~middleware} middleware + */ + use(method, middleware) { + if (typeof method === 'function') { + middleware = method; + method = '*'; + } + + // NB: If middleware function takes 4 arguments, it is an error handler + const type = middleware.length === 4 ? '_errors' : '_middlewares'; + const stack = this[type][method] = this[type][method] || []; + + stack.push(middleware); + } + /** + * @callback AbstractNode~middleware + * @param {error} [error] - Error object resulting from a middleware + * @param {AbstractNode~request} request - The incoming message object + * @param {AbstractNode~response} response - The outgoing response object + * @param {AbstractNode~next} next - Call to proceed to next middleware + */ + + /** + * Passes through to the {@link AbstractNode~transport} + */ + listen() { + let handlers = new ErrorRules(this); + + this.use(handlers.methodNotFound.bind(handlers)); + this.use(handlers.internalError.bind(handlers)); + + this.transport.listen(...arguments); + } + + /** + * Processes a the given arguments by sending them through the appropriate + * middleware stack + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + */ + receive(request, response) { + const self = this; + const { method } = request; + + // NB: First pass the the arguments through the * middleware stack + // NB: Then pass the arguments through the METHOD middleware stack + function processRequest(callback) { + async.series([ + (next) => self._middleware('*', [request, response], next), + (next) => self._middleware(method, [request, response], next) + ], callback) + } + + // NB: Repeat the same steps for the error stack + function handleErrors(err) { + async.series([ + (next) => self._error('*', [err, request, response], next), + (next) => self._error(method, [err, request, response], next) + ]); + } + + processRequest(handleErrors); + } + + /** + * Send the arguments through the stack type + * @private + */ + _stack(type, method, args, callback) { + async.eachSeries(this[type][method] || [], (middleware, done) => { + try { + middleware(...args, done); + } catch (err) { + done(err); + } + }, callback); + } + + /** + * Send the arguments through the middleware + * @private + */ + _middleware() { + this._stack('_middlewares', ...arguments); + } + + /** + * Send the arguments through the error handlers + * @private + */ + _error() { + this._stack('_errors', ...arguments); + } + +} + +module.exports = AbstractNode; diff --git a/lib/node-kademlia.js b/lib/node-kademlia.js new file mode 100644 index 0000000..0c2135e --- /dev/null +++ b/lib/node-kademlia.js @@ -0,0 +1,653 @@ +'use strict'; + +const async = require('async'); +const { Writable: WritableStream } = require('stream'); +const constants = require('./constants'); +const { knuthShuffle: shuffle } = require('knuth-shuffle'); +const utils = require('./utils'); +const AbstractNode = require('./node-abstract'); +const KademliaRules = require('./rules-kademlia'); +const ContactList = require('./contact-list'); +const MetaPipe = require('metapipe'); + + +/** + * Extends {@link AbstractNode} with Kademlia-specific rules + * @class + * @extends {AbstractNode} + */ +class KademliaNode extends AbstractNode { + + /** + * @typedef {object} KademliaNode~entry + * @property {string|object|array} value - The primary entry value + * @property {string} publisher - Node identity of the original publisher + * @property {number} timestamp - Last update/replicate time + */ + + /** + * @constructor + */ + constructor(options) { + super(options); + + this._lookups = new Map(); // NB: Track the last lookup time for buckets + this._pings = new Map(); + this._updateContactQueue = async.queue( + (task, cb) => this._updateContactWorker(task, cb), + 1 + ); + + this.replicatePipeline = new MetaPipe({ objectMode: true }); + this.expirePipeline = new MetaPipe({ objectMode: true }); + } + + /** + * Adds the kademlia rule handlers before calling super#listen() + */ + listen() { + let handlers = new KademliaRules(this); + + this.use('PING', handlers.ping.bind(handlers)); + this.use('STORE', handlers.store.bind(handlers)); + this.use('FIND_NODE', handlers.findNode.bind(handlers)); + this.use('FIND_VALUE', handlers.findValue.bind(handlers)); + + setInterval( + utils.preventConvoy(() => this.refresh(0)), + constants.T_REFRESH + ); + setInterval( + utils.preventConvoy(() => this.replicate(() => this.expire())), + constants.T_REPLICATE + ); + + super.listen(...arguments); + } + + /** + * Inserts the given contact into the routing table and uses it to perform + * a {@link KademliaNode#iterativeFindNode} for this node's identity, + * then refreshes all buckets further than it's closest neighbor, which will + * be in the occupied bucket with the lowest index + * @param {Bucket~contact} peer - Peer to bootstrap from + * @param {function} [joinListener] - Function to set as join listener + * @returns {Promise} + */ + join(peer, callback) { + if (typeof callback === 'function') { + return this._join(peer).then(function() { + callback(null, ...arguments); + }, callback); + } else { + return this._join(peer); + } + } + + /** + * @private + */ + _join([identity, contact]) { + return new Promise((resolve, reject) => { + this.router.addContactByNodeId(identity, contact); + async.series([ + (next) => this.iterativeFindNode(this.identity.toString('hex'), next), + (next) => this.refresh(this.router.getClosestBucket() + 1, next) + ], (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + /** + * Sends a PING message to the supplied contact, resolves with latency + * @param {Bucket~contact} peer + * @param {KademliaNode~pingCallback} [callback] + * @returns {Promise} + */ + ping(contact, callback) { + if (typeof callback ==='function') { + return this._ping(contact).then(function() { + callback(null, ...arguments); + }, callback); + } else { + return this._ping(contact); + } + } + /** + * @callback KademliaNode~pingCallback + * @param {error|null} error + * @param {number} latency - Milliseconds before response received + */ + + /** + * @private + */ + _ping(contact) { + return new Promise((resolve, reject) => { + const start = Date.now(); + + this.send('PING', [], contact, (err) => { + if (err) { + return reject(err); + } + + resolve(Date.now() - start); + }); + }); + } + + /** + * @private + */ + _createStorageItem(value) { + const keys = Object.keys(value); + const alreadyHasMetadata = keys.includes('value') && + keys.includes('publisher') && + keys.includes('timestamp'); + + if (alreadyHasMetadata) { + value.timestamp = Date.now(); + value.publisher = value.publisher.toString('hex'); + return value; + } + + return { + value: value, + timestamp: Date.now(), + publisher: this.identity.toString('hex') + }; + } + + /** + * Performs a {@link KademliaNode#iterativeFindNode} to collect K contacts + * nearest to the given key, sending a STORE message to each of them. + * @param {buffer|string} key - Key to store data under + * @param {buffer|string|object} value - Value to store by key + * @param {KademliaNode~iterativeStoreCallback} callback + * @returns {Promise} + */ + iterativeStore(key, value, callback) { + if (typeof callback === 'function') { + return this._iterativeStore(key, value).then(function() { + callback(null, ...arguments); + }, callback); + } else { + return this._iterativeStore(key, value); + } + } + /** + * Note that if there is a protocol/validation error, you will not receive + * it as an error in the callback. Be sure to also check that stored > 0 as + * part of error handling here. + * @callback KademliaNode~iterativeStoreCallback + * @param {error|null} error + * @param {number} stored - Total nodes who stored the pair + */ + + /** + * @private + */ + _iterativeStore(key, value) { + return new Promise((resolve, reject) => { + key = key.toString('hex'); + let stored = 0; + + const createStoreRpc = (target) => { + return ['STORE', [key, this._createStorageItem(value)], target]; + }; + + const dispatchStoreRpcs = (contacts, callback) => { + async.eachLimit(contacts, constants.ALPHA, (target, done) => { + this.send(...createStoreRpc(target), (err) => { + stored = err ? stored : stored + 1; + done(); + }); + }, callback); + }; + + async.waterfall([ + (next) => this.iterativeFindNode(key, next), + (contacts, next) => dispatchStoreRpcs(contacts, next), + (next) => { + this.storage.put(key, this._createStorageItem(value), { + valueEncoding: 'json' + }, next); + } + ], () => { + if (stored === 0) { + return reject(new Error('Failed to stored entry with peers')); + } + resolve(stored); + }); + }); + } + + /** + * Basic kademlia lookup operation that builds a set of K contacts closest + * to the given key + * @param {buffer|string} key - Reference key for node lookup + * @param {KademliaNode~iterativeFindNodeCallback} [callback] + * @returns {Promise} + */ + iterativeFindNode(key, callback) { + key = key.toString('hex'); + + if (typeof callback === 'function') { + return this._iterativeFind('FIND_NODE', key).then(function() { + callback(null, ...arguments); + }, callback); + } else { + return this._iterativeFind('FIND_NODE', key); + } + } + /** + * @callback KademliaNode~iterativeFindNodeCallback + * @param {error|null} error + * @param {Bucket~contact[]} contacts - Result of the lookup operation + */ + + /** + * Kademlia search operation that is conducted as a node lookup and builds + * a list of K closest contacts. If at any time during the lookup the value + * is returned, the search is abandoned. If no value is found, the K closest + * contacts are returned. Upon success, we must store the value at the + * nearest node seen during the search that did not return the value. + * @param {buffer|string} key - Key for value lookup + * @param {KademliaNode~iterativeFindValueCallback} [callback] + * @returns {Promise} + */ + iterativeFindValue(key, callback) { + key = key.toString('hex'); + + if (typeof callback === 'function') { + return this._iterativeFind('FIND_VALUE', key).then(function() { + callback(null, ...arguments); + }, callback); + } else { + return this._iterativeFind('FIND_VALUE', key); + } + } + /** + * @callback KademliaNode~iterativeFindValueCallback + * @param {error|null} error + * @param {KademliaNode~entry} value + * @param {null|Bucket~contact} contact - Contact responded with entry + */ + + /** + * Performs a scan of the storage adapter and performs + * republishing/replication of items stored. Items that we did not publish + * ourselves get republished every T_REPLICATE. Items we did publish get + * republished every T_REPUBLISH. + * @param {KademliaNode~replicateCallback} [callback] + * @returns {Promise} + */ + replicate(callback) { + if (typeof callback === 'function') { + return this._replicate().then(callback, callback); + } else { + return this._replicate(); + } + } + /** + * @callback KademliaNode~replicateCallback + * @param {error|null} error + */ + + /** + * @private + */ + _replicate() { + const self = this; + const now = Date.now(); + + return new Promise((resolve, reject) => { + const itemStream = this.storage.createReadStream({ + valueEncoding: 'json' + }); + const replicateStream = new WritableStream({ + objectMode: true, + write: maybeReplicate + }); + + function maybeReplicate({ key, value }, enc, next) { + const isPublisher = value.publisher === self.identity.toString('hex'); + const republishDue = (value.timestamp + constants.T_REPUBLISH) <= now; + const replicateDue = (value.timestamp + constants.T_REPLICATE) <= now; + const shouldRepublish = isPublisher && republishDue; + const shouldReplicate = !isPublisher && replicateDue; + + if (shouldReplicate || shouldRepublish) { + return self.iterativeStore(key, value, next); + } + + next(); + } + + function triggerCallback(err) { + itemStream.removeAllListeners(); + replicateStream.removeAllListeners(); + + if (err) { + return reject(err); + } + + resolve(); + } + + itemStream.on('error', triggerCallback); + replicateStream.on('error', triggerCallback); + replicateStream.on('finish', triggerCallback); + itemStream.pipe(this.replicatePipeline).pipe(replicateStream); + }); + } + + /** + * Items expire T_EXPIRE seconds after the original publication. All items + * are assigned an expiration time which is "exponentially inversely + * proportional to the number of nodes between the current node and the node + * whose ID is closest to the key", where this number is "inferred from the + * bucket structure of the current node". + * @param {KademliaNode~expireCallback} [callback] + * @returns {Promise} + */ + expire(callback) { + if (typeof callback === 'function') { + return this._expire().then(callback, callback); + } else { + return this._expire(); + } + } + /** + * @callback KademliaNode~expireCallback + * @param {error|null} error + */ + + /** + * @private + */ + _expire() { + const self = this; + const now = Date.now(); + + return new Promise((resolve, reject) => { + const itemStream = this.storage.createReadStream({ + valueEncoding: 'json' + }); + const expireStream = new WritableStream({ + objectMode: true, + write: maybeExpire + }); + + function maybeExpire({ key, value }, enc, next) { + if ((value.timestamp + constants.T_EXPIRE) <= now) { + return self.storage.del(key, next); + } + + next(); + } + + function triggerCallback(err) { + itemStream.removeAllListeners(); + expireStream.removeAllListeners(); + + if (err) { + return reject(err); + } + + resolve(); + } + + itemStream.on('error', triggerCallback); + expireStream.on('error', triggerCallback); + expireStream.on('finish', triggerCallback); + itemStream.pipe(this.expirePipeline).pipe(expireStream); + }); + } + + /** + * If no node lookups have been performed in any given bucket's range for + * T_REFRESH, the node selects a random number in that range and does a + * refresh, an iterativeFindNode using that number as key. + * @param {number} startIndex - bucket index to start refresh from + * @param {KademliaNode~refreshCallback} [callback] + * @returns {Promise} + */ + refresh(startIndex = 0, callback) { + if (typeof callback === 'function') { + return this._refresh(startIndex).then(callback, callback); + } else { + return this._refresh(startIndex); + } + } + /** + * @callback KademliaNode~refreshCallback + * @param {error|null} error + * @param {array} bucketsRefreshed + */ + + /** + * @private + */ + _refresh(startIndex) { + const now = Date.now(); + const indices = [ + ...this.router.entries() + ].slice(startIndex).map((entry) => entry[0]); + + // NB: We want to avoid high churn during refresh and prevent further + // NB: refreshes if lookups in the next bucket do not return any new + // NB: contacts. To do this we will shuffle the bucket indexes we are + // NB: going to check and only continue to refresh if new contacts were + // NB: discovered in the last MAX_UNIMPROVED_REFRESHES consecutive lookups. + let results = new Set(), consecutiveUnimprovedLookups = 0; + + function isDiscoveringNewContacts() { + return consecutiveUnimprovedLookups < constants.MAX_UNIMPROVED_REFRESHES; + } + + return new Promise((resolve, reject) => { + async.eachSeries(shuffle(indices), (index, next) => { + if (!isDiscoveringNewContacts()) { + return resolve(); + } + + const lastBucketLookup = this._lookups.get(index) || 0; + const needsRefresh = lastBucketLookup + constants.T_REFRESH <= now; + + if (needsRefresh) { + return this.iterativeFindNode( + utils.getRandomBufferInBucketRange(this.identity, index) + .toString('hex'), + (err, contacts) => { + if (err) { + return next(err); + } + + let discoveredNewContacts = false; + + for (let [identity] of contacts) { + if (!results.has(identity)) { + discoveredNewContacts = true; + consecutiveUnimprovedLookups = 0; + results.add(identity); + } + } + + if (!discoveredNewContacts) { + consecutiveUnimprovedLookups++; + } + + next(); + } + ); + } + + next(); + }, (err) => { + if (err) { + return reject(err); + } + + resolve(); + }); + }); + } + + /** + * Builds an list of closest contacts for a particular RPC + * @private + */ + _iterativeFind(method, key) { + return new Promise((resolve) => { + function createRpc(target) { + return [method, [key], target]; + } + + let shortlist = new ContactList(key, [ + ...this.router.getClosestContactsToKey(key, constants.ALPHA) + ]); + let closest = shortlist.closest; + + this._lookups.set(utils.getBucketIndex(this.identity, key), Date.now()); + + function iterativeLookup(selection, continueLookup = true) { + if (!selection.length) { + return resolve(shortlist.active.slice(0, constants.K)); + } + + async.each(selection, (contact, next) => { + // NB: mark this node as contacted so as to avoid repeats + shortlist.contacted(contact); + + this.send(...createRpc(contact), (err, result) => { + if (err) { + return next(); + } + + // NB: mark this node as active to include it in any return values + shortlist.responded(contact); + + // NB: If the result is a contact/node list, just keep track of it + // NB: Otherwise, do not proceed with iteration, just callback + if (Array.isArray(result) || method !== 'FIND_VALUE') { + shortlist + .add(Array.isArray(result) ? result : []) + .forEach(contact => { + // NB: If it wasn't in the shortlist, we haven't added to the + // NB: routing table, so do that now. + this._updateContact(...contact); + }); + + return next(); + } + + // NB: If we did get an item back, get the closest node we contacted + // NB: who is missing the value and store a copy with them + const closestMissingValue = shortlist.active[0] + + if (closestMissingValue) { + this.send('STORE', [ + key, + this._createStorageItem(result) + ], closestMissingValue, () => null); + } + + // NB: we found a value, so stop searching + resolve(result, contact); + }); + }, () => { + + // NB: If we have reached at least K active nodes, or haven't found a + // NB: closer node, even on our finishing trip, return to the caller + // NB: the K closest active nodes. + if (shortlist.active.length >= constants.K || + (closest[0] === shortlist.closest[0] && !continueLookup) + ) { + return resolve(shortlist.active.slice(0, constants.K)); + } + + // NB: we haven't discovered a closer node, call k uncalled nodes and + // NB: finish up + if (closest[0] === shortlist.closest[0]) { + return iterativeLookup.call( + this, + shortlist.uncontacted.slice(0, constants.K), + false + ); + } + + closest = shortlist.closest; + + // NB: continue the lookup with ALPHA close, uncontacted nodes + iterativeLookup.call( + this, + shortlist.uncontacted.slice(0, constants.ALPHA), + true + ); + }); + } + + iterativeLookup.call( + this, + shortlist.uncontacted.slice(0, constants.ALPHA), + true + ); + }); + } + /** + * Adds the given contact to the routing table + * @private + */ + _updateContact(identity, contact) { + this._updateContactQueue.push({ identity, contact }, (err, headId) => { + if (err) { + this.router.removeContactByNodeId(headId); + this.router.addContactByNodeId(identity, contact); + } + }); + } + + /** + * Worker for updating contact in a routing table bucket + * @private + */ + _updateContactWorker(task, callback) { + const { identity, contact } = task; + + if (identity === this.identity.toString('hex')) { + return callback(); + } + + const now = Date.now(); + const reset = 600000; + const [, bucket, contactIndex] = this.router.addContactByNodeId( + identity, + contact + ); + + const [headId, headContact] = bucket.head; + const lastPing = this._pings.get(headId); + + if (contactIndex !== -1) { + return callback(); + } + + if (lastPing && lastPing.responded && lastPing.timestamp > (now - reset)) { + return callback(); + } + + this.ping([headId, headContact], (err) => { + this._pings.set(headId, { timestamp: Date.now(), responded: !err }); + callback(err, headId); + }); + } + +} + +module.exports = KademliaNode; diff --git a/lib/plugin-churnfilter.js b/lib/plugin-churnfilter.js new file mode 100644 index 0000000..b286309 --- /dev/null +++ b/lib/plugin-churnfilter.js @@ -0,0 +1,210 @@ +/** + * @module kadence/churnfilter + */ + +'use strict'; + +const ms = require('ms'); +const merge = require('merge'); + + +/** + * Plugin that tracks contacts that are not online and evicts them from the + * routing table, prevents re-entry into the routing table using an exponential + * cooldown time. + */ +class ChurnFilterPlugin { + + static get DEFAULTS() { + return { + cooldownBaseTimeout: '1M', // Start block at N minutes + cooldownMultiplier: 2, // Multiply the block time by M every offense + cooldownResetTime: '10M' // Until no offense has occured for K minutes + }; + } + + /** + * @constructor + * @param {AbstractNode} node + * @param {object} [options] + * @param {number} [options.cooldownMultiplier=2] - Multiply cooldown time + * by this number after every offense + * @param {string} [options.cooldownResetTime="10M"] - Human time string + * for resetting the cooldown multiplier after no block added for a given + * peer fingerprint + * @param {string} [options.cooldownBaseTimeout="1M"] - Human time string + * for starting timeout, multiplied by two every time the cooldown is reset + * and broken again + */ + constructor(node, options) { + this.node = node; + this.opts = merge(ChurnFilterPlugin.DEFAULTS, options); + this.cooldown = new Map(); + this.blocked = new Set(); + + // Not sure how well this is going to work in a production environment yet + // so let's warn users that it could be problematic + this.node.logger.warn( + 'the churn filter plugin may not be suitable for production networks' + ); + + this._wrapAbstractNodeSend(); // Detect timeouts and network errors + this._wrapAbstractNodeUpdateContact(); // Gatekeep the routing table + + setInterval( + this.resetCooldownForStablePeers.bind(this), + ms(this.opts.cooldownBaseTimeout) + ); + } + + /** + * @private + */ + _wrapAbstractNodeUpdateContact() { + const _updateContact = this.node._updateContact.bind(this.node); + + this.node._updateContact = (identity, contact) => { + if (this.hasBlock(identity)) { + this.node.logger.debug( + 'preventing entry of blocked fingerprint %s into routing table', + identity + ); + return null; + } + + _updateContact(identity, contact); + }; + } + + /** + * @private + */ + _wrapAbstractNodeSend() { + const send = this.node.send.bind(this.node); + + this.node.send = (method, params, target, handler) => { + if (this.hasBlock(target[0])) { + this.node.logger.warn( + 'sending message to contact %s with active block', + target[0] + ); + } + + send(method, params, target, (err, result) => { + if (err && (err.type === 'TIMEOUT' || err.dispose)) { + this.node.logger.info('setting temporary block for %s', target[0]); + this.setBlock(target[0]); + } + + handler(err, result); + }); + }; + } + + /** + * Checks if the fingerprint is blocked + * @param {string|buffer} fingerprint - Node ID to check + * @returns {boolean} + */ + hasBlock(fingerprint) { + fingerprint = fingerprint.toString('hex'); + + if (this.blocked.has(fingerprint)) { + return !this.cooldown.get(fingerprint).expired; + } + + return false; + } + + /** + * Creates a new block or renews the cooldown for an existing block + * @param {string|buffer} fingerprint - Node ID to block + * @returns {object} + */ + setBlock(fingerprint) { + fingerprint = fingerprint.toString('hex'); + + let cooldown = this.cooldown.get(fingerprint); + + if (cooldown) { + cooldown.duration = cooldown.expired + ? cooldown.duration + : cooldown.duration * this.opts.cooldownMultiplier; + cooldown.time = Date.now(); + } else { + cooldown = { + duration: ms(this.opts.cooldownBaseTimeout), + time: Date.now(), + get expiration() { + return this.time + this.duration; + }, + get expired() { + return this.expiration <= Date.now(); + } + }; + } + + this.cooldown.set(fingerprint, cooldown); + this.blocked.add(fingerprint); + this.node.router.removeContactByNodeId(fingerprint); + } + + /** + * Deletes the blocked fingerprint + * @param {string|buffer} fingerprint - Node ID to remove block + */ + delBlock(fingerprint) { + this.cooldown.delete(fingerprint); + this.blocked.delete(fingerprint); + } + + /** + * Clears all blocked and cooldown data + */ + reset() { + this.cooldown.clear(); + this.blocked.clear(); + } + + /** + * Releases blocked to reset cooldown multipliers for fingerprints with + * cooldowns that are long expired and not blocked + */ + resetCooldownForStablePeers() { + const now = Date.now(); + + for (let [fingerprint, cooldown] of this.cooldown) { + if (this.hasBlock(fingerprint)) { + continue; + } + + let { expired, expiration } = cooldown; + + if (expired && (now - expiration >= ms(this.opts.cooldownResetTime))) { + this.delBlock(fingerprint); + } + } + } + +} + +/** + * Registers a {@link module:kadence/contentaddress~ChurnFilterPlugin} with + * a {@link KademliaNode} + * @param {object} [options] + * @param {number} [options.cooldownMultiplier=2] - Multiply cooldown time + * by this number after every offense + * @param {string} [options.cooldownResetTime="60M"] - Human time string + * for resetting the cooldown multiplier after no block added for a given + * peer fingerprint + * @param {string} [options.cooldownBaseTimeout="5M"] - Human time string + * for starting timeout, multiplied by two every time the cooldown is reset + * and broken again + */ +module.exports = function(options) { + return function(node) { + return new ChurnFilterPlugin(node, options); + } +}; + +module.exports.ChurnFilterPlugin = ChurnFilterPlugin; diff --git a/lib/plugin-contentaddress.js b/lib/plugin-contentaddress.js new file mode 100644 index 0000000..4649599 --- /dev/null +++ b/lib/plugin-contentaddress.js @@ -0,0 +1,97 @@ +/** + * @module kadence/contentaddress + */ + +'use strict'; + +const { createHash } = require('crypto'); +const merge = require('merge'); +const assert = require('assert'); + + +/** + * Enforces that any {@link KademliaNode~entry} stored in the DHT must be + * content-addressable (keyed by the hash of it's value). + */ +class ContentAddressPlugin { + + static get DEFAULTS() { + return { + keyAlgorithm: 'ripemd160', + valueEncoding: 'base64' + }; + } + + /** + * @constructor + * @param {AbstractNode} node + * @param {object} [options] + * @param {string} [options.keyAlgorithm="rmd160"] - Algorithm for hashing + * @param {string} [options.valueEncoding="base64"] - Text encoding of value + */ + constructor(node, options) { + this.node = node; + this.opts = merge(ContentAddressPlugin.DEFAULTS, options); + + this.node.use('STORE', (req, res, next) => this.validate(req, res, next)); + this._wrapIterativeStore(); + } + + /** + * @private + */ + _wrapIterativeStore() { + let iterativeStore = this.node.iterativeStore.bind(this.node); + + this.node.iterativeStore = (key, value, callback) => { + try { + const buffer = Buffer.from(value, this.opts.valueEncoding); + const hash = createHash(this.opts.keyAlgorithm).update(buffer) + .digest('hex'); + + assert(key === hash); + } catch (err) { + return callback(new Error('Item failed validation check')); + } + + iterativeStore(key, value, callback); + }; + } + + /** + * Validate the the key matches the hash of the value + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + validate(request, response, next) { + let buffer, hash, [key, item] = request.params; + + try { + buffer = Buffer.from(item.value, this.opts.valueEncoding); + hash = createHash(this.opts.keyAlgorithm).update(buffer).digest('hex'); + + assert(key === hash); + } catch (err) { + return next(new Error('Item failed validation check')); + } + + next(); + } + +} + +/** + * Registers a {@link module:kadence/contentaddress~ContentAddressPlugin} with + * a {@link KademliaNode} + * @param {object} [options] + * @param {string} [options.keyAlgorithm="rmd160"] - Algorithm for hashing + * @param {string} [options.valueEncoding="base64"] - Text encoding of value + */ +module.exports = function(options) { + return function(node) { + return new ContentAddressPlugin(node, options); + } +}; + +module.exports.ContentAddressPlugin = ContentAddressPlugin; diff --git a/lib/plugin-eclipse.js b/lib/plugin-eclipse.js new file mode 100644 index 0000000..2dd3177 --- /dev/null +++ b/lib/plugin-eclipse.js @@ -0,0 +1,148 @@ +/** + * @module kadence/eclipse + */ + +'use strict'; + +const assert = require('assert'); +const utils = require('./utils'); +const constants = require('./constants'); +const { EventEmitter } = require('events'); + + +/** + * Generates an identity for use with the + * {@link module:kadence/spartacus~SpartacusPlugin} that satisfies a proof of + * work + */ +class EclipseIdentity extends EventEmitter { + + /** + * @constructor + * @param {string} publicKey - SECP256K1 public key + * @param {number} [nonce] - Equihash proof nonce + * @param {buffer} [proof] - Equihash proof value + */ + constructor(publicKey, nonce, proof) { + super(); + + this.pubkey = publicKey; + this.nonce = nonce || 0; + this.proof = proof || Buffer.from([]); + this.fingerprint = utils.hash160(this.proof); + } + + /** + * Returns a equihash proof and resulting fingerprint + * @returns {Promise} + */ + solve() { + return new Promise((resolve, reject) => { + utils.eqsolve( + utils.hash256(this.pubkey), + constants.IDENTITY_DIFFICULTY + ).then(proof => { + this.nonce = proof.nonce; + this.proof = proof.value; + this.fingerprint = utils.hash160(this.proof); + resolve(this); + }, reject); + }); + } + + /** + * Validates the + * @returns {boolean} + */ + validate() { + return utils.eqverify(utils.hash256(this.pubkey), { + n: constants.IDENTITY_DIFFICULTY.n, + k: constants.IDENTITY_DIFFICULTY.k, + nonce: this.nonce, + value: this.proof + }); + } + +} + +/** + * Enforces identities that satisfy a proof of work + */ +class EclipseRules { + + /** + * @constructor + * @param {Node} node + */ + constructor(node) { + this.node = node; + } + + /** + * Validates all incoming RPC messages + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + */ + validate(request, response, next) { + const [fingerprint, contact] = request.contact; + const identity = new EclipseIdentity( + Buffer.from(contact.pubkey || '', 'hex'), + contact.nonce, + Buffer.from(contact.proof || '', 'hex') + ); + + try { + assert(identity.fingerprint.toString('hex') === fingerprint, + 'Fingerprint does not match the proof hash'); + assert(identity.validate(), + 'Identity proof is invalid or does not satisfy the difficulty'); + } catch (err) { + return next(err); + } + + return next(); + } + +} + +/** + * Enforces proof of work difficulty for entering the routing table and ensures + * a high degree of randomness in resulting node identity + */ +class EclipsePlugin { + + /** + * @constructor + * @param {KademliaNode} node + * @param {EclipseIdentity} identity + */ + constructor(node, identity) { + assert(identity instanceof EclipseIdentity, 'No eclipse identity supplied'); + + this.node = node; + this.rules = new EclipseRules(this.node); + this.identity = identity; + this.node.contact.pubkey = identity.pubkey.toString('hex'); + this.node.contact.nonce = identity.nonce; + this.node.contact.proof = identity.proof.toString('hex'); + this.node.identity = identity.fingerprint; + + this.node.use(this.rules.validate.bind(this.rules)); + } + +} + +/** + * Registers a {@link module:kadence/eclipse~EclipsePlugin} with a + * {@link KademliaNode} + * @param {EclipseIdentity} identity + */ +module.exports = function(identity) { + return function(node) { + return new EclipsePlugin(node, identity); + } +}; + +module.exports.EclipsePlugin = EclipsePlugin; +module.exports.EclipseRules = EclipseRules; +module.exports.EclipseIdentity = EclipseIdentity; diff --git a/lib/plugin-hashcash.js b/lib/plugin-hashcash.js new file mode 100644 index 0000000..5688b66 --- /dev/null +++ b/lib/plugin-hashcash.js @@ -0,0 +1,301 @@ +/** + * @module kadence/hashcash + */ + +'use strict'; + +const { fork } = require('child_process'); +const path = require('path'); +const { Transform } = require('stream'); +const async = require('async'); +const merge = require('merge'); +const jsonrpc = require('jsonrpc-lite'); +const crypto = require('crypto'); +const assert = require('assert'); +const LRUCache = require('lru-cache'); +const utils = require('./utils'); + + +/** + * Requires proof of work to process messages and performs said work before + * issuing RPC messages to peers + */ +class HashCashPlugin { + + static get METHOD() { + return 'HASHCASH'; + } + + static get DEFAULTS() { + return { + methods: [], // All methods by default + difficulty: 8, // 8 leading zeroes + timeframe: 172800000 // 2 day window + }; + } + + /** + * @constructor + * @param {object} node + * @param {object} [options] + * @param {string[]} [options.methods=[]] - RPC methods to enforce hashcash + * @param {number} [options.difficulty=8] - Leading zero bits in stamp + * @param {number} [options.timeframe=172800000] - Timestamp valid window + */ + constructor(node, options = {}) { + this._opts = merge(HashCashPlugin.DEFAULTS, options); + this._node = node; + this._cache = new LRUCache(1000); + + this._node.rpc.deserializer.prepend(() => new Transform({ + transform: this.verify.bind(this), + objectMode: true + })); + + this._node.rpc.serializer.append(() => new Transform({ + transform: this.prove.bind(this), + objectMode: true + })); + } + + /** + * Verifies the proof of work on the request object + * @implements {Messenger~deserializer} + */ + verify(data, encoding, callback) { + /* eslint max-statements: [2, 26] */ + let payload = jsonrpc.parse(data.toString('utf8')).map((obj) => { + return obj.payload; + }); + let verifyMessage = (this._opts.methods.includes(payload[0].method) || + this._opts.methods.length === 0) && + typeof payload[0].method !== 'undefined'; + + if (!verifyMessage) { + return callback(null, data); + } + + let proof = payload.filter(m => m.method === HashCashPlugin.METHOD).pop(); + let contact = payload.filter(m => m.method === 'IDENTIFY').pop(); + + if (!proof) { + return callback(new Error('HashCash stamp is missing from payload')); + } + + let stamp = HashCashPlugin.parse(proof.params[0]); + let sender = stamp.resource.substr(0, 40); + let target = Buffer.from(stamp.resource.substr(40, 40), 'hex'); + let method = Buffer.from( + stamp.resource.substr(80), + 'hex' + ).toString('utf8'); + + try { + assert(this._cache.get(stamp.toString()) !== 1, 'Cannot reuse proof'); + assert(stamp.bits === this._opts.difficulty, 'Invalid proof difficulty'); + assert(sender === contact.params[0], 'Invalid sender in proof'); + assert( + Buffer.compare(target, this._node.identity) === 0, + 'Invalid target in proof' + ); + assert(method === payload[0].method, 'Invalid proof for called method'); + + let now = Date.now(); + + assert(utils.satisfiesDifficulty(utils.hash160(stamp.toString()), + this._opts.difficulty), 'Invalid HashCash stamp'); + assert( + now - Math.abs(stamp.date) <= this._opts.timeframe, + 'HashCash stamp is expired' + ); + } catch (err) { + return callback(err); + } + + this._cache.set(stamp.toString(), 1); + callback(null, data); + } + + /** + * Add proof of work to outgoing message + * @implements {Messenger~serializer} + */ + prove(data, encoding, callback) { + let [id, buffer, target] = data; + let now = Date.now(); + let payload = jsonrpc.parse(buffer.toString('utf8')).map((obj) => { + return obj.payload; + }); + let stampMessage = (this._opts.methods.includes(payload[0].method) || + this._opts.methods.length === 0) && + typeof payload[0].method !== 'undefined'; + + if (!stampMessage) { + return callback(null, data); + } + + this._node.logger.debug(`mining hashcash stamp for ${payload[0].method}`); + + // NB: "Pause" the timeout timer for the request this is associated with + // NB: so that out mining does not eat into the reasonable time for a + // NB: response. + const pending = this._node._pending.get(id) || {}; + pending.timestamp = Infinity; + + HashCashPlugin.create( + this._node.identity.toString('hex'), + target[0], + payload[0].method, + this._opts.difficulty, + (err, result) => { + if (err) { + return callback(err); + } + + pending.timestamp = Date.now(); // NB: Reset the timeout counter + + let delta = Date.now() - now; + let proof = jsonrpc.notification(HashCashPlugin.METHOD, [ + result.header + ]); + + this._node.logger.debug(`mined stamp ${result.header} in ${delta}ms`); + payload.push(proof); + callback(null, [ + id, + Buffer.from(JSON.stringify(payload), 'utf8'), + target + ]); + } + ); + } + + /** + * Parses hashcash stamp header into an object + * @static + * @param {string} header - Hashcash header proof stamp + * @returns {module:kadence/hashcash~HashCashPlugin~stamp} + */ + static parse(header) { + let parts = header.split(':'); + let parsed = { + ver: parseInt(parts[0]), + bits: parseInt(parts[1]), + date: parseInt(parts[2]), + resource: parts[3], + ext: '', + rand: parts[5], + counter: parseInt(parts[6], 16), + toString() { + return [ + this.ver, this.bits, this.date, this.resource, + this.ext, this.rand, this.counter.toString(16) + ].join(':'); + } + }; + + return parsed; + } + /** + * @typedef module:kadence/hashcash~HashCashPlugin~stamp + * @property {number} ver - Hashcash version + * @property {number} bits - Number of zero bits of difficulty + * @property {number} date - UNIX timestamp + * @property {string} resource - Sender and target node identities + * @property {string} ext - Empty string + * @property {string} rand - String encoded random number + * @property {number} counter - Base 16 counter + * @property {function} toString - Reserializes the parsed header + */ + + /** + * Creates the hashcash stamp header + * @static + * @param {string} sender + * @param {string} target + * @param {string} method + * @param {number} difficulty + * @param {function} callback + */ + /* eslint max-params: [2, 5] */ + static create(sender = '00', target = '00', method = '00', bits = 8, cb) { + const proc = fork( + path.join(__dirname, 'plugin-hashcash.worker.js'), + [ + sender, + target, + method, + bits + ], + { + env: process.env + } + ); + + proc.on('message', msg => { + if (msg.error) { + return cb(new Error(msg.error)); + } + + cb(null, msg); + }); + } + + /** + * @private + */ + static _worker(sender = '00', target = '00', method = '00', bits = 8, cb) { + let header = { + ver: 1, + bits: bits, + date: Date.now(), + resource: Buffer.concat([ + Buffer.from(sender, 'hex'), + Buffer.from(target, 'hex'), + Buffer.from(method) + ]).toString('hex'), + ext: '', + rand: crypto.randomBytes(12).toString('base64'), + counter: Math.ceil(Math.random() * 10000000000), + toString() { + return [ + this.ver, this.bits, this.date, this.resource, + this.ext, this.rand, this.counter.toString(16) + ].join(':'); + } + }; + + function isSolution() { + return utils.satisfiesDifficulty(utils.hash160(header.toString()), bits); + } + + async.whilst(() => !isSolution(), (done) => { + setImmediate(() => { + header.counter++; + done(); + }); + }, () => { + cb(null, { + header: header.toString(), + time: Date.now() - header.date + }); + }); + } + +} + +/** + * Registers the {@link module:kadence/hashcash~HashCashPlugin} with an + * {@link AbstractNode} + * @param {object} [options] + * @param {string[]} [options.methods=[]] - RPC methods to enforce hashcash + * @param {number} [options.difficulty=8] - Leading zero bits in stamp + * @param {number} [options.timeframe=172800000] - Timestamp valid window + */ +module.exports = function(options) { + return function(node) { + return new HashCashPlugin(node, options); + } +}; + +module.exports.HashCashPlugin = HashCashPlugin; diff --git a/lib/plugin-hashcash.worker.js b/lib/plugin-hashcash.worker.js new file mode 100644 index 0000000..73e622e --- /dev/null +++ b/lib/plugin-hashcash.worker.js @@ -0,0 +1,22 @@ +'use strict'; + +const [,, sender, target, method, bits] = process.argv; +const { HashCashPlugin: hc } = require('./plugin-hashcash'); + +function _err(msg) { + process.send({ error: msg }); + process.exit(1); +} + +try { + hc._worker(sender, target, method, parseInt(bits), function(err, result) { + if (err) { + return _err(err.message); + } + + process.send(result); + process.exit(0); + }); +} catch (err) { + _err(err.message); +} diff --git a/lib/plugin-hibernate.js b/lib/plugin-hibernate.js new file mode 100644 index 0000000..5e65b54 --- /dev/null +++ b/lib/plugin-hibernate.js @@ -0,0 +1,155 @@ +/** + * @module kadence/hibernate + */ + +'use strict'; + +const { EventEmitter } = require('events'); +const { Transform } = require('stream'); +const merge = require('merge'); +const bytes = require('bytes'); +const ms = require('ms'); + + +/** + * Represents a bandwidth meter which will trigger hibernation + */ +class HibernatePlugin extends EventEmitter { + + static get DEFAULTS() { + return { + limit: '5gb', + interval: '1d', + reject: ['STORE', 'FIND_VALUE'] + }; + } + + /** + * @constructor + * @param {KademliaNode} node + * @param {object} [options] + * @param {string} [options.limit=5gb] - The accounting max bandwidth + * @param {string} [options.interval=1d] - The accounting reset interval + * @param {string[]} [options.reject] - List of methods to reject during + * hibernation + */ + constructor(node, options) { + super(); + + this.node = node; + this.opts = merge(HibernatePlugin.DEFAULTS, options); + this.limit = bytes(this.opts.limit); + this.interval = ms(this.opts.interval); + this.reject = this.opts.reject; + + // This plugin could potentially be used for denial of service attacks + // so let's warn users that it could be problematic + this.node.logger.warn( + 'the hibernation plugin may not be suitable for production networks' + ); + + this.node.rpc.deserializer.prepend(() => this.meter('inbound')); + this.node.rpc.serializer.append(() => this.meter('outbound')); + this.node.use((req, res, next) => this.detect(req, res, next)); + this.start(); + } + + + /** + * @property {boolean} hibernating - Indicates if our limits are reached + */ + get hibernating() { + return this.accounting.total >= this.limit; + } + + /** + * Starts the accounting reset timeout + */ + start() { + const now = Date.now(); + + if (this.accounting) { + this.emit('reset', merge({}, this.accounting, { + hibernating: this.hibernating + })); + } else { + this.emit('start'); + } + + this.accounting = { + start: now, + end: now + this.interval, + inbound: 0, + outbound: 0, + unknown: 0, + get total() { + return this.inbound + this.outbound + this.unknown; + }, + get reset() { + return this.end - Date.now(); + } + }; + + setTimeout(() => this.start(), this.interval); + } + + /** + * Return a meter stream that increments the given accounting property + * @param {string} type - ['inbound', 'outbound', 'unknown'] + * @returns {stream.Transform} + */ + meter(type) { + if (!['inbound', 'outbound'].includes(type)) { + type = 'unknown'; + } + + const inc = (data) => { + if (Buffer.isBuffer(data)) { + this.accounting[type] += data.length; + } else if (Array.isArray(data)) { + this.accounting[type] += data[1].length; + } else { + this.accounting[type] = Buffer.from(data).length; + } + } + + return new Transform({ + transform: (data, enc, callback) => { + inc(data); + callback(null, data); + }, + objectMode: true + }); + } + + /** + * Check if hibernating when messages received + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + detect(request, response, next) { + if (this.hibernating && this.reject.includes(request.method)) { + next(new Error(`Hibernating, try ${request.method} again later`)); + } else { + next(); + } + } + +} + +/** + * Regsiters a {@link HibernatePlugin} with an {@link AbstractNode} + * @param {object} [options] + * @param {string} [options.limit=5gb] - The accounting max bandwidth + * @param {string} [options.interval=1d] - The accounting reset interval + * @param {string[]} [options.reject] - List of methods to reject during + * hibernation + */ +module.exports = function(options) { + return function(node) { + return new module.exports.HibernatePlugin(node, options); + }; +}; + +module.exports.HibernatePlugin = HibernatePlugin; diff --git a/lib/plugin-logger.js b/lib/plugin-logger.js new file mode 100644 index 0000000..207b752 --- /dev/null +++ b/lib/plugin-logger.js @@ -0,0 +1,110 @@ +/** + * @module kadence/logger + */ + +'use strict'; + +const { Transform } = require('stream'); +const bunyan = require('bunyan'); + + +/** + * Logs all incoming messages + */ +class IncomingMessageLogger extends Transform { + + /** + * @constructor + * @param {AbstractNode~logger} logger - Logger to use + */ + constructor(logger) { + super({ objectMode: true }); + this.logger = logger; + } + + /** + * @private + */ + _transform(data, enc, callback) { + let [rpc, ident] = data; + + if (!ident.payload.params[0] || !ident.payload.params[1]) { + return callback(); + } + + if (rpc.payload.method) { + this.logger.info( + `received ${rpc.payload.method} (${rpc.payload.id}) from ` + + `${ident.payload.params[0]} ` + + `(${ident.payload.params[1].hostname}:` + + `${ident.payload.params[1].port})` + ); + } else { + this.logger.info( + `received response from ${ident.payload.params[0]} to ` + + `${rpc.payload.id}` + ); + } + + callback(null, data); + } + +} + +/** + * Logs all outgoing messages + */ +class OutgoingMessageLogger extends Transform { + + /** + * @constructor + * @param {AbstractNode~logger} logger - Logger to use + */ + constructor(logger) { + super({ objectMode: true }); + this.logger = logger; + } + + /** + * @private + */ + _transform(data, enc, callback) { + let [rpc,, recv] = data; + + if (!recv[0] || !recv[1]) { + return callback(); + } + + if (rpc.method) { + this.logger.info( + `sending ${rpc.method} (${rpc.id}) to ${recv[0]} ` + + `(${recv[1].hostname}:${recv[1].port})` + ); + } else { + this.logger.info( + `sending response to ${recv[0]} for ${rpc.id}` + ); + } + + callback(null, data); + } + +} + +/** + * Attaches a verbose logger to a {@link AbstractNode} + * @param {AbstractNode~logger} [logger] - Custom logger + */ +module.exports = function(logger) { + logger = logger = bunyan.createLogger({ name: 'kadence' }); + + return function(node) { + node.rpc.deserializer.append(() => new IncomingMessageLogger(logger)); + node.rpc.serializer.prepend(() => new OutgoingMessageLogger(logger)); + + return logger; + }; +}; + +module.exports.IncomingMessage = IncomingMessageLogger; +module.exports.OutgoingMessage = OutgoingMessageLogger; diff --git a/lib/plugin-onion.js b/lib/plugin-onion.js new file mode 100644 index 0000000..003baef --- /dev/null +++ b/lib/plugin-onion.js @@ -0,0 +1,232 @@ +/** + * @module kadence/onion + */ + +'use strict'; + +const os = require('os'); +const fs = require('fs'); +const path = require('path'); +const split = require('split'); +const merge = require('merge'); +const socks = require('socks'); +const hsv3 = require('@tacticalchihuahua/granax/hsv3'); + + +/** + * SOCKS5 proxy plugin, wraps HTTP* transports createRequest method + */ +class OnionPlugin { + + static get DEFAULTS() { + return { + dataDirectory: path.join(os.tmpdir(), 'kad-onion-default'), + virtualPort: 80, + localMapping: '127.0.0.1:8080', + passthroughLoggingEnabled: false, + torrcEntries: {}, + socksVersion: 5 + }; + } + + /** + * Creates the transport wrapper for using a SOCKS5 proxy + * @constructor + * @param {object} node + * @param {object} [options] + * @param {string} [options.dataDirectory] - Write hidden service data + * @param {number} [options.virtualPort] - Virtual hidden service port + * @param {string} [options.localMapping] - IP/Port string of target service + * @param {object} [options.torrcEntries] - Additional torrc entries + * @param {boolean} [options.passthroughLoggingEnabled] - Passthrough tor log + */ + constructor(node, options) { + this._opts = merge(OnionPlugin.DEFAULTS, options); + this.logger = node.logger; + this.node = node; + this.node.onion = this; + + this._wrapNodeListen(node); + } + + /** + * Returns an agent instance to use for the provided target + * @returns {Agent} + */ + createSecureAgent() { + return new socks.Agent({ + proxy: { + ipaddress: '127.0.0.1', + port: this.socksPort, + type: this._opts.socksVersion + }, + timeout: 30000 + }, true, false); + } + + /** + * Returns a clear text agent instance to use for the provided target + * @returns {Agent} + */ + createClearAgent() { + return new socks.Agent({ + proxy: { + ipaddress: '127.0.0.1', + port: this.socksPort, + type: this._opts.socksVersion + }, + timeout: 30000 + }, false, false); + } + + /** + * @private + */ + _wrapTransportRequest(transport) { + this._createRequest = this._createRequest || + transport._createRequest.bind(transport); + + transport._createRequest = (options) => { + options.agent = options.protocol === 'https:' + ? this.createSecureAgent() + : this.createClearAgent(); + + return this._createRequest(options); + }; + } + + /** + * @private + */ + _waitForBootstrap() { + return new Promise(resolve => { + this.tor.on('STATUS_CLIENT', (status) => { + let notice = status[0].split(' ')[1]; + let summary = null; + + if (status[0].includes('SUMMARY')) { + summary = status[0].split('SUMMARY="'); + summary = summary[summary.length - 1].split('"')[0]; + } + + if (notice === 'CIRCUIT_ESTABLISHED') { + this.logger.info('connected to the tor network'); + this.tor.removeEventListeners(() => resolve()); + } else if (summary) { + this.logger.info('bootstrapping tor, ' + summary.toLowerCase()); + } + }); + + this.tor.addEventListeners(['STATUS_CLIENT'], () => { + this.logger.info('listening for bootstrap status for tor client'); + }); + }); + } + + /** + * @private + */ + _getSocksProxyPort() { + return new Promise((resolve, reject) => { + this.logger.info('connected to tor control port'); + this.logger.info('querying tor for socks proxy port'); + + this.tor.getInfo('net/listeners/socks', (err, result) => { + if (err) { + return reject(err); + } + + let [, socksPort] = result.replace(/"/g, '').split(':'); + this.socksPort = parseInt(socksPort); + + resolve(this.socksPort); + }); + }); + } + + /** + * @private + */ + async _setupTorController() { + return new Promise((resolve, reject) => { + this.tor = hsv3([ + { + dataDirectory: path.join(this._opts.dataDirectory, 'hidden_service'), + virtualPort: this._opts.virtualPort, + localMapping: this._opts.localMapping + } + ], merge(this._opts.torrcEntries, { + DataDirectory: this._opts.dataDirectory + })); + + this.tor.on('error', reject).on('ready', async () => { + await this._waitForBootstrap(); + await this._getSocksProxyPort(); + + this.node.contact.hostname = fs.readFileSync( + path.join(this._opts.dataDirectory, 'hidden_service', 'hostname') + ).toString().trim(); + this.node.contact.port = this._opts.virtualPort; + + this._wrapTransportRequest(this.node.transport); + resolve(); + }); + + if (this._opts.passthroughLoggingEnabled) { + this.tor.process.stdout.pipe(split()).on('data', (data) => { + let message = data.toString().split(/\[(.*?)\]/); + + message.shift(); // NB: Remove timestamp + message.shift(); // NB: Remove type + message[0] = message[0] ? message[0].trim() : ''; // NB: Trim white + message = message.join(''); // NB: Put it back together + + this.logger.info(`tor process: ${message}`); + }); + } + }); + } + + /** + * @private + */ + async _wrapNodeListen(node) { + const listen = node.listen.bind(node); + + node.listen = async (port, address, callback) => { + this.logger.info('spawning tor client and controller'); + + if (typeof address === 'function') { + callback = address; + address = '127.0.0.1'; + } + + try { + await this._setupTorController(); + } catch (err) { + return node.emit('error', err); + } + + listen(port, address, callback); + }; + } + +} + +/** + * Registers a {@link OnionPlugin} with an {@link AbstractNode} + * @param {object} node + * @param {object} [options] + * @param {string} [options.dataDirectory] - Write hidden service data + * @param {number} [options.virtualPort] - Virtual hidden service port + * @param {string} [options.localMapping] - IP/Port string of target service + * @param {object} [options.torrcEntries] - Additional torrc entries + * @param {boolean} [options.passthroughLoggingEnabled] - Passthrough tor log + */ +module.exports = function(options) { + return function(node) { + return new OnionPlugin(node, options); + } +}; + +module.exports.OnionPlugin = OnionPlugin; diff --git a/lib/plugin-quasar.js b/lib/plugin-quasar.js new file mode 100644 index 0000000..acc842b --- /dev/null +++ b/lib/plugin-quasar.js @@ -0,0 +1,447 @@ +/** + * @module kadence/quasar + */ + +'use strict'; + +const assert = require('assert'); +const merge = require('merge'); +const async = require('async'); +const { knuthShuffle } = require('knuth-shuffle'); +const uuid = require('uuid'); +const constants = require('./constants'); +const utils = require('./utils'); +const BloomFilter = require('atbf'); +const LruCache = require('lru-cache'); + + +/** + * Implements the handlers for Quasar message types + */ +class QuasarRules { + + /** + * @constructor + * @param {module:kadence/quasar~QuasarPlugin} quasar + */ + constructor(quasar) { + this.quasar = quasar; + } + + /** + * Upon receipt of a PUBLISH message, we validate it, then check if we or + * our neighbors are subscribed. If we are subscribed, we execute our + * handler. If our neighbors are subscribed, we relay the publication to + * ALPHA random of the closest K. If our neighbors are not subscribed, we + * relay the publication to a random contact + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + publish(request, response, next) { + /* eslint max-statements: [2, 18] */ + let { ttl, topic, uuid, contents } = request.params; + let neighbors = [...this.quasar.node.router.getClosestContactsToKey( + this.quasar.node.identity, + constants.K + ).entries()]; + + if (this.quasar.cached.get(uuid)) { + return next(new Error('Message previously routed')); + } + + if (ttl > constants.MAX_RELAY_HOPS || ttl < 0) { + return next(new Error('Message includes invalid TTL')); + } + + neighbors = knuthShuffle(neighbors.filter(([nodeId]) => { + return request.params.publishers.indexOf(nodeId) === -1; + })).splice(0, constants.ALPHA); + + request.params.publishers.push(this.quasar.node.identity.toString('hex')); + this.quasar.cached.set(uuid, Date.now()); + + if (this.quasar.isSubscribedTo(topic)) { + this.quasar.groups.get(topic)(contents, topic); + + async.each(neighbors, (contact, done) => { + this._relayPublication(request, contact, done); + }); + return response.send([]); + } + + if (ttl - 1 === 0) { + return response.send([]); + } + + async.each(neighbors, (contact, done) => { + this.quasar.pullFilterFrom(contact, (err, filter) => { + if (err) { + return done(); + } + + if (!QuasarRules.shouldRelayPublication(request, filter)) { + contact = this.quasar._getRandomContact(); + } + + this._relayPublication(request, contact, done); + }); + }); + response.send([]); + } + + /** + * Upon receipt of a SUBSCRIBE message, we simply respond with a serialized + * version of our attenuated bloom filter + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + */ + subscribe(request, response) { + response.send(this.quasar.filter.toHexArray()); + } + + /** + * Upon receipt of an UPDATE message we merge the delivered attenuated bloom + * filter with our own + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + update(request, response, next) { + if (!Array.isArray(request.params)) { + return next(new Error('Invalid bloom filters supplied')); + } + + try { + request.params.forEach(str => assert(utils.isHexaString(str), + 'Invalid hex string')); + this.quasar.filter.merge(BloomFilter.from(request.params)); + } catch (err) { + return next(err); + } + + response.send([]); + } + + /** + * Returns a boolean indicating if we should relay the message to the contact + * @param {AbstractNode~request} request + * @param {array} attenuatedBloomFilter - List of topic bloom filters + */ + static shouldRelayPublication(request, filter) { + let negated = true; + + filter.forEach((level) => { + if (level.has(request.params.topic)) { + negated = false; + } + }); + + request.params.publishers.forEach((pub) => { + filter.forEach((level) => { + if (level.has(pub)) { + negated = true; + } + }); + }); + + return !negated; + } + + /** + * Takes a request object for a publication and relays it to the supplied + * contact + * @private + */ + _relayPublication(request, contact, callback) { + this.quasar.node.send( + request.method, + merge({}, request.params, { ttl: request.params.ttl - 1 }), + contact, + callback + ); + } + +} + + +/** + * Implements the primary interface for the publish-subscribe system + * and decorates the given node object with it's public methods + */ +class QuasarPlugin { + + static get PUBLISH_METHOD() { + return 'PUBLISH'; + } + + static get SUBSCRIBE_METHOD() { + return 'SUBSCRIBE'; + } + + static get UPDATE_METHOD() { + return 'UPDATE'; + } + + /** + * @constructor + * @param {KademliaNode} node + */ + constructor(node) { + const handlers = new QuasarRules(this); + + this.cached = new LruCache(constants.LRU_CACHE_SIZE) + this.groups = new Map(); + this.filter = new BloomFilter({ + filterDepth: constants.FILTER_DEPTH, + bitfieldSize: constants.B + }); + this._lastUpdate = 0; + + this.node = node; + this.node.quasarSubscribe = this.quasarSubscribe.bind(this); + this.node.quasarPublish = this.quasarPublish.bind(this); + + this.node.use(QuasarPlugin.UPDATE_METHOD, handlers.update.bind(handlers)); + this.node.use(QuasarPlugin.PUBLISH_METHOD, + handlers.publish.bind(handlers)); + this.node.use(QuasarPlugin.SUBSCRIBE_METHOD, + handlers.subscribe.bind(handlers)); + + this.filter[0].add(this.node.identity.toString('hex')); + } + + /** + * Returns our ALPHA closest neighbors + * @property {Bucket~contact[]} neighbors + */ + get neighbors() { + return [...this.node.router.getClosestContactsToKey( + this.node.identity.toString('hex'), + constants.ALPHA + ).entries()]; + } + + /** + * Publishes the content to the network by selecting ALPHA contacts closest + * to the node identity (or the supplied routing key). Errors if message is + * unable to be delivered to any contacts. Tries to deliver to ALPHA contacts + * until exhausted. + * @param {string} topic - Identifier for subscribers + * @param {object} contents - Arbitrary publication payload + * @param {object} [options] + * @param {string} [options.routingKey] - Publish to neighbors close to this + * key instead of our own identity + * @param {QuasarPlugin~quasarPublishCallback} [callback] + */ + quasarPublish(topic, contents, options = {}, callback = () => null) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + const publicationId = uuid.v4(); + const neighbors = [...this.node.router.getClosestContactsToKey( + options.routingKey || this.node.identity.toString('hex'), + this.node.router.size + ).entries()]; + + let deliveries = []; + + async.until(() => { + return deliveries.length === constants.ALPHA || !neighbors.length; + }, done => { + const candidates = []; + + for (let i = 0; i < constants.ALPHA - deliveries.length; i++) { + candidates.push(neighbors.shift()); + } + + async.each(candidates, (contact, next) => { + this.node.send(QuasarPlugin.PUBLISH_METHOD, { + uuid: publicationId, + topic, + contents, + publishers: [this.node.identity.toString('hex')], + ttl: constants.MAX_RELAY_HOPS + }, contact, err => { + if (err) { + this.node.logger.warn(err.message); + } else { + deliveries.push(contact); + } + + next(); + }); + }, done); + }, err => { + if (!err && deliveries.length === 0) { + err = new Error('Failed to deliver any publication messages'); + } + + callback(err, deliveries); + }); + } + /** + * @callback QuasarPlugin~quasarPublishCallback + * @param {error|null} err + * @param {Bucket~contact[]} deliveries + */ + + /** + * Publishes the content to the network + * @param {string|string[]} topics - Identifier for subscribers + * @param {QuasarPlugin~quasarSubscribeHandler} handler + */ + quasarSubscribe(topics, handler) { + const self = this; + + if (Array.isArray(topics)) { + topics.forEach((topic) => addTopicToFilter(topic)); + } else { + addTopicToFilter(topics); + } + + function addTopicToFilter(topic) { + self.filter[0].add(topic); + self.groups.set(topic, handler); + } + + this.pullFilters(() => this.pushFilters()); + } + /** + * @callback QuasarPlugin~quasarSubscribeHandler + * @param {object} publicationContent + */ + + /** + * Requests neighbor bloom filters and merges with our records + * @param {function} [callback] + */ + pullFilters(callback = () => null) { + const now = Date.now(); + + if (this._lastUpdate > now - constants.SOFT_STATE_TIMEOUT) { + return callback(); + } else { + this._lastUpdate = now; + } + + async.each(this.neighbors, (contact, done) => { + this.pullFilterFrom(contact, (err, filter) => { + if (err) { + this.node.logger.warn('failed to pull filter from %s, reason: %s', + contact[0], err.message); + } else { + this.filter.merge(filter); + } + + done(err); + }); + }, callback); + } + + /** + * Requests the attenuated bloom filter from the supplied contact + * @param {Bucket~contact} contact + * @param {function} callback + */ + pullFilterFrom(contact, callback) { + const method = QuasarPlugin.SUBSCRIBE_METHOD; + + this.node.send(method, [], contact, (err, result) => { + if (err) { + return callback(err); + } + + try { + result.forEach(str => assert(utils.isHexaString(str), + 'Invalid hex string')); + return callback(null, BloomFilter.from(result)); + } catch (err) { + return callback(err); + } + }); + } + + /** + * Notifies neighbors that our subscriptions have changed + * @param {function} [callback] + */ + pushFilters(callback = () => null) { + const now = Date.now(); + + if (this._lastUpdate > now - constants.SOFT_STATE_TIMEOUT) { + return callback(); + } else { + this._lastUpdate = now; + } + + async.each(this.neighbors, (contact, done) => { + this.pushFilterTo(contact, done); + }, callback); + } + + /** + * Sends our attenuated bloom filter to the supplied contact + * @param {Bucket~contact} contact + * @param {function} callback + */ + pushFilterTo(contact, callback) { + this.node.send(QuasarPlugin.UPDATE_METHOD, this.filter.toHexArray(), + contact, callback); + } + + /** + * Check if we are subscribed to the topic + * @param {string} topic - Topic to check subscription + * @returns {boolean} + */ + isSubscribedTo(topic) { + return this.filter[0].has(topic) && this.groups.has(topic); + } + + /** + * Check if our neighbors are subscribed to the topic + * @param {string} topic - Topic to check subscription + * @returns {boolean} + */ + hasNeighborSubscribedTo(topic) { + let index = 1; + + while (this.filter[index]) { + if (this.filter[index].has(topic)) { + return true; + } else { + index++; + } + } + + return false; + } + + /** + * Returns a random contact from the routing table + * @private + */ + _getRandomContact() { + return knuthShuffle([...this.node.router.getClosestContactsToKey( + this.node.identity.toString('hex'), + this.node.router.size, + true + ).entries()]).shift(); + } + +} + +/** + * Registers a {@link module:kadence/quasar~QuasarPlugin} with a {@link KademliaNode} + */ +module.exports = function() { + return function(node) { + return new QuasarPlugin(node); + }; +}; + +module.exports.QuasarPlugin = QuasarPlugin; +module.exports.QuasarRules = QuasarRules; diff --git a/lib/plugin-rolodex.js b/lib/plugin-rolodex.js new file mode 100644 index 0000000..c7385e6 --- /dev/null +++ b/lib/plugin-rolodex.js @@ -0,0 +1,222 @@ +/** + * @module kadence/rolodex + */ + +'use strict'; + +const fs = require('fs'); +const utils = require('./utils'); +const { EventEmitter } = require('events'); + + +/** + * Keeps track of seen contacts in a compact file so they can be used as + * bootstrap nodes + */ +class RolodexPlugin extends EventEmitter { + + static get EXTERNAL_PREFIX() { + return 'external'; + } + + static get INTERNAL_PREFIX() { + return 'internal'; + } + + /** + * @constructor + * @param {KademliaNode} node + * @param {string} peerCacheFilePath - Path to file to use for storing peers + */ + constructor(node, peerCacheFilePath) { + super(); + + this._peerCacheFilePath = peerCacheFilePath; + this._cache = {}; + this.node = node; + + // When a contact is added to the routing table, cache it + this.node.router.events.on('add', identity => { + this.node.logger.debug(`updating cached peer profile ${identity}`); + const contact = this.node.router.getContactByNodeId(identity); + if (contact) { + contact.timestamp = Date.now(); + this.setExternalPeerInfo(identity, contact); + } + }); + + // When a contact is dropped from the routing table, remove it from cache + this.node.router.events.on('remove', identity => { + this.node.logger.debug(`dropping cached peer profile ${identity}`); + delete this._cache[`${RolodexPlugin.EXTERNAL_PREFIX}:${identity}`]; + delete this._cache[`${RolodexPlugin.INTERNAL_PREFIX}:${identity}`]; + }); + + this._sync(); + } + + /** + * @private + */ + _sync() { + const _syncRecursive = () => { + setTimeout(() => { + this._syncToFile().then(() => { + _syncRecursive(); + }, (err) => { + this.node.logger.error(`failed to write peer cache, ${err.message}`); + }); + }, 60 * 1000); + }; + + this._syncFromFile().then(() => { + _syncRecursive(); + }, (err) => { + this.node.logger.error(`failed to read peer cache, ${err.message}`); + _syncRecursive(); + }); + } + + /** + * @private + */ + _syncToFile() { + return new Promise((resolve, reject) => { + if (!this._peerCacheFilePath) { + return resolve(); + } + + fs.writeFile( + this._peerCacheFilePath, + JSON.stringify(this._cache), + (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + } + ); + }); + } + + /** + * @private + */ + _syncFromFile() { + return new Promise((resolve, reject) => { + if (!this._peerCacheFilePath) { + return resolve(); + } + + fs.readFile(this._peerCacheFilePath, (err, data) => { + if (err) { + return reject(err); + } + + try { + this._cache = JSON.parse(data.toString()); + } catch (err) { + return reject(err); + } + + resolve(); + }); + }); + } + + /** + * Returns a list of bootstrap nodes from local profiles + * @returns {string[]} urls + */ + getBootstrapCandidates() { + const candidates = []; + return new Promise(resolve => { + for (let key in this._cache) { + const [prefix, identity] = key.split(':'); + + /* istanbul ignore else */ + if (prefix === RolodexPlugin.EXTERNAL_PREFIX) { + candidates.push([identity, this._cache[key]]); + } + } + + resolve(candidates.sort((a, b) => b[1].timestamp - a[1].timestamp) + .map(utils.getContactURL)); + }); + } + + /** + * Returns the external peer data for the given identity + * @param {string} identity - Identity key for the peer + * @returns {object} + */ + getExternalPeerInfo(identity) { + return new Promise((resolve, reject) => { + const data = this._cache[`${RolodexPlugin.EXTERNAL_PREFIX}:${identity}`]; + /* istanbul ignore if */ + if (!data) { + reject(new Error('Peer not found')); + } else { + resolve(data); + } + }); + } + + /** + * Returns the internal peer data for the given identity + * @param {string} identity - Identity key for the peer + * @returns {object} + */ + getInternalPeerInfo(identity) { + return new Promise((resolve, reject) => { + const data = this._cache[`${RolodexPlugin.INTERNAL_PREFIX}:${identity}`]; + /* istanbul ignore if */ + if (!data) { + reject(new Error('Peer not found')); + } else { + resolve(data); + } + }); + } + + /** + * Returns the external peer data for the given identity + * @param {string} identity - Identity key for the peer + * @param {object} data - Peer's external contact information + * @returns {object} + */ + setExternalPeerInfo(identity, data) { + return new Promise((resolve) => { + this._cache[`${RolodexPlugin.EXTERNAL_PREFIX}:${identity}`] = data; + resolve(data); + }); + } + + /** + * Returns the internal peer data for the given identity + * @param {string} identity - Identity key for the peer + * @param {object} data - Our own internal peer information + * @returns {object} + */ + setInternalPeerInfo(identity, data) { + return new Promise((resolve) => { + this._cache[`${RolodexPlugin.INTERNAL_PREFIX}:${identity}`] = data; + resolve(data); + }); + } + +} + +/** + * Registers a {@link module:kadence/rolodex~RolodexPlugin} with a + * {@link KademliaNode} + * @param {string} peerCacheFilePath - Path to file to use for storing peers + */ +module.exports = function(peerCacheFilePath) { + return function(node) { + return new RolodexPlugin(node, peerCacheFilePath); + } +}; + +module.exports.RolodexPlugin = RolodexPlugin; diff --git a/lib/plugin-spartacus.js b/lib/plugin-spartacus.js new file mode 100644 index 0000000..35012c6 --- /dev/null +++ b/lib/plugin-spartacus.js @@ -0,0 +1,220 @@ +/** + * @module kadence/spartacus + */ + +'use strict'; + +const merge = require('merge'); +const assert = require('assert'); +const secp256k1 = require('secp256k1'); +const utils = require('./utils'); +const jsonrpc = require('jsonrpc-lite'); +const { Transform } = require('stream'); + + +/** + * Implements the spartacus decorations to the node object + */ +class SpartacusPlugin { + + static get DEFAULTS() { + return { + checkPublicKeyHash: true + }; + } + + /** + * Creates the plugin instance given a node and optional identity + * @constructor + * @param {KademliaNode} node + * @param {buffer} [privateKey] - SECP256K1 private key + * @param {object} [options={}] + * @param {boolean} [options.checkPublicKeyHash=true] + */ + constructor(node, priv, opts) { + priv = priv || utils.generatePrivateKey(); + + this.opts = merge(SpartacusPlugin.DEFAULTS, opts); + this.privateKey = priv; + this.publicKey = secp256k1.publicKeyCreate(this.privateKey); + this.identity = utils.toPublicKeyHash(this.publicKey); + this._validatedContacts = new Map(); + this._pendingValidators = new Map(); + + node.contact.pubkey = this.publicKey.toString('hex'); + node.identity = node.router.identity = this.identity; + + node.rpc.serializer.append(() => new Transform({ + transform: this.serialize.bind(this), + objectMode: true + })); + node.rpc.deserializer.prepend(() => new Transform({ + transform: this.deserialize.bind(this), + objectMode: true + })); + node.use((req, res, next) => this.validate(node, req, res, next)); + this.setValidationPeriod(); + } + + /** + * Sets the validation period for nodes + * @param {number} period - Milliseconds to honor a proven contact response + */ + setValidationPeriod(n = 10800000) { + this._validationPeriod = n; + } + + /** + * Checks if the sender is addressable at the claimed contact information + * and cross checks signatures between the original sender and the node + * addressed. This is intended to prevent reflection attacks and general + * DDoS via spam. + * @param {KademliaNode} node + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + validate(node, req, res, next) { + const period = this._validationPeriod; + const record = this._validatedContacts.get(req.contact[0]); + const validated = record && record.validated; + const fresh = validated && ((Date.now() - record.timestamp) < period); + + if (this._pendingValidators.get(req.contact[0])) { + return next(); // NB: Let's not get into an infinte validation loop + } + + if (validated && fresh) { + return next(); + } + + this._pendingValidators.set(req.contact[0], req.contact[1]); + node.ping(req.contact, (err) => { + this._pendingValidators.delete(req.contact[0]); + + if (err) { + return this._validatedContacts.set(req.contact[0], { + validated: false, + timestamp: Date.now() + }); + } + + this._validatedContacts.set(req.contact[0], { + validated: true, + timestamp: Date.now() + }); + next(); + }); + } + + /** + * Processes with JsonRpcSerializer then signs the result and appends an + * additional payload containing signature+identity information + * @implements {Messenger~serializer} + */ + serialize(data, encoding, callback) { + let [id, buffer, target] = data; + let payload = jsonrpc.parse(buffer.toString('utf8')).map((obj) => { + return obj.payload; + }); + let { signature, recovery } = secp256k1.sign( + utils._sha256(buffer), + this.privateKey + ); + let authenticate = jsonrpc.notification('AUTHENTICATE', [ + Buffer.concat([Buffer.from([recovery]), signature]).toString('base64'), + this.publicKey.toString('hex') + ]); + + payload.push(authenticate); + callback(null, [ + id, + Buffer.from(JSON.stringify(payload), 'utf8'), + target + ]); + } + + /** + * Parses and verifies the signature payload, then passes through to the + * JsonRpcDeserializer if successful + * @implements {Messenger~deserializer} + */ + deserialize(buffer, encoding, callback) { + /* eslint max-statements: [2, 30] */ + /* eslint complexity: [2, 12] */ + let payload = jsonrpc.parse(buffer.toString('utf8')) + + try { + payload = payload.map(obj => { + assert(obj.type !== 'invalid'); + return obj.payload; + }); + } catch (err) { + return callback(new Error('Failed to parse received payload')); + } + + let [, identify] = payload; + let authenticate = payload.filter(m => m.method === 'AUTHENTICATE').pop(); + + if (typeof authenticate === 'undefined') { + return callback(new Error('Missing authentication payload in message')); + } + + let identity = Buffer.from(identify.params[0], 'hex'); + let [signature, publicKey] = authenticate.params; + + let signedPayload = []; + + for (let i = 0; i < payload.length; i++) { + if (payload[i].method === 'AUTHENTICATE') { + break; + } else { + signedPayload.push(payload[i]); + } + } + + signedPayload = utils._sha256( + Buffer.from(JSON.stringify(signedPayload), 'utf8') + ); + + let publicKeyHash = utils.toPublicKeyHash(Buffer.from(publicKey, 'hex')); + let pendingValid = this._pendingValidators.get( + identity.toString('hex') + ); + + if (pendingValid && pendingValid.pubkey !== publicKey) { + return callback(new Error('Failed pending contact validation')); + } + + if (this.opts.checkPublicKeyHash && publicKeyHash.compare(identity) !== 0) { + return callback(new Error('Identity does not match public key')); + } + + try { + assert.ok(secp256k1.verify( + signedPayload, + Buffer.from(signature, 'base64').slice(1), + Buffer.from(publicKey, 'hex') + )); + } catch (err) { + return callback(new Error('Message includes invalid signature')); + } + + callback(null, buffer); + } + +} + +/** + * Registers a {@link module:kadence/spartacus~SpartacusPlugin} with a + * {@link KademliaNode} + * @param {string} priv - Private key + * @param {object} opts - Plugin options + */ +module.exports = function(priv, opts) { + return function(node) { + return new SpartacusPlugin(node, priv, opts); + }; +}; + +module.exports.SpartacusPlugin = SpartacusPlugin; diff --git a/lib/plugin-traverse.js b/lib/plugin-traverse.js new file mode 100644 index 0000000..0d3f0bd --- /dev/null +++ b/lib/plugin-traverse.js @@ -0,0 +1,364 @@ +/** + * @module kadence/traverse + */ + +'use strict'; + +const { createLogger } = require('bunyan'); +const ip = require('ip'); +const merge = require('merge'); +const async = require('async'); +const { get_gateway_ip: getGatewayIp } = require('network'); +const natpmp = require('nat-pmp'); +const natupnp = require('nat-upnp'); +const url = require('url'); +const diglet = require('@tacticalchihuahua/diglet'); + + +/** + * Establishes a series of NAT traversal strategies to execute before + * {@link AbstractNode#listen} + */ +class TraversePlugin { + + static get TEST_INTERVAL() { + return 600000; + } + + /** + * @constructor + * @param {KademliaNode} node + * @param {module:kadence/traverse~TraverseStrategy[]} strategies + */ + constructor(node, strategies) { + this.node = node; + this.strategies = strategies; + this._originalContact = merge({}, node.contact); + + this._wrapNodeListen(); + } + + /** + * @private + * @param {function} callback + */ + _execTraversalStrategies(callback) { + async.detectSeries(this.strategies, (strategy, test) => { + this.node.logger.info( + `attempting nat traversal strategy ${strategy.constructor.name}` + ); + this.node.contact = this._originalContact; + strategy.exec(this.node, (err) => { + if (err) { + this.node.logger.warn(err.message); + test(null, false); + } else { + this._testIfReachable(test); + } + }); + }, callback); + } + + /** + * @private + */ + _startTestInterval() { + clearInterval(this._testInterval); + + this._testInterval = setInterval(() => { + this._testIfReachable((err, isReachable) => { + /* istanbul ignore else */ + if (!isReachable) { + this.node.logger.warn('no longer reachable, retrying traversal'); + this._execTraversalStrategies(() => null); + } + }); + }, TraversePlugin.TEST_INTERVAL); + } + + /** + * @private + */ + _testIfReachable(callback) { + if (!ip.isPublic(this.node.contact.hostname)) { + this.node.logger.warn('traversal strategy failed, not reachable'); + return callback(null, false); + } + + callback(null, true); + } + + /** + * @private + */ + _wrapNodeListen() { + const self = this; + const listen = this.node.listen.bind(this.node); + + this.node.listen = function() { + let args = [...arguments]; + let listenCallback = () => null; + + if (typeof args[args.length - 1] === 'function') { + listenCallback = args.pop(); + } + + listen(...args, () => { + self._execTraversalStrategies((err, strategy) => { + if (err) { + self.node.logger.error('traversal errored %s', err.message); + } else if (!strategy) { + self.node.logger.warn('traversal failed - may not be reachable'); + } else { + self.node.logger.info('traversal succeeded - you are reachable'); + } + + self._startTestInterval(); + listenCallback(); + }); + }); + }; + } + +} + +/** + * Uses NAT-PMP to attempt port forward on gateway device + * @extends {module:kadence/traverse~TraverseStrategy} + */ +class NATPMPStrategy { + + static get DEFAULTS() { + return { + publicPort: 0, + mappingTtl: 0, + timeout: 10000 + }; + } + + /** + * @constructor + * @param {object} [options] + * @param {number} [options.publicPort=contact.port] - Port number to map + * @param {number} [options.mappingTtl=0] - TTL for port mapping on router + */ + constructor(options) { + this.options = merge(NATPMPStrategy.DEFAULTS, options); + } + + /** + * @param {KademliaNode} node + * @param {function} callback + */ + exec(node, callback) { + async.waterfall([ + (next) => getGatewayIp(next), + (gateway, next) => { + const timeout = setTimeout(() => { + next(new Error('NAT-PMP traversal timed out')); + }, this.options.timeout); + this.client = natpmp.connect(gateway); + this.client.portMapping({ + public: this.options.publicPort || node.contact.port, + private: node.contact.port, + ttl: this.options.mappingTtl + }, err => { + clearTimeout(timeout); + next(err); + }); + }, + (next) => this.client.externalIp(next) + ], (err, info) => { + if (err) { + return callback(err); + } + + node.contact.port = this.options.publicPort; + node.contact.hostname = info.ip.join('.'); + + callback(null); + }); + } + +} + +/** + * Uses UPnP to attempt port forward on gateway device + * @extends {module:kadence/traverse~TraverseStrategy} + */ +class UPNPStrategy { + + static get DEFAULTS() { + return { + publicPort: 0, + mappingTtl: 0 + }; + } + + /** + * @constructor + * @param {object} [options] + * @param {number} [options.publicPort=contact.port] - Port number to map + * @param {number} [options.mappingTtl=0] - TTL for mapping on router + */ + constructor(options) { + this.client = natupnp.createClient(); + this.options = merge(UPNPStrategy.DEFAULTS, options); + } + + /** + * @param {KademliaNode} node + * @param {function} callback + */ + exec(node, callback) { + async.waterfall([ + (next) => { + this.client.portMapping({ + public: this.options.publicPort || node.contact.port, + private: node.contact.port, + ttl: this.options.mappingTtl + }, err => next(err)); + }, + (next) => this.client.externalIp(next) + ], (err, ip) => { + if (err) { + return callback(err); + } + + node.contact.port = this.options.publicPort; + node.contact.hostname = ip; + + callback(null); + }); + } + +} + +/** + * Uses a secure reverse HTTPS tunnel via the Diglet package to traverse NAT. + * This requires a running Diglet server on the internet. By default, this + * plugin will use a test server operated by bookchin, but this may not be + * reliable or available. It is highly recommended to deploy your own Diglet + * server and configure your nodes to use them instead. + * There is {@link https://gitlab.com/bookchin/diglet detailed documentation} + * on deploying a Diglet server at the project page. + * @extends {module:kadence/traverse~TraverseStrategy} + */ +class ReverseTunnelStrategy { + + static get DEFAULTS() { + return { + remoteAddress: 'tun.tacticalchihuahua.lol', + remotePort: 8443, + secureLocalConnection: false, + verboseLogging: false + }; + } + + /** + * @constructor + * @param {object} [options] + * @param {string} [options.remoteAddress=tunnel.bookch.in] - Diglet server address + * @param {number} [options.remotePort=8443] - Diglet server port + * @param {buffer} [options.privateKey] - SECP256K1 private key if using spartacus + * @param {boolean} [options.secureLocalConnection=false] - Set to true if using {@link HTTPSTransport} + * @param {boolean} [options.verboseLogging=false] - Useful for debugging + */ + constructor(options) { + this.options = merge(ReverseTunnelStrategy.DEFAULTS, options); + } + + /** + * @param {KademliaNode} node + * @param {function} callback + */ + exec(node, callback) { + const opts = { + localAddress: '127.0.0.1', + localPort: node.contact.port, + remoteAddress: this.options.remoteAddress, + remotePort: this.options.remotePort, + logger: this.options.verboseLogging + ? node.logger + : createLogger({ name: 'kadence', level: 'warn' }), + secureLocalConnection: this.options.secureLocalConnection + }; + + if (this.options.privateKey) { + opts.privateKey = this.options.privateKey; + } + + this.tunnel = new diglet.Tunnel(opts); + + this.tunnel.once('connected', () => { + node.contact.hostname = url.parse(this.tunnel.url).hostname; + node.contact.port = 443; + node.contact.protocol = 'https:'; + + this.tunnel.removeListener('disconnected', callback); + callback() + }); + + this.tunnel.once('disconnected', callback); + this.tunnel.open(); + } + +} + +/** + * @class + */ +class TraverseStrategy { + + constructor() {} + + /** + * @param {KademliaNode} node + * @param {function} callback - Called on travere complete or failed + */ + exec(node, callback) { + callback(new Error('Not implemented')); + } + +} + +/** + * Registers a {@link module:kadence/traverse~TraversePlugin} with an + * {@link AbstractNode}. Strategies are attempted in the order they are + * defined. + * @param {module:kadence/traverse~TraverseStrategy[]} strategies + * @example Proper Configuration + * const node = new kadence.KademliaNode(node_options); + * const keys = node.plugin(kadence.spartacus(key_options)); + * + * node.plugin(kadence.traverse([ + * new kadence.traverse.UPNPStrategy({ + * publicPort: 8080, + * mappingTtl: 0 + * }), + * new kadence.traverse.NATPMPStrategy({ + * publicPort: 8080, + * mappingTtl: 0 + * }), + * new kadence.traverse.ReverseTunnelStrategy({ + * remoteAddress: 'my.diglet.server', + * remotePort: 8443, + * privateKey: keys.privateKey, + * secureLocalConnection: false, + * verboseLogging: false + * }) + * ])); + * + * node.listen(node.contact.port); + */ +module.exports = function(strategies) { + return function(node) { + return new module.exports.TraversePlugin(node, strategies); + }; +}; + +module.exports.ReverseTunnelStrategy = ReverseTunnelStrategy; +module.exports.UPNPStrategy = UPNPStrategy; +module.exports.NATPMPStrategy = NATPMPStrategy; +module.exports.TraverseStrategy = TraverseStrategy; +module.exports.TraversePlugin = TraversePlugin; diff --git a/lib/plugin-trust.js b/lib/plugin-trust.js new file mode 100644 index 0000000..6f9b7ad --- /dev/null +++ b/lib/plugin-trust.js @@ -0,0 +1,196 @@ +/** + * @module kadence/trust + */ + +'use strict'; + +const assert = require('assert'); +const utils = require('./utils'); + + +/** + * Handles user-defined rules for allowing and preventing the processing of + * messages from given identities + */ +class TrustPlugin { + + /** + * @typedef {object} module:kadence/trust~TrustPlugin~policy + * @property {string|buffer} identity - Node identity key + * @property {string[]} methods - Methods, wildcard (*) supported for all + */ + + /** + * Validates the trust policy format + * @private + */ + static validatePolicy(policy) { + assert(typeof policy === 'object', 'Invalid policy object'); + assert( + utils.keyBufferIsValid(policy.identity) || + utils.keyStringIsValid(policy.identity) || policy.identity === '*', + 'Invalid policy identity' + ); + assert(Array.isArray(policy.methods) && policy.methods.length, + 'No policy methods defined'); + } + + /** + * Mode flag passed to {@link TrustPlugin} to place into blacklist mode + * @static + */ + static get MODE_BLACKLIST() { + return 0x000; + } + + /** + * Mode flag passed to {@link TrustPlugin} to place into whitelist mode + * @static + */ + static get MODE_WHITELIST() { + return 0xfff; + } + + /** + * @constructor + * @param {module:kadence/trust~TrustPlugin~policy[]} policies + * @param {number} [mode=TrustPlugin.MODE_BLACKLIST] - Blacklist or whitelist + */ + constructor(node, policies = [], mode = TrustPlugin.MODE_BLACKLIST) { + assert([ + TrustPlugin.MODE_BLACKLIST, + TrustPlugin.MODE_WHITELIST + ].includes(mode), `Invalid trust policy mode "${mode}"`); + + this.mode = mode; + this.policies = new Map(); + this.node = node; + + policies.forEach(policy => this.addTrustPolicy(policy)); + + // NB: Automatically trust ourselves if this is a whitelist + if (this.mode === TrustPlugin.MODE_WHITELIST) { + this.addTrustPolicy({ + identity: node.identity.toString('hex'), + methods: ['*'] + }); + } + + const send = this.node.send.bind(this.node); + + this.node.use(this._checkIncoming.bind(this)); + this.node.send = (method, params, contact, callback) => { + this._checkOutgoing(method, contact, err => { + if (err) { + return callback(err); + } + send(method, params, contact, callback); + }); + }; + } + + /** + * Checks the incoming message + * @private + */ + _checkIncoming(request, response, callback) { + const [identity] = request.contact; + const method = request.method; + const policy = this.getTrustPolicy(identity); + + this._checkPolicy(identity, method, policy, callback); + } + + /** + * Checks the outgoing message + * @private + */ + _checkOutgoing(method, contact, callback) { + const [identity] = contact; + const policy = this.getTrustPolicy(identity); + + this._checkPolicy(identity, method, policy, callback); + } + + /** + * Checks policy against identity and method + * @private + */ + _checkPolicy(identity, method, policy, next) { + /* eslint complexity: [2, 10] */ + switch (this.mode) { + case TrustPlugin.MODE_BLACKLIST: + if (!policy) { + next(); + } else if (policy.includes('*') || policy.includes(method)) { + next(new Error(`Refusing to handle ${method} message to/from ` + + `${identity} due to trust policy`)); + } else { + next(); + } + break; + case TrustPlugin.MODE_WHITELIST: + if (!policy) { + next(new Error(`Refusing to handle ${method} message to/from ` + + `${identity} due to trust policy`)); + } else if (policy.includes('*') || policy.includes(method)) { + next(); + } else { + next(new Error(`Refusing to handle ${method} message to/from ` + + `${identity} due to trust policy`)); + } + break; + default: + /* istanbul ignore next */ + throw new Error('Failed to determine trust mode'); + } + } + + /** + * Adds a new trust policy + * @param {module:kadence/trust~TrustPlugin~policy} policy + * @returns {TrustPlugin} + */ + addTrustPolicy(policy) { + TrustPlugin.validatePolicy(policy); + this.policies.set(policy.identity.toString('hex'), policy.methods); + return this; + } + + /** + * Returns the trust policy for the given identity + * @param {string|buffer} identity - Identity key for the policy + * @returns {module:kadence/trust~TrustPlugin~policy|null} + */ + getTrustPolicy(identity) { + return this.policies.get(identity.toString('hex')) || + this.policies.get('*'); + } + + /** + * Removes an existing trust policy + * @param {string|buffer} identity - Trust policy to remove + * @returns {TrustPlugin} + */ + removeTrustPolicy(identity) { + this.policies.delete(identity.toString('hex')); + return this; + } + +} + +/** + * Registers a {@link module:kadence/trust~TrustPlugin} with a + * {@link KademliaNode} + * @param {module:kadence/trust~TrustPlugin~policy[]} policies + * @param {number} [mode=TrustPlugin.MODE_BLACKLIST] - Blacklist or whitelist + */ +module.exports = function(policies, mode) { + return function(node) { + return new TrustPlugin(node, policies, mode); + } +}; + +module.exports.TrustPlugin = TrustPlugin; +module.exports.MODE_BLACKLIST = TrustPlugin.MODE_BLACKLIST; +module.exports.MODE_WHITELIST = TrustPlugin.MODE_WHITELIST; diff --git a/lib/routing-table.js b/lib/routing-table.js new file mode 100644 index 0000000..9dd9825 --- /dev/null +++ b/lib/routing-table.js @@ -0,0 +1,158 @@ +'use strict'; + +const { EventEmitter } = require('events'); +const Bucket = require('./bucket'); +const utils = require('./utils'); +const constants = require('./constants'); + + +/** + * Represents a kademlia routing table + */ +class RoutingTable extends Map { + + /** + * Constructs a routing table + * @constructor + * @param {buffer} identity - Reference point for calculating distances + */ + constructor(identity) { + super(); + + this.identity = identity || utils.getRandomKeyBuffer(); + this.events = new EventEmitter(); + + for (let b = 0; b < constants.B; b++) { + this.set(b, new Bucket()); + } + } + + /** + * Returns the total contacts in the routing table + * @property {number} size + */ + get size() { + let contacts = 0; + this.forEach((bucket) => contacts += bucket.length); + return contacts; + } + + /** + * Returns the total buckets in the routing table + * @property {number} length + */ + get length() { + let buckets = 0; + this.forEach(() => buckets++); + return buckets; + } + + /** + * Returns the bucket index of the given node id + * @param {string|buffer} nodeId - Node identity to get index for + * @returns {number} + */ + indexOf(nodeId) { + return utils.getBucketIndex(this.identity, nodeId); + } + + /** + * Returns the contact object associated with the given node id + * @param {string|buffer} nodeId - Node identity of the contact + * @returns {Bucket~contact} + */ + getContactByNodeId(nodeId) { + nodeId = nodeId.toString('hex'); + + return this.get(this.indexOf(nodeId)).get(nodeId); + } + + /** + * Removes the contact from the routing table given a node id + * @param {string|buffer} nodeId - Node identity to remove + * @return {boolean} + */ + removeContactByNodeId(nodeId) { + nodeId = nodeId.toString('hex'); + + this.events.emit('remove', nodeId); + return this.get(this.indexOf(nodeId)).delete(nodeId); + } + + /** + * Adds the contact to the routing table in the proper bucket position, + * returning the [bucketIndex, bucket, contactIndex, contact]; if the + * returned contactIndex is -1, it indicates the bucket is full and the + * contact was not added; kademlia implementations should PING the contact + * at bucket.head to determine if it should be dropped before calling this + * method again + * @param {string|buffer} nodeId - Node identity to add + * @param {object} contact - contact information for peer + * @returns {array} + */ + addContactByNodeId(nodeId, contact) { + nodeId = nodeId.toString('hex'); + + const bucketIndex = this.indexOf(nodeId); + const bucket = this.get(bucketIndex); + const contactIndex = bucket.set(nodeId, contact); + + this.events.emit('add', nodeId); + return [bucketIndex, bucket, contactIndex, contact]; + } + + /** + * Returns the [index, bucket] of the occupied bucket with the lowest index + * @returns {Bucket} + */ + getClosestBucket() { + for (let [index, bucket] of this) { + if (index < constants.B - 1 && bucket.length === 0) { + continue; + } + return [index, bucket]; + } + } + + /** + * Returns a array of N contacts closest to the supplied key + * @param {string|buffer} key - Key to get buckets for + * @param {number} [n=20] - Number of results to return + * @param {boolean} [exclusive=false] - Exclude exact matches + * @returns {map} + */ + getClosestContactsToKey(key, n = constants.K, exclusive = false) { + const bucketIndex = this.indexOf(key); + const contactResults = new Map(); + + function _addNearestFromBucket(bucket) { + let entries = [...bucket.getClosestToKey(key, n, exclusive).entries()]; + + entries.splice(0, n - contactResults.size) + .forEach(([id, contact]) => { + /* istanbul ignore else */ + if (contactResults.size < n) { + contactResults.set(id, contact); + } + }); + } + + let ascIndex = bucketIndex; + let descIndex = bucketIndex; + + _addNearestFromBucket(this.get(bucketIndex)); + + while (contactResults.size < n && descIndex >= 0) { + _addNearestFromBucket(this.get(descIndex--)); + } + + while (contactResults.size < n && ascIndex < constants.B) { + _addNearestFromBucket(this.get(ascIndex++)); + } + + return contactResults; + } + +} + +module.exports = RoutingTable; diff --git a/lib/rules-errors.js b/lib/rules-errors.js new file mode 100644 index 0000000..aaa91b8 --- /dev/null +++ b/lib/rules-errors.js @@ -0,0 +1,47 @@ +'use strict'; + +/** + * @class + */ +class ErrorRules { + + /** + * Constructs a error rules instance in the context of a + * {@link AbstractNode} + * @constructor + * @param {AbstractNode} node + */ + constructor(node) { + this.node = node; + } + + /** + * Assumes if no error object exists, then there is simply no method defined + * @param {error|null} error + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + methodNotFound(err, request, response, next) { + if (err) { + return next(); + } + + response.error('Method not found', -32601); + } + + /** + * Formats the errors response according to the error object given + * @param {error|null} error + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + internalError(err, request, response, next) { + response.error(err.message, err.code || -32603); + next() + } + +} + +module.exports = ErrorRules; diff --git a/lib/rules-kademlia.js b/lib/rules-kademlia.js new file mode 100644 index 0000000..5e85911 --- /dev/null +++ b/lib/rules-kademlia.js @@ -0,0 +1,115 @@ +'use strict'; + +const assert = require('assert'); +const utils = require('./utils'); + + +/** + * Represent kademlia protocol handlers + */ +class KademliaRules { + + /** + * Constructs a kademlia rules instance in the context of a + * {@link KademliaNode} + * @constructor + * @param {KademliaNode} node + */ + constructor(node) { + this.node = node; + } + + /** + * This RPC involves one node sending a PING message to another, which + * presumably replies with a PONG. This has a two-fold effect: the + * recipient of the PING must update the bucket corresponding to the + * sender; and, if there is a reply, the sender must update the bucket + * appropriate to the recipient. + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + */ + ping(request, response) { + response.send([]); + } + + /** + * The sender of the STORE RPC provides a key and a block of data and + * requires that the recipient store the data and make it available for + * later retrieval by that key. + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + store(request, response, next) { + const [key, item] = request.params; + + try { + assert(typeof item === 'object', + 'Invalid storage item supplied'); + assert(typeof item.timestamp === 'number', + 'Invalid timestamp supplied'); + assert(utils.keyStringIsValid(item.publisher), + 'Invalid publisher identity supplied'); + assert(utils.keyStringIsValid(key), + 'Invalid item key supplied'); + assert(typeof item.value !== 'undefined', + 'Invalid item value supplied'); + } catch (err) { + return next(err); + } + + this.node.storage.put(key, item, { valueEncoding: 'json' }, (err) => { + if (err) { + return next(err); + } + + response.send([key, item]); // NB: Echo back what was stored + }); + } + + /** + * The FIND_NODE RPC includes a 160-bit key. The recipient of the RPC returns + * up to K contacts that it knows to be closest to the key. The recipient + * must return K contacts if at all possible. It may only return fewer than K + * if it is returning all of the contacts that it has knowledge of. + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + findNode(request, response, next) { + const [key] = request.params; + + if (!utils.keyStringIsValid(key)) { + return next(new Error('Invalid lookup key supplied')); + } + + response.send([...this.node.router.getClosestContactsToKey(key).entries()]); + } + + /** + * A FIND_VALUE RPC includes a B=160-bit key. If a corresponding value is + * present on the recipient, the associated data is returned. Otherwise the + * RPC is equivalent to a FIND_NODE and a set of K contacts is returned. + * @param {AbstractNode~request} request + * @param {AbstractNode~response} response + * @param {AbstractNode~next} next + */ + findValue(request, response, next) { + const [key] = request.params; + + if (!utils.keyStringIsValid(key)) { + return next(new Error('Invalid lookup key supplied')); + } + + this.node.storage.get(key, { valueEncoding: 'json' }, (err, item) => { + if (err) { + return this.findNode(request, response, next); + } + + response.send(item); + }); + } + +} + +module.exports = KademliaRules; diff --git a/lib/transport-http.js b/lib/transport-http.js new file mode 100644 index 0000000..f717d48 --- /dev/null +++ b/lib/transport-http.js @@ -0,0 +1,193 @@ +'use strict'; + +const http = require('http'); +const https = require('https'); +const { Duplex: DuplexStream } = require('stream'); +const merge = require('merge'); +const concat = require('concat-stream'); +const constants = require('./constants'); +const utils = require('./utils'); + + +/** + * Represents a transport adapter over HTTP + */ +class HTTPTransport extends DuplexStream { + + static get DEFAULTS() { + return { + allowLoopbackAddresses: true + }; + } + + /** + * Contructs a HTTP transport adapter + * @constructor + */ + constructor(options) { + super({ objectMode: true }); + + this._options = merge({}, HTTPTransport.DEFAULTS, options); + this._pending = new Map(); + this.server = this._createServer(this._options); + + this.server.on('error', (err) => this.emit('error', err)); + setInterval(() => this._timeoutPending(), constants.T_RESPONSETIMEOUT); + } + + /** + * Creates the HTTP server object + * @private + */ + _createServer() { + return http.createServer(); + } + + /** + * Returns a HTTP request object + * @private + */ + _createRequest(options) { + if (options.protocol === 'https:') { + return https.request(...arguments); + } + + return http.request(...arguments); + } + + /** + * Implements the readable interface + * @private + */ + _read() { + if (this.server.listeners('request').length) { + return; + } + + this.server.on('request', (req, res) => this._handle(req, res)); + } + + /** + * Every T_RESPONSETIMEOUT, we destroy any open sockets that are still + * waiting + * @private + */ + _timeoutPending() { + const now = Date.now(); + + this._pending.forEach(({ timestamp, response }, id) => { + let timeout = timestamp + constants.T_RESPONSETIMEOUT; + + if (now >= timeout) { + response.statusCode = 504; + response.end('Gateway Timeout'); + this._pending.delete(id); + } + }); + } + + /** + * Implements the writable interface + * @private + */ + _write([id, buffer, target], encoding, callback) { + let [, contact] = target; + + // NB: If responding to a received request... + if (this._pending.has(id)) { + this._pending.get(id).response.end(buffer); + this._pending.delete(id); + return callback(null); + } + + // NB: If originating an outbound request... + const reqopts = { + hostname: contact.hostname, + port: contact.port, + protocol: contact.protocol, + method: 'POST', + headers: { + 'x-kad-message-id': id + } + }; + + if (typeof contact.path === 'string') { + reqopts.path = contact.path; + } + + const request = this._createRequest(reqopts); + + request.on('response', (response) => { + response.on('error', (err) => this.emit('error', err)); + response.pipe(concat((buffer) => { + if (response.statusCode >= 400) { + let err = new Error(buffer.toString()); + err.dispose = id; + this.emit('error', err); + } else { + this.push(buffer); + } + })); + }); + + request.on('error', (err) => { + err.dispose = id; + this.emit('error', err); + }); + request.end(buffer); + callback(); + } + + /** + * Default request handler + * @private + */ + _handle(req, res) { + req.on('error', (err) => this.emit('error', err)); + res.on('error', (err) => this.emit('error', err)); + + if (!req.headers['x-kad-message-id']) { + res.statusCode = 400; + return res.end(); + } + + res.setHeader('X-Kad-Message-ID', req.headers['x-kad-message-id']); + res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*'); + res.setHeader('Access-Control-Allow-Methods', '*'); + res.setHeader('Access-Control-Allow-Headers', '*'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + + if (!['POST', 'OPTIONS'].includes(req.method)) { + res.statusCode = 405; + } + + if (req.method !== 'POST') { + return res.end(); + } + + req.pipe(concat((buffer) => { + this._pending.set(req.headers['x-kad-message-id'], { + timestamp: Date.now(), + response: res + }); + this.push(buffer); + })); + } + + /** + * @private + */ + _validate(contact) { + return utils.isValidContact(contact, this._options.allowLoopbackAddresses); + } + + /** + * Binds the server to the given address/port + */ + listen() { + this.server.listen(...arguments); + } + +} + +module.exports = HTTPTransport; diff --git a/lib/transport-https.js b/lib/transport-https.js new file mode 100644 index 0000000..1bf64d3 --- /dev/null +++ b/lib/transport-https.js @@ -0,0 +1,47 @@ +'use strict'; + +const HTTPTransport = require('./transport-http'); +const https = require('https'); +const merge = require('merge'); + +/** + * Extends the HTTP transport with SSL + */ +class HTTPSTransport extends HTTPTransport { + + static get DEFAULTS() { + return {}; + } + + /** + * Contructs a new HTTPS transport adapter + * @constructor + * @extends {HTTPTransport} + * @param {object} options + * @param {buffer} options.key - SSL private key buffer + * @param {buffer} options.cert - SSL certificate buffer + * @param {buffer[]} options.ca - List of certificate authority certificates + */ + constructor(options) { + super(merge({}, HTTPSTransport.DEFAULTS, options)); + } + + /** + * Constructs the HTTPS server + * @private + */ + _createServer() { + return https.createServer(...arguments); + } + + /** + * Constructs the HTTPS request + * @private + */ + _createRequest() { + return https.request(...arguments); + } + +} + +module.exports = HTTPSTransport; diff --git a/lib/transport-udp.js b/lib/transport-udp.js new file mode 100644 index 0000000..dc5c745 --- /dev/null +++ b/lib/transport-udp.js @@ -0,0 +1,79 @@ +'use strict'; + +const merge = require('merge'); +const { Duplex: DuplexStream } = require('stream'); +const dgram = require('dgram'); +const utils = require('./utils'); + + +/** + * Implements a UDP transport adapter + */ +class UDPTransport extends DuplexStream { + + static get DEFAULTS() { + return { + type: 'udp4', + reuseAddr: false, + allowLoopbackAddresses: true + }; + } + + /** + * Constructs a datagram socket interface + * @constructor + * @param {object} [socketOpts] - Passed to dgram.createSocket(options) + */ + constructor(options) { + super({ objectMode: true }); + this._options = merge(UDPTransport.DEFAULTS, options); + + this.socket = dgram.createSocket({ + type: this._options.type, + reuseAddr: this._options.reuseAddr + }); + + this.socket.on('error', (err) => this.emit('error', err)); + } + + /** + * Implements the writable interface + * @private + */ + _write([, buffer, target], encoding, callback) { + let [, contact] = target; + + this.socket.send(buffer, 0, buffer.length, contact.port, contact.hostname, + callback); + } + + /** + * Implements the readable interface + * @private + */ + _read() { + this.socket.once('message', (buffer) => { + this.push(buffer); + }); + } + + /** + * @private + */ + _validate(contact) { + return utils.isValidContact(contact, this._options.allowLoopbackAddresses); + } + + /** + * Binds the socket to the [port] [, address] [, callback] + * @param {number} [port=0] - Port to bind to + * @param {string} [address=0.0.0.0] - Address to bind to + * @param {function} [callback] - called after bind complete + */ + listen() { + this.socket.bind(...arguments); + } + +} + +module.exports = UDPTransport; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..c1952e5 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,447 @@ +/** +* @module kadence/utils +*/ + +'use strict'; + +const secp256k1 = require('secp256k1'); +const url = require('url'); +const constants = require('./constants'); +const semver = require('semver'); +const ip = require('ip'); +const crypto = require('crypto'); +const assert = require('assert'); +const { randomBytes, createHash } = crypto; +const ms = require('ms'); +//const equihash = require('equihash/lib/khovratovich'); // does not build on gcc-11 + + + +/** + * Tests if a string is valid hex + * @param {string} str + * @returns {boolean} + */ +module.exports.isHexaString = function(str) { + return Buffer.from(str, 'hex').length === str.length / 2; +}; + +/** + * Returns a random valid key/identity as a string + * @returns {string} + */ +exports.getRandomKeyString = function() { + return exports.getRandomKeyBuffer().toString('hex'); +}; + +/** + * Returns a random valid key/identity as a buffer + * @returns {buffer} + */ +exports.getRandomKeyBuffer = function() { + return crypto.randomBytes(constants.B / 8); +}; + +/** + * Determines if the given string key is valid + * @param {string} key - Node ID or item key + * @returns {boolean} + */ +exports.keyStringIsValid = function(key) { + let buf; + + try { + buf = Buffer.from(key, 'hex'); + } catch (err) { + return false; + } + + return exports.keyBufferIsValid(buf); +}; + +/** + * Determines if the given buffer key is valid + * @param {buffer} key - Node ID or item key + * @returns {boolean} + */ +exports.keyBufferIsValid = function(key) { + return Buffer.isBuffer(key) && key.length === constants.B / 8; +}; + +/** + * Calculate the distance between two keys + * @param {string} key1 - Identity key to compare + * @param {string} key2 - Identity key to compare + * @returns {buffer} + */ +exports.getDistance = function(id1, id2) { + id1 = !Buffer.isBuffer(id1) + ? Buffer.from(id1, 'hex') + : id1; + id2 = !Buffer.isBuffer(id2) + ? Buffer.from(id2, 'hex') + : id2; + + assert(exports.keyBufferIsValid(id1), 'Invalid key supplied'); + assert(exports.keyBufferIsValid(id2), 'Invalid key supplied'); + + return Buffer.alloc(constants.B / 8) + .map((b, index) => id1[index] ^ id2[index]); +}; + +/** + * Compare two buffers for sorting + * @param {buffer} b1 - Buffer to compare + * @param {buffer} b2 - Buffer to compare + * @returns {number} + */ +exports.compareKeyBuffers = function(b1, b2) { + assert(exports.keyBufferIsValid(b1), 'Invalid key supplied'); + assert(exports.keyBufferIsValid(b2), 'Invalid key supplied'); + + for (let index = 0; index < b1.length; index++) { + let bits = b1[index]; + + if (bits !== b2[index]) { + return bits < b2[index] ? -1 : 1; + } + } + + return 0; +}; + +/** + * Calculate the index of the bucket that key would belong to + * @param {string} referenceKey - Key to compare + * @param {string} foreignKey - Key to compare + * @returns {number} + */ +exports.getBucketIndex = function(referenceKey, foreignKey) { + let distance = exports.getDistance(referenceKey, foreignKey); + let bucketIndex = constants.B; + + for (let byteValue of distance) { + if (byteValue === 0) { + bucketIndex -= 8; + continue; + } + + for (let i = 0; i < 8; i++) { + if (byteValue & (0x80 >> i)) { + return --bucketIndex; + } else { + bucketIndex--; + } + } + } + + return bucketIndex; +}; + +/** + * Returns a buffer with a power-of-two value given a bucket index + * @param {string|buffer} referenceKey - Key to find next power of two + * @param {number} bucketIndex - Bucket index for key + * @returns {buffer} + */ +exports.getPowerOfTwoBufferForIndex = function(referenceKey, exp) { + assert(exp >= 0 && exp < constants.B, 'Index out of range'); + + const buffer = Buffer.isBuffer(referenceKey) + ? Buffer.from(referenceKey) + : Buffer.from(referenceKey, 'hex'); + const byteValue = parseInt(exp / 8); + + // NB: We set the byte containing the bit to the right left shifted amount + buffer[constants.K - byteValue - 1] = 1 << (exp % 8); + + return buffer; +}; + +/** + * Generate a random number within the bucket's range + * @param {buffer} referenceKey - Key for bucket distance reference + * @param {number} index - Bucket index for random buffer selection + */ +exports.getRandomBufferInBucketRange = function(referenceKey, index) { + let base = exports.getPowerOfTwoBufferForIndex(referenceKey, index); + let byte = parseInt(index / 8); // NB: Randomize bytes below the power of two + + for (let i = constants.K - 1; i > (constants.K - byte - 1); i--) { + base[i] = parseInt(Math.random() * 256); + } + + // NB: Also randomize the bits below the number in that byte and remember + // NB: arrays are off by 1 + for (let j = index - 1; j >= byte * 8; j--) { + let one = Math.random() >= 0.5; + let shiftAmount = j - byte * 8; + + base[constants.K - byte - 1] |= one ? (1 << shiftAmount) : 0; + } + + return base; +}; + +/** + * Validates the given object is a storage adapter + * @param {AbstractNode~storage} storageAdapter + */ +exports.validateStorageAdapter = function(storage) { + assert(typeof storage === 'object', + 'No storage adapter supplied'); + assert(typeof storage.get === 'function', + 'Store has no get method'); + assert(typeof storage.put === 'function', + 'Store has no put method'); + assert(typeof storage.del === 'function', + 'Store has no del method'); + assert(typeof storage.createReadStream === 'function', + 'Store has no createReadStream method'); +}; + +/** + * Validates the given object is a logger + * @param {AbstractNode~logger} logger + */ +exports.validateLogger = function(logger) { + assert(typeof logger === 'object', + 'No logger object supplied'); + assert(typeof logger.debug === 'function', + 'Logger has no debug method'); + assert(typeof logger.info === 'function', + 'Logger has no info method'); + assert(typeof logger.warn === 'function', + 'Logger has no warn method'); + assert(typeof logger.error === 'function', + 'Logger has no error method'); +}; + +/** + * Validates the given object is a transport + * @param {AbstractNode~transport} transport + */ +exports.validateTransport = function(transport) { + assert(typeof transport === 'object', + 'No transport adapter supplied'); + assert(typeof transport.read === 'function', + 'Transport has no read method'); + assert(typeof transport.write === 'function', + 'Transport has no write method'); +}; + +/** + * Returns the SHA-256 hash of the input + * @param {buffer} input - Data to hash + */ +module.exports.hash256 = function(input) { + return crypto.createHash('sha256').update(input).digest(); +}; + +/** + * @typedef EquihashProof + * @type {object} + * @property {number} n + * @property {number} k + * @property {number} nonce + * @property {buffer} value + */ + +/** + * Performs an equihash solution using defaults + * @param {buffer} input - Input hash to solve + * @returns {Promise} + */ +module.exports.eqsolve = function() { + return Promise.reject(new Error('Equihash implementation is not functional')); +}; + +/** + * Perform an equihash proof verification + * @param {buffer} input - Input hash for proof + * @param {buffer} proof - Equihash proof to verify + * @returns {boolean} + */ +module.exports.eqverify = function(/*input, proof*/) { + throw new Error('Equihash implementation is not functional'); + // return equihash.verify(input, proof); +}; + +/** + * Returns the RMD-160 hash of the input + * @param {buffer} input - Data to hash + */ +module.exports.hash160 = function(input) { + return crypto.createHash('ripemd160').update(input).digest(); +}; + +/** + * Returns a stringified URL from the supplied contact object + * @param {Bucket~contact} contact + * @returns {string} + */ +module.exports.getContactURL = function(contact) { + const [id, info] = contact; + + return `${info.protocol}//${info.hostname}:${info.port}/#${id}`; +}; + +/** + * Returns a parsed contact object from a URL + * @returns {object} + */ +module.exports.parseContactURL = function(addr) { + const { protocol, hostname, port, hash } = url.parse(addr); + const contact = [ + (hash ? hash.substr(1) : null) || + Buffer.alloc(constants.B / 8).fill(0).toString('hex'), + { + protocol, + hostname, + port + } + ]; + + return contact; +}; + +/** + * Returns whether or not the supplied semver tag is compatible + * @param {string} version - The semver tag from the contact + * @returns {boolean} + */ +module.exports.isCompatibleVersion = function(version) { + const local = require('./version').protocol; + const remote = version; + const sameMajor = semver.major(local) === semver.major(remote); + const diffs = ['prerelease', 'prepatch', 'preminor', 'premajor']; + + if (diffs.indexOf(semver.diff(remote, local)) !== -1) { + return false; + } else { + return sameMajor; + } +}; + +/** + * Determines if the supplied contact is valid + * @param {Bucket~contact} contact - The contact information for a given peer + * @param {boolean} loopback - Allows contacts that are localhost + * @returns {boolean} + */ +module.exports.isValidContact = function(contact, loopback) { + const [, info] = contact; + const isValidAddr = ip.isV4Format(info.hostname) || + ip.isV6Format(info.hostname) || + ip.isPublic(info.hostname); + const isValidPort = info.port > 0; + const isAllowedAddr = ip.isLoopback(info.hostname) ? !!loopback : true; + + return isValidPort && isValidAddr && isAllowedAddr; +}; + +/** + * Converts a buffer to a string representation of binary + * @param {buffer} buffer - Byte array to convert to binary string + * @returns {string} + */ +module.exports.toBinaryStringFromBuffer = function(buffer) { + const mapping = { + '0': '0000', + '1': '0001', + '2': '0010', + '3': '0011', + '4': '0100', + '5': '0101', + '6': '0110', + '7': '0111', + '8': '1000', + '9': '1001', + 'a': '1010', + 'b': '1011', + 'c': '1100', + 'd': '1101', + 'e': '1110', + 'f': '1111' + }; + const hexaString = buffer.toString('hex').toLowerCase(); + const bitmaps = []; + + for (let i = 0; i < hexaString.length; i++) { + bitmaps.push(mapping[hexaString[i]]); + } + + return bitmaps.join(''); +}; + +/** + * Returns a boolean indicating if the supplied buffer meets the given + * difficulty requirement + * @param {buffer} buffer - Buffer to check difficulty + * @param {number} difficulty - Number of leading zeroes + * @returns {boolean} + */ +module.exports.satisfiesDifficulty = function(buffer, difficulty) { + const binString = module.exports.toBinaryStringFromBuffer(buffer); + const prefix = Array(difficulty).fill('0').join(''); + + return binString.substr(0, difficulty) === prefix; +}; + +/** + * @private + */ +module.exports._sha256 = function(input) { + return createHash('sha256').update(input).digest(); +}; + +/** + * @private + */ +module.exports._rmd160 = function(input) { + return createHash('ripemd160').update(input).digest(); +}; + +/** + * Generates a private key + * @returns {buffer} + */ +module.exports.generatePrivateKey = function() { + let privKey + + do { + privKey = randomBytes(32); + } while (!secp256k1.privateKeyVerify(privKey)) + + return privKey; +}; + +/** + * Takes a public key are returns the identity + * @param {buffer} publicKey - Raw public key bytes + * @returns {buffer} + */ +module.exports.toPublicKeyHash = function(publicKey) { + return exports._rmd160(exports._sha256(publicKey)); +}; + +/** + * Wraps the supplied function in a pseudo-random length timeout to help + * prevent convoy effects. These occur when a number of processes need to use + * a resource in turn. There is a tendency for such bursts of activity to + * drift towards synchronization, which can be disasterous. In Kademlia all + * nodes are requird to republish their contents every hour (T_REPLICATE). A + * convoy effect might lead to this being synchronized across the network, + * which would appear to users as the network dying every hour. The default + * timeout will be between 0 and 30 minutes unless specified. + * @param {function} func - Function to wrap to execution later + * @param {number} [maxtime] - Maximum timeout + * @returns {function} + */ +module.exports.preventConvoy = function(func, timeout) { + return function() { + let t = Math.ceil( + Math.random() * (typeof timeout !== 'number' ? ms('30m') : timeout) + ); + return setTimeout(func, t); + }; +}; diff --git a/lib/version.js b/lib/version.js new file mode 100644 index 0000000..18d56d9 --- /dev/null +++ b/lib/version.js @@ -0,0 +1,33 @@ +/** + * @module kadence/version + */ + +'use strict'; + +var semver = require('semver'); +var assert = require('assert'); + +module.exports = { + /** + * @constant {string} protocol - The supported protocol version + */ + protocol: '2.0.0', + /** + * @constant {string} software - The current software version + */ + software: require('../package').version, + /** + * Returns human readable string of versions + * @function + * @returns {string} + */ + toString: function() { + let { software, protocol } = module.exports; + return `kadence v${software} protocol v${protocol}`; + } +}; + +assert( + semver.valid(module.exports.protocol), + 'Invalid protocol version specified' +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bdecc6f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5862 @@ +{ + "name": "@tacticalchihuahua/kadence", + "version": "6.1.9", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "7zip": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/7zip/-/7zip-0.0.6.tgz", + "integrity": "sha512-ns8vKbKhIQm338AeWo/YdDSWil3pldwCMoyR2npoM2qDAzF8Vuko8BtDxpNt/wE15SXOh5K5WbjSLR4kTOAHLA==", + "optional": true + }, + "@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + } + }, + "@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" + }, + "@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + }, + "@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "requires": { + "@babel/types": "^7.26.0" + } + }, + "@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "requires": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + } + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "requires": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + } + } + }, + "@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "requires": { + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + } + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, + "@tacticalchihuahua/diglet": { + "version": "github:tacticalchihuahua/diglet#a962209dda1fb3dbe4454f48a3074bf5eda1fb44", + "from": "github:tacticalchihuahua/diglet", + "requires": { + "async": "^2.6.1", + "bunyan": "^1.8.5", + "camel-case": "^4.1.1", + "colors": "^1.4.0", + "commander": "^2.9.0", + "electron-squirrel-startup": "^1.0.0", + "express": "^4.17.1", + "http-server": "^0.12.1", + "is-electron": "^2.2.0", + "merge": "^1.2.1", + "mkdirp": "^1.0.3", + "pug": "^2.0.4", + "random-word": "^2.0.0", + "rc": "^1.1.6", + "secp256k1": "^3.8.0", + "serve-static": "^1.14.1", + "tldjs": "^1.7.0", + "vue": "^2.6.11" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "secp256k1": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.1.tgz", + "integrity": "sha512-tArjQw2P0RTdY7QmkNehgp6TVvQXq6ulIhxv8gaH6YubKG/wxxAoNKcbuXjDhybbc+b2Ihc7e0xxiGN744UIiQ==", + "requires": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.5.7", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + } + } + } + }, + "@tacticalchihuahua/granax": { + "version": "github:tacticalchihuahua/granax#7092e680596c34f484a58d0c43787122ea36113f", + "from": "github:tacticalchihuahua/granax", + "requires": { + "7zip": "0.0.6", + "async": "^2.3.0", + "cheerio": "1.0.0-rc.2", + "commander": "^2.11.0", + "follow-redirects": "^1.2.4", + "merge": "^1.2.1", + "mkdirp": "^0.5.1", + "mv": "^2.1.1", + "ncp": "^2.0.0", + "progress": "^2.0.3", + "rimraf": "^2.6.3", + "semver": "^5.4.1" + } + }, + "@types/babel-types": { + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.16.tgz", + "integrity": "sha512-5QXs9GBFTNTmilLlWBhnsprqpjfrotyrnzUdwDrywEL/DA4LuCWQT300BTOXA3Y9ngT9F2uvmCoIxI6z8DlJEA==" + }, + "@types/babylon": { + "version": "6.16.9", + "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.9.tgz", + "integrity": "sha512-sEKyxMVEowhcr8WLfN0jJYe4gS4Z9KC2DGz0vqfC7+MXFbmvOF7jSjALC77thvAO2TLgFUPa9vDeOak+AcUrZA==", + "requires": { + "@types/babel-types": "*" + } + }, + "@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "requires": { + "undici-types": "~6.19.8" + } + }, + "@vue/compiler-sfc": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", + "requires": { + "@babel/parser": "^7.23.5", + "postcss": "^8.4.14", + "prettier": "^1.18.2 || ^2.0.0", + "source-map": "^0.6.1" + } + }, + "abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==" + }, + "abstract-leveldown": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz", + "integrity": "sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ==", + "requires": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==" + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha512-uWttZCk96+7itPxK8xCzY86PnxKTMrReKDqrHzv42VQY0K30PUO8WY13WMOuI+cOdX4EIdzdvQ8k6jkuGRFMYw==", + "requires": { + "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha512-fu2ygVGuMmlzG8ZeRJ0bvR41nsAkxxhbyk8bZ1SS521Z7vmgJFTQQlfz/Mp/nJexGBz+v8sC9bM6+lNgskt4Ug==" + } + } + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "append-transform": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha512-Yisb7ew0ZEyDtRYQ+b+26o9KbiYPFxwcsxKzbssigzRRMJ9LpExPVUg6Fos7eP7yP3q7///tzze4nm4lTptPBw==", + "dev": true, + "requires": { + "default-require-extensions": "^1.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + } + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" + }, + "assertion-error": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", + "integrity": "sha512-g/gZV+G476cnmtYI+Ko9d5khxSoCSoom/EaNmmCfwpOvBXEJ18qwFrxfP1/CsIqk2no1sAKKwxndV0tP7ROOFQ==", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "requires": { + "lodash": "^4.17.14" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "atbf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/atbf/-/atbf-1.1.1.tgz", + "integrity": "sha512-NtFI3g9nKmKFtmlzingqzZ5kh5yXjdOU7JfFdhc6Ec0tuhe5tTNUkvuaEn72AOJ52yhpmyzHhtUC3EiI5jHlug==", + "requires": { + "bloem": "^0.2.4" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" + }, + "aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + }, + "dependencies": { + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "basic-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", + "integrity": "sha512-CtGuTyWf3ig+sgRyC7uP6DM3N+5ur/p8L+FPfsd+BbIfIs74TFfCajZTHnCw6K5dqM0bZEbRIqRy1fAdiUJhTA==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bitbuffer": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/bitbuffer/-/bitbuffer-0.1.3.tgz", + "integrity": "sha512-SyRG3h70w/2MJaN27Z7gQufBDjVx1fiYsxLnZ9XBYZxUXXRC053nEkb3/wBoa9yFEKvsH5HxMPrjiG95DDQXCw==" + }, + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + } + } + }, + "bloem": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/bloem/-/bloem-0.2.4.tgz", + "integrity": "sha512-9NC03n3Eaj+hkBalu+6GKRe6fkzHAfo5kUT0etwQXVAb1efDTx86aGnbimV4kEgWi52Perprr0TMWO3dTs8ayA==", + "requires": { + "bitbuffer": "0.1.x", + "fnv": "0.1.x" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "boscar": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/boscar/-/boscar-2.0.1.tgz", + "integrity": "sha512-9vYu7QS+yrRF9wytfcHlzqQ80lfzdi6YCgeqbzuh2hf3L1CtTgNU1fUlnlvvy+mJ/jQedfQwe3Zk85HFwr146A==", + "requires": { + "jsonrpc-lite": "^1.2.3", + "uuid": "^3.0.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==" + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" + }, + "bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, + "bunyan-rotating-file-stream": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/bunyan-rotating-file-stream/-/bunyan-rotating-file-stream-1.6.3.tgz", + "integrity": "sha512-pJFBvosqXjYXsOtDr72SDhZr3d+1ADDBO8TiU0ju0DbNBS4+vjI3GatczzB7LCcBWqWevQ91j1HYsl8cgnI+HQ==", + "requires": { + "async": "^1.5.2", + "lodash": "^4.2.1", + "semver": "^5.1.0", + "strftime": "^0.9.2" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==" + } + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "requires": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chai": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-2.3.0.tgz", + "integrity": "sha512-/HtcZZzZolyYgU8x3qClyPGxWS0/TKihLbIuQHam40hZwgyx/6sbm2iLfk8I43mBm2Dk8GoyVstgQLSxUwgShw==", + "dev": true, + "requires": { + "assertion-error": "1.0.0", + "deep-eql": "0.1.3" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + } + } + }, + "character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "requires": { + "is-regex": "^1.0.3" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" + }, + "cheerio": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", + "integrity": "sha512-9LDHQy1jHc/eXMzPN6/oah9Qba4CjdKECC7YYEE/2zge/tsGwt19NQp5NFdfd5Lx6TZlyC5SXNQkG41P9r6XDg==", + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash": "^4.15.0", + "parse5": "^3.0.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + } + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "requires": { + "source-map": "~0.6.0" + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "constantinople": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", + "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "requires": { + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "babel-types": "^6.26.0", + "babylon": "^6.18.0" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==" + }, + "coveralls": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.1.1.tgz", + "integrity": "sha512-+dxnG2NHncSD1NrqbSM3dn/lE57O6Qf/koe9+I7c+wzkqRmEvcp0kgJdxKInzYzkICKkFMZsX3Vct3++tsF9ww==", + "dev": true, + "requires": { + "js-yaml": "^3.13.1", + "lcov-parse": "^1.0.0", + "log-driver": "^1.2.7", + "minimist": "^1.2.5", + "request": "^2.88.2" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" + }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "daemon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/daemon/-/daemon-1.1.0.tgz", + "integrity": "sha512-1vX9YVcP21gt12nSD3SQRC/uPU7fyA6M8qyClTBIFuiRWoylFn57PwXhjBAqRl085bZAje7sILhZU48qcS9SWw==" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "deep-eql": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha512-6sEotTRGBFiNcqVoeHwnfopbSpi5NbH1VWJmYCVkmxMmaVTT0bUTrNaGyBwhgP4MZL012W/mkzIn3Da+iDYweg==", + "dev": true, + "requires": { + "type-detect": "0.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha512-Dn2eAftOqXhNXs5f/Xjn7QTZ6kDYkx7u0EXQInN1oyYwsZysu11q7oTtaKcbzLxZRJiDHa8VmwpWmb4lY5FqgA==", + "dev": true, + "requires": { + "strip-bom": "^2.0.0" + } + }, + "deferred-leveldown": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "requires": { + "abstract-leveldown": "~6.2.1", + "inherits": "^2.0.3" + }, + "dependencies": { + "abstract-leveldown": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "requires": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + } + } + } + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==", + "requires": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + } + }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecstatic": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz", + "integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==", + "requires": { + "he": "^1.1.1", + "mime": "^1.6.0", + "minimist": "^1.1.0", + "url-join": "^2.0.5" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "electron-squirrel-startup": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.1.tgz", + "integrity": "sha512-sTfFIHGku+7PsHLJ7v0dRcZNkALrV+YEozINTW8X1nM//e5O3L+rfYuvSW00lmGHnYmUjARZulD8F2V8ISI9RA==", + "requires": { + "debug": "^2.2.0" + } + }, + "elliptic": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", + "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "encoding-down": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", + "requires": { + "abstract-leveldown": "^6.2.1", + "inherits": "^2.0.3", + "level-codec": "^9.0.0", + "level-errors": "^2.0.0" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "requires": { + "prr": "~1.0.1" + } + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es6-promisify": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz", + "integrity": "sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" + }, + "express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-future": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fast-future/-/fast-future-1.0.2.tgz", + "integrity": "sha512-ZdgcQC4CDq0OlirlbmaV1Hvl9hrQBKwSJokpemb2Y82uzQ3mFaGxBCCTbr78t3obRsgjqrJAsEaYWZSK3oEcJQ==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha512-UxowFKnAFIwtmSxgKjWAVgjE3Fk7MQJT0ZIyl0NwIFZTrx4913rLaonGJ84V+x/2+w/pe4ULHRns+GZPs1TVuw==", + "dev": true, + "requires": { + "glob": "^7.0.3", + "minimatch": "^3.0.3" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", + "dev": true, + "requires": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + } + }, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fnv": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fnv/-/fnv-0.1.3.tgz", + "integrity": "sha512-5TtgKckVqRRjybT6Yogzprs7Lz4yrsXazxxuZ2CJeUzKCP4yc2lHWemqnM/9BuEk4LWXCntNbesiBQly8c7IHw==" + }, + "follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" + }, + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formatio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", + "integrity": "sha512-YAF05v8+XCxAyHOdiiAmHdgCVPrWO8X744fYIPtBciIorh5LndWfi1gjeJ16sTbJhzek9kd+j3YByhohtz5Wmg==", + "dev": true, + "requires": { + "samsam": "1.x" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "requires": { + "minipass": "^7.0.3" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==" + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + } + } + }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "http-server": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.12.3.tgz", + "integrity": "sha512-be0dKG6pni92bRjq0kvExtj/NrrAd28/8fCXkaI/4piTwQMSDSLMhWyW0NI1V+DBI3aa1HMlQu46/HjVLfmugA==", + "requires": { + "basic-auth": "^1.0.3", + "colors": "^1.4.0", + "corser": "^2.0.1", + "ecstatic": "^3.3.2", + "http-proxy": "^1.18.0", + "minimist": "^1.2.5", + "opener": "^1.5.1", + "portfinder": "^1.0.25", + "secure-compare": "3.0.1", + "union": "~0.5.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + } + } + } + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha512-rBtCAQAJm8A110nbwn6YdveUnuZH3WrC36IwkRXxDnq53JvXA2NVQvB7IHyKomxK1MJ4VDNw3UtFDdXQ+AvLYA==" + }, + "ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "dependencies": { + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + } + } + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "requires": { + "hasown": "^2.0.2" + } + }, + "is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==" + }, + "is-expression": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", + "integrity": "sha512-vyMeQMq+AiH5uUnoBfMTwf18tO3bM6k1QXBE9D6ueAAquEfCZe3AJPtud9g6qS0+4X8xA7ndpZiDyeb2l2qOBw==", + "requires": { + "acorn": "~4.0.2", + "object-assign": "^4.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha512-fu2ygVGuMmlzG8ZeRJ0bvR41nsAkxxhbyk8bZ1SS521Z7vmgJFTQQlfz/Mp/nJexGBz+v8sC9bM6+lNgskt4Ug==" + } + } + }, + "is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" + }, + "is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true + }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "istanbul": { + "version": "1.1.0-alpha.1", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-1.1.0-alpha.1.tgz", + "integrity": "sha512-acLlAtOI1itxA7agb2+jKoRFjQa9vuV2G48jUyerkrPXaSBh0q4hMre7qYhGoskk8R5XFbt6v5mOITAlf5LFgQ==", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "istanbul-api": "^1.1.0-alpha", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + } + } + }, + "istanbul-api": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", + "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", + "dev": true, + "requires": { + "async": "^2.1.4", + "fileset": "^2.0.2", + "istanbul-lib-coverage": "^1.2.1", + "istanbul-lib-hook": "^1.2.2", + "istanbul-lib-instrument": "^1.10.2", + "istanbul-lib-report": "^1.1.5", + "istanbul-lib-source-maps": "^1.2.6", + "istanbul-reports": "^1.5.1", + "js-yaml": "^3.7.0", + "mkdirp": "^0.5.1", + "once": "^1.4.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", + "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", + "dev": true, + "requires": { + "append-transform": "^0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.1", + "semver": "^5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", + "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^1.2.1", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", + "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.2.1", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", + "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", + "dev": true, + "requires": { + "handlebars": "^4.0.3" + } + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "requires": { + "xmlcreate": "^2.0.4" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "jsdoc": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", + "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", + "dev": true, + "requires": { + "@babel/parser": "^7.9.4", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "taffydb": "2.6.2", + "underscore": "~1.13.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==", + "dev": true + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz", + "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==", + "requires": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + } + } + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==" + }, + "jsonrpc-lite": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonrpc-lite/-/jsonrpc-lite-1.3.1.tgz", + "integrity": "sha512-U8qzbbTFLWk58kKQHnvQufx8beO4InZH+XEZJEoObh6ENuLdVJV7UdFkWkFyjSjUaHD9Tfs5iEjtGrtVsNgevA==" + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "requires": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "knuth-shuffle": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/knuth-shuffle/-/knuth-shuffle-1.0.8.tgz", + "integrity": "sha512-IdC4Hpp+mx53zTt6VAGsAtbGM0g4BV9fP8tTcviCosSwocHcRDw9uG5Rnv6wLWckF4r72qeXFoK9NkvV1gUJCQ==" + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==" + }, + "lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", + "dev": true + }, + "level-codec": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "requires": { + "buffer": "^5.6.0" + } + }, + "level-concat-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==" + }, + "level-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "requires": { + "errno": "~0.1.1" + } + }, + "level-iterator-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.4.0", + "xtend": "^4.0.2" + } + }, + "level-supports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "requires": { + "xtend": "^4.0.2" + } + }, + "leveldown": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-4.0.2.tgz", + "integrity": "sha512-SUgSRTWFh3eeiTdIt2a4Fi9TZO5oWzE9uC/Iw8+fVr1sk8x1S2l151UWwSmrMFZB3GxJhZIf4bQ0n+051Cctpw==", + "requires": { + "abstract-leveldown": "~5.0.0", + "bindings": "~1.3.0", + "fast-future": "~1.0.2", + "nan": "~2.12.1", + "prebuild-install": "~5.2.4" + }, + "dependencies": { + "abstract-leveldown": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-5.0.0.tgz", + "integrity": "sha512-5mU5P1gXtsMIXg65/rsYGsi93+MlogXZ9FA8JnwKurHQg64bfXwGYVdVdijNTVNOlAsuIiOwHdvFFD5JqCJQ7A==", + "requires": { + "xtend": "~4.0.0" + } + }, + "bindings": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", + "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==" + }, + "nan": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", + "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" + } + } + }, + "levelup": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "requires": { + "deferred-leveldown": "~5.3.0", + "level-errors": "~2.0.0", + "level-iterator-stream": "~4.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, + "lolex": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", + "integrity": "sha512-/bpxDL56TG5LS5zoXxKqA6Ro5tkOS5M8cm/7yQcwLIKIcM2HR5fjjNCaIhJNv96SEk4hNGSafYMZK42Xv5fihQ==", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "requires": { + "tslib": "^2.0.3" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "ltgt": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", + "dev": true + }, + "make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "requires": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + } + }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + } + } + }, + "markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true + }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "memdown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/memdown/-/memdown-2.0.0.tgz", + "integrity": "sha512-3AKuLjJTlxLK6jBg0uHYQ2o6RLLfi0QqICv5GjUIiovDYWnwMVEGp/oe7Rauff0M8Cp5YOkyTBArLEBZENqsFA==", + "dev": true, + "requires": { + "abstract-leveldown": "~4.0.0", + "functional-red-black-tree": "~1.0.1", + "immediate": "~3.2.3", + "inherits": "~2.0.1", + "ltgt": "~2.2.0", + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "abstract-leveldown": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-4.0.3.tgz", + "integrity": "sha512-qsIHFQy0u17JqSY+3ZUT+ykqxYY17yOfvAsLkFkw8kSQqi05d1jyj0bCuSX6sjYlXuY9cKpgUt5EudQdP4aXyA==", + "dev": true, + "requires": { + "xtend": "~4.0.0" + } + }, + "immediate": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", + "integrity": "sha512-RrGCXRm/fRVqMIhqXrGEX9rRADavPiDFSoMb/k64i9XMk8uH4r/Omi5Ctierj6XzNecwDbO4WuFbDD1zmpl3Tg==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "merge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==" + }, + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + }, + "metapipe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/metapipe/-/metapipe-2.0.3.tgz", + "integrity": "sha512-WeylW3ANIw88AeAqfYCPWoeHWfVWpAd5hk/CZEdni4diFq/M40ioXomJH1yqEK0yWsuZDcFnV5LfyZicryPlzw==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "requires": { + "minipass": "^7.0.3" + } + }, + "minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "requires": { + "encoding": "^0.1.13", + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "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" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==", + "dev": true + }, + "moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "optional": true + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "dev": true + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "requires": { + "glob": "^6.0.1" + } + } + } + }, + "nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==" + }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "nat-pmp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nat-pmp/-/nat-pmp-1.0.0.tgz", + "integrity": "sha512-DHh1Ko06d17x9iUoiWse39W8pYed34uPJj5sxAnfCju22zBF2LU2Dh3UhUhiQz9Rfp4otcBFmMeQhfJNGYdfRw==", + "requires": { + "debug": "~0.7.0" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q==" + } + } + }, + "nat-upnp": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nat-upnp/-/nat-upnp-1.1.1.tgz", + "integrity": "sha512-b1Q+sf9fHGCXhlWErNgTTEto8A02MnNysw3vx3kD1657+/Ae23vPEAB6QBh+9RqLL4+xw/LmjVTiLy6A7Cx0xw==", + "requires": { + "async": "^2.1.5", + "ip": "^1.1.4", + "request": "^2.79.0", + "xml2js": "~0.1.14" + } + }, + "native-promise-only": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", + "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==" + }, + "needle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/needle/-/needle-1.1.2.tgz", + "integrity": "sha512-MldJ0Wrh5cXJgH7wZj9nf56K4DFK5uJIAz1qZ5QORcxEVYxdK7gj8vvNsq1nrsWcVwJC7i6DxWb1avfZi1p2Fw==", + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4" + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "network": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/network/-/network-0.4.1.tgz", + "integrity": "sha512-b3ohQdwYtqdtHEPVxlkgP9wF3mYyhdv618wIlKCiSp4jm47o8Fe625LLPr5bH9GGSu4QQkoU0NZ7z5A+YuSj7g==", + "requires": { + "async": "^1.5.2", + "commander": "2.9.0", + "needle": "1.1.2", + "wmic": "^0.1.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==" + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", + "requires": { + "graceful-readlink": ">= 1.0.0" + } + } + } + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-abi": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", + "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "requires": { + "semver": "^5.4.1" + } + }, + "node-gyp": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", + "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", + "requires": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + } + } + }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==" + }, + "nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "requires": { + "abbrev": "^2.0.0" + } + }, + "npid": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/npid/-/npid-0.4.0.tgz", + "integrity": "sha512-qYQJ07u0tvNfFALt371vcDw7ypjGxEkT0xo3DP6PiUf39FMQ5sDyvGCsAUAm+GhKuimOSxs2Xn3aU+fgFxn3EA==" + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "requires": { + "@types/node": "*" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + } + } + }, + "path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + }, + "pem": { + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/pem/-/pem-1.14.8.tgz", + "integrity": "sha512-ZpbOf4dj9/fQg5tQzTqv4jSKJQsK7tPl0pm4/pvPcZVjZcJg7TMfr3PBk6gJH97lnpJDu4e4v8UUqEz5daipCg==", + "requires": { + "es6-promisify": "^7.0.0", + "md5": "^2.3.0", + "os-tmpdir": "^1.0.2", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "requires": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + } + }, + "prebuild-install": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.2.5.tgz", + "integrity": "sha512-6uZgMVg7yDfqlP5CPurVhtq3hUKBFNufiar4J5hZrlHTo59DDBEtyxw01xCdFss9j0Zb9+qzFVf/s4niayba3w==", + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "os-homedir": "^1.0.1", + "pump": "^2.0.1", + "rc": "^1.2.7", + "simple-get": "^2.7.0", + "tar-fs": "^1.13.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "optional": true + }, + "proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxyquire": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", + "integrity": "sha512-mZZq4F50qaBkngvlf9paNfaSb5gtJ0mFPnBjda4NxCpXpMAaVfSLguRr9y2KXF6koOSBf4AanD2inuEQw3aCcA==", + "dev": true, + "requires": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.0", + "resolve": "~1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "dev": true + } + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "psl": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.10.0.tgz", + "integrity": "sha512-KSKHEbjAnpUuAUserOq0FxGXCUrzC3WniuSJhvdbs102rL55266ZcHBqLWOsG30spQMlPdpy7icATiAQehg/iA==", + "requires": { + "punycode": "^2.3.1" + }, + "dependencies": { + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + } + } + }, + "pug": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", + "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "requires": { + "pug-code-gen": "^2.0.2", + "pug-filters": "^3.1.1", + "pug-lexer": "^4.1.0", + "pug-linker": "^3.0.6", + "pug-load": "^2.0.12", + "pug-parser": "^5.0.1", + "pug-runtime": "^2.0.5", + "pug-strip-comments": "^1.0.4" + } + }, + "pug-attrs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", + "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "requires": { + "constantinople": "^3.0.1", + "js-stringify": "^1.0.1", + "pug-runtime": "^2.0.5" + } + }, + "pug-code-gen": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.3.tgz", + "integrity": "sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==", + "requires": { + "constantinople": "^3.1.2", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.1", + "pug-attrs": "^2.0.4", + "pug-error": "^1.3.3", + "pug-runtime": "^2.0.5", + "void-elements": "^2.0.1", + "with": "^5.0.0" + } + }, + "pug-error": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", + "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + }, + "pug-filters": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", + "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "requires": { + "clean-css": "^4.1.11", + "constantinople": "^3.0.1", + "jstransformer": "1.0.0", + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8", + "resolve": "^1.1.6", + "uglify-js": "^2.6.1" + } + }, + "pug-lexer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", + "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "requires": { + "character-parser": "^2.1.1", + "is-expression": "^3.0.0", + "pug-error": "^1.3.3" + } + }, + "pug-linker": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", + "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "requires": { + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8" + } + }, + "pug-load": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", + "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "requires": { + "object-assign": "^4.1.0", + "pug-walk": "^1.1.8" + } + }, + "pug-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", + "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "requires": { + "pug-error": "^1.3.3", + "token-stream": "0.0.1" + } + }, + "pug-runtime": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", + "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + }, + "pug-strip-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", + "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "requires": { + "pug-error": "^1.3.3" + } + }, + "pug-walk": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", + "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + }, + "random-word": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/random-word/-/random-word-2.0.0.tgz", + "integrity": "sha512-+Guq9cOBwUK1WSuwtV8ZpNqCyc9T1sbqYUh/sI17zKZuJo/VkyQgmgofZHPeqh75P1anTP7oKhyAMpO/uIbkzA==", + "requires": { + "unique-random-array": "^1.0.0", + "word-list": "^2.0.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + } + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==" + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "dev": true + }, + "sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, + "secp256k1": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.2.2.tgz", + "integrity": "sha512-FkQSg9taImredU6mxiZcaUcBTQoTKWuVh+5jvnSzXs+tx3yQOaK5wLu3fpRZnPgNhkNJ45LbN9scm0oRi2N68A==", + "requires": { + "bindings": "^1.2.1", + "bip66": "^1.1.3", + "bn.js": "^4.11.3", + "create-hash": "^1.1.2", + "drbg.js": "^1.0.1", + "elliptic": "^6.2.3", + "nan": "^2.2.1" + } + }, + "secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==" + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + } + } + }, + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.2.tgz", + "integrity": "sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw==", + "requires": { + "decompress-response": "^3.3.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "sinon": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.4.1.tgz", + "integrity": "sha512-vFTrO9Wt0ECffDYIPSP/E5bBugt0UjcBQOfQUMh66xzkyPEnhl/vM2LRZi2ajuTdkH07sA6DzrM6KvdvGIH8xw==", + "dev": true, + "requires": { + "diff": "^3.1.0", + "formatio": "1.2.0", + "lolex": "^1.6.0", + "native-promise-only": "^0.8.1", + "path-to-regexp": "^1.7.0", + "samsam": "^1.1.3", + "text-encoding": "0.6.4", + "type-detect": "^4.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true + } + } + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + } + } + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, + "socks": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-1.1.10.tgz", + "integrity": "sha512-ArX4vGPULWjKDKgUnW8YzfI2uXW7kzgkJuB0GnFBA/PfT3exrrOk+7Wk2oeb894Qf20u1PWv9LEgrO0Z82qAzA==", + "requires": { + "ip": "^1.1.4", + "smart-buffer": "^1.0.13" + }, + "dependencies": { + "smart-buffer": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz", + "integrity": "sha512-1+8bxygjTsNfvQe0/0pNBesTOlSHtOeG6b6LYbvsZCCHDKYZ40zcQo6YTnZBWrBSLWOCbrHljLdEmGMYebu7aQ==" + } + } + }, + "socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "requires": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "requires": { + "through": "2" + } + }, + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "requires": { + "minipass": "^7.0.3" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "strftime": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.9.2.tgz", + "integrity": "sha512-wu4xPBupxILrTO8TPaQRSTG7UOEWPhk232QXK3cdTejPeTWsXRpIMkgGF1JKnPY4OHNKYWUa//k0crofghxAFA==" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + } + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "taffydb": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", + "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==", + "dev": true + }, + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "tar-fs": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", + "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", + "requires": { + "chownr": "^1.0.1", + "mkdirp": "^0.5.1", + "pump": "^1.0.0", + "tar-stream": "^1.1.2" + }, + "dependencies": { + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "tldjs": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-1.8.0.tgz", + "integrity": "sha512-oJHVrNdgPYlU7vszbSBMYDykdnJ8DmjaGML5KysOn2MIYtnpl1Yn+bCSF/vNo8vZkd0U+eRGg1qkQjHDLD/Teg==", + "requires": { + "punycode": "^1.4.1" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "token-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", + "integrity": "sha512-nfjOAu/zAWmX9tgwi5NRp7O7zTDUD1miHiB40klUnAh9qnL1iXdgzcz/i5dMaL5jahcBAaSfmNOBBJBLJW8TEg==" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + } + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", + "dev": true + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha512-5rqszGVwYgBoDkIm2oUtvkfZMQ0vk29iDMU0W2qCa3rG0vPDNczCMT4hV/bLBgLg8k8ri6+u3Zbt+S/14eMzlA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==", + "optional": true + }, + "underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "requires": { + "qs": "^6.4.0" + } + }, + "unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "requires": { + "unique-slug": "^4.0.0" + } + }, + "unique-random": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-random/-/unique-random-1.0.0.tgz", + "integrity": "sha512-K1sUkPf9EXCZFNIlMCoX4icAqcvkR4FMPH4Z61HbyiWhQl1ZGo0zYeV2bJmocK8Cp6tnKYrCnpkeKGebXZoRTQ==" + }, + "unique-random-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unique-random-array/-/unique-random-array-1.0.1.tgz", + "integrity": "sha512-z9J/SV8CUIhIRROcHe9YUoAT6XthUJt0oUyLGgobiXJprDP9O9dsErNevvSaAv5BkhwFEVPn6nIEOKeNE6Ck1Q==", + "requires": { + "unique-random": "^1.0.0" + } + }, + "unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + } + } + }, + "url-join": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", + "integrity": "sha512-c2H1fIgpUdwFRIru9HFno5DT73Ok8hg5oOb5AT3ayIgvCRfxgs2jyt5Slw8kEB7j3QUr6yJmMPDT/odjk7jXow==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + } + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==" + }, + "vue": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "requires": { + "@vue/compiler-sfc": "2.7.16", + "csstype": "^3.1.0" + } + }, + "which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "requires": { + "isexe": "^3.1.1" + }, + "dependencies": { + "isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==" + } + } + }, + "which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==" + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==" + }, + "with": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", + "integrity": "sha512-uAnSsFGfSpF6DNhBXStvlZILfHJfJu4eUkfbRGk94kGO1Ta7bg6FwfvoOhhyHAJuFbCw+0xk4uJ3u57jLvlCJg==", + "requires": { + "acorn": "^3.1.0", + "acorn-globals": "^3.0.0" + } + }, + "wmic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wmic/-/wmic-0.1.0.tgz", + "integrity": "sha512-fSIOzCT2oSGN9v8d+outSdjcoA7WUGbsPBWZq5o4zF7jZOLaUO84rDuYIO3OsvIOYxUgjrBbADbH7sdT80nZ8w==", + "requires": { + "async": "^3.2.6", + "iconv-lite": "^0.4.13" + }, + "dependencies": { + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + } + } + }, + "word-list": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/word-list/-/word-list-2.0.0.tgz", + "integrity": "sha512-DlZQRBKGq8MZgU451Wp5VuIIQZjArEuh/7P7z73RHUnXeVQB1JOjBLofNQadgDIr5o2odVsxOSq16HbzVj6T3A==" + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==" + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xml2js": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.1.14.tgz", + "integrity": "sha512-pbdws4PPPNc1HPluSUKamY4GWMk592K7qwcj6BExbVOhhubub8+pMda/ql68b6L3luZs/OGjGSB5goV7SnmgnA==", + "requires": { + "sax": ">=0.1.1" + } + }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..24d0766 --- /dev/null +++ b/package.json @@ -0,0 +1,108 @@ +{ + "author": { + "name": "Lily Anne Hall", + "email": "lily@tacticalchihuahua.lol" + }, + "bin": { + "kadence": "bin/kadence.js" + }, + "bugs": { + "url": "https://github.com/tacticalchihuahua/kadence/issues" + }, + "contributors": [ + { + "name": "omphalos", + "url": "https://github.com/omphalos" + }, + { + "name": "Alexander Leitner", + "url": "https://github.com/aleitner" + }, + { + "name": "Trey Griffith", + "url": "https://github.com/treygriffith" + }, + { + "name": "Janko Simonovic", + "url": "https://gitlab.com/simonovic86" + } + ], + "dependencies": { + "@tacticalchihuahua/diglet": "github:tacticalchihuahua/diglet", + "@tacticalchihuahua/granax": "github:tacticalchihuahua/granax", + "async": "^2.6.0", + "atbf": "^1.1.0", + "boscar": "^2.0.0", + "bunyan": "^1.8.12", + "bunyan-rotating-file-stream": "^1.6.3", + "bytes": "^3.0.0", + "commander": "^2.9.0", + "concat-stream": "^1.6.0", + "daemon": "^1.1.0", + "encoding-down": "^6.0.1", + "ip": "^1.1.5", + "json-stable-stringify": "^1.0.1", + "jsonrpc-lite": "^1.3.0", + "knuth-shuffle": "^1.0.8", + "leveldown": "^4.0.2", + "levelup": "^4.0.0", + "lru-cache": "^4.1.1", + "merge": "^1.2.1", + "metapipe": "^2.0.2", + "mkdirp": "^0.5.1", + "ms": "^2.1.1", + "nat-pmp": "*", + "nat-upnp": "^1.1.1", + "network": "^0.4.1", + "node-gyp": "^10.2.0", + "npid": "^0.4.0", + "pem": "^1.13.2", + "rc": "^1.2.8", + "secp256k1": "=3.2.2", + "semver": "^5.1.0", + "socks": "^1.1.10", + "split": "^1.0.1", + "uuid": "^3.2.1" + }, + "description": "extensible, hardened, and flexible distributed systems framework", + "devDependencies": { + "chai": "^2.2.0", + "coveralls": "^3.0.1", + "eslint": "^5.14.1", + "istanbul": "^1.1.0-alpha.1", + "jsdoc": "^3.6.3", + "memdown": "^2.0.0", + "mocha": "^5.2.0", + "proxyquire": "^1.8.0", + "rimraf": "^2.6.1", + "sinon": "^2.4.1" + }, + "directories": { + "test": "test", + "lib": "lib", + "doc": "doc" + }, + "engines": { + "node": ">=10.x.x" + }, + "homepage": "https://tacticalchihuahua.lol/kadence", + "keywords": [], + "license": "AGPL-3.0", + "main": "index.js", + "name": "@tacticalchihuahua/kadence", + "repository": { + "type": "git", + "url": "git+https://github.com/tacticalchihuahua/kadence.git" + }, + "scripts": { + "clean": "docker rm $(docker ps -a -q --filter 'network=kadence_default')", + "coverage": "istanbul cover _mocha -- --exit --recursive", + "e2e-tests": "mocha --exit test/*.e2e.js", + "integration-tests": "mocha --exit test/*.integration.js", + "linter": "eslint ./index.js ./lib", + "start": "docker-compose up --build --force-recreate --always-recreate-deps", + "test": "npm run unit-tests && npm run integration-tests && npm run e2e-tests && npm run linter", + "unit-tests": "mocha --exit test/*.unit.js" + }, + "version": "7.0.0-unstable" +} diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..b10c51c --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,24 @@ +{ + "env": { + "node": true, + "es6": true + }, + "globals": { + "it": true, + "describe": true, + "before": true, + "after": true, + "beforeEach": true, + "afterEach": true + }, + "rules": { + "max-statements": [ + 2, + 100 + ], + "complexity": [ + 2, + 100 + ] + } +} diff --git a/test/bucket.unit.js b/test/bucket.unit.js new file mode 100644 index 0000000..69c2c95 --- /dev/null +++ b/test/bucket.unit.js @@ -0,0 +1,165 @@ +'use strict' + +const { expect } = require('chai'); +const Bucket = require('../lib/bucket'); + + +describe('@class Bucket', function() { + + const bucket = new Bucket(); + const entries = [ + '0000000000000000000000000000000000000000', + '0000000000000000000000000000000000000001', + '0000000000000000000000000000000000000002', + '0000000000000000000000000000000000000003', + '0000000000000000000000000000000000000004', + '0000000000000000000000000000000000000005', + '0000000000000000000000000000000000000006', + '0000000000000000000000000000000000000007', + '0000000000000000000000000000000000000008', + '0000000000000000000000000000000000000009', + '0000000000000000000000000000000000000010', + '0000000000000000000000000000000000000011', + '0000000000000000000000000000000000000012', + '0000000000000000000000000000000000000013', + '0000000000000000000000000000000000000014', + '0000000000000000000000000000000000000015', + '0000000000000000000000000000000000000016', + '0000000000000000000000000000000000000017', + '0000000000000000000000000000000000000018', + '0000000000000000000000000000000000000019' + ]; + + describe('@method set', function() { + + it('should add each entry to the head', function() { + entries.forEach((entry) => bucket.set(entry, entry)); + [...bucket.keys()].forEach((key, i) => { + expect(entries.indexOf(key)).to.equal(entries.length - (i + 1)); + }); + }); + + it('should move existing contacts to the tail', function() { + bucket.set(entries[4], entries[4]); + expect([...bucket.keys()].pop()).to.equal(entries[4]); + }); + + it('should not add new contacts if bucket is full', function() { + expect( + bucket.set('0000000000000000000000000000000000000020') + ).to.equal(-1); + }); + + }); + + describe('@method indexOf', function() { + + it('should return -1 if not found', function() { + expect(bucket.indexOf('NOTVALIDKEY')).to.equal(-1); + }); + + it('should return the correct index', function() { + expect(bucket.indexOf(entries[6])).to.equal(13); + expect(bucket.indexOf(entries[4])).to.equal(19); + expect(bucket.indexOf(entries[19])).to.equal(0); + }); + + }); + + describe('@method getClosestToKey', function() { + + it('should return a sorted list of contacts by distance', function() { + expect(JSON.stringify( + [ + ...bucket.getClosestToKey( + '0000000000000000000000000000000000000010' + ).keys() + ] + )).to.equal(JSON.stringify([ + '0000000000000000000000000000000000000010', + '0000000000000000000000000000000000000011', + '0000000000000000000000000000000000000012', + '0000000000000000000000000000000000000013', + '0000000000000000000000000000000000000014', + '0000000000000000000000000000000000000015', + '0000000000000000000000000000000000000016', + '0000000000000000000000000000000000000017', + '0000000000000000000000000000000000000018', + '0000000000000000000000000000000000000019', + '0000000000000000000000000000000000000000', + '0000000000000000000000000000000000000001', + '0000000000000000000000000000000000000002', + '0000000000000000000000000000000000000003', + '0000000000000000000000000000000000000004', + '0000000000000000000000000000000000000005', + '0000000000000000000000000000000000000006', + '0000000000000000000000000000000000000007', + '0000000000000000000000000000000000000008', + '0000000000000000000000000000000000000009' + ])); + }); + + it('should exclude the provided key', function() { + expect(JSON.stringify( + [ + ...bucket.getClosestToKey( + '0000000000000000000000000000000000000010', + 20, + true + ).keys() + ] + )).to.equal(JSON.stringify([ + '0000000000000000000000000000000000000011', + '0000000000000000000000000000000000000012', + '0000000000000000000000000000000000000013', + '0000000000000000000000000000000000000014', + '0000000000000000000000000000000000000015', + '0000000000000000000000000000000000000016', + '0000000000000000000000000000000000000017', + '0000000000000000000000000000000000000018', + '0000000000000000000000000000000000000019', + '0000000000000000000000000000000000000000', + '0000000000000000000000000000000000000001', + '0000000000000000000000000000000000000002', + '0000000000000000000000000000000000000003', + '0000000000000000000000000000000000000004', + '0000000000000000000000000000000000000005', + '0000000000000000000000000000000000000006', + '0000000000000000000000000000000000000007', + '0000000000000000000000000000000000000008', + '0000000000000000000000000000000000000009' + ])); + }); + + }); + + describe('@property length', function() { + + it('should alias the size property', function() { + expect(bucket.length).to.equal(bucket.size); + expect(bucket.length).to.equal(20); + }); + + }); + + describe('@property head', function() { + + it('should return the head contact', function() { + expect(bucket.head[0]).to.equal( + '0000000000000000000000000000000000000019' + ); + }); + + }); + + describe('@property tail', function() { + + it('should return the tail contact', function() { + expect(bucket.tail[0]).to.equal( + '0000000000000000000000000000000000000004' + ); + }); + + }); + +}); diff --git a/test/contact-list.unit.js b/test/contact-list.unit.js new file mode 100644 index 0000000..62d3775 --- /dev/null +++ b/test/contact-list.unit.js @@ -0,0 +1,130 @@ +'use strict'; + +const { expect } = require('chai'); +const ContactList = require('../lib/contact-list'); + +describe('@class ContactList', function() { + describe('@property closest', function() { + it('returns the closest node to the key', function() { + let contact = { hostname: 'localhost', port: 8080 }; + let shortlist = new ContactList( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + [ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact] + ] + ); + expect(shortlist.closest[0]).to.equal( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc127' + ); + }); + }); + + describe('@property active', function() { + it('returns nodes that have responded', function() { + let contact = { hostname: 'localhost', port: 8080 }; + let shortlist = new ContactList( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + [ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact] + ] + ); + shortlist.responded( + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact] + ); + expect(shortlist.active.length).to.equal(1); + expect(shortlist.active).to.deep.equal( + [['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact]] + ); + }); + }); + + describe('@property uncontacted', function() { + it('returns uncontacted nodes', function() { + let contact = { hostname: 'localhost', port: 8080 }; + let shortlist = new ContactList( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + [ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact] + ] + ); + shortlist.contacted( + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact] + ); + expect(shortlist.uncontacted.length).to.equal(2); + expect(shortlist.uncontacted).to.not.deep.include( + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact] + ); + }); + }); + + describe('@method add', function() { + it('adds new nodes in distance order', function() { + let contact = { hostname: 'localhost', port: 8080 }; + let shortlist = new ContactList( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + [ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact] + ] + ); + shortlist.add( + [ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc124', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc129', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc123', contact] + ] + ); + expect(shortlist.closest[0]).to.equal( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc127' + ); + expect(shortlist._contacts.slice(-1)[0][0]).to.equal( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc129' + ); + }); + + it('does not insert duplicates', function() { + let contact = { hostname: 'localhost', port: 8080 }; + let shortlist = new ContactList( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + [ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact] + ] + ); + shortlist.add( + [ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact] + ] + ); + expect(shortlist._contacts.length).to.equal(3); + }); + + it('returns the inserted nodes', function() { + let contact = { hostname: 'localhost', port: 8080 }; + let shortlist = new ContactList( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + [ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact] + ] + ); + let added = shortlist.add( + [ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc129', contact] + ] + ); + expect(added.length).to.equal(1); + expect(added[0][0]).to.equal('ea48d3f07a5241291ed0b4cab6483fa8b8fcc129'); + }); + }); +}); \ No newline at end of file diff --git a/test/control.unit.js b/test/control.unit.js new file mode 100644 index 0000000..909e0b9 --- /dev/null +++ b/test/control.unit.js @@ -0,0 +1,54 @@ +'use strict'; + +const { expect } = require('chai'); +const Control = require('../lib/control'); +const RoutingTable = require('../lib/routing-table'); +const utils = require('../lib/utils'); + + +describe('@class Control', function() { + + describe('@method listMethods', function() { + + it('should return all the supported methods', function(done) { + const control = new Control({}); + control.listMethods((err, results) => { + expect(results).to.have.lengthOf(7); + done(); + }); + }); + + }); + + describe('@method getProtocolInfo', function() { + + it('should return general information', function(done) { + const control = new Control({ + router: new RoutingTable( + Buffer.from(utils.getRandomKeyString(), 'hex') + ), + identity: Buffer.from(utils.getRandomKeyString(), 'hex'), + contact: { + hostname: 'localhost', + port: 8080 + } + }); + control.node.router.addContactByNodeId( + utils.getRandomKeyString(), + { hostname: 'localhost', port: 8081 } + ); + control.getProtocolInfo((err, result) => { + expect(typeof result.versions.software).to.equal('string'); + expect(typeof result.versions.protocol).to.equal('string'); + expect(typeof result.identity).to.equal('string'); + expect(typeof result.contact.port).to.equal('number'); + expect(typeof result.contact.hostname).to.equal('string'); + expect(Array.isArray(result.peers)).to.equal(true); + expect(result.peers).to.have.lengthOf(1); + done(); + }); + }); + + }); + +}); diff --git a/test/fixtures/node-generator.js b/test/fixtures/node-generator.js new file mode 100644 index 0000000..bf02c53 --- /dev/null +++ b/test/fixtures/node-generator.js @@ -0,0 +1,39 @@ +'use strict'; + +const bunyan = require('bunyan'); +const levelup = require('levelup'); +const memdown = require('memdown'); +const kadence = require('../..'); +const encoding = require('encoding-down'); + +let startPort = 65000; + + +module.exports = function(numNodes, Transport) { + + const nodes = []; + + const logger = bunyan.createLogger({ + levels: ['fatal'], + name: 'node-kademlia' + }); + const storage = levelup(encoding(memdown())); + + function createNode() { + let transport = new Transport(); + let contact = { hostname: 'localhost', port: startPort-- }; + + return new kadence.KademliaNode({ + transport: transport, + contact: contact, + storage: storage, + logger: logger + }); + } + + for (let i = 0; i < numNodes; i++) { + nodes.push(createNode()); + } + + return nodes; +}; diff --git a/test/fixtures/transport-fake.js b/test/fixtures/transport-fake.js new file mode 100644 index 0000000..5f4faa0 --- /dev/null +++ b/test/fixtures/transport-fake.js @@ -0,0 +1,38 @@ +'use strict'; + +const { Duplex: DuplexStream } = require('stream'); + + +/** + * Represents a fake transport adapter that implements the read/write interface + * and just emits a send and recv event + */ +class FakeTransport extends DuplexStream { + + /** + * @constructor + */ + constructor() { + super({ objectMode: true }); + } + + /** + * @private + */ + _read() { + this.once('recv', (data) => this.push(data)); + } + + /** + * @private + */ + _write(data, enc, callback) { + this.emit('send', data); + callback(); + } + + listen() {} + +} + +module.exports = FakeTransport; diff --git a/test/messenger.unit.js b/test/messenger.unit.js new file mode 100644 index 0000000..ab2a7ef --- /dev/null +++ b/test/messenger.unit.js @@ -0,0 +1,108 @@ +'use strict'; + +const { expect } = require('chai'); +const jsonrpc = require('jsonrpc-lite'); +const Messenger = require('../lib/messenger'); + + +describe('@class Messenger', function() { + + describe('@property serializer', function() { + + it('should use the supplied serializer', function(done) { + let messenger = new Messenger({ + serializer: (obj, cb) => cb(null, 'SERIALIZED') + }); + messenger.serializer.create().once('data', (data) => { + expect(data).to.equal('SERIALIZED'); + done(); + }).write({}); + }); + + }); + + describe('@property deserializer', function() { + + it('should use the supplied deserializer', function(done) { + let messenger = new Messenger({ + deserializer: (obj, cb) => cb(null, 'DESERIALIZED') + }); + messenger.deserializer.create().once('data', (data) => { + expect(data).to.equal('DESERIALIZED'); + done(); + }).write(Buffer.from('test')); + }); + + it('should emit error if bad data to deserialize', function(done) { + let messenger = new Messenger({ + deserializer: (obj, cb) => cb(null, 'DESERIALIZED') + }); + let deserializer = messenger.deserializer.create(); + deserializer.once('error', (err) => { + expect(err.message).to.equal('Cannot deserialize non-buffer chunk'); + done(); + }); + setImmediate(() => deserializer.write({})); + }); + + }); + +}); + +describe('@static Messenger#JsonRpcSerializer', function() { + + it('should serialize the data as a json-rpc payload', function(done) { + Messenger.JsonRpcSerializer([ + { method: 'TEST', params: ['test'] }, + ['SENDER', {}], + { hostname: 'localhost', port: 8080 } + ], (err, [id, buffer, receiver]) => { + expect(typeof id).to.equal('string'); + expect(Buffer.isBuffer(buffer)).to.equal(true); + let [message, identity] = jsonrpc.parse(buffer.toString()); + expect(message.payload.params[0]).to.equal('test'); + expect(identity.payload.params[0]).to.equal('SENDER'); + expect(receiver.hostname).to.equal('localhost'); + done(); + }); + }); + + it('should callback error with invalid data', function(done) { + Messenger.JsonRpcSerializer([ + { invalid: { data: 'object' } }, + { identity: 'SENDER' }, + { identity: 'RECEIVER' } + ], (err) => { + expect(err.message).to.equal('Invalid message type "invalid"'); + done(); + }); + }); + +}); + +describe('@static Messenger#JsonRpcDeserializer', function() { + + it('should deserialize the buffer as a json object', function(done) { + Messenger.JsonRpcDeserializer(Buffer.from(JSON.stringify([ + { jsonrpc: '2.0', id: 'x', method: 'TEST', params: ['test'] }, + { jsonrpc: '2.0', method: 'IDENTIFY', params: ['SENDER', {}] } + ])), (err, [message, contact]) => { + expect(message.payload.params[0]).to.equal('test'); + expect(message.payload.id).to.equal('x'); + expect(message.payload.method).to.equal('TEST'); + expect(contact.payload.params[0]).to.equal('SENDER'); + done(); + }); + }); + + it('should callback error with invalid data', function(done) { + Messenger.JsonRpcDeserializer(Buffer.from(JSON.stringify([ + { invalid: { data: 'object' } }, + { jsonrpc: '2.0', method: 'IDENTIFY', params: ['SENDER', {}] } + ])), (err) => { + expect(err.message).to.equal('Invalid message type "invalid"'); + done(); + }); + }); + +}); diff --git a/test/node-abstract.unit.js b/test/node-abstract.unit.js new file mode 100644 index 0000000..c22e619 --- /dev/null +++ b/test/node-abstract.unit.js @@ -0,0 +1,603 @@ +'use strict'; + +const { expect } = require('chai'); +const sinon = require('sinon'); +const AbstractNode = require('../lib/node-abstract'); +const FakeTransport = require('./fixtures/transport-fake'); +const levelup = require('levelup'); +const memdown = require('memdown'); +const storage = levelup('test:node-abstract', memdown); +const bunyan = require('bunyan'); +const constants = require('../lib/constants'); +const utils = require('../lib/utils'); +const { EventEmitter } = require('events'); + + +describe('@class AbstractNode', function() { + + let logger, logwarn, transport, abstractNode, clock; + + before(() => { + clock = sinon.useFakeTimers(Date.now(), 'setInterval'); + logger = bunyan.createLogger({ + name: 'test:node-abstract:unit', + level: 'fatal' + }); + logwarn = sinon.stub(logger, 'warn'); + transport = new FakeTransport(); + abstractNode = new AbstractNode({ + contact: { name: 'test:node-abstract:unit' }, + storage, + transport, + logger, + identity: utils.getRandomKeyString() + }); + }); + + after(() => { + clock.restore(); + }); + + describe('@private _init', function() { + + it('should log warnings on transport error', function(done) { + abstractNode.transport.emit('error', new Error('Transport error')); + setImmediate(() => { + expect(logwarn.called).to.equal(true); + logwarn.reset(); + done(); + }); + }); + + it('should call the _timeout method on interval', function(done) { + let _timeout = sinon.stub(abstractNode, '_timeout'); + setImmediate(() => { + _timeout.restore(); + expect(_timeout.called).to.equal(true); + done(); + }, constants.T_RESPONSETIMEOUT); + clock.tick(constants.T_RESPONSETIMEOUT); + }); + + }); + + describe('@private _process', function() { + + it('should call receive with error arguments', function(done) { + let _updateContact = sinon.stub(abstractNode, '_updateContact'); + let write = sinon.stub(); + let emitter = new EventEmitter(); + emitter.write = write; + let create = sinon.stub(abstractNode.rpc.serializer, 'create') + .returns(emitter); + let receive = sinon.stub(abstractNode, 'receive') + .callsFake(function(req, res) { + receive.restore(); + _updateContact.restore(); + expect(_updateContact.called).to.equal(true); + expect(req.method).to.equal('PING'); + expect(req.id).to.equal('message id'); + expect(req.params).to.have.lengthOf(0); + expect(req.contact[0]).to.equal('SENDERID'); + expect(req.contact[1].hostname).to.equal('localhost'); + expect(req.contact[1].port).to.equal(8080); + res.error('Error', 500); + create.restore(); + let writeArgs = write.args[0][0]; + expect(writeArgs[0].id).to.equal('message id'); + expect(writeArgs[0].error.message).to.equal('Error'); + expect(typeof writeArgs[1][0]).to.equal('string'); + expect(writeArgs[1][1].name).to.equal('test:node-abstract:unit'); + expect(writeArgs[2][1].hostname).to.equal('localhost'); + expect(writeArgs[2][1].port).to.equal(8080); + done(); + }); + abstractNode._process([ + { + type: 'request', + payload: { + id: 'message id', + method: 'PING', + params: [] + } + }, + { + type: 'notification', + payload: { + params: [ + 'SENDERID', + { + hostname: 'localhost', + port: 8080 + } + ] + } + } + ]); + }); + + it('should call receive with success arguments', function(done) { + let _updateContact = sinon.stub(abstractNode, '_updateContact'); + let write = sinon.stub(); + let emitter = new EventEmitter(); + emitter.write = write; + let create = sinon.stub(abstractNode.rpc.serializer, 'create') + .returns(emitter); + let receive = sinon.stub(abstractNode, 'receive') + .callsFake(function(req, res) { + receive.restore(); + _updateContact.restore(); + expect(_updateContact.called).to.equal(true); + expect(req.method).to.equal('PING'); + expect(req.id).to.equal('message id'); + expect(req.params).to.have.lengthOf(0); + expect(req.contact[0]).to.equal('SENDERID'); + expect(req.contact[1].hostname).to.equal('localhost'); + expect(req.contact[1].port).to.equal(8080); + res.send([]); + create.restore(); + let writeArgs = write.args[0][0]; + expect(writeArgs[0].id).to.equal('message id'); + expect(writeArgs[0].result).to.have.lengthOf(0); + expect(typeof writeArgs[1][0]).to.equal('string'); + expect(writeArgs[1][1].name).to.equal('test:node-abstract:unit'); + expect(writeArgs[2][1].hostname).to.equal('localhost'); + expect(writeArgs[2][1].port).to.equal(8080); + done(); + }); + abstractNode._process([ + { + type: 'request', + payload: { + id: 'message id', + method: 'PING', + params: [] + } + }, + { + type: 'notification', + payload: { + params: [ + 'SENDERID', + { + hostname: 'localhost', + port: 8080 + } + ] + } + } + ]); + }); + + it('should log a warning if not expecting response', function(done) { + let _updateContact = sinon.stub(abstractNode, '_updateContact'); + abstractNode._process([ + { + type: 'success', + payload: { + id: 'message id', + result: [] + } + }, + { + type: 'notification', + payload: { + params: [ + 'SENDERID', + { + hostname: 'localhost', + port: 8080 + } + ] + } + } + ]); + _updateContact.restore(); + expect(logwarn.called).to.equal(true); + logwarn.reset(); + done(); + }); + + it('should execute the expected handler with an error', function(done) { + let _updateContact = sinon.stub(abstractNode, '_updateContact'); + abstractNode._pending.set('message id', { + timestamp: Date.now(), + handler: (err, result) => { + expect(result).to.equal(null); + expect(err.message).to.equal('Error response'); + done(); + }, + fingerprint: 'SENDERID' + }); + abstractNode._process([ + { + type: 'error', + payload: { + id: 'message id', + error: { + code: 0, + message: 'Error response' + } + } + }, + { + type: 'notification', + payload: { + params: [ + 'SENDERID', + { + hostname: 'localhost', + port: 8080 + } + ] + } + } + ]); + _updateContact.restore(); + }); + + it('should execute the expected handler with data', function(done) { + let _updateContact = sinon.stub(abstractNode, '_updateContact'); + abstractNode._pending.set('message id', { + timestamp: Date.now(), + handler: (err, result) => { + expect(result).to.have.lengthOf(0); + expect(err).to.equal(null); + done(); + }, + fingerprint: 'SENDERID' + }); + abstractNode._process([ + { + type: 'success', + payload: { + id: 'message id', + result: [] + } + }, + { + type: 'notification', + payload: { + params: [ + 'SENDERID', + { + hostname: 'localhost', + port: 8080 + } + ] + } + } + ]); + _updateContact.restore(); + }); + + it('should error with unexpected fingerprint', function(done) { + let _updateContact = sinon.stub(abstractNode, '_updateContact'); + abstractNode._pending.set('message id', { + timestamp: Date.now(), + handler: (err, result) => { + expect(result).to.equal(null); + expect(err.message).to.equal( + 'Response fingerprint differs from request destination' + ); + done(); + }, + fingerprint: 'SENDERID1' + }); + abstractNode._process([ + { + type: 'success', + payload: { + id: 'message id', + result: [] + } + }, + { + type: 'notification', + payload: { + params: [ + 'SENDERID2', + { + hostname: 'localhost', + port: 8080 + } + ] + } + } + ]); + _updateContact.restore(); + + }); + + + it('should execute the expected handler with data', function(done) { + let _updateContact = sinon.stub(abstractNode, '_updateContact'); + abstractNode._pending.set('message id', { + timestamp: Date.now(), + handler: (err, result) => { + expect(result).to.have.lengthOf(0); + expect(err).to.equal(null); + done(); + }, + fingerprint: Buffer.alloc(20, 0).toString('hex') + }); + abstractNode._process([ + { + type: 'success', + payload: { + id: 'message id', + result: [] + } + }, + { + type: 'notification', + payload: { + params: [ + 'SENDERID', + { + hostname: 'localhost', + port: 8080 + } + ] + } + } + ]); + _updateContact.restore(); + }); + + }); + + describe('@private _timeout', function() { + + it('should call handlers of old requests and reap references', function() { + let handler0 = sinon.stub(); + let handler1 = sinon.stub(); + let handler2 = sinon.stub(); + abstractNode._pending.set(0, { + handler: handler0, + timestamp: Date.now() + }); + abstractNode._pending.set(1, { + handler: handler1, + timestamp: Date.now() - constants.T_RESPONSETIMEOUT - 200 + }); + abstractNode._pending.set(2, { + handler: handler2, + timestamp: 0 + }); + abstractNode._timeout(); + expect(handler0.called).to.equal(false); + expect(handler1.called).to.equal(true); + expect(handler1.args[0][0]).to.be.instanceOf(Error); + expect(handler2.called).to.equal(true); + expect(handler2.args[0][0]).to.be.instanceOf(Error); + expect(abstractNode._pending.size).to.equal(1); + }); + + }); + + describe('@private _updateContact', function() { + + it('should call RoutingTable#addContactByNodeId', function() { + let _addContactByNodeId = sinon.stub(abstractNode.router, + 'addContactByNodeId'); + abstractNode._updateContact('node id', {}); + _addContactByNodeId.restore(); + expect( + _addContactByNodeId.calledWithMatch('node id', {}) + ).to.equal(true); + }); + + it('should not call RoutingTable#addContactByNodeId', function() { + let _addContactByNodeId = sinon.stub(abstractNode.router, + 'addContactByNodeId'); + abstractNode._updateContact(abstractNode.identity.toString('hex'), {}); + _addContactByNodeId.restore(); + expect( + _addContactByNodeId.calledWithMatch( + abstractNode.identity.toString('hex'), + {} + ) + ).to.equal(false); + }); + + }); + + describe('@private _stack', function() { + + it('should call all functions in the stack with args', function(done) { + let mw1 = sinon.stub().callsArg(2); + let mw2 = sinon.stub().callsArg(2); + let mw3 = sinon.stub().callsArg(2); + let mw4 = sinon.stub().callsArg(2); + let request = {}; + let response = {}; + abstractNode._testStack = { + '*': [mw1, mw2, mw3, mw4] + }; + abstractNode._stack('_testStack', '*', [request, response], () => { + delete abstractNode._testStack; + expect(mw1.calledWithMatch(request, response)).to.equal(true); + expect(mw2.calledWithMatch(request, response)).to.equal(true); + expect(mw3.calledWithMatch(request, response)).to.equal(true); + expect(mw4.calledWithMatch(request, response)).to.equal(true); + done(); + }); + }); + + it('should trap exceptions in middleware and callback', function(done) { + let mw1 = sinon.stub().callsArg(2); + let mw2 = sinon.stub().throws(new Error('Syntax error')); + let mw3 = sinon.stub().callsArg(2); + let mw4 = sinon.stub().callsArg(2); + let request = {}; + let response = {}; + abstractNode._testStack = { + '*': [mw1, mw2, mw3, mw4] + }; + abstractNode._stack('_testStack', '*', [request, response], () => { + delete abstractNode._testStack; + expect(mw1.calledWithMatch(request, response)).to.equal(true); + expect(mw2.calledWithMatch(request, response)).to.equal(true); + expect(mw3.calledWithMatch(request, response)).to.equal(false); + expect(mw4.calledWithMatch(request, response)).to.equal(false); + done(); + }); + }); + + it('should fire callback if no stack exists', function(done) { + abstractNode._stack('_middlewares', 'NOTAMETHOD', [{}, {}], done); + }); + + }); + + describe('@private _middleware', function() { + + it('should call _stack with the correct arguments', function() { + let _stack = sinon.stub(abstractNode, '_stack'); + let args = ['REQUEST', 'RESPONSE']; + abstractNode._middleware(...args); + _stack.restore(); + expect( + _stack.calledWithMatch('_middleware', 'REQUEST', 'RESPONSE') + ).to.equal(true); + }); + + }); + + describe('@private _error', function() { + + it('should call _stack with the correct arguments', function() { + let _stack = sinon.stub(abstractNode, '_stack'); + let args = ['REQUEST', 'RESPONSE']; + abstractNode._error(...args); + _stack.restore(); + expect( + _stack.calledWithMatch('_errors', 'REQUEST', 'RESPONSE') + ).to.equal(true); + }); + + }); + + describe('@method send', function() { + + it('should write to serializer and queue handler', function() { + let write = sinon.stub(); + let emitter = new EventEmitter(); + emitter.write = write; + let create = sinon.stub(abstractNode.rpc.serializer, 'create') + .returns(emitter); + let handler = sinon.stub(); + abstractNode.send('PING', [], ['000000', { + hostname: 'localhost', + port: 8080 + }], handler); + let [calledWith] = write.args[0]; + create.restore(); + expect(calledWith[0].method).to.equal('PING'); + expect(calledWith[0].params).to.have.lengthOf(0); + expect(typeof calledWith[0].id).to.equal('string'); + expect(calledWith[1][0]).to.equal(abstractNode.identity.toString('hex')); + expect(calledWith[1][1].name).to.equal('test:node-abstract:unit'); + expect(calledWith[2][1].hostname).to.equal('localhost'); + expect(calledWith[2][1].port).to.equal(8080); + }); + + it('should error and not send if invalid target', function(done) { + abstractNode.send('PING', [], { + hostname: 'localhost', + port: 8080 + }, (err) => { + expect(err.message).to.equal( + 'Refusing to send message to invalid contact' + ); + done(); + }); + }); + + it('should error and not send if invalid target', function(done) { + abstractNode.send('PING', [], [ + '0000000000000000000000000000000000000000', + null + ], (err) => { + expect(err.message).to.equal( + 'Refusing to send message to invalid contact' + ); + done(); + }); + }); + + }); + + describe('@method use', function() { + + it('should use the * method if none supplied', function() { + abstractNode.use((req, res, next) => next()); + expect(abstractNode._middlewares['*']).to.have.lengthOf(1); + }); + + it('should place it in _errors if 4 args', function() { + abstractNode.use((err, req, res, next) => next()); + expect(abstractNode._errors['*']).to.have.lengthOf(1); + }); + + it('should use a custom method stack', function() { + abstractNode.use('TEST', (req, res, next) => next()); + expect(abstractNode._middlewares.TEST).to.have.lengthOf(1); + }); + + }); + + describe('@method plugin', function() { + + it('should throw if not a function', function() { + expect(function() { + abstractNode.plugin({}); + }).to.throw(Error, 'Invalid plugin supplied'); + }); + + it('should call the function with itself as the first arg', function(done) { + abstractNode.plugin(function(node) { + expect(node).to.equal(abstractNode); + done(); + }); + }); + + }); + + describe('@method receive', function() { + + it('should pass the args through all middleware', function(done) { + let _middleware = sinon.stub(abstractNode, '_middleware').callsArg(2); + let _error = sinon.stub(abstractNode, '_error').callsArg(2); + let args = [{ method: 'TEST' }, {}]; + abstractNode.receive(...args); + setTimeout(() => { + expect(_middleware.calledWithMatch('*', args)).to.equal(true); + expect(_middleware.calledWithMatch('TEST', args)).to.equal(true); + expect( + _error.calledWithMatch('*', [null, ...args]) + ).to.equal(true); + expect( + _error.calledWithMatch('TEST', [null, ...args]) + ).to.equal(true); + done(); + }, 50); + }); + + }); + + describe('@method listen', function() { + + it('should add error middleware and init transport', function() { + let _listen = sinon.stub(abstractNode.transport, 'listen'); + abstractNode._errors['*'] = []; + abstractNode.listen(8080, 'localhost'); + _listen.restore(); + expect(_listen.calledWithMatch(8080, 'localhost')).to.equal(true); + expect(abstractNode._errors['*']).to.have.lengthOf(2); + }); + + }); + +}); diff --git a/test/node-kademlia+transport-http.integration.js b/test/node-kademlia+transport-http.integration.js new file mode 100644 index 0000000..1c70904 --- /dev/null +++ b/test/node-kademlia+transport-http.integration.js @@ -0,0 +1,68 @@ +'use strict'; + +const { expect } = require('chai'); +const kad = require('..'); + +describe('@class AbstractNode / @class HTTPTransport', function() { + + const [node1, node2] = require('./fixtures/node-generator')( + 2, + kad.HTTPTransport + ); + + before(function() { + node1.listen(node1.contact.port, node1.contact.hostname); + node2.listen(node2.contact.port, node2.contact.hostname); + }); + + after(function() { + node1.transport.server.close(); + node2.transport.server.close(); + }); + + it('node1 should send TEST to node2 and receive success', function(done) { + node2.use('TEST', function(request, response) { + expect(request.params[0]).to.equal('test parameter'); + response.send(['test result']); + }); + node1.send( + 'TEST', + ['test parameter'], + [node2.identity.toString('hex'), node2.contact], + function(err, result) { + expect(err).to.equal(null); + expect(result[0]).to.equal('test result'); + done(); + } + ); + }); + + it('node2 should send TEST to node2 and receive error', function(done) { + node1.use('TEST', function(request, response) { + expect(request.params[0]).to.equal('test parameter'); + response.error('test error', 500); + }); + node2.send( + 'TEST', + ['test parameter'], + [node1.identity.toString('hex'), node1.contact], + function(err) { + expect(err.message).to.equal('test error'); + done(); + } + ); + }); + + it('node1 should send to invalid and immediately callback', function(done) { + node1.send( + 'TEST', + ['test parameter'], + [node2.identity.toString('hex'), { hostname: 'localhost', port: 1 }], + function(err) { + expect(err.message).to.equal('connect ECONNREFUSED 127.0.0.1:1'); + done(); + } + ); + }); + +}); diff --git a/test/node-kademlia+transport-udp.integration.js b/test/node-kademlia+transport-udp.integration.js new file mode 100644 index 0000000..14953cc --- /dev/null +++ b/test/node-kademlia+transport-udp.integration.js @@ -0,0 +1,56 @@ +'use strict'; + +const { expect } = require('chai'); +const kad = require('..'); + +describe('@class AbstractNode / @class UDPTransport', function() { + + const [node1, node2] = require('./fixtures/node-generator')( + 2, + kad.UDPTransport + ); + + before(function() { + node1.listen(node1.contact.port, node1.contact.hostname); + node2.listen(node2.contact.port, node2.contact.hostname); + }); + + after(function() { + node1.transport.socket.close(); + node2.transport.socket.close(); + }); + + it('node1 should send TEST to node2 and receive success', function(done) { + node2.use('TEST', function(request, response) { + expect(request.params[0]).to.equal('test parameter'); + response.send(['test result']); + }); + node1.send( + 'TEST', + ['test parameter'], + [node2.identity.toString('hex'), node2.contact], + function(err, result) { + expect(err).to.equal(null); + expect(result[0]).to.equal('test result'); + done(); + } + ); + }); + + it('node2 should send TEST to node2 and receive error', function(done) { + node1.use('TEST', function(request, response) { + expect(request.params[0]).to.equal('test parameter'); + response.error('test error', 500); + }); + node2.send( + 'TEST', + ['test parameter'], + [node1.identity.toString('hex'), node1.contact], + function(err) { + expect(err.message).to.equal('test error'); + done(); + } + ); + }); + +}); diff --git a/test/node-kademlia.e2e.js b/test/node-kademlia.e2e.js new file mode 100644 index 0000000..ec5a467 --- /dev/null +++ b/test/node-kademlia.e2e.js @@ -0,0 +1,198 @@ +'use strict'; + +const { expect } = require('chai'); +const kadence= require('..'); +const network = require('./fixtures/node-generator'); +const async = require('async'); +const TOTAL_NODES = 50; + + +function registerEndToEndSuite(transportName, transportAdapter) { + + describe(`@class KademliaNode / @class ${transportName})`, function() { + this.timeout(20000); + + let nodes, seed, pairs; + + before(function(done) { + nodes = network(TOTAL_NODES, transportAdapter); + async.each(nodes, (node, done) => { + node.listen(node.contact.port, node.contact.hostname, done); + }, function() { + seed = nodes.shift(); + nodes.forEach((node) => { + seed.router.addContactByNodeId( + node.identity.toString('hex'), + node.contact + ); + }); + pairs = nodes.map(() => { + return [ + kadence.utils.getRandomKeyString(), + kadence.utils.getRandomKeyString() + ]; + }); + done(); + }); + }); + + after(function() { + nodes.forEach((node) => { + switch (transportName) { + case 'UDPTransport': + node.transport.socket.close(); + break; + case 'HTTPTransport': + node.transport.server.close(); + break; + default: + } + }); + }); + + describe('@method join', function() { + + this.timeout(400000); + + it('all nodes should succeed in joining the network', function(done) { + async.eachLimit(nodes, 3, function(node, next) { + node.join([ + seed.identity.toString('hex'), + seed.contact + ], next); + }, function(err) { + expect(err).to.equal(null); + nodes.forEach((node) => { + expect(node.router.size >= kadence.constants.K).to.equal(true); + }); + done(); + }); + }); + + }); + + describe('@method iterativeFindNode', function() { + + it('all nodes should be able to find K contacts', function(done) { + async.eachLimit(nodes, 3, function(node, next) { + node.iterativeFindNode( + node.identity.toString('hex'), + function(err, result) { + expect(err).to.equal(null); + expect(result).to.have.lengthOf(kadence.constants.K); + next(); + } + ); + }, done); + }); + + it('all nodes should be able to find existing contact', function(done) { + this.timeout(400000); + async.eachSeries(nodes, function(node, next) { + async.eachSeries(nodes, function(target, next) { + if (Buffer.compare(node.identity, target.identity) === 0) { + return next(); + } + node.iterativeFindNode( + target.identity.toString('hex'), + function(err, result) { + expect(err).to.equal(null); + expect(result.map(c => c[0]) + .includes(target.identity.toString('hex'))).to.equal(true); + next(); + } + ); + }, next); + }, done); + }); + + it('all nodes should find the closest node to a key', function(done) { + let key = kadence.utils.getRandomKeyString(); + let closest = nodes.map(node => { + return { + identity: node.identity, + distance: kadence.utils.getDistance(node.identity, key) + }; + }).sort( (a, b) => { + return kadence.utils.compareKeyBuffers( + Buffer.from(a.distance, 'hex'), + Buffer.from(b.distance, 'hex') + ); + })[0].identity.toString('hex') + + async.eachLimit(nodes, 3, function(node, next) { + node.iterativeFindNode( + key, + function(err, result) { + expect(err).to.equal(null); + expect(result[0][0]).to.equal(closest); + next(); + } + ); + }, done); + }); + + }); + + describe('@method iterativeStore', function() { + + it('all nodes should be able to store key-values', function(done) { + async.eachOfLimit(nodes, 3, function(node, index, next) { + let [key, value] = pairs[index]; + node.iterativeStore(key, value, function(err, totalStored) { + expect(totalStored > 10).to.equal(true); + next(); + }); + }, done); + }); + + }); + + describe('@method iterativeFindValue', function() { + + it('all nodes should be able to retrieve key-values', function(done) { + async.eachOfLimit(nodes, 3, function(node, index, next) { + let [key, value] = pairs[index]; + node.iterativeFindValue(key, function(err, result) { + expect(value).to.equal(result.value); + next(); + }); + }, (err) => { + // Use a timeout here because if we close the sockets early, + // responses from STORE operations will fail + setTimeout(done.bind(this, err), 3000); + }); + }); + + }); + + describe('@method expire / @method replicate', function() { + + before(function(done) { + async.eachLimit(nodes, 3, function(node, next) { + node.replicate(() => node.expire(() => next())); + }, done); + }); + + it('all nodes should be able to retrieve after expire', function(done) { + async.eachOfLimit(nodes, 3, function(node, index, next) { + let [key, value] = pairs[index]; + node.iterativeFindValue(key, function(err, result) { + expect(value).to.equal(result.value); + next(); + }); + }, (err) => { + // Use a timeout here because if we close the sockets early, + // responses from STORE operations will fail + setTimeout(done.bind(this, err), 3000); + }); + }); + + }); + + }); + +} + +registerEndToEndSuite('UDPTransport', kadence.UDPTransport); +registerEndToEndSuite('HTTPTransport', kadence.HTTPTransport); diff --git a/test/node-kademlia.unit.js b/test/node-kademlia.unit.js new file mode 100644 index 0000000..1681c07 --- /dev/null +++ b/test/node-kademlia.unit.js @@ -0,0 +1,792 @@ +'use strict'; + +const { Readable: ReadableStream } = require('stream'); +const { expect } = require('chai'); +const sinon = require('sinon'); +const utils = require('../lib/utils'); +const KademliaNode = require('../lib/node-kademlia'); +const FakeTransport = require('./fixtures/transport-fake'); +const levelup = require('levelup'); +const memdown = require('memdown'); +const storage = levelup('test:node-kademlia', memdown); +const bunyan = require('bunyan'); +const constants = require('../lib/constants'); +const ms = require('ms'); + + +describe('@class KademliaNode', function() { + + this.timeout(12000) + + let logger, transport, kademliaNode, clock; + + before(() => { + clock = sinon.useFakeTimers(Date.now(), 'setInterval'); + logger = bunyan.createLogger({ + name: 'test:node-abstract:unit', + level: 'fatal' + }); + transport = new FakeTransport(); + kademliaNode = new KademliaNode({ + contact: { name: 'test:node-kademlia:unit' }, + storage, + transport, + logger, + identity: Buffer.from('aa48d3f07a5241291ed0b4cab6483fa8b8fcc128', 'hex') + }); + }); + + describe('@private _updateContact', function() { + + it('should add the contact to the routing table', function(done) { + let contact = { hostname: 'localhost', port: 8080 } + kademliaNode._updateContact( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', + contact + ); + setImmediate(() => { + expect(kademliaNode.router.getContactByNodeId( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc128' + )).to.equal(contact); + done(); + }); + }); + + it('should not add itself to the routing table', function() { + let contact = { hostname: 'localhost', port: 8080 } + kademliaNode._updateContact( + 'aa48d3f07a5241291ed0b4cab6483fa8b8fcc128', + contact + ); + expect(kademliaNode.router.getContactByNodeId( + 'aa48d3f07a5241291ed0b4cab6483fa8b8fcc128' + )).to.equal(undefined); + }); + + it('should replace the head contact if ping fails', function(done) { + let bucketIndex = kademliaNode.router.indexOf( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc128' + ); + let addContactByNodeId = sinon.stub( + kademliaNode.router, + 'addContactByNodeId' + ); + addContactByNodeId.onCall(0).returns( + [bucketIndex, kademliaNode.router.get(bucketIndex), -1] + ); + addContactByNodeId.onCall(1).returns( + [bucketIndex, kademliaNode.router.get(bucketIndex), 0] + ); + let ping = sinon.stub(kademliaNode, 'ping').callsArgWith( + 1, + new Error('Timeout') + ); + let removeContactByNodeId = sinon.stub( + kademliaNode.router, + 'removeContactByNodeId' + ); + kademliaNode._updateContact('ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', + { hostname: 'localhost', port: 8080 }); + setImmediate(() => { + addContactByNodeId.restore(); + ping.restore(); + removeContactByNodeId.restore(); + expect(addContactByNodeId.callCount).to.equal(2); + expect(removeContactByNodeId.callCount).to.equal(1); + done(); + }); + }); + + it('should do nothing if the head contact responds', function(done) { + let bucketIndex = kademliaNode.router.indexOf( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc128' + ); + let addContactByNodeId = sinon.stub( + kademliaNode.router, + 'addContactByNodeId' + ); + addContactByNodeId.onCall(0).returns( + [bucketIndex, kademliaNode.router.get(bucketIndex), -1] + ); + addContactByNodeId.onCall(1).returns( + [bucketIndex, kademliaNode.router.get(bucketIndex), 0] + ); + let ping = sinon.stub(kademliaNode, 'ping').callsArg(1); + let removeContactByNodeId = sinon.stub( + kademliaNode.router, + 'removeContactByNodeId' + ); + kademliaNode._updateContact('ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', + { hostname: 'localhost', port: 8080 }); + setImmediate(() => { + addContactByNodeId.restore(); + ping.restore(); + removeContactByNodeId.restore(); + expect(addContactByNodeId.callCount).to.equal(1); + expect(removeContactByNodeId.callCount).to.equal(0); + done(); + }); + }); + + }); + + describe('@method listen', function() { + + it('should use kad rules and setup refresh/replicate', function(done) { + let sandbox = sinon.sandbox.create(); + let clock2 = sinon.useFakeTimers(Date.now(), 'setTimeout'); + let use = sandbox.stub(kademliaNode, 'use'); + let refresh = sandbox.stub(kademliaNode, 'refresh'); + let replicate = sandbox.stub(kademliaNode, 'replicate').callsArg(0); + let expire = sandbox.stub(kademliaNode, 'expire'); + sandbox.stub(transport, 'listen'); + kademliaNode.listen(); + clock.tick(constants.T_REPLICATE); + clock2.tick(ms('30m')); // Account for convoy prevention + setImmediate(() => { + sandbox.restore(); + clock2.restore(); + expect(use.calledWithMatch('PING')).to.equal(true); + expect(use.calledWithMatch('STORE')).to.equal(true); + expect(use.calledWithMatch('FIND_NODE')).to.equal(true); + expect(use.calledWithMatch('FIND_VALUE')).to.equal(true); + expect(refresh.calledWithMatch(0)).to.equal(true); + expect(replicate.callCount).to.equal(1); + expect(expire.callCount).to.equal(1); + done(); + }); + }); + + }); + + describe('@method join', function() { + + it('should insert contact, lookup, and refresh buckets', function(done) { + let addContactByNodeId = sinon.stub( + kademliaNode.router, + 'addContactByNodeId' + ); + let iterativeFindNode = sinon.stub( + kademliaNode, + 'iterativeFindNode' + ).callsFake(function(p, cb) { + addContactByNodeId.restore(); + kademliaNode.router.addContactByNodeId( + 'da48d3f07a5241291ed0b4cab6483fa8b8fcc128', + {} + ); + kademliaNode.router.addContactByNodeId( + 'ca48d3f07a5241291ed0b4cab6483fa8b8fcc128', + {} + ); + kademliaNode.router.addContactByNodeId( + 'ba48d3f07a5241291ed0b4cab6483fa8b8fcc128', + {} + ); + cb(); + }); + let getClosestBucket = sinon.stub( + kademliaNode.router, + 'getClosestBucket' + ).returns([constants.B - 1, kademliaNode.router.get(constants.B - 1)]); + let refresh = sinon.stub(kademliaNode, 'refresh').callsArg(1); + kademliaNode.join(['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', { + hostname: 'localhost', + port: 8080 + }], (err) => { + kademliaNode.router.removeContactByNodeId( + 'da48d3f07a5241291ed0b4cab6483fa8b8fcc128' + ); + kademliaNode.router.removeContactByNodeId( + 'ca48d3f07a5241291ed0b4cab6483fa8b8fcc128' + ); + kademliaNode.router.removeContactByNodeId( + 'ba48d3f07a5241291ed0b4cab6483fa8b8fcc128' + ); + iterativeFindNode.restore(); + getClosestBucket.restore(); + refresh.restore(); + expect(err).to.not.be.instanceOf(Error); + expect(addContactByNodeId.calledWithMatch( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc128' + )).to.equal(true); + expect(iterativeFindNode.calledWithMatch( + kademliaNode.identity.toString('hex') + )).to.equal(true); + expect(refresh.callCount).to.equal(1); + done(); + }); + }); + + it('should error if lookup fails', function(done) { + let addContactByNodeId = sinon.stub( + kademliaNode.router, + 'addContactByNodeId' + ); + let iterativeFindNode = sinon.stub( + kademliaNode, + 'iterativeFindNode' + ).callsArgWith(1, new Error('Lookup failed')); + let getClosestBucket = sinon.stub( + kademliaNode.router, + 'getClosestBucket' + ).returns([constants.B - 1, kademliaNode.router.get(constants.B - 1)]); + let refresh = sinon.stub(kademliaNode, 'refresh').callsArg(1); + kademliaNode.join(['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', { + hostname: 'localhost', + port: 8080 + }], (err) => { + addContactByNodeId.restore(); + iterativeFindNode.restore(); + getClosestBucket.restore(); + refresh.restore(); + expect(err.message).to.equal('Lookup failed'); + expect(addContactByNodeId.calledWithMatch( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc128' + )).to.equal(true); + expect(iterativeFindNode.calledWithMatch( + kademliaNode.identity.toString('hex') + )).to.equal(true); + expect(refresh.callCount).to.equal(0); + done(); + }); + }); + + }); + + describe('@method ping', function() { + + it('should call send with PING message', function(done) { + let send = sinon.stub(kademliaNode, 'send').callsFake((a, b, c, d) => { + setTimeout(d, 10); + }); + let contact = ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', { + hostname: 'localhost', + port: 8080 + }]; + kademliaNode.ping(contact, (err, latency) => { + send.restore(); + expect(send.calledWithMatch('PING', [], contact)).to.equal(true); + expect(latency > 0).to.equal(true); + done(); + }); + }); + + }); + + describe('@method iterativeStore', function() { + + it('should send store rpc to found contacts and keep copy', function(done) { + let sandbox = sinon.sandbox.create(); + let contact = { hostname: 'localhost', port: 8080 }; + sandbox.stub( + kademliaNode, + 'iterativeFindNode' + ).callsArgWith( + 1, + null, + Array(20).fill(null).map(() => [utils.getRandomKeyString(), contact]) + ); + let send = sandbox.stub(kademliaNode, 'send').callsArgWith(3, null); + send.onCall(4).callsArgWith(3, new Error('Failed to store')); + let put = sandbox.stub(kademliaNode.storage, 'put').callsArg(3); + kademliaNode.iterativeStore( + utils.getRandomKeyString(), + 'some storage item data', + (err, stored) => { + sandbox.restore(); + expect(stored).to.equal(19); + expect(send.callCount).to.equal(20); + expect(put.callCount).to.equal(1); + done(); + } + ); + }); + + it('should send the store rpc with the existing metadata', function(done) { + let sandbox = sinon.sandbox.create(); + let contact = { hostname: 'localhost', port: 8080 }; + sandbox.stub( + kademliaNode, + 'iterativeFindNode' + ).callsArgWith( + 1, + null, + Array(20).fill(null).map(() => [utils.getRandomKeyString(), contact]) + ); + let send = sandbox.stub(kademliaNode, 'send').callsArgWith(3, null); + send.onCall(4).callsArgWith(3, new Error('Failed to store')); + let put = sandbox.stub(kademliaNode.storage, 'put').callsArg(3); + kademliaNode.iterativeStore( + utils.getRandomKeyString(), + { + value: 'some storage item data', + publisher: 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', + timestamp: Date.now() + }, + (err, stored) => { + sandbox.restore(); + expect(send.args[0][1][1].publisher).to.equal( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc127' + ); + expect(stored).to.equal(19); + expect(send.callCount).to.equal(20); + expect(put.callCount).to.equal(1); + done(); + } + ); + }); + + }); + + describe('@method iterativeFindNode', function() { + + it('should send iterative FIND_NODE calls', function(done) { + let contact = { hostname: 'localhost', port: 8080 }; + let getClosestContactsToKey = sinon.stub( + kademliaNode.router, + 'getClosestContactsToKey' + ).returns([ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc129', contact] + ]); + let _updateContact = sinon.stub(kademliaNode, '_updateContact'); + let send = sinon.stub(kademliaNode, 'send'); + let contacts = Array(20).fill(null).map(() => { + return [utils.getRandomKeyString(), contact] + }); + send.onCall(0).callsArgWith( + 3, + null, + contacts + ); + send.onCall(1).callsArgWith( + 3, + new Error('Lookup failed') + ); + send.onCall(2).callsArgWith( + 3, + null, + contacts + ); + for (var i=0; i<20; i++) { + send.onCall(i + 3).callsArgWith( + 3, + new Error('Lookup failed') + ); + } + kademliaNode.iterativeFindNode( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + (err, results) => { + getClosestContactsToKey.restore(); + _updateContact.restore(); + send.restore(); + expect(err).to.equal(null); + expect(send.callCount).to.equal(23); + expect(_updateContact.callCount).to.equal(20); + expect(results).to.have.lengthOf(2); + results.forEach(([key, c]) => { + expect(utils.keyStringIsValid(key)).to.equal(true); + expect(contact).to.equal(c); + }); + done(); + } + ); + }); + + it('should iterate through closer nodes', function(done) { + let contact = { hostname: 'localhost', port: 8080 }; + let getClosestContactsToKey = sinon.stub( + kademliaNode.router, + 'getClosestContactsToKey' + ).returns([ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc129', contact] + ]); + let _updateContact = sinon.stub(kademliaNode, '_updateContact'); + let send = sinon.stub(kademliaNode, 'send'); + send.callsArgWith( + 3, + null, + Array(20).fill(null).map(() => { + return [utils.getRandomKeyString(), contact] + }) + ); + send.onCall(0).callsArgWith( + 3, + null, + [['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', contact]].concat( + Array(20).fill(null).map(() => { + return [utils.getRandomKeyString(), contact] + }) + ) + ) + kademliaNode.iterativeFindNode( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + (err, results) => { + getClosestContactsToKey.restore(); + _updateContact.restore(); + send.restore(); + expect(err).to.equal(null); + expect(results).to.have.lengthOf(constants.K); + expect(results[0][0]).to.equal( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc127' + ); + expect(results[1][0]).to.equal( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc125' + ); + done(); + } + ); + }); + + it('should call each node a maximum of once', function(done) { + let contact = { hostname: 'localhost', port: 8080 }; + let getClosestContactsToKey = sinon.stub( + kademliaNode.router, + 'getClosestContactsToKey' + ).returns([ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc125', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc129', contact] + ]); + let _updateContact = sinon.stub(kademliaNode, '_updateContact'); + let send = sinon.stub(kademliaNode, 'send'); + send.callsArgWith( + 3, + null, + Array(20).fill(null).map(() => { + return [utils.getRandomKeyString(), contact] + }) + ); + kademliaNode.iterativeFindNode( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + () => { + let sentNodes = send.args.map( args => args[2][0]); + expect(sentNodes).to.deep.equal(sentNodes.filter( + (value, index, self) => { + return self.indexOf(value) === index; + }) + ) + getClosestContactsToKey.restore(); + _updateContact.restore(); + send.restore(); + done(); + } + ); + }); + + it('should not include inactive nodes in the result', function(done) { + let contact = { hostname: 'localhost', port: 8080 }; + let getClosestContactsToKey = sinon.stub( + kademliaNode.router, + 'getClosestContactsToKey' + ).returns([ + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc127', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc128', contact], + ['ea48d3f07a5241291ed0b4cab6483fa8b8fcc129', contact] + ]); + let _updateContact = sinon.stub(kademliaNode, '_updateContact'); + let send = sinon.stub(kademliaNode, 'send'); + let contacts = Array(20).fill(null).map(() => { + return [utils.getRandomKeyString(), contact] + }); + send.onCall(0).callsArgWith( + 3, + null, + contacts + ); + send.onCall(1).callsArgWith( + 3, + new Error('Lookup failed') + ); + send.onCall(2).callsArgWith( + 3, + null, + contacts + ); + for (var i=0; i<20; i++) { + send.onCall(i + 3).callsArgWith( + 3, + contacts + ); + } + kademliaNode.iterativeFindNode( + 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc126', + (err, results) => { + getClosestContactsToKey.restore(); + _updateContact.restore(); + send.restore(); + expect(err).to.equal(null); + results.forEach(([key]) => { + expect(key).to.not.equal('ea48d3f07a5241291ed0b4cab6483fa8b8fcc128') + }); + done(); + } + ); + }); + }); + + describe('@method iterativeFindValue', function() { + + it('should return a node list if no value is found', function(done) { + let sandbox = sinon.sandbox.create(); + let contact = { hostname: 'localhost', port: 8080 }; + sandbox.stub( + kademliaNode.router, + 'getClosestContactsToKey' + ).returns(new Map(Array(20).fill(null).map(() => [ + utils.getRandomKeyString(), + contact + ]))); + sandbox.stub(kademliaNode, 'send').callsArgWith( + 3, + null, + Array(20).fill(20).map(() => [utils.getRandomKeyString(), contact]) + ); + kademliaNode.iterativeFindValue( + utils.getRandomKeyString(), + (err, result) => { + sandbox.restore(); + expect(Array.isArray(result)).to.equal(true); + expect(result).to.have.lengthOf(constants.K); + done(); + } + ); + }); + + it('should find a value at a currently unknown node', function(done) { + let sandbox = sinon.sandbox.create(); + let contact = { hostname: 'localhost', port: 8080 }; + sandbox.stub( + kademliaNode.router, + 'getClosestContactsToKey' + ).returns(new Map(Array(10).fill(null).map(() => [ + utils.getRandomKeyString(), + contact + ]))); + let send = sandbox.stub(kademliaNode, 'send').callsArgWith( + 3, + null, + Array(20).fill(null).map(() => { + return [utils.getRandomKeyString(), contact] + }) + ); + send.onCall(10).callsArgWith(3, null, { + value: 'some data value', + timestamp: Date.now(), + publisher: 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc127' + }); + kademliaNode.iterativeFindValue( + utils.getRandomKeyString(), + (err, result) => { + sandbox.restore(); + expect(result.value).to.equal('some data value'); + done(); + } + ); + }); + + it('should store the value at the closest missing node', function(done) { + let sandbox = sinon.sandbox.create(); + let contact = { hostname: 'localhost', port: 8080 }; + sandbox.stub( + kademliaNode.router, + 'getClosestContactsToKey' + ).returns(new Map(Array(20).fill(null).map(() => [ + utils.getRandomKeyString(), + contact + ]))); + let send = sandbox.stub(kademliaNode, '_send').returns(Promise.resolve( + Array(20).fill(20).map(() => [utils.getRandomKeyString(), contact]) + )); + send.onCall(4).returns(Promise.resolve({ + value: 'some data value', + timestamp: Date.now(), + publisher: 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc127' + })); + kademliaNode.iterativeFindValue( + utils.getRandomKeyString(), + (err, result) => { + sandbox.restore(); + expect(result.value).to.equal('some data value'); + expect(send.callCount >= 6).to.equal(true); + done(); + } + ); + }); + + it('should immediately callback if value found', function(done) { + let sandbox = sinon.sandbox.create(); + let contact = { hostname: 'localhost', port: 8080 }; + sandbox.stub( + kademliaNode.router, + 'getClosestContactsToKey' + ).returns(new Map(Array(20).fill(null).map(() => [ + utils.getRandomKeyString(), + contact + ]))); + let send = sandbox.stub(kademliaNode, '_send').returns(Promise.resolve({ + value: 'some data value', + timestamp: Date.now(), + publisher: 'ea48d3f07a5241291ed0b4cab6483fa8b8fcc127' + })); + send.onCall(0).returns(Promise.reject(new Error('Request timeout'))); + kademliaNode.iterativeFindValue( + utils.getRandomKeyString(), + (err, result) => { + sandbox.restore(); + expect(result.value).to.equal('some data value'); + expect(send.callCount >= 3).to.equal(true); + done(); + } + ); + }); + + }); + + describe('@method replicate', function() { + + it('should replicate and republish the correct items', function(done) { + let sandbox = sinon.sandbox.create(); + let items = [ + { + key: utils.getRandomKeyString(), + value: { + value: 'some value', + timestamp: Date.now() - constants.T_REPUBLISH, + publisher: kademliaNode.identity.toString('hex') + } + }, + { + key: utils.getRandomKeyString(), + value: { + value: 'some value', + timestamp: Date.now() - constants.T_REPLICATE, + publisher: utils.getRandomKeyString() + } + }, + { + key: utils.getRandomKeyString(), + value: { + value: 'some value', + timestamp: Date.now() - 1000, + publisher: utils.getRandomKeyString() + } + } + ]; + sandbox.stub( + kademliaNode.storage, + 'createReadStream' + ).returns(new ReadableStream({ + objectMode: true, + read: function() { + if (items.length) { + this.push(items.shift()); + } else { + this.push(null); + } + } + })); + let iterativeStore = sandbox.stub(kademliaNode, 'iterativeStore') + .callsArg(2); + kademliaNode.replicate((err) => { + sandbox.restore(); + expect(err).to.equal(undefined); + expect(iterativeStore.callCount).to.equal(2); + done(); + }); + }); + + }); + + describe('@method expire', function() { + + it('should expire the correct items', function(done) { + let sandbox = sinon.sandbox.create(); + let items = [ + { + key: utils.getRandomKeyString(), + value: { + value: 'some value', + timestamp: Date.now() - constants.T_EXPIRE, + publisher: kademliaNode.identity.toString('hex') + } + }, + { + key: utils.getRandomKeyString(), + value: { + value: 'some value', + timestamp: Date.now() - constants.T_EXPIRE, + publisher: utils.getRandomKeyString() + } + }, + { + key: utils.getRandomKeyString(), + value: { + value: 'some value', + timestamp: Date.now() - 1000, + publisher: utils.getRandomKeyString() + } + } + ]; + sandbox.stub( + kademliaNode.storage, + 'createReadStream' + ).returns(new ReadableStream({ + objectMode: true, + read: function() { + if (items.length) { + this.push(items.shift()); + } else { + this.push(null); + } + } + })); + let del = sandbox.stub( + kademliaNode.storage, + 'del' + ).callsArg(1); + kademliaNode.expire((err) => { + sandbox.restore(); + expect(err).to.equal(undefined); + expect(del.callCount).to.equal(2); + done(); + }); + }); + + }); + + describe('@method refresh', function() { + + it('should refresh the correct buckets', function(done) { + let sandbox = sinon.sandbox.create(); + let iterativeFindNode = sandbox.stub( + kademliaNode, + 'iterativeFindNode' + ).callsArgWith(1, null, []); + kademliaNode.router.get(0).set( + utils.getRandomKeyString(), + { hostname: 'localhost', port: 8080 } + ); + kademliaNode.router.get(2).set( + utils.getRandomKeyString(), + { hostname: 'localhost', port: 8080 } + ); + for (var i=0; i { + sandbox.restore(); + expect(iterativeFindNode.callCount).to.equal(2); + done(); + }); + }); + + }); + +}); diff --git a/test/plugin-churnfilter.e2e.js b/test/plugin-churnfilter.e2e.js new file mode 100644 index 0000000..d2c143e --- /dev/null +++ b/test/plugin-churnfilter.e2e.js @@ -0,0 +1,71 @@ +'use strict'; + +const async = require('async'); +const { expect } = require('chai'); +const network = require('./fixtures/node-generator'); +const kadence = require('..'); +const churnfilter = require('../lib/plugin-churnfilter'); + + +describe('@module kadence/churnfilter + @class UDPTransport', function() { + + let timeout = null; + let node1, node2, node3, node4; + + before(function(done) { + timeout = kadence.constants.T_RESPONSETIMEOUT; + kadence.constants.T_RESPONSETIMEOUT = 200; + [node1, node2, node3, node4] = network(4, kadence.HTTPTransport); + [node1, node2, node3, node4].forEach((node) => { + node.blacklist = node.plugin(churnfilter()); + node.listen(node.contact.port); + }); + setTimeout(done, 200); + }); + + after(() => kadence.constants.T_RESPONSETIMEOUT = timeout); + + it('should allow nodes to enter the routing table', function(done) { + this.timeout(10000); + async.eachOf([node3, node4, node1, node2], function(node, i, done) { + if (i === 3) { + node.join([node4.identity, node4.contact], done); + } else { + node.join([node2.identity, node2.contact], done); + } + }, function() { + expect(node1.router.size).to.equal(3); + expect(node2.router.size).to.equal(3); + expect(node3.router.size).to.equal(3); + expect(node4.router.size).to.equal(3); + expect(node1.blacklist.hasBlock(node3.identity)).to.equal(false); + expect(node2.blacklist.hasBlock(node3.identity)).to.equal(false); + expect(node3.blacklist.hasBlock(node3.identity)).to.equal(false); + expect(node4.blacklist.hasBlock(node3.identity)).to.equal(false); + done(); + }); + }); + + it('should block node 3 from entering the routing table', function(done) { + node3.transport.server.close(); + this.timeout(10000); + async.eachOf([node3, node4, node1, node2], function(node, i, done) { + if (i === 3) { + node.join([node4.identity, node4.contact], done); + } else { + node.join([node2.identity, node2.contact], done); + } + }, function() { + expect(node1.router.size).to.equal(2); + expect(node2.router.size).to.equal(2); + expect(node3.router.size).to.equal(3); + expect(node4.router.size).to.equal(2); + expect(node1.blacklist.hasBlock(node3.identity)).to.equal(true); + expect(node2.blacklist.hasBlock(node3.identity)).to.equal(true); + expect(node3.blacklist.hasBlock(node3.identity)).to.equal(false); + expect(node4.blacklist.hasBlock(node3.identity)).to.equal(true); + done(); + }); + }); + +}); diff --git a/test/plugin-churnfilter.unit.js b/test/plugin-churnfilter.unit.js new file mode 100644 index 0000000..4e8a8b0 --- /dev/null +++ b/test/plugin-churnfilter.unit.js @@ -0,0 +1,99 @@ +'use strict'; + +const { expect } = require('chai'); +const sinon = require('sinon'); +const ms = require('ms'); +const { ChurnFilterPlugin } = require('../lib/plugin-churnfilter'); + + +describe('@class ChurnFilterPlugin', function() { + + let node = { + use: sinon.stub(), + send: sinon.stub(), + _updateContact: sinon.stub(), + logger: { + warn: sinon.stub(), + debug: sinon.stub() + }, + router: { + removeContactByNodeId: sinon.stub() + } + }; + let blacklist = null; + let clock = null; + + before(() => { + clock = sinon.useFakeTimers(); + blacklist = new ChurnFilterPlugin(node); + }); + + after(() => clock.restore()); + + describe('@method setBlock', function() { + + it('should create blocked with cooldown', function() { + blacklist.setBlock('fingerprint 1'); + blacklist.setBlock('fingerprint 2'); + blacklist.setBlock('fingerprint 3'); + expect(blacklist.cooldown.size).to.equal(3); + expect(blacklist.blocked.size).to.equal(3); + }); + + it('should multiply the cooldown time', function() { + blacklist.setBlock('fingerprint 1'); + expect(blacklist.cooldown.get('fingerprint 1').expiration).to.not.equal( + blacklist.cooldown.get('fingerprint 2').expiration + ); + }); + + }); + + describe('@method delBlock', function() { + + it('should remove the block altogether', function() { + blacklist.delBlock('fingerprint 3'); + expect(blacklist.blocked.size).to.equal(2); + expect(blacklist.cooldown.size).to.equal(2); + }); + + }); + + describe('@method hasBlock', function() { + + it('should expire the blocked correctly', function() { + expect(blacklist.hasBlock('fingerprint 1')).to.equal(true); + expect(blacklist.hasBlock('fingerprint 2')).to.equal(true); + clock.tick(ms(blacklist.opts.cooldownBaseTimeout)); + expect(blacklist.hasBlock('fingerprint 1')).to.equal(true); + expect(blacklist.hasBlock('fingerprint 2')).to.equal(false); + clock.tick(ms(blacklist.opts.cooldownBaseTimeout)); + expect(blacklist.hasBlock('fingerprint 1')).to.equal(false); + }); + + }); + + describe('@method resetCooldownForStablePeers', function() { + + it('should reset the cooldown multiplier for stable peers', function() { + clock.tick(ms(blacklist.opts.cooldownResetTime) / 2); + blacklist.setBlock('fingerprint 1'); + clock.tick(ms(blacklist.opts.cooldownResetTime) / 2); + blacklist.resetCooldownForStablePeers(); + expect(blacklist.blocked.size).to.equal(1); + expect(blacklist.cooldown.size).to.equal(1); + }); + + }); + + describe('@method reset', function() { + + it('should clear out all blocked', function() { + blacklist.reset(); + expect(blacklist.blocked.size).to.equal(0); + expect(blacklist.cooldown.size).to.equal(0); + }); + + }); + +}); diff --git a/test/plugin-contentaddress.e2e.js b/test/plugin-contentaddress.e2e.js new file mode 100644 index 0000000..c61d50a --- /dev/null +++ b/test/plugin-contentaddress.e2e.js @@ -0,0 +1,48 @@ +'use strict'; + +const { expect } = require('chai'); +const network = require('./fixtures/node-generator'); +const kadence = require('..'); +const contentaddress = require('../lib/plugin-contentaddress'); +const { createHash } = require('crypto'); + + +describe('@module kadence/contentaddress + @class UDPTransport', function() { + + let [node1, node2] = network(2, kadence.UDPTransport); + let data = Buffer.from('data'); + let key = createHash('rmd160').update(data).digest('hex'); + + before(function(done) { + [node1, node2].forEach((node) => { + node.plugin(contentaddress()); + node.listen(node.contact.port); + }); + setTimeout(() => { + node1.join([ + node2.identity.toString('hex'), + node2.contact + ], () => done()); + }, 1000); + }); + + it('should succeed in storing item', function(done) { + node1.iterativeStore(key, data.toString('base64'), (err, stored) => { + expect(err).to.equal(null); + expect(stored).to.equal(2); + done(); + }); + }); + + it('should fail in storing item', function(done) { + node2.iterativeStore( + createHash('rmd160').update(Buffer.from('fail')).digest('hex'), + data, + (err) => { + expect(err.message).to.equal('Item failed validation check'); + done(); + } + ); + }); + +}); diff --git a/test/plugin-contentaddress.unit.js b/test/plugin-contentaddress.unit.js new file mode 100644 index 0000000..886bd39 --- /dev/null +++ b/test/plugin-contentaddress.unit.js @@ -0,0 +1,48 @@ +'use strict'; + +const { expect } = require('chai'); +const sinon = require('sinon'); +const { createHash } = require('crypto'); +const { ContentAddressPlugin } = require('../lib/plugin-contentaddress'); + + +describe('@class ContentAddressPlugin', function() { + + let node = { + use: sinon.stub(), + iterativeStore: sinon.stub() + }; + let content = null; + + before(() => { + content = new ContentAddressPlugin(node); + }); + + describe('@method validate', function() { + + it('should error if key not equal to hash', function(done) { + content.validate({ + params: ['000000', { + value: Buffer.from('data').toString('base64') + }] + }, {}, (err) => { + expect(err.message).to.equal('Item failed validation check'); + done(); + }); + }); + + it('should callback if key equals hash', function(done) { + content.validate({ + params: [ + createHash('rmd160').update(Buffer.from('data')).digest('hex'), + { value: Buffer.from('data').toString('base64') } + ] + }, {}, (err) => { + expect(err).to.equal(undefined); + done(); + }); + }); + + }); + +}); diff --git a/test/plugin-eclipse.unit.js b/test/plugin-eclipse.unit.js new file mode 100644 index 0000000..bae245a --- /dev/null +++ b/test/plugin-eclipse.unit.js @@ -0,0 +1,71 @@ +'use strict'; + +const { expect } = require('chai'); +const sinon = require('sinon'); +const eclipse = require('../lib/plugin-eclipse'); +const constants = require('../lib/constants'); +const utils = require('../lib/utils'); +const secp256k1 = require('secp256k1'); + + +constants.IDENTITY_DIFFICULTY = constants.TESTNET_DIFFICULTY; + +describe('@module kadence/eclipse', function() { + + describe('@class EclipseRules', function() { + + const prv = utils.generatePrivateKey(); + const pub = secp256k1.publicKeyCreate(prv); + + it.skip('should validate the contact', function(done) { + const rules = new eclipse.EclipseRules({}); + const ident = new eclipse.EclipseIdentity(pub); + ident.solve().then(() => { + rules.validate({ + contact: [ + ident.fingerprint.toString('hex'), + { + pubkey: pub.toString('hex'), + nonce: ident.nonce, + proof: ident.proof.toString('hex') + } + ] + }, {}, err => { + expect(err).to.equal(undefined); + done(); + }); + }, done); + }); + + it('should invalidate the request', function(done) { + const rules = new eclipse.EclipseRules({}); + rules.validate({ + contact: [ + utils.toPublicKeyHash(pub).toString('hex'), + {} + ] + }, {}, err => { + expect(err.message).to.equal( + 'Fingerprint does not match the proof hash' + ); + done(); + }); + }); + + }); + + describe('@class EclipsePlugin', function() { + + const prv = utils.generatePrivateKey(); + const pub = secp256k1.publicKeyCreate(prv); + const ident = new eclipse.EclipseIdentity(pub); + + it('should call AbstractNode#use', function() { + const use = sinon.stub(); + eclipse(ident)({ use, contact: {} }); + expect(use.called).to.equal(true); + }); + + }); + +}); diff --git a/test/plugin-hashcash.e2e.js b/test/plugin-hashcash.e2e.js new file mode 100644 index 0000000..f149486 --- /dev/null +++ b/test/plugin-hashcash.e2e.js @@ -0,0 +1,34 @@ +'use strict'; + +const { expect } = require('chai'); +const kadence = require('..'); +const network = require('./fixtures/node-generator'); +const hashcash = require('../lib/plugin-hashcash'); + + +describe('@module kadence/hashcash + @class UDPTransport', function() { + + let [node1, node2] = network(2, kadence.UDPTransport); + + before(function(done) { + kadence.constants.T_RESPONSETIMEOUT = 200; + [node1, node2].forEach((node) => { + node.hashcash = node.plugin(hashcash()); + node.listen(node.contact.port); + }); + setTimeout(done, 1000); + }); + + after(function() { + process._getActiveHandles().forEach((h) => h.unref()); + }) + + it('should stamp and verify proof of work', function(done) { + this.timeout(8000); + node1.ping([node2.identity.toString('hex'), node2.contact], (err) => { + expect(err).to.equal(null); + done(); + }); + }); + +}); diff --git a/test/plugin-hibernate.e2e.js b/test/plugin-hibernate.e2e.js new file mode 100644 index 0000000..92170e0 --- /dev/null +++ b/test/plugin-hibernate.e2e.js @@ -0,0 +1,49 @@ +'use strict'; + +const { expect } = require('chai'); +const kadence = require('..'); +const network = require('./fixtures/node-generator'); +const hibernate = require('../lib/plugin-hibernate'); + + +describe('@module kadence/hibernate + @class UDPTransport', function() { + + let [node1, node2] = network(2, kadence.UDPTransport); + + before(function(done) { + [node1, node2].forEach((node) => { + node.plugin(hibernate({ + limit: '222b', + interval: '2s', + reject: ['PING'] + })); + node.listen(node.contact.port); + }); + setTimeout(done, 1000); + }); + + it('should succeed in exchanging ping', function(done) { + node1.ping([node2.identity.toString('hex'), node2.contact], (err) => { + expect(err).to.equal(null); + done(); + }); + }); + + it('should fail in exchanging ping', function(done) { + node2.ping([node1.identity.toString('hex'), node1.contact], (err) => { + expect(err.message).to.equal('Hibernating, try PING again later'); + done(); + }); + }); + + it('should succeed in exchanging ping after reset', function(done) { + this.timeout(3000); + setTimeout(() => { + node2.ping([node1.identity.toString('hex'), node1.contact], (err) => { + expect(err).to.equal(null); + done(); + }); + }, 2000); + }); + +}); diff --git a/test/plugin-hibernate.unit.js b/test/plugin-hibernate.unit.js new file mode 100644 index 0000000..402a289 --- /dev/null +++ b/test/plugin-hibernate.unit.js @@ -0,0 +1,145 @@ +'use strict'; + +const { expect } = require('chai'); +const sinon = require('sinon'); +const { HibernatePlugin } = require('../lib/plugin-hibernate'); + + +describe('@module kademlia/hibernate', function() { + + let clock = null; + let node = { + rpc: { + serializer: { + append: sinon.stub() + }, + deserializer: { + prepend: sinon.stub() + } + }, + use: sinon.stub(), + logger: { + warn: sinon.stub() + } + }; + let hibernate = null; + + before(() => { + clock = sinon.useFakeTimers('setTimeout'); + hibernate = new HibernatePlugin(node, { + limit: '5gb', + interval: '24hr', + reject: ['STORE', 'FIND_VALUE'] + }); + }); + + after(() => clock.restore()); + + describe('@constructor', function() { + + it('should convert limit and interval to numbers', function() { + expect(hibernate.limit).to.equal(5368709120); + }); + + it('should prepend meter("inbound") to deserializer', function() { + expect(hibernate.interval).to.equal(86400000); + }); + + it('should call HibernatePlugin#start', function() { + expect(hibernate.accounting).to.not.equal(undefined); + }); + + }); + + describe('@property hibernating', function() { + + it('should return false if total is greater than limit', function() { + expect(hibernate.hibernating).to.equal(false); + }); + + it('should return true if total is less than limit', function() { + hibernate.accounting.inbound = 5368709120 / 2; + hibernate.accounting.outbound = 5368709120 / 2; + expect(hibernate.hibernating).to.equal(true); + }); + + }); + + describe('@method start', function() { + + it('should emit start on first run', function(done) { + delete hibernate.accounting; + hibernate.once('start', done); + setImmediate(() => hibernate.start()); + }); + + it('should emit reset on each consecutive run', function(done) { + hibernate.once('reset', () => done()); + setImmediate(() => clock.tick(hibernate.interval)); + }); + + }); + + describe('@method meter', function() { + + it('should return transform stream increments outbound', function(done) { + const stream = hibernate.meter('outbound'); + stream.once('data', () => { + expect(hibernate.accounting.outbound).to.equal(4); + done(); + }); + setImmediate(() => stream.write([ + 'message-id', + Buffer.from([0, 0, 0, 0]), + ['identity', { hostname: 'localhost', port: 8080 }] + ])); + }); + + it('should return transform stream increments inbound', function(done) { + const stream = hibernate.meter('inbound'); + stream.once('data', () => { + expect(hibernate.accounting.inbound).to.equal(6); + done(); + }); + setImmediate(() => stream.write(Buffer.from([0, 0, 0, 0, 0, 0]))); + }); + + it('should return transform stream increments unknown', function(done) { + const stream = hibernate.meter(); + stream.once('data', () => { + expect(hibernate.accounting.unknown).to.equal(3); + done(); + }); + setImmediate(() => stream.write('lol')); + }); + + }); + + describe('@method detect', function() { + + it('should error if hibernating and reject method', function(done) { + hibernate.accounting.inbound = 5368709120; + hibernate.detect({ method: 'STORE' }, {}, (err) => { + expect(err.message).to.equal('Hibernating, try STORE again later'); + done(); + }); + }); + + it('should callback if hibernating and accept method', function(done) { + hibernate.detect({ method: 'FIND_NODE' }, {}, (err) => { + expect(err).to.equal(undefined); + done(); + }); + }); + + it('should callback if not hibernating', function(done) { + hibernate.accounting.inbound = 0; + hibernate.detect({ method: 'STORE' }, {}, (err) => { + expect(err).to.equal(undefined); + done(); + }); + }); + + }); + +}); diff --git a/test/plugin-logger.unit.js b/test/plugin-logger.unit.js new file mode 100644 index 0000000..1d138d8 --- /dev/null +++ b/test/plugin-logger.unit.js @@ -0,0 +1,95 @@ +'use strict'; + +const { expect } = require('chai'); +const sinon = require('sinon'); +const logger = require('../lib/plugin-logger'); + + +describe('@class logger#IncomingMessage', function() { + + it('should not log if there is no contact', function(done) { + let log = { info: sinon.stub() }; + let incoming = new logger.IncomingMessage(log); + incoming.write([ + { payload: { method: 'PING' } }, + { payload: { params: [] } } + ]); + setImmediate(() => { + expect(log.info.called).to.equal(false); + done(); + }); + }); + + it('should log if there is message is request', function(done) { + let log = { info: sinon.stub() }; + let incoming = new logger.IncomingMessage(log); + incoming.write([ + { payload: { method: 'PING' } }, + { payload: { params: ['identity', { hostname: 'test', port: 80 }] } } + ]); + setImmediate(() => { + expect(log.info.calledWithMatch('received PING')).to.equal(true); + done(); + }); + }); + + it('should log if there is message is response', function(done) { + let log = { info: sinon.stub() }; + let incoming = new logger.IncomingMessage(log); + incoming.write([ + { payload: { id: 'test' } }, + { payload: { params: ['identity', { hostname: 'test', port: 80 }] } } + ]); + setImmediate(() => { + expect(log.info.calledWithMatch('received response')).to.equal(true); + done(); + }); + }); + +}); + +describe('@class logger#OutgoingMessage', function() { + + it('should not log if there is no contact', function(done) { + let log = { info: sinon.stub() }; + let incoming = new logger.OutgoingMessage(log); + incoming.write([ + { method: 'PING' }, + { }, + [] + ]); + setImmediate(() => { + expect(log.info.called).to.equal(false); + done(); + }); + }); + + it('should log if there is message is request', function(done) { + let log = { info: sinon.stub() }; + let incoming = new logger.OutgoingMessage(log); + incoming.write([ + { method: 'PING' }, + { }, + ['identity', { hostname: 'test', port: 80 }] + ]); + setImmediate(() => { + expect(log.info.calledWithMatch('sending PING')).to.equal(true); + done(); + }); + }); + + it('should log if there is message is response', function(done) { + let log = { info: sinon.stub() }; + let incoming = new logger.OutgoingMessage(log); + incoming.write([ + { id: 'test' }, + { }, + ['identity', { hostname: 'test', port: 80 }] + ]); + setImmediate(() => { + expect(log.info.calledWithMatch('sending response')).to.equal(true); + done(); + }); + }); + +}); diff --git a/test/plugin-onion.unit.js b/test/plugin-onion.unit.js new file mode 100644 index 0000000..4fba760 --- /dev/null +++ b/test/plugin-onion.unit.js @@ -0,0 +1,97 @@ +'use strict'; + +const { expect } = require('chai'); +const { stub } = require('sinon'); +const onion = require('../lib/plugin-onion'); +const { EventEmitter } = require('events'); +const proxyquire = require('proxyquire'); +const logger = { info: stub() }; + + +describe('@module kademlia/onion', function() { + + describe('@constructor', function() { + + it('should establish a hidden service on node#listen', function(done) { + let controller = new EventEmitter(); + controller.getInfo = stub().callsArgWith( + 1, + null, + '"127.0.0.1:9050"' + ); + controller.addEventListeners = stub().callsArg(1); + controller.removeEventListeners = stub().callsArg(0); + let onionStubbed = proxyquire('../lib/plugin-onion', { + '@tacticalchihuahua/granax/hsv3': stub().returns(controller), + fs: { + readFileSync: stub().returns( + Buffer.from('myonionaddress.onion') + ) + } + }); + let transport = { + _createRequest: stub() + }; + let node = new EventEmitter(); + node.transport = transport; + node.listen = stub().callsArg(2); + node.contact = { port: 80 }; + node.logger = logger; + let plugin = new onionStubbed.OnionPlugin(node); + node.listen(8080, function() { + expect(node.contact.hostname).to.equal('myonionaddress.onion'); + expect(node.contact.port).to.equal(80); + expect(plugin.tor).to.equal(controller); + done(); + }); + setTimeout(() => { + controller.emit('ready'); + setTimeout(() => controller.emit('STATUS_CLIENT', [ + 'NOTICE CIRCUIT_ESTABLISHED' + ]), 20); + }, 20); + }); + + it('should emit error if tor control fails', function(done) { + let controller = new EventEmitter(); + let onionStubbed = proxyquire('../lib/plugin-onion', { + '@tacticalchihuahua/granax/hsv3': stub().returns(controller) + }); + let transport = { + _createRequest: stub() + }; + let node = new EventEmitter(); + node.transport = transport; + node.listen = stub().callsArg(2); + node.contact = {}; + node.logger = logger; + let plugin = new onionStubbed.OnionPlugin(node); + node.once('error', (err) => { + expect(plugin.tor).to.equal(controller); + expect(err.message).to.equal('Tor control failed'); + done(); + }); + node.listen(8080); + setTimeout( + () => controller.emit('error', new Error('Tor control failed')), + 20 + ); + }); + + }); + +}); + +describe('@exports', function() { + + it('should return a plugin function', function() { + let transport = { _createRequest: stub() }; + let node = { transport: transport, listen: stub() }; + let options = {}; + let plugin = onion(options); + expect(typeof plugin).to.equal('function'); + let instance = plugin(node); + expect(instance).to.be.instanceOf(onion.OnionPlugin); + }); + +}); diff --git a/test/plugin-quasar.e2e.js b/test/plugin-quasar.e2e.js new file mode 100644 index 0000000..1ad2c19 --- /dev/null +++ b/test/plugin-quasar.e2e.js @@ -0,0 +1,93 @@ +'use strict'; + +const async = require('async'); +const kadence = require('..'); +const quasar = require('../lib/plugin-quasar'); +const network = require('./fixtures/node-generator'); + + +const TOTAL_NODES = 12; + +describe('@module kadence/quasar + @class UDPTransport', function() { + + this.timeout(400000); + + let nodes, seed; + + let topics = { + topic1: (TOTAL_NODES - 1) / 4, + topic2: (TOTAL_NODES - 1) / 4, + topic3: (TOTAL_NODES - 1) / 4, + topic4: (TOTAL_NODES - 1) / 4 + }; + + before(function(done) { + kadence.constants.T_RESPONSETIMEOUT = 30000; + nodes = network(TOTAL_NODES, kadence.UDPTransport); + async.each(nodes, (node, done) => { + node.plugin(quasar()); + node.listen(node.contact.port, node.contact.hostname, done); + }, () => { + seed = nodes.shift(); + nodes.forEach((node) => { + seed.router.addContactByNodeId( + node.identity.toString('hex'), + node.contact + ); + }); + async.each(nodes, (node, done) => node.join([ + seed.identity.toString('hex'), + seed.contact + ], done), done); + }); + }); + + after(function() { + nodes.forEach((node) => node.transport.socket.close()); + }); + + it('nodes subscribed to a topic should receive publication', function(done) { + let topicCounter = 0; + function getTopicName() { + if (topicCounter === 0 || topicCounter < 4) { + return 'topic' + (++topicCounter); + } else { + topicCounter = 0; + return getTopicName(); + } + } + function confirmPublicationReceipt(topic) { + topics[topic]--; + for (let t in topics) { + if (topics[t] > 0) { + return; + } + } + done(); + } + async.eachLimit(nodes, 4, (node, next) => { + let topic = getTopicName(); + node.quasarSubscribe(topic, () => confirmPublicationReceipt(topic)); + setTimeout(() => next(), 500); + }, () => { + let publishers = nodes.splice(0, 4); + async.each(publishers, (node, done) => { + node.quasarPublish(getTopicName(), {}, done); + }, () => { + setTimeout(() => { + let totalMembersRemaining = 0; + for (let t in topics) { + totalMembersRemaining += topics[t]; + } + if (totalMembersRemaining > Math.floor((TOTAL_NODES - 1) * 0.15)) { + return done(new Error( + `${totalMembersRemaining} group members did not get message` + )); + } + done(); + }, 12000); + }); + }); + }); + +}); diff --git a/test/plugin-quasar.unit.js b/test/plugin-quasar.unit.js new file mode 100644 index 0000000..f41e654 --- /dev/null +++ b/test/plugin-quasar.unit.js @@ -0,0 +1,686 @@ +'use strict'; + +const { expect } = require('chai'); +const sinon = require('sinon'); +const uuid = require('uuid'); +const utils = require('../lib/utils'); +const RoutingTable = require('../lib/routing-table'); +const kadence = require('..'); +const constants = require('../lib/constants'); +const BloomFilter = require('atbf'); +const { QuasarPlugin, QuasarRules } = require('../lib/plugin-quasar'); + + +describe('@module kadence/quasar', function() { + + describe('@class QuasarPlugin', function() { + + const logger = { + warn: sinon.stub(), + info: sinon.stub(), + debug: sinon.stub(), + error: sinon.stub() + }; + const identity = kadence.utils.getRandomKeyBuffer(); + const router = new kadence.RoutingTable(identity); + const use = sinon.stub(); + + before(function() { + let numContacts = 32; + + while (numContacts > 0) { + router.addContactByNodeId(kadence.utils.getRandomKeyString(), { + hostname: 'localhost', + port: 8080 + }); + numContacts--; + } + }); + + describe('@constructor', function() { + + it('should add middleware, self to filter, decorate node', function() { + let plugin = new QuasarPlugin({ identity, router, use }); + expect(use.callCount).to.equal(3); + expect( + use.calledWithMatch(QuasarPlugin.PUBLISH_METHOD) + ).to.equal(true); + expect( + use.calledWithMatch(QuasarPlugin.SUBSCRIBE_METHOD) + ).to.equal(true); + expect(use.calledWithMatch(QuasarPlugin.UPDATE_METHOD)).to.equal(true); + expect(plugin.filter[0].has(identity.toString('hex'))).to.equal(true); + use.reset(); + }); + + }); + + describe('@property neighbors', function() { + + it('should return ALPHA contact objects', function() { + let plugin = new QuasarPlugin({ identity, router, use }); + expect(plugin.neighbors).to.have.lengthOf(kadence.constants.ALPHA); + }); + + }); + + describe('@method quasarPublish', function() { + + it('should node#send to each neighbor', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + plugin.node.send = sinon.stub().callsArg(3); + plugin.node.logger = { + warn: sinon.stub() + }; + plugin.node.send.onCall(1).callsArgWith(3, new Error('Timeout')); + plugin.quasarPublish('topic string', { + some: 'data' + }, (err, deliveries) => { + expect(plugin.node.send.callCount).to.equal(4); + expect( + plugin.node.send.calledWithMatch(QuasarPlugin.PUBLISH_METHOD) + ).to.equal(true); + expect(deliveries).to.have.lengthOf(3); + expect(plugin.node.logger.warn.callCount).to.equal(1); + let content = plugin.node.send.args[0][1]; + expect(typeof content.uuid).to.equal('string'); + expect(content.topic).to.equal('topic string'); + expect(content.contents.some).to.equal('data'); + expect(content.ttl).to.equal(constants.MAX_RELAY_HOPS); + expect(content.publishers.indexOf( + identity.toString('hex') + )).to.equal(0); + done(); + }); + }); + + it('should use the routing key if supplied', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + let getClosestContactsToKey = sinon.spy( + router, + 'getClosestContactsToKey' + ); + let routingKey = kadence.utils.getRandomKeyString(); + plugin.node.send = sinon.stub().callsArg(3); + plugin.quasarPublish('topic string', { + some: 'data' + }, { routingKey }, () => { + expect(getClosestContactsToKey.calledWithMatch( + routingKey + )).to.equal(true); + done(); + }); + }); + + }); + + describe('@method quasarSubscribe', function() { + + it('should add a topic subscription + refresh filters', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + let pullFilters = sinon.stub(plugin, 'pullFilters').callsArg(0); + let pushFilters = sinon.stub(plugin, 'pushFilters'); + plugin.quasarSubscribe('single topic', true); + setImmediate(() => { + expect(plugin.filter[0].has('single topic')).to.equal(true); + expect(plugin.groups.has('single topic')).to.equal(true); + expect(pushFilters.called).to.equal(true); + expect(pullFilters.called).to.equal(true); + done(); + }); + }); + + it('should add a topic subscription + refresh filters', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + let pullFilters = sinon.stub(plugin, 'pullFilters').callsArg(0); + let pushFilters = sinon.stub(plugin, 'pushFilters'); + plugin.quasarSubscribe(['multi topic 1', 'multi topic 2'], true); + setImmediate(() => { + expect(plugin.filter[0].has('multi topic 1')).to.equal(true); + expect(plugin.filter[0].has('multi topic 2')).to.equal(true); + expect(plugin.groups.has('multi topic 1')).to.equal(true); + expect(plugin.groups.has('multi topic 2')).to.equal(true); + expect(pushFilters.called).to.equal(true); + expect(pullFilters.called).to.equal(true); + done(); + }); + }); + + }); + + describe('@method pullFilters', function() { + + it('should callback early if updated within an hour', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + let pullFilterFrom = sinon.stub(plugin, 'pullFilterFrom').callsArg(1); + plugin._lastUpdate = Date.now(); + plugin.pullFilters(() => { + expect(pullFilterFrom.callCount).to.equal(0); + done(); + }); + }); + + it('should bubble errors from pulling the filter', function(done) { + let plugin = new QuasarPlugin({ identity, router, use, logger }); + sinon.stub(plugin, 'pullFilterFrom').callsArgWith( + 1, + new Error('Request timed out') + ); + plugin.pullFilters((err) => { + expect(logger.warn.called).to.equal(true); + logger.warn.reset(); + expect(err.message).to.equal('Request timed out'); + done(); + }); + }); + + it('should merge all the filters with local', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + let remote1 = new BloomFilter({ filterDepth: 3, bitfieldSize: 160 }); + let remote2 = new BloomFilter({ filterDepth: 3, bitfieldSize: 160 }); + let remote3 = new BloomFilter({ filterDepth: 3, bitfieldSize: 160 }); + remote1[0].add('remote 1'); + remote2[0].add('remote 2'); + remote3[0].add('remote 3'); + let pullFilterFrom = sinon.stub(plugin, 'pullFilterFrom'); + pullFilterFrom.onCall(0).callsArgWith(1, null, remote1); + pullFilterFrom.onCall(1).callsArgWith(1, null, remote2); + pullFilterFrom.onCall(2).callsArgWith(1, null, remote3); + plugin.pullFilters(() => { + expect(pullFilterFrom.callCount).to.equal(3); + expect(plugin.hasNeighborSubscribedTo('remote 1')).to.equal(true); + expect(plugin.hasNeighborSubscribedTo('remote 2')).to.equal(true); + expect(plugin.hasNeighborSubscribedTo('remote 3')).to.equal(true); + done(); + }); + }); + + }); + + describe('@method pullFilterFrom', function() { + + it('should node#send with args and callback with atbf', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + let remote = new BloomFilter({ filterDepth: 3, bitfieldSize: 160 }); + remote[0].add('some topic'); + plugin.node.send = function(method, params, contact, callback) { + expect(method).to.equal(QuasarPlugin.SUBSCRIBE_METHOD); + expect(params).to.have.lengthOf(0); + callback(null, remote.toHexArray()); + }; + plugin.pullFilterFrom([], (err, filter) => { + expect(err).to.equal(null); + expect(filter[0].has('some topic')).to.equal(true); + done(); + }); + }); + + it('should callback if transport error', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + let remote = new BloomFilter({ filterDepth: 3, bitfieldSize: 160 }); + remote[0].add('some topic'); + plugin.node.send = function(method, params, contact, callback) { + callback(new Error('Timeout')); + }; + plugin.pullFilterFrom([], (err) => { + expect(err.message).to.equal('Timeout'); + done(); + }); + }); + + it('should callback if bad result', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + let remote = new BloomFilter({ filterDepth: 3, bitfieldSize: 160 }); + remote[0].add('some topic'); + plugin.node.send = function(method, params, contact, callback) { + callback(null, ['some', 'bad', 'data?']); + }; + plugin.pullFilterFrom([], (err) => { + expect(err.message).to.equal('Invalid hex string'); + done(); + }); + }); + + }); + + describe('@method pushFilters', function() { + + it('should push filters to each neighbor', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + let pushFilterTo = sinon.stub(plugin, 'pushFilterTo').callsArg(1); + plugin.pushFilters(() => { + expect(pushFilterTo.callCount).to.equal(3); + done(); + }); + }); + + it('should callback early if we updated within an hour', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + plugin._lastUpdate = Date.now(); + let pushFilterTo = sinon.stub(plugin, 'pushFilterTo').callsArg(1); + plugin.pushFilters(() => { + expect(pushFilterTo.callCount).to.equal(0); + done(); + }); + }); + + }); + + describe('@method pushFilterTo', function() { + + it('should call node#send with correct args', function(done) { + let plugin = new QuasarPlugin({ identity, router, use }); + plugin.node.send = function(method, params, contact, callback) { + expect(method).to.equal(QuasarPlugin.UPDATE_METHOD); + expect(params).to.have.lengthOf(constants.FILTER_DEPTH); + callback(); + }; + plugin.pushFilterTo([], done); + }); + + }); + + describe('@method isSubscribedTo', function() { + + it('should return true if subscribed and handling', function() { + let plugin = new QuasarPlugin({ identity, router, use }); + plugin.filter[0].add('local topic'); + plugin.groups.set('local topic', true); + expect(plugin.isSubscribedTo('local topic')).to.equal(true); + }); + + it('should return false if not subscribed and handling', function() { + let plugin = new QuasarPlugin({ identity, router, use }); + expect(plugin.isSubscribedTo('local topic')).to.equal(false); + }); + + }); + + describe('@hasNeighborSubscribedTo', function() { + + it('should return true if a neighbor is subscribed', function() { + let plugin = new QuasarPlugin({ identity, router, use }); + plugin.filter[2].add('neighbor topic'); + expect(plugin.hasNeighborSubscribedTo('neighbor topic')).to.equal(true); + }); + + it('should return false if a neighbor is not subscribed', function() { + let plugin = new QuasarPlugin({ identity, router, use }); + plugin.filter[2].add('neighbor topic'); + expect(plugin.hasNeighborSubscribedTo('wrong topic')).to.equal(false); + }); + + }); + + describe('@private _getRandomContact', function() { + + it('should return a random contact', function() { + let plugin = new QuasarPlugin({ identity, router, use }); + let result = plugin._getRandomContact(); + expect(result).to.have.lengthOf(2); + }); + + }); + + }); + + describe('@class QuasarRules', function() { + + const identity = utils.getRandomKeyBuffer(); + const router = new RoutingTable(identity); + + before(function() { + let numContacts = 32; + + while (numContacts > 0) { + router.addContactByNodeId(utils.getRandomKeyString(), { + hostname: 'localhost', + port: 8080 + }); + numContacts--; + } + }); + + describe('@method publish', function() { + + it('should callback error if already routed', function(done) { + let rules = new QuasarRules({ + node: { + router, + identity + }, + cached: { get: sinon.stub().returns(true) } + }); + let send = sinon.stub(); + rules.publish({ + params: { + uuid: uuid.v4(), + topic: 'test', + ttl: 3, + contents: {} + } + }, { send }, (err) => { + expect(err.message).to.equal('Message previously routed'); + expect(send.called).to.equal(false); + done(); + }); + }); + + it('should callback error if ttl greater than max', function(done) { + let rules = new QuasarRules({ + node: { + router, + identity + }, + cached: { get: sinon.stub().returns(false) } + }); + let send = sinon.stub(); + rules.publish({ + params: { + uuid: uuid.v4(), + topic: 'test', + ttl: 24, + contents: {} + } + }, { send }, (err) => { + expect(err.message).to.equal('Message includes invalid TTL'); + expect(send.called).to.equal(false); + done(); + }); + }); + + it('should callback error if ttl greater than max', function(done) { + let rules = new QuasarRules({ + node: { + router, + identity + }, + cached: { get: sinon.stub().returns(false) } + }); + let send = sinon.stub(); + rules.publish({ + params: { + uuid: uuid.v4(), + topic: 'test', + ttl: -1, + contents: {} + } + }, { send }, (err) => { + expect(err.message).to.equal('Message includes invalid TTL'); + expect(send.called).to.equal(false); + done(); + }); + }); + + it('should add to pubs, cache id, exec handler, relay', function(done) { + let cachedSet = sinon.stub(); + let handler = sinon.stub(); + let rules = new QuasarRules({ + node: { + router, + identity + }, + groups: { + get: sinon.stub().returns(handler) + }, + isSubscribedTo: sinon.stub().returns(true), + cached: { + get: sinon.stub().returns(false), + set: cachedSet + } + }); + let id = uuid.v4(); + let _relayPublication = sinon.stub(rules, '_relayPublication') + .callsArg(2); + let msg = { + uuid: id, + topic: 'test', + ttl: 3, + contents: {}, + publishers: [] + }; + let send = (params) => { + expect(Array.isArray(params)).to.equal(true); + expect(params).to.have.lengthOf(0); + expect(cachedSet.calledWithMatch(id)).to.equal(true); + expect(msg.publishers.indexOf(identity.toString('hex'))).to.equal(0); + expect(_relayPublication.callCount).to.equal(3); + expect(handler.called).to.equal(true); + done(); + }; + rules.publish({ params: msg }, { send }); + }); + + it('should do nothing if not subscribed and ttl is 1', function(done) { + let cachedSet = sinon.stub(); + let handler = sinon.stub(); + let rules = new QuasarRules({ + node: { + router, + identity + }, + groups: { + get: sinon.stub().returns(handler) + }, + isSubscribedTo: sinon.stub().returns(false), + cached: { + get: sinon.stub().returns(false), + set: cachedSet + } + }); + let id = uuid.v4(); + let _relayPublication = sinon.stub(rules, '_relayPublication') + .callsArg(2); + let msg = { + uuid: id, + topic: 'test', + ttl: 1, + contents: {}, + publishers: [] + }; + let send = (params) => { + expect(Array.isArray(params)).to.equal(true); + expect(params).to.have.lengthOf(0); + expect(cachedSet.calledWithMatch(id)).to.equal(true); + expect(_relayPublication.callCount).to.equal(0); + done(); + }; + rules.publish({ params: msg }, { send }); + }); + + it('should relay to neighbors if interested or random', function(done) { + let cachedSet = sinon.stub(); + let handler = sinon.stub(); + let pullFilterFrom = sinon.stub().callsArgWith(1, null, []); + let _getRandomContact = sinon.stub().returns([]) + let rules = new QuasarRules({ + node: { + router, + identity + }, + pullFilterFrom: pullFilterFrom, + _getRandomContact: _getRandomContact, + groups: { + get: sinon.stub().returns(handler) + }, + isSubscribedTo: sinon.stub().returns(false), + cached: { + get: sinon.stub().returns(false), + set: cachedSet + } + }); + let shouldRelayPublication = sinon.stub( + QuasarRules, + 'shouldRelayPublication' + ).returns(true); + shouldRelayPublication.onCall(0).returns(false); + let id = uuid.v4(); + let _relayPublication = sinon.stub(rules, '_relayPublication') + .callsArg(2); + let msg = { + uuid: id, + topic: 'test', + ttl: 3, + contents: {}, + publishers: [] + }; + let send = (params) => { + shouldRelayPublication.restore(); + expect(Array.isArray(params)).to.equal(true); + expect(params).to.have.lengthOf(0); + expect(cachedSet.calledWithMatch(id)).to.equal(true); + expect(_relayPublication.callCount).to.equal(3); + expect(_getRandomContact.callCount).to.equal(1); + done(); + }; + rules.publish({ params: msg }, { send }); + }); + + }); + + describe('@method subscribe', function() { + + it('should respond with a hex array of our filter', function(done) { + let filter = new BloomFilter({ filterDepth: 3, bitfieldSize: 160 }); + filter[0].add('beep'); + filter[1].add('boop'); + filter[2].add('buup'); + let rules = new QuasarRules({ filter }); + rules.subscribe({}, { + send: (params) => { + expect(params).to.have.lengthOf(3); + let filter = BloomFilter.from(params); + expect(filter[0].has('beep')).to.equal(true); + expect(filter[1].has('boop')).to.equal(true); + expect(filter[2].has('buup')).to.equal(true); + done(); + } + }); + }); + + }); + + describe('@method update', function() { + + it('should merge remote filter with the local filter', function(done) { + let local = new BloomFilter({ bitfieldSize: 160, filterDepth: 3 }); + let rules = new QuasarRules({ filter: local }); + let send = sinon.stub(); + rules.update({ params: { bad: 'data' } }, { send }, function(err) { + expect(err.message).to.equal('Invalid bloom filters supplied'); + expect(send.called).to.equal(false); + done(); + }); + }); + + it('should callback error if failed to merge', function(done) { + let local = new BloomFilter({ bitfieldSize: 160, filterDepth: 3 }); + let rules = new QuasarRules({ filter: local }); + let send = sinon.stub(); + rules.update({ params: ['bad', 'data?'] }, { send }, function(err) { + expect(err.message).to.equal('Invalid hex string'); + expect(send.called).to.equal(false); + done(); + }); + }); + + it('should merge remote filter with the local filter', function(done) { + let local = new BloomFilter({ bitfieldSize: 160, filterDepth: 3 }); + let remote = new BloomFilter({ bitfieldSize: 160, filterDepth: 3 }); + remote[0].add('test'); + let rules = new QuasarRules({ filter: local }); + rules.update({ params: remote.toHexArray() }, { + send: (params) => { + expect(params).to.have.lengthOf(0); + expect(local[1].has('test')).to.equal(true); + done(); + } + }) + }); + + }); + + describe('@static shouldRelayPublication', function() { + + it('should return false if not in filter', function() { + let request = { + params: { + topic: 'test topic', + publishers: [ + 'publisher 1' + ] + } + }; + let filters = new BloomFilter({ bitfieldSize: 160, filterDepth: 3 }); + expect( + QuasarRules.shouldRelayPublication(request, filters) + ).to.equal(false); + }); + + it('should return false if negative publisher info', function() { + let request = { + params: { + topic: 'test topic', + publishers: [ + 'publisher 1' + ] + } + }; + let filters = new BloomFilter({ bitfieldSize: 160, filterDepth: 3 }); + filters[1].add('test topic'); + filters[2].add('publisher 1'); + expect( + QuasarRules.shouldRelayPublication(request, filters) + ).to.equal(false); + }); + + it('should return true if in filter and no negative info', function() { + let request = { + params: { + topic: 'test topic', + publishers: [ + 'publisher 1' + ] + } + }; + let filters = new BloomFilter({ bitfieldSize: 160, filterDepth: 3 }); + filters[0].add('test topic'); + expect( + QuasarRules.shouldRelayPublication(request, filters) + ).to.equal(true); + }); + + }); + + describe('@private _relayPublication', function() { + + it('should call node#send with correct args', function(done) { + let send = sinon.stub().callsArg(3); + let rules = new QuasarRules({ + node: { send } + }); + let request = { + method: 'PUBLISH', + params: { + topic: 'test topic', + ttl: 3 + } + }; + let contact = [ + utils.getRandomKeyString(), + { hostname: 'localhost', port: 8080 } + ] + rules._relayPublication(request, contact, () => { + let args = send.args[0]; + expect(args[0]).to.equal('PUBLISH'); + expect(args[1].ttl).to.equal(2); + expect(args[2]).to.equal(contact); + done(); + }); + }); + + }); + + }); + +}); diff --git a/test/plugin-rolodex.unit.js b/test/plugin-rolodex.unit.js new file mode 100644 index 0000000..f9daf3d --- /dev/null +++ b/test/plugin-rolodex.unit.js @@ -0,0 +1,96 @@ +'use strict'; + +const { expect } = require('chai'); +const rolodex = require('../lib/plugin-rolodex'); +const sinon = require('sinon'); +const RoutingTable = require('../lib/routing-table'); +const utils = require('../lib/utils'); +const path = require('path'); +const os = require('os'); + + +describe('@module kadence/rolodex', function() { + + let plugin; + + const id = Buffer.from(utils.getRandomKeyString(), 'hex'); + const node = { + router: new RoutingTable(id), + logger: { + warn: sinon.stub(), + info: sinon.stub(), + debug: sinon.stub(), + error: sinon.stub() + } + }; + + let nodeid1 = utils.getRandomKeyString(); + let nodeid2 = utils.getRandomKeyString(); + + before(function() { + plugin = rolodex(path.join(os.tmpdir(), id.toString('hex')))(node); + }); + + it('should store the contact in the db', function(done) { + let contact1 = { + hostname: 'localhost', + port: 8080, + protocol: 'http:' + }; + let contact2 = { + hostname: 'localhost', + port: 8081, + protocol: 'http:' + }; + node.router.addContactByNodeId(nodeid1, contact1); + setTimeout(function() { + node.router.addContactByNodeId(nodeid2, contact2); + setTimeout(function() { + plugin.getBootstrapCandidates().then(function(peers) { + expect(peers[0]).to.equal(`http://localhost:8081/#${nodeid2}`); + expect(peers[1]).to.equal(`http://localhost:8080/#${nodeid1}`); + done(); + }, done); + }, 20); + }, 20); + }); + + describe('@class RolodexPlugin', function() { + + describe('@method getExternalPeerInfo', function() { + + it('should return the peer info', function(done) { + plugin.getExternalPeerInfo(nodeid1).then(contact => { + expect(contact.hostname).to.equal('localhost'); + expect(contact.port).to.equal(8080); + expect(contact.protocol).to.equal('http:'); + done(); + }, done); + }); + + }); + + describe('@method setInternalPeerInfo', function() { + + it('should set the internal peer info', function(done) { + plugin.setInternalPeerInfo(nodeid1, { + reputation: 95 + }).then(() => done(), done); + }); + + }); + + describe('@method getInternalPeerInfo', function() { + + it('should return the internal peer info', function(done) { + plugin.getInternalPeerInfo(nodeid1).then(info => { + expect(info.reputation).to.equal(95); + done(); + }, done); + }); + + }); + + }); + +}); diff --git a/test/plugin-spartacus.e2e.js b/test/plugin-spartacus.e2e.js new file mode 100644 index 0000000..90dcdf5 --- /dev/null +++ b/test/plugin-spartacus.e2e.js @@ -0,0 +1,69 @@ +'use strict'; + +const { expect } = require('chai'); +const kadence = require('..'); +const network = require('./fixtures/node-generator'); +const spartacus = require('../lib/plugin-spartacus'); + + +describe('@module kadence/spartacus + @class UDPTransport)', function() { + + kadence.constants.T_RESPONSETIMEOUT = 1000; + + let [node1, node2, node3, node4] = network(4, kadence.UDPTransport); + let node3pub = null; + + before(function(done) { + kadence.constants.T_RESPONSETIMEOUT = 1000; + [node1, node2, node3].forEach((node) => { + node.spartacus = node.plugin(spartacus()); + node.listen(node.contact.port); + }); + node4.listen(node4.contact.port); // NB: Not a spartacus node + setTimeout(done, 1000); + }); + + it('should sign and verify messages', function(done) { + node1.ping([node2.identity.toString('hex'), node2.contact], (err) => { + expect(err).to.equal(null); + done(); + }); + }); + + it('should sign and verify messages', function(done) { + node2.ping([node1.identity.toString('hex'), node1.contact], (err) => { + expect(err).to.equal(null); + done(); + }); + }); + + it('should fail to validate if reflection attack', function(done) { + this.timeout(4000); + node3pub = node3.contact.pubkey; + node3.contact.pubkey = '000000'; + node3.ping([node1.identity.toString('hex'), node1.contact], (err) => { + expect(err.message).to.equal('Timed out waiting for response'); + done(); + }); + }); + + it('should fail to validate if no response', function(done) { + this.timeout(4000); + node3.contact.pubkey = node3pub; + node3.contact.port = 0; + node1.spartacus.setValidationPeriod(0); + node3.ping([node1.identity.toString('hex'), node1.contact], (err) => { + expect(err.message).to.equal('Timed out waiting for response'); + done(); + }); + }); + + it('should timeout and not crash if no auth payload', function(done) { + this.timeout(4000); + node4.ping([node2.identity.toString('hex'), node2.contact], (err) => { + expect(err.message).to.equal('Timed out waiting for response'); + done(); + }); + }); + +}); diff --git a/test/plugin-spartacus.unit.js b/test/plugin-spartacus.unit.js new file mode 100644 index 0000000..022fb44 --- /dev/null +++ b/test/plugin-spartacus.unit.js @@ -0,0 +1,162 @@ +'use strict'; + +const { expect } = require('chai'); +const sinon = require('sinon'); +const kadence = require('..'); +const { SpartacusPlugin } = require('../lib/plugin-spartacus'); +const utils = require('../lib/utils'); + + +describe('@module kadence/spartacus', function() { + + describe('@constructor', function() { + + it('should replace node identity and add transforms', function() { + let id = utils.toPublicKeyHash('test'); + let node = { + identity: id, + rpc: { + serializer: { append: sinon.stub() }, + deserializer: { prepend: sinon.stub() } + }, + router: {}, + contact: {}, + use: sinon.stub() + }; + let plugin = new SpartacusPlugin(node); + expect(node.identity).to.equal(plugin.identity); + expect(node.router.identity).to.equal(plugin.identity); + expect(node.rpc.serializer.append.called).to.equal(true); + expect(node.rpc.deserializer.prepend.called).to.equal(true); + }); + + }); + + describe('@method serialize', function() { + + it('should add an AUTHENTICATE message to the payload', function(done) { + let node = { + identity: null, + rpc: { + serializer: { append: sinon.stub() }, + deserializer: { prepend: sinon.stub() } + }, + router: {}, + contact: {}, + use: sinon.stub() + }; + let plugin = new SpartacusPlugin(node); + kadence.Messenger.JsonRpcSerializer([ + { method: 'PING', params: [] }, + [ + plugin.identity.toString('hex'), + { hostname: 'localhost', port: 8080 } + ], + [ + utils.toPublicKeyHash('test').toString('hex'), + { hostname: 'localhost', port: 8080} + ] + ], (err, data) => { + plugin.serialize(data, null, (err, [, buffer]) => { + let result = JSON.parse(buffer.toString()); + expect(result).to.have.lengthOf(3); + expect(result[1].params[0]).to.equal(plugin.identity.toString('hex')); + expect(result[2].params).to.have.lengthOf(2); + done(); + }); + }); + }); + + }); + + describe('@method deserialize', function() { + + it('should callback error if id is not pubkeyhash', function(done) { + let node = { + identity: null, + rpc: { + serializer: { append: sinon.stub() }, + deserializer: { prepend: sinon.stub() } + }, + router: {}, + contact: {}, + use: sinon.stub() + }; + let plugin = new SpartacusPlugin(node); + plugin.deserialize(Buffer.from(JSON.stringify([ + { + jsonrpc: '2.0', + id: 'test', + method: 'PING', + params: [] + }, + { + jsonrpc: '2.0', + method: 'IDENTIFY', + params: [ + '0000ff', + { hostname: 'localhost', port: 8080 } + ] + }, + { + jsonrpc: '2.0', + method: 'AUTHENTICATE', + params: [ + plugin.publicKey.toString('hex'), + '0000ff', + [plugin.publicExtendedKey, plugin.derivationIndex] + ] + } + ])), null, (err) => { + expect(err.message).to.equal('Identity does not match public key'); + done(); + }); + }); + + it('should callback error if invalid signature', function(done) { + let node = { + identity: null, + rpc: { + serializer: { append: sinon.stub() }, + deserializer: { prepend: sinon.stub() } + }, + router: {}, + contact: {}, + use: sinon.stub() + }; + let plugin = new SpartacusPlugin(node); + plugin.deserialize(Buffer.from(JSON.stringify([ + { + jsonrpc: '2.0', + id: 'test', + method: 'PING', + params: [] + }, + { + jsonrpc: '2.0', + method: 'IDENTIFY', + params: [ + plugin.identity.toString('hex'), + { hostname: 'localhost', port: 8080 } + ] + }, + { + jsonrpc: '2.0', + method: 'AUTHENTICATE', + params: [ + '583b0eb005a94f22410d2da645b62dc7cdd9288f7fece1fd67fd6d90f4ce0284' + + '48d3fd353969dde8e9e73ad69178efda22008a4e642f32845e89c59ec83a68' + + '8f', + plugin.publicKey.toString('hex'), + [plugin.publicExtendedKey, plugin.derivationIndex] + ] + } + ])), null, (err) => { + expect(err.message).to.equal('Message includes invalid signature'); + done(); + }); + }); + + }); + +}); diff --git a/test/plugin-traverse.unit.js b/test/plugin-traverse.unit.js new file mode 100644 index 0000000..a7e37ba --- /dev/null +++ b/test/plugin-traverse.unit.js @@ -0,0 +1,371 @@ +'use strict'; + +const { EventEmitter } = require('events'); +const bunyan = require('bunyan'); +const sinon = require('sinon'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire'); +const { + TraversePlugin, + UPNPStrategy, +} = require('../lib/plugin-traverse'); +const logger= bunyan.createLogger({ + name: 'kadence-traverse-test', + level: 'fatal' +}); + + +describe('@module kadence/traverse', function() { + + describe('@constructor', function() { + + let sandbox; + + before(() => sandbox = sinon.sandbox.create()); + + it('should wrap node#listen', function() { + let _wrap = sandbox.stub(TraversePlugin.prototype, '_wrapNodeListen'); + let plugin = new TraversePlugin({ + contact: { hostname: '127.0.0.1', port: 8080 }, + logger + }, []); + expect(_wrap.called).to.equal(true); + expect(plugin._originalContact.hostname).to.equal('127.0.0.1'); + expect(plugin._originalContact.port).to.equal(8080); + }); + + after(() => sandbox.restore()); + + }); + + describe('@private @method _execTraversalStrategies', function() { + + let sandbox; + + before(() => sandbox = sinon.sandbox.create()); + + it('should exec strategies until test passes', function(done) { + let err = new Error('failed'); + let s1 = { exec: sandbox.stub().callsArgWith(1, err) }; + let s2 = { exec: sandbox.stub().callsArgWith(1) }; + let s3 = { exec: sandbox.stub().callsArgWith(1) }; + let _test = sandbox.stub( + TraversePlugin.prototype, + '_testIfReachable' + ).callsArg(0); + _test.onCall(0).callsArgWith(0, null, false); + sandbox.stub(TraversePlugin.prototype, '_wrapNodeListen'); + let plugin = new TraversePlugin({ + contact: { hostname: '127.0.0.1', port: 8080 }, + logger + }, [s1, s2, s3]); + plugin._execTraversalStrategies(() => { + expect(s1.exec.called).to.equal(true); + expect(s2.exec.called).to.equal(true); + expect(s3.exec.called).to.equal(true); + expect(_test.callCount).to.equal(2); + done() + }); + }); + + after(() => sandbox.restore()) + + }); + + describe('@private @method _testIfReachable', function() { + + let sandbox; + + before(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(TraversePlugin.prototype, '_wrapNodeListen'); + }); + + it('should callback false if hostname not public', function(done) { + let plugin = new TraversePlugin({ + contact: { hostname: '127.0.0.1', port: 8080 }, + logger + }, []); + plugin._testIfReachable((err, result) => { + expect(result).to.equal(false); + done(); + }); + }); + + it('should callback true if ping succeeds', function(done) { + let plugin = new TraversePlugin({ + contact: { hostname: 'public.hostname', port: 8080 }, + ping: sandbox.stub().callsArgWith(1, null), + identity: Buffer.from('nodeid'), + logger + }, []); + plugin._testIfReachable((err, result) => { + expect(result).to.equal(true); + done(); + }); + }); + + after(() => sandbox.restore()) + + }); + + describe('@private @method _wrapNodeListen', function() { + + let sandbox; + + before(() => { + sandbox = sinon.sandbox.create(); + }); + + it('should call listen callback', function(done) { + let listen = sandbox.stub().callsArg(1); + let node = { + contact: { hostname: '127.0.0.1', port: 8080 }, + listen, + logger + }; + let plugin = new TraversePlugin(node); + let _exec = sandbox.stub(plugin, '_execTraversalStrategies').callsArg(0); + node.listen(8080, () => { + expect(_exec.called).to.equal(true); + done(); + }); + }); + + it('should call node#listen and log error from _exec...', function(done) { + let listen = sandbox.stub().callsArg(1); + let error = sandbox.stub(); + let node = { + contact: { hostname: '127.0.0.1', port: 8080 }, + listen, + logger: { error } + }; + let plugin = new TraversePlugin(node); + sandbox.stub( + plugin, + '_execTraversalStrategies' + ).callsArgWith(0, new Error('failed')); + node.listen(8080); + setImmediate(() => { + expect(error.called).to.equal(true); + done(); + }); + }); + + it('should call node#listen and log warn from _exec...', function(done) { + let listen = sandbox.stub().callsArg(1); + let warn = sandbox.stub(); + let node = { + contact: { hostname: '127.0.0.1', port: 8080 }, + listen, + logger: { warn } + }; + let plugin = new TraversePlugin(node); + sandbox.stub( + plugin, + '_execTraversalStrategies' + ).callsArg(0); + node.listen(8080); + setImmediate(() => { + expect(warn.called).to.equal(true); + done(); + }); + }); + + it('should call node#listen and log info from _exec...', function(done) { + let listen = sandbox.stub().callsArg(1); + let info = sandbox.stub(); + let node = { + contact: { hostname: '127.0.0.1', port: 8080 }, + listen, + logger: { info } + }; + let plugin = new TraversePlugin(node); + sandbox.stub( + plugin, + '_execTraversalStrategies' + ).callsArgWith(0, null, true); + node.listen(8080); + setImmediate(() => { + expect(info.called).to.equal(true); + done(); + }); + }); + + it('should set test interval and rety traversal if fail', function(done) { + let clock = sandbox.useFakeTimers('setInterval'); + let listen = sandbox.stub().callsArg(1); + let info = sandbox.stub(); + let warn = sandbox.stub(); + let node = { + contact: { hostname: '127.0.0.1', port: 8080 }, + listen, + logger: { info, warn } + }; + let plugin = new TraversePlugin(node); + let _testIfReachable = sandbox.stub( + plugin, + '_testIfReachable' + ).callsArgWith(0, null, true); + _testIfReachable.onCall(1).callsArgWith(0, null, false); + let _execTraversalStrategies = sandbox.stub( + plugin, + '_execTraversalStrategies' + ).callsArgWith(0, null, true); + node.listen(8080, () => { + expect(info.called).to.equal(true); + expect(plugin._testInterval).to.not.equal(undefined); + expect(_execTraversalStrategies.callCount).to.equal(1); + clock.tick(1200000); + expect(_execTraversalStrategies.callCount).to.equal(2); + expect(warn.called).to.equal(true); + done(); + }); + }); + + after(() => sandbox.restore()); + + }); + +}); + +describe('NATPMPStrategy', function() { + + describe('@method exec', function() { + + it('should create port mapping and get ip', function(done) { + let { NATPMPStrategy } = proxyquire('../lib/plugin-traverse', { + 'nat-pmp': { + connect: sinon.stub().returns({ + portMapping: sinon.stub().callsArg(1), + externalIp: sinon.stub().callsArgWith(0, null, { + ip: ['some', 'ip', 'addr'] + }) + }) + }, + network: { + get_gateway_ip: sinon.stub().callsArgWith( + 0, + null, + 'gateway.ip.addr' + ) + } + }); + let strategy = new NATPMPStrategy({ publicPort: 8081 }); + let node = { contact: { hostname: '127.0.0.1', port: 8080 }, logger }; + strategy.exec(node, (err) => { + expect(err).to.equal(null); + expect(node.contact.hostname).to.equal('some.ip.addr'); + expect(node.contact.port).to.equal(8081); + done(); + }); + }); + + it('should callback with error', function(done) { + let { NATPMPStrategy } = proxyquire('../lib/plugin-traverse', { + 'nat-pmp': { + connect: sinon.stub().returns({ + portMapping: sinon.stub().callsArg(1), + externalIp: sinon.stub().callsArgWith( + 0, + new Error('Failed to get IP') + ) + }) + }, + network: { + get_gateway_ip: sinon.stub().callsArgWith( + 0, + null, + 'gateway.ip.addr' + ) + } + }); + let strategy = new NATPMPStrategy(); + let node = { contact: { hostname: '127.0.0.1', port: 8080 }, logger }; + strategy.exec(node, (err) => { + expect(err.message).to.equal('Failed to get IP'); + done(); + }); + }); + + }); + +}); + +describe('ReverseTunnelStrategy', function() { + + describe('@method exec', function() { + + it('should update the contact info', function(done) { + let tunnel = new EventEmitter(); + tunnel.open = sinon.stub(); + tunnel.url = 'https://nodeid.tun.tacticalchihuahua.lol:443'; + let { ReverseTunnelStrategy } = proxyquire('../lib/plugin-traverse', { + '@tacticalchihuahua/diglet': { + Tunnel: function(opts) { + expect(opts.localAddress).to.equal('127.0.0.1'); + expect(opts.localPort).to.equal(8080); + expect(opts.remoteAddress).to.equal('tun.tacticalchihuahua.lol'); + expect(opts.remotePort).to.equal(8443); + return tunnel; + } + } + }); + let strategy = new ReverseTunnelStrategy(); + let node = { + contact: { hostname: '127.0.0.1', port: 8080 }, + identity: Buffer.from('nodeid') + }; + strategy.exec(node, (err) => { + expect(err).to.equal(undefined); + expect(tunnel.open.called).to.equal(true); + expect(node.contact.hostname).to.equal('nodeid.tun.tacticalchihuahua.lol'); + expect(node.contact.port).to.equal(443); + expect(node.contact.protocol).to.equal('https:'); + done(); + }); + setImmediate(() => tunnel.emit('connected')); + }); + + }); + +}); + +describe('UPNPStrategy', function() { + + describe('@method exec', function() { + + it('should create port mapping and get ip', function(done) { + let strategy = new UPNPStrategy({ publicPort: 8081 }); + sinon.stub(strategy.client, 'portMapping').callsArg(1); + sinon.stub(strategy.client, 'externalIp').callsArgWith( + 0, + null, + 'some.ip.addr' + ); + let node = { contact: { hostname: '127.0.0.1', port: 8080 } }; + strategy.exec(node, (err) => { + expect(err).to.equal(null); + expect(node.contact.port).to.equal(8081); + expect(node.contact.hostname).to.equal('some.ip.addr'); + done(); + }); + }); + + it('should callback with error', function(done) { + let strategy = new UPNPStrategy(); + sinon.stub(strategy.client, 'portMapping').callsArg(1); + sinon.stub(strategy.client, 'externalIp').callsArgWith( + 0, + new Error('Failed to get IP') + ); + let node = { contact: { hostname: '127.0.0.1', port: 8080 } }; + strategy.exec(node, (err) => { + expect(err.message).to.equal('Failed to get IP'); + done(); + }); + }); + + }); + +}); diff --git a/test/plugin-trust.e2e.js b/test/plugin-trust.e2e.js new file mode 100644 index 0000000..aa08b31 --- /dev/null +++ b/test/plugin-trust.e2e.js @@ -0,0 +1,155 @@ +'use strict'; + +const { expect } = require('chai'); +const kadence = require('..'); +const network = require('./fixtures/node-generator'); +const trust = require('../lib/plugin-trust'); +const sinon = require('sinon'); +const async = require('async'); + + +describe('@module kadence/trust + @class UDPTransport', function() { + + let clock = null; + let [node1, node2, node3, node4] = network(4, kadence.UDPTransport); + + before(function(done) { + this.timeout(12000); + clock = sinon.useFakeTimers(0); + async.eachSeries([node1, node2, node3, node4], (node, next) => { + node.listen(node.contact.port, next); + }, done); + }); + + after(function() { + clock.restore(); + process._getActiveHandles().forEach((h) => h.unref()); + }) + + it('should allow the whitelisted contact', function(done) { + node2.trust = node2.plugin(trust([ + { + identity: node1.identity, + methods: ['PING'] + } + ], trust.MODE_WHITELIST)); + node1.trust = node1.plugin(trust([ + { + identity: node2.identity, + methods: ['PING'] + } + ], trust.MODE_WHITELIST)); + node1.send('PING', [], [ + node2.identity.toString('hex'), + node2.contact + ], done); + }); + + it('should prevent the blacklisted contact', function(done) { + node3.trust = node3.plugin(trust([ + { + identity: node1.identity, + methods: ['PING'] + } + ], trust.MODE_BLACKLIST)); + node1.trust.addTrustPolicy({ + identity: node3.identity, + methods: ['*'] + }) + node1.send('PING', [], [ + node3.identity.toString('hex'), + node3.contact + ], err => { + expect(err.message.includes('Refusing')).to.equal(true); + done(); + }); + }); + + it('should allow the non-blacklisted contact', function(done) { + node2.trust.addTrustPolicy({ + identity: node3.identity.toString('hex'), + methods: ['PING'] + }) + node2.send('PING', [], [ + node3.identity.toString('hex'), + node3.contact + ], done); + }); + + it('should prevent the non-whitelisted contact', function(done) { + node4.send('PING', [], [ + node2.identity.toString('hex'), + node2.contact + ], err => { + expect(err.message.includes('Refusing')).to.equal(true); + done(); + }); + }); + + it('should blacklist all nodes from using PING', function(done) { + node3.trust.addTrustPolicy({ + identity: '*', + methods: ['PING'] + }); + node2.send('PING', [], [ + node3.identity.toString('hex'), + node3.contact + ], err => { + expect(err.message.includes('Refusing')).to.equal(true); + node2.send('PING', [], [ + node3.identity.toString('hex'), + node3.contact + ], err => { + expect(err.message.includes('Refusing')).to.equal(true); + done(); + }); + }); + }); + + it('should refuse send to node with missing trust policy', function(done) { + node1.trust.removeTrustPolicy(node2.identity); + node1.send('PING', [], [ + node2.identity.toString('hex'), + node2.contact + ], err => { + expect(err.message.includes('Refusing')).to.equal(true); + done(); + }); + }); + + it('should allow if method is not blacklisted', function(done) { + node2.trust.addTrustPolicy({ + identity: node3.identity, + methods: ['PING'] + }); + node3.trust.addTrustPolicy({ + identity: node2.identity, + methods: ['FIND_NODE'] + }); + node2.send('PING', [], [ + node3.identity, + node3.contact + ], done); + }); + + it('should reject if method is not whitelisted', function(done) { + node4.trust = node4.plugin(trust([ + { + identity: node2.identity, + methods: ['FIND_NODE'] + } + ], trust.MODE_WHITELIST)); + node2.trust.addTrustPolicy({ + identity: node4.identity, + methods: ['PING'] + }); + node4.send('FIND_NODE', [], [ + node2.identity.toString('hex'), + node2.contact + ], err => { + expect(err.message.includes('Refusing')).to.equal(true); + done(); + }); + }); + +}); diff --git a/test/routing-table.unit.js b/test/routing-table.unit.js new file mode 100644 index 0000000..f14b782 --- /dev/null +++ b/test/routing-table.unit.js @@ -0,0 +1,170 @@ +'use strict'; + +const { expect } = require('chai'); +const utils = require('../lib/utils'); +const constants = require('../lib/constants'); +const RoutingTable = require('../lib/routing-table'); + + +describe('@class RoutingTable', function() { + + describe('@constructor', function() { + + it('should use the given identity and create B buckets', function() { + let identity = utils.getRandomKeyBuffer(); + let router = new RoutingTable(identity); + expect(router.identity).to.equal(identity); + expect([...router.entries()]).to.have.lengthOf(constants.B); + }); + + }); + + describe('@property size', function() { + + it('should return the total contacts across all buckets', function() { + let router = new RoutingTable(); + let contacts = 20; + let counter = 0; + while (counter < contacts) { + router.addContactByNodeId(utils.getRandomKeyString(), {}); + counter++; + } + expect(router.size).to.equal(contacts); + }); + + }); + + describe('@property length', function() { + + it('should return the number of buckets', function() { + let router = new RoutingTable(); + expect(router.length).to.equal(constants.B); + }); + + }); + + describe('@method indexOf', function() { + + it('should return the bucket index for the given contact', function() { + let router = new RoutingTable(); + let nodeId = utils.getRandomKeyString(); + let [bucketIndex] = router.addContactByNodeId(nodeId, {}); + expect(router.indexOf(nodeId)).to.equal(bucketIndex); + }); + + }); + + describe('@method getContactByNodeId', function() { + + it('should return the contact object by node id', function() { + let router = new RoutingTable(); + let nodeId = utils.getRandomKeyString(); + let contactObj = { hostname: 'localhost', port: 8080 }; + router.addContactByNodeId(nodeId, contactObj); + expect(router.getContactByNodeId(nodeId)).to.equal(contactObj); + }); + + }); + + describe('@method removeContactByNodeId', function() { + + it('should remove the contact object by node id', function() { + let router = new RoutingTable(); + let nodeId = utils.getRandomKeyString(); + let contactObj = { hostname: 'localhost', port: 8080 }; + router.addContactByNodeId(nodeId, contactObj); + router.removeContactByNodeId(nodeId); + expect(router.getContactByNodeId(nodeId)).to.equal(undefined); + }); + + }); + + describe('@method addContactByNodeId', function() { + + it('shoulod add the contact to the appropriate bucket', function() { + let identity = Buffer.from('ab61ae6158346ec83b178f389d1574589f86dd4e', + 'hex'); + let nodeId = Buffer.from('c22e091488e9502c09f7c9f8115f386253148a38', + 'hex'); + let router = new RoutingTable(identity); + let [bucketIndex] = router.addContactByNodeId(nodeId, {}); + expect(bucketIndex).to.equal(158); + }); + + }); + + describe('@method getClosestBucket', function() { + + it('should return the lowest occupied bucket', function() { + let router = new RoutingTable(); + router.get(10).set('0', {}); + router.get(20).set('1', {}); + router.get(50).set('2', {}); + router.get(140).set('3', {}); + expect(router.getClosestBucket()[0]).to.equal(10); + expect(router.getClosestBucket()[1]).to.equal(router.get(10)); + }); + + }); + + describe('@method getClosestContactsToKey', function() { + + it('should return the contacts closest to the key', function() { + let identity = Buffer.from('9e64985b4256a273614165ee75a26076ed8ee5df', + 'hex'); + let router = new RoutingTable(identity); + let contacts = [ + 'ca284a43d8e3028eefeb0fde889b4cab0953d799', + '0093f9f3b8435e6eac92a4651241c8463420c9e1', + '8724a5e6251cff03f09616cb142c60398a4f2c8d', + 'db61a7f76abe933e8f949150a27ace0df9278706', + 'e9a951fe6e6f5eb0ff1c02a49bd7339d2d07ff75', + '0f3e8bc3ae0bfd4e204fbf1e06ff042d485c32b7', + '72ea79157be0ddfb0404e0ae35a1df10d382f900', + '536496314bb9b26b89e409ebe80956ca0d5081da', + '056ca971e4714ee25b7c08cb64b748d812918a7d', + '52b16f4d2c53e32a06086ea7a57cf48112dcd370', + '3bc5ceb7e3f0ea4587a430f88145aa9ef4bdc3c4', + 'efbd1cfedcc0d0215b6c4a500bfeeaf96f77d98c', + 'a5eeda3fc29fa699d252f21a820ea161870bc021', + '20baafefa205bfed00bca8356ec0c65bbefeac18', + '08c7cfd5eecded3bc32b94e908cfc5ec59094cc7', + 'e120819a5392b44a5a7a77725de0e50c33dac099', + 'ca6facc2445dbb615dedab4ac0d4dce838d192cf', + 'ed4b23470abd66d8466db8f7d097a6726a52ceb7', + '784c30f33b9b703bf87b0622823f5c7142628438', + 'c1bb8a8a74135e936115af58b7057122582caff7', + '2a3e0e87d1591b08e951fd03cecb06e652fef628', + '35d7aa4cc240b47e3af901b9ea764725aa4dd15c', + '41bb17f1ec4e8dc84f975f648b4d1e2794474df9', + 'a5c59b275121762079cdfa5aa6f89c9aab3893af', + 'f8608e690d64f66639d5b54a0efd87ac89d97148' + ]; + let key = '9e6498b8f241d8ec1eaf33110b12451f224fa444'; + contacts.forEach((id) => router.addContactByNodeId(id, { id })) + let closest = [...router.getClosestContactsToKey(key).keys()]; + expect(contacts.indexOf(closest[0])).to.equal(2); + expect(contacts.indexOf(closest[1])).to.equal(12); + expect(contacts.indexOf(closest[2])).to.equal(23); + expect(contacts.indexOf(closest[3])).to.equal(3); + expect(contacts.indexOf(closest[4])).to.equal(16); + expect(contacts.indexOf(closest[5])).to.equal(0); + expect(contacts.indexOf(closest[6])).to.equal(19); + expect(contacts.indexOf(closest[7])).to.equal(24); + expect(contacts.indexOf(closest[8])).to.equal(11); + expect(contacts.indexOf(closest[9])).to.equal(17); + expect(contacts.indexOf(closest[10])).to.equal(4); + expect(contacts.indexOf(closest[11])).to.equal(15); + expect(contacts.indexOf(closest[12])).to.equal(5); + expect(contacts.indexOf(closest[13])).to.equal(14); + expect(contacts.indexOf(closest[14])).to.equal(8); + expect(contacts.indexOf(closest[15])).to.equal(1); + expect(contacts.indexOf(closest[16])).to.equal(10); + expect(contacts.indexOf(closest[17])).to.equal(21); + expect(contacts.indexOf(closest[18])).to.equal(20); + expect(contacts.indexOf(closest[19])).to.equal(13); + }); + + }); + +}); diff --git a/test/rules-errors.unit.js b/test/rules-errors.unit.js new file mode 100644 index 0000000..2661624 --- /dev/null +++ b/test/rules-errors.unit.js @@ -0,0 +1,62 @@ +'use strict'; + +const { expect } = require('chai'); +const { stub } = require('sinon'); +const ErrorRules = require('../lib/rules-errors'); + + +describe('@class ErrorRules', function() { + + const errors = new ErrorRules(); + + describe('@method methodNotFound', function() { + + it('should call the next function if error', function(done) { + let send = stub(); + let error = stub(); + errors.methodNotFound(new Error('Some error'), {}, { + send, error + }, () => { + expect(send.called).to.equal(false); + expect(error.called).to.equal(false); + done(); + }); + }); + + it('should call send with error params', function(done) { + errors.methodNotFound(null, {}, { + error: (message, code) => { + expect(message).to.equal('Method not found'); + expect(code).to.equal(-32601); + done(); + } + }); + }); + + }); + + describe('@method internalError', function() { + + it('should call send with error params and next', function(done) { + errors.internalError(new Error('Some error'), {}, { + error: (message, code) => { + expect(message).to.equal('Some error'); + expect(code).to.equal(-32603); + } + }, done); + }); + + it('should call use the given error code', function(done) { + let err = new Error('Some error'); + err.code = 500; + errors.internalError(err, {}, { + error: (message, code) => { + expect(message).to.equal('Some error'); + expect(code).to.equal(500); + } + }, done); + }); + + }); + +}); diff --git a/test/rules-kademlia.unit.js b/test/rules-kademlia.unit.js new file mode 100644 index 0000000..4b059c8 --- /dev/null +++ b/test/rules-kademlia.unit.js @@ -0,0 +1,295 @@ +'use strict'; + +const { expect } = require('chai'); +const { stub } = require('sinon'); +const utils = require('../lib/utils'); +const KademliaRules = require('../lib/rules-kademlia'); + + +describe('@class KademliaRules', function() { + + describe('@method ping', function() { + + it('should respond with empty params', function(done) { + let rules = new KademliaRules(); + rules.ping({ + id: 'message_id', + method: 'PING', + params: [] + }, { + send: function(result) { + expect(Array.isArray(result)).to.equal(true); + expect(result).to.have.lengthOf(0); + done(); + } + }); + }); + + }); + + describe('@method store', function() { + + it('should pass to error handler if no item', function(done) { + let rules = new KademliaRules(); + let send = stub(); + rules.store({ + id: 'message_id', + method: 'STORE', + params: [ + utils.getRandomKeyString() + ] + }, { send }, (err) => { + expect(send.called).to.equal(false); + expect(err.message).to.equal('Invalid storage item supplied'); + done(); + }); + }); + + it('should pass to error handler if no timestamp', function(done) { + let rules = new KademliaRules(); + let send = stub(); + rules.store({ + id: 'message_id', + method: 'STORE', + params: [ + utils.getRandomKeyString(), + { + timestamp: 'wednesday', + publisher: utils.getRandomKeyString(), + value: 'some string' + } + ] + }, { send }, (err) => { + expect(send.called).to.equal(false); + expect(err.message).to.equal('Invalid timestamp supplied'); + done(); + }); + }); + + it('should pass to error handler if invalid publisher', function(done) { + let rules = new KademliaRules(); + let send = stub(); + rules.store({ + id: 'message_id', + method: 'STORE', + params: [ + utils.getRandomKeyString(), + { + timestamp: Date.now(), + publisher: 'bookchin', + value: 'some string' + } + ] + }, { send }, (err) => { + expect(send.called).to.equal(false); + expect(err.message).to.equal('Invalid publisher identity supplied'); + done(); + }); + }); + + it('should pass to error handler if invalid key', function(done) { + let rules = new KademliaRules(); + let send = stub(); + rules.store({ + id: 'message_id', + method: 'STORE', + params: [ + 'some key', + { + timestamp: Date.now(), + publisher: utils.getRandomKeyString(), + value: 'some string' + } + ] + }, { send }, (err) => { + expect(send.called).to.equal(false); + expect(err.message).to.equal('Invalid item key supplied'); + done(); + }); + }); + + it('should pass to error handler if undefined value', function(done) { + let rules = new KademliaRules(); + let send = stub(); + rules.store({ + id: 'message_id', + method: 'STORE', + params: [ + utils.getRandomKeyString(), + { + timestamp: Date.now(), + publisher: utils.getRandomKeyString(), + value: undefined + } + ] + }, { send }, (err) => { + expect(send.called).to.equal(false); + expect(err.message).to.equal('Invalid item value supplied'); + done(); + }); + }); + + it('should pass to error handler if store fail', function(done) { + let rules = new KademliaRules({ + storage: { + put: stub().callsArgWith(3, new Error('Failed to store item')) + } + }); + let send = stub(); + rules.store({ + id: 'message_id', + method: 'STORE', + params: [ + utils.getRandomKeyString(), + { + timestamp: Date.now(), + publisher: utils.getRandomKeyString(), + value: 'some string' + } + ] + }, { send }, (err) => { + expect(send.called).to.equal(false); + expect(err.message).to.equal('Failed to store item'); + done(); + }); + }); + + it('should echo back arguments if stored', function(done) { + let rules = new KademliaRules({ + storage: { + put: stub().callsArgWith(3, null) + } + }); + let key = utils.getRandomKeyString(); + let timestamp = Date.now(); + let publisher = utils.getRandomKeyString(); + let value = 'some string'; + rules.store({ + id: 'message_id', + method: 'STORE', + params: [ + key, + { timestamp, publisher, value } + ] + }, { + send: (result) => { + expect(result[0]).to.equal(key); + expect(result[1].timestamp).to.equal(timestamp); + expect(result[1].publisher).to.equal(publisher); + expect(result[1].value).to.equal(value); + done(); + } + }); + }); + + }); + + describe('@method findNode', function() { + + it('should pass to error handler if invalid key', function(done) { + let rules = new KademliaRules(); + let send = stub(); + rules.findNode({ + id: 'message_id', + method: 'FIND_NODE', + params: ['invalid key'] + }, { send }, (err) => { + expect(send.called).to.equal(false); + expect(err.message).to.equal('Invalid lookup key supplied'); + done(); + }); + }); + + it('should send result router#getClosestContactsToKey', function(done) { + let contacts = new Map(); + contacts.set('node id', { hostname: 'localhost', port: 8080 }); + let rules = new KademliaRules({ + router: { + getClosestContactsToKey: () => contacts + } + }); + rules.findNode({ + id: 'message_id', + method: 'FIND_NODE', + params: [utils.getRandomKeyString()] + }, { + send: (result) => { + expect(Array.isArray(result)).to.equal(true); + expect(result[0][0]).to.equal('node id'); + expect(result[0][1].hostname).to.equal('localhost'); + expect(result[0][1].port).to.equal(8080); + done(); + } + }); + }); + + }); + + describe('@method findValue', function() { + + it('should pass to error handler if invalid key', function(done) { + let rules = new KademliaRules(); + let send = stub(); + rules.findValue({ + id: 'message_id', + method: 'FIND_VALUE', + params: ['invalid key'] + }, { send }, (err) => { + expect(send.called).to.equal(false); + expect(err.message).to.equal('Invalid lookup key supplied'); + done(); + }); + }); + + it('should call findNode if item not found', function(done) { + let contacts = new Map(); + contacts.set('node id', { hostname: 'localhost', port: 8080 }); + let rules = new KademliaRules({ + storage: { + get: stub().callsArgWith(2, new Error('Not found')) + }, + router: { + getClosestContactsToKey: stub().returns(contacts) + } + }); + rules.findValue({ + id: 'message_id', + method: 'FIND_VALUE', + params: [utils.getRandomKeyString()] + }, { + send: (result) => { + expect(Array.isArray(result)).to.equal(true); + expect(result[0][0]).to.equal('node id'); + expect(result[0][1].hostname).to.equal('localhost'); + expect(result[0][1].port).to.equal(8080); + done(); + } + }); + }); + + it('should respond with the item if found', function(done) { + let item = { + timestamp: Date.now(), + publisher: utils.getRandomKeyString(), + value: 'some string' + }; + let rules = new KademliaRules({ + storage: { + get: stub().callsArgWith(2, null, item) + } + }); + rules.findValue({ + id: 'message_id', + method: 'FIND_VALUE', + params: [utils.getRandomKeyString()] + }, { + send: (result) => { + expect(result).to.equal(item); + done(); + } + }); + }); + + }); + +}); diff --git a/test/transport-http.unit.js b/test/transport-http.unit.js new file mode 100644 index 0000000..8ace37b --- /dev/null +++ b/test/transport-http.unit.js @@ -0,0 +1,352 @@ +'use strict'; + +const { expect } = require('chai'); +const { Server, ClientRequest } = require('http'); +const { Socket } = require('net'); +const { EventEmitter } = require('events'); +const { Readable: ReadableStream } = require('stream'); +const sinon = require('sinon'); +const constants = require('../lib/constants'); +const HTTPTransport = require('../lib/transport-http'); + + +describe('@class HTTPTransport', function() { + + describe('@constructor', function() { + + it('should bubble errors from the underlying server', function(done) { + let httpTransport = new HTTPTransport(); + httpTransport.once('error', (err) => { + expect(err.message).to.equal('Server error'); + done(); + }); + setImmediate( + () => httpTransport.server.emit('error', new Error('Server error')) + ); + }); + + it('should call timeout pending requests every interval', function(done) { + let clock = sinon.useFakeTimers(); + let httpTransport = new HTTPTransport(); + let _timeoutPending = sinon.stub(httpTransport, '_timeoutPending'); + setTimeout(() => { + clock.restore(); + setImmediate(() => { + expect(_timeoutPending.called).to.equal(true); + done(); + }); + }, constants.T_RESPONSETIMEOUT); + clock.tick(constants.T_RESPONSETIMEOUT); + }); + + }); + + describe('@private _createRequest', function() { + + it('should return a client request object', function() { + let httpTransport = new HTTPTransport(); + expect(httpTransport._createRequest({ + hostname: 'localhost', + port: 8080, + createConnection: () => new Socket() + })).to.be.instanceOf(ClientRequest); + }); + + }); + + describe('@private _createServer', function() { + + it('should return a http server object', function() { + let httpTransport = new HTTPTransport(); + expect(httpTransport._createServer()).to.be.instanceOf(Server); + }); + + }); + + describe('@private _timeoutPending', function() { + + it('should close the sockets that are timed out', function() { + let httpTransport = new HTTPTransport(); + let end = sinon.stub(); + httpTransport._pending.set('1', { + timestamp: Date.now() - constants.T_RESPONSETIMEOUT, + response: { end } + }); + httpTransport._pending.set('2', { + timestamp: Date.now() - constants.T_RESPONSETIMEOUT, + response: { end } + }); + httpTransport._pending.set('3', { + timestamp: Date.now(), + response: { end } + }); + httpTransport._timeoutPending(); + expect(httpTransport._pending.size).to.equal(1); + expect(end.callCount).to.equal(2); + }); + + }); + + describe('@private _read', function() { + + it('should bubble errors the incoming request', function(done) { + let httpTransport = new HTTPTransport(); + let request = new ReadableStream({ read: () => null }); + request.headers = {}; + let response = new EventEmitter(); + response.end = sinon.stub(); + httpTransport.resume(); + setImmediate(() => { + httpTransport.server.emit('request', request, response); + setImmediate(() => { + request.emit('error', new Error('Request error')); + }); + }); + httpTransport.on('error', (err) => { + expect(err.message).to.equal('Request error'); + done(); + }); + }); + + it('should bubble errors the outgoing response', function(done) { + let httpTransport = new HTTPTransport(); + let request = new ReadableStream({ read: () => null }); + request.headers = {}; + let response = new EventEmitter(); + response.end = sinon.stub(); + httpTransport.resume(); + setImmediate(() => { + httpTransport.server.emit('request', request, response); + setImmediate(() => { + response.emit('error', new Error('Response error')); + }); + }); + httpTransport.on('error', (err) => { + expect(err.message).to.equal('Response error'); + done(); + }); + }); + + it('should send back 400 if no message id header', function(done) { + let httpTransport = new HTTPTransport(); + let request = new ReadableStream({ read: () => null }); + request.headers = {}; + let response = new EventEmitter(); + response.end = sinon.stub(); + httpTransport.resume(); + setImmediate(() => { + httpTransport.server.emit('request', request, response); + setImmediate(() => { + expect(response.statusCode).to.equal(400); + expect(response.end.called).to.equal(true); + done(); + }); + }); + }); + + it('should set code to 405 if not post or options', function(done) { + let httpTransport = new HTTPTransport(); + let request = new ReadableStream({ read: () => null }); + request.headers = { + 'x-kad-message-id': 'message-id' + }; + request.method = 'GET'; + let response = new EventEmitter(); + response.end = sinon.stub(); + response.setHeader = sinon.stub(); + httpTransport.resume(); + setImmediate(() => { + httpTransport.server.emit('request', request, response); + setImmediate(() => { + expect(response.statusCode).to.equal(405); + expect(response.end.called).to.equal(true); + done(); + }); + }); + }); + + it('should not process request if not post method', function(done) { + let httpTransport = new HTTPTransport(); + let request = new ReadableStream({ read: () => null }); + request.headers = { + 'x-kad-message-id': 'message-id' + }; + request.method = 'OPTIONS'; + let response = new EventEmitter(); + response.end = sinon.stub(); + response.setHeader = sinon.stub(); + httpTransport.resume(); + setImmediate(() => { + httpTransport.server.emit('request', request, response); + setImmediate(() => { + expect(response.end.called).to.equal(true); + done(); + }); + }); + }); + + it('should buffer message, set pending, and push data', function(done) { + let httpTransport = new HTTPTransport(); + let request = new ReadableStream({ read: () => null }); + request.headers = { + 'x-kad-message-id': 'message-id' + }; + request.method = 'POST'; + let response = new EventEmitter(); + response.end = sinon.stub(); + response.setHeader = sinon.stub(); + httpTransport.once('data', (buffer) => { + expect(buffer.toString()).to.equal('test'); + expect(httpTransport._pending.get('message-id').response).to.equal( + response + ); + done(); + }); + setImmediate(() => { + httpTransport.server.emit('request', request, response); + setImmediate(() => { + request.push(Buffer.from('test')); + request.push(null); + }); + }); + }); + + }); + + describe('@private _write', function() { + + it('should respond to the pending request if matched', function(done) { + let httpTransport = new HTTPTransport(); + let response = { end: sinon.stub() }; + httpTransport._pending.set('test', { + timestamp: Date.now(), + response + }); + httpTransport.write(['test', Buffer.from('test'), [ + 'RECEIVER', + { + hostname: 'localhost', + port: 8080 + } + ]]); + setImmediate(() => { + expect(response.end.called).to.equal(true); + expect(httpTransport._pending.size).to.equal(0); + done(); + }); + }); + + it('should create a request and push the response back', function(done) { + let httpTransport = new HTTPTransport(); + let request = new EventEmitter(); + request.end = sinon.stub(); + let _createRequest = sinon.stub(httpTransport, '_createRequest') + .returns(request); + httpTransport.write(['test', Buffer.from('test'), ['RECEIVER', { + hostname: 'localhost', + port: 8080, + protocol: 'http:' + }]]); + setImmediate(() => { + let response = new ReadableStream({ read: ()=> null }); + request.emit('response', response); + setImmediate(() => { + response.push(Buffer.from('test')); + response.push(null); + setImmediate(() => { + expect(httpTransport.read().toString()).to.equal('test'); + _createRequest.restore(); + done(); + }); + }); + }); + }); + + it('should create a request and emit an error event', function(done) { + let httpTransport = new HTTPTransport(); + let request = new EventEmitter(); + request.end = sinon.stub(); + let _createRequest = sinon.stub(httpTransport, '_createRequest') + .returns(request); + httpTransport.write(['test', Buffer.from('test'), ['RECEIVER', { + hostname: 'localhost', + port: 8080, + protocol: 'http:' + }]]); + setImmediate(() => { + let response = new ReadableStream({ read: ()=> null }); + response.statusCode = 400; + request.emit('response', response); + httpTransport.once('error', (err) => { + _createRequest.restore(); + expect(err.message).to.equal('Bad request'); + done(); + }); + setImmediate(() => { + response.push(Buffer.from('Bad request')); + response.push(null); + }); + }); + }); + + it('should bubble response errors', function(done) { + let httpTransport = new HTTPTransport(); + let request = new EventEmitter(); + request.end = sinon.stub(); + let _createRequest = sinon.stub(httpTransport, '_createRequest') + .returns(request); + httpTransport.write(['test', Buffer.from('test'), ['RECEIVER', { + hostname: 'localhost', + port: 8080, + protocol: 'http:' + }]]); + setImmediate(() => { + let response = new ReadableStream({ read: ()=> null }); + request.emit('response', response); + setImmediate(() => { + httpTransport.once('error', (err) => { + expect(err.message).to.equal('Response error'); + _createRequest.restore(); + done(); + }); + setImmediate(() => { + response.emit('error', new Error('Response error')); + }); + }); + }); + }); + + it('should bubble request errors', function(done) { + let httpTransport = new HTTPTransport(); + let request = new EventEmitter(); + request.end = sinon.stub(); + let _createRequest = sinon.stub(httpTransport, '_createRequest') + .returns(request); + httpTransport.write(['test', Buffer.from('test'), ['RECEIVER', { + hostname: 'localhost', + port: 8080, + protocol: 'http:' + }]]); + httpTransport.once('error', (err) => { + expect(err.message).to.equal('Request error'); + _createRequest.restore(); + done(); + }); + setImmediate(() => { + request.emit('error', new Error('Request error')); + }); + }); + }); + + describe('@method listen', function() { + + it('should call Server#listen with args', function() { + let httpTransport = new HTTPTransport(); + let listen = sinon.stub(httpTransport.server, 'listen'); + httpTransport.listen(8080, 'localhost'); + expect(listen.calledWithMatch(8080, 'localhost')); + }); + + }); + +}); diff --git a/test/transport-https.unit.js b/test/transport-https.unit.js new file mode 100644 index 0000000..728d537 --- /dev/null +++ b/test/transport-https.unit.js @@ -0,0 +1,46 @@ +'use strict'; + +const { expect } = require('chai'); +const { Server } = require('https'); +const { ClientRequest } = require('http'); +const { Socket } = require('net'); +const pem = require('pem'); +const HTTPSTransport = require('../lib/transport-https'); + + +describe('@class HTTPSTransport', function() { + + let httpsTransport; + + before((done) => { + pem.createCertificate({ days: 1, selfSigned: true }, (err, keys) => { + httpsTransport = new HTTPSTransport({ + key: keys.serviceKey, + cert: keys.certificate + }); + done(); + }); + }); + + describe('@private _createRequest', function() { + + it('should return a client request object', function() { + let req = httpsTransport._createRequest({ + hostname: 'localhost', + port: 8080, + createConnection: () => new Socket() + }); + expect(req).to.be.instanceOf(ClientRequest); + }); + + }); + + describe('@private _createServer', function() { + + it('should return an instance of https.Server', function() { + expect(httpsTransport.server).to.be.instanceOf(Server); + }); + + }); + +}); diff --git a/test/transport-udp.unit.js b/test/transport-udp.unit.js new file mode 100644 index 0000000..1042c04 --- /dev/null +++ b/test/transport-udp.unit.js @@ -0,0 +1,110 @@ +'use strict'; + +const { EventEmitter } = require('events'); +const { expect } = require('chai'); +const { stub } = require('sinon'); +const proxyquire = require('proxyquire'); + + +describe('@class UDPTransport', function() { + + describe('@constructor', function() { + + it('should add error listener to socket', function(done) { + const socket = new EventEmitter(); + const FakeUDPTransport = proxyquire('../lib/transport-udp', { + dgram: { + createSocket: () => socket + } + }); + const udpTransport = new FakeUDPTransport(); + udpTransport.on('error', (err) => { + expect(err.message).to.equal('Socket error'); + done(); + }); + setImmediate(() => socket.emit('error', new Error('Socket error'))); + }); + + }); + + describe('@private _write', function() { + + it('should send the buffer to the contact over the socket', function() { + const socket = new EventEmitter(); + socket.send = stub(); + const FakeUDPTransport = proxyquire('../lib/transport-udp', { + dgram: { + createSocket: () => socket + } + }); + const udpTransport = new FakeUDPTransport(); + const message = Buffer.from('hello world'); + udpTransport.write([ + 'message_id', + message, + [ + 'identityKey', + { hostname: 'localhost', port: 8080 } + ] + ]); + expect(socket.send.calledWithMatch( + message, + 0, + message.length, + 8080, + 'localhost' + )).to.equal(true); + }); + + }); + + describe('@private _read', function() { + + it('should emit data for every message received', function(done) { + const socket = new EventEmitter(); + const FakeUDPTransport = proxyquire('../lib/transport-udp', { + dgram: { + createSocket: () => socket + } + }); + const udpTransport = new FakeUDPTransport(); + let message = Buffer.from('hello world'); + let receivedMessages = 0; + udpTransport.on('data', (buffer) => { + receivedMessages++; + expect(Buffer.compare(message, buffer)).to.equal(0); + if (receivedMessages === 3) { + done(); + } + }); + setImmediate(() => { + udpTransport.socket.emit('message', message); + setImmediate(() => { + udpTransport.socket.emit('message', message); + setImmediate(() => { + udpTransport.socket.emit('message', message); + }); + }); + }); + }); + + }); + + describe('@method listen', function() { + + it('should call socket#bind', function() { + const socket = new EventEmitter(); + socket.bind = stub(); + const FakeUDPTransport = proxyquire('../lib/transport-udp', { + dgram: { + createSocket: () => socket + } + }); + const udpTransport = new FakeUDPTransport(); + udpTransport.listen(8080, 'localhost'); + expect(socket.bind.calledWithMatch(8080, 'localhost')).to.equal(true); + }); + + }); + +}); diff --git a/test/utils.unit.js b/test/utils.unit.js new file mode 100644 index 0000000..a21e31e --- /dev/null +++ b/test/utils.unit.js @@ -0,0 +1,348 @@ +'use strict'; + +const { expect } = require('chai'); +const { stub } = require('sinon'); +const utils = require('../lib/utils'); +const constants = require('../lib/constants'); + + +describe('@module utils', function() { + + describe('@function getRandomKeyString', function() { + + it('should return a random hex string', function() { + expect(utils.getRandomKeyString()).to.have.lengthOf(40); + expect(utils.getRandomKeyString()).to.not.equal( + utils.getRandomKeyString() + ); + }); + + }); + + describe('@function getRandomKeyBuffer', function() { + + it('should return a B bit random buffer', function() { + expect(utils.getRandomKeyBuffer()).to.have.lengthOf(constants.B / 8); + expect(utils.getRandomKeyBuffer()).to.not.equal( + utils.getRandomKeyBuffer() + ); + }); + + }); + + describe('@function keyStringIsValid', function() { + + it('should return true for valid key string', function() { + expect(utils.keyStringIsValid( + '765fa7f8bff0872d512a65beed2c2843a4bc7fb5' + )).to.equal(true); + }); + + it('should return false for valid key string', function() { + expect(utils.keyStringIsValid( + 'jf;alksduf-0a9sfdaksjd;lfkajs;lkdfjas9df' + )).to.equal(false); + }); + + it('should return false for valid key string', function() { + expect(utils.keyStringIsValid('0')).to.equal(false); + }); + + }); + + describe('@function keyBufferIsValid', function() { + + it('should return true for valid key buffer', function() { + expect(utils.keyBufferIsValid( + Buffer.from('765fa7f8bff0872d512a65beed2c2843a4bc7fb5', 'hex') + )).to.equal(true); + }); + + it('should return false for valid key string', function() { + expect(utils.keyBufferIsValid( + Buffer.from('kf;aslkdjf;alksdjf;aslksa;lkdjf;aslkdjf;', 'utf8') + )).to.equal(false); + }); + + }); + + describe('@function getDistance', function() { + + it('should return the correct distance buffer', function() { + let keys = [ + [ + [ + 'ceb5136e0bf02772b6917543b4e03629bc23a1d8', + '63e7a67f3a1841c94a433be9c5071651b1923a0c' + ], + 'ad52b51131e866bbfcd24eaa71e720780db19bd4' + ], + [ + [ + 'cc0eb01763bcceda0550cc4407eabf5f42ba1673', + 'a8e14ca6a42c1845112b89f24080f70f0dc6d421' + ], + '64effcb1c790d69f147b45b6476a48504f7cc252' + ], + [ + [ + '9a736bc23d17a8cee12027693e3c6256248ab58b', + '679ae587e365aeb05dd11fb97dfa1a94cbfb8afe' + ], + 'fde98e45de72067ebcf138d043c678c2ef713f75' + ] + ]; + keys.forEach(([compare, result]) => { + expect(utils.getDistance(...compare).toString('hex')).to.equal(result); + }); + }); + + it('should return the correct distance buffer', function() { + let keys = [ + [ + [ + Buffer.from('ceb5136e0bf02772b6917543b4e03629bc23a1d8','hex'), + Buffer.from('63e7a67f3a1841c94a433be9c5071651b1923a0c', 'hex') + ], + 'ad52b51131e866bbfcd24eaa71e720780db19bd4' + ], + [ + [ + Buffer.from('cc0eb01763bcceda0550cc4407eabf5f42ba1673', 'hex'), + Buffer.from('a8e14ca6a42c1845112b89f24080f70f0dc6d421', 'hex') + ], + '64effcb1c790d69f147b45b6476a48504f7cc252' + ], + [ + [ + Buffer.from('9a736bc23d17a8cee12027693e3c6256248ab58b', 'hex'), + Buffer.from('679ae587e365aeb05dd11fb97dfa1a94cbfb8afe', 'hex') + ], + 'fde98e45de72067ebcf138d043c678c2ef713f75' + ] + ]; + keys.forEach(([compare, result]) => { + expect(utils.getDistance(...compare).toString('hex')).to.equal(result); + }); + }); + }); + + describe('@function compareKeyBuffers', function() { + + let keys = [ + Buffer.from('c8f53a8431f5412e4303acfb9409b61b56001ee1', 'hex'), + Buffer.from('4c380b21c28a42d1f64b363d03cd0851fa177cca', 'hex') + ]; + + it('should return 1 for sort function', function() { + expect(utils.compareKeyBuffers(...keys)).to.equal(1); + }); + + it('should return -1 for sort function', function() { + expect(utils.compareKeyBuffers(...keys.reverse())).to.equal(-1); + }); + + it('should return 0 for sort function', function() { + expect(utils.compareKeyBuffers(keys[0], keys[0])).to.equal(0); + expect(utils.compareKeyBuffers(keys[1], keys[1])).to.equal(0); + }); + + }); + + describe('@function getBucketIndex', function() { + + let reference = '6f8901bbfdc23790f02e4593268133c78771109a'; + let testCases = [ + ['bd0fced1cb5692c8bd7cfb4def2112bc53cdbcfb', 159], + ['28830cc8267086b4b80b2d579a5b48b893622b75', 158], + ['6fc1c73fd70671c4d2b046ee91029c734959b0cb', 150], + ['2015bc8a0faf52a5d673981ac119fc3c5e9db4d0', 158], + ['64d2122805e25ccb4556475a7fcedaf16e0c6697', 155], + ['6f8901bbec3df42cb187b60cfa0b9de838ce9d8f', 124], + [reference, 0] + ]; + + it('should return the correct index based on distance', function() { + testCases.forEach(([foreign, result]) => { + expect(utils.getBucketIndex(reference, foreign)).to.equal(result); + }); + }); + + }); + + describe('@function getPowerOfTwoBufferForIndex', function() { + + let testCases = [ + [ + '65753df5f9e4faa6efc071e15d8f865b35b5677f', + 50, + '65753df5f9e4faa6efc071e15d04865b35b5677f' + ], + [ + 'b9eae665761df86d6d8655d0255be793f481c0ba', + 100, + 'b9eae665761df8106d8655d0255be793f481c0ba' + ], + [ + Buffer.from('e4c1000352253152453cc069ab9ae71a8bb4e8e8', 'hex'), + 150, + 'e440000352253152453cc069ab9ae71a8bb4e8e8' + ] + ]; + + it('should return a power of two version of the given key', function() { + testCases.forEach(([key, index, result]) => { + expect( + utils.getPowerOfTwoBufferForIndex(key, index).toString('hex') + ).to.equal(result); + }); + }); + + }); + + describe('@function getRandomBufferInBucketRange', function() { + + let testCases = [ + ['54a1d84e56b0380b7878596cd094804154d8079a', 36], + ['4cde832be44a98364ac81467521b7ae6e25953e3', 54], + ['2fdd001069eeacad2881f49058c9e368e994ef51', 71], + ['48b954272a4cae8e72c7fb5ab681fba7661eeeaf', 98], + ['65663df335e47def178280607980abac6ced8948', 124], + ['adde001ace4abe9e30b429914a4510f7226ea2da', 142], + ['0b182a6f7f5a10641ff94473ff83df96a1493e3d', 158] + ]; + + it('should return a reasonably close random key in range', function() { + testCases.forEach(([key, index]) => { + let randomInRange = utils.getRandomBufferInBucketRange(key, index); + let bucketIndex = utils.getBucketIndex(key, randomInRange); + expect(Math.abs(index - bucketIndex) <= 7).to.equal(true); + }); + }); + + }); + + describe('@function validateStorageAdapter', function() { + + it('should fail if invalid storage adapter', function() { + expect(function() { + utils.validateStorageAdapter(); + }).to.throw(Error, 'No storage adapter supplied'); + expect(function() { + utils.validateStorageAdapter({ + put: stub(), + del: stub(), + createReadStream: stub() + }); + }).to.throw(Error, 'Store has no get method'); + expect(function() { + utils.validateStorageAdapter({ + get: stub(), + del: stub(), + createReadStream: stub() + }); + }).to.throw(Error, 'Store has no put method'); + expect(function() { + utils.validateStorageAdapter({ + get: stub(), + put: stub(), + createReadStream: stub() + }); + }).to.throw(Error, 'Store has no del method'); + expect(function() { + utils.validateStorageAdapter({ + get: stub(), + put: stub(), + del: stub(), + }); + }).to.throw(Error, 'Store has no createReadStream method'); + }); + + it('should pass if valid storage adapter', function() { + expect(function() { + utils.validateStorageAdapter({ + get: stub(), + put: stub(), + del: stub(), + createReadStream: stub() + }); + }).to.not.throw(Error); + }); + + }); + + describe('@function validateLogger', function() { + + it('should fail if invalid logger', function() { + expect(function() { + utils.validateLogger(); + }).to.throw(Error, 'No logger object supplied'); + expect(function() { + utils.validateLogger({ + info: stub(), + warn: stub(), + error: stub() + }); + }).to.throw(Error, 'Logger has no debug method'); + expect(function() { + utils.validateLogger({ + debug: stub(), + warn: stub(), + error: stub() + }); + }).to.throw(Error, 'Logger has no info method'); + expect(function() { + utils.validateLogger({ + debug: stub(), + info: stub(), + error: stub() + }); + }).to.throw(Error, 'Logger has no warn method'); + expect(function() { + utils.validateLogger({ + debug: stub(), + info: stub(), + warn: stub() + }); + }).to.throw(Error, 'Logger has no error method'); + }); + + it('should pass if valid logger', function() { + expect(function() { + utils.validateLogger({ + debug: stub(), + info: stub(), + warn: stub(), + error: stub() + }); + }).to.not.throw(Error); + }); + + }); + + describe('@function validateTransport', function() { + + it('should fail if invalid transport', function() { + expect(function() { + utils.validateTransport(); + }).to.throw(Error, 'No transport adapter supplied'); + expect(function() { + utils.validateTransport({ write: stub() }); + }).to.throw(Error, 'Transport has no read method'); + expect(function() { + utils.validateTransport({ read: stub() }); + }).to.throw(Error, 'Transport has no write method'); + }); + + it('should pass if valid transport', function() { + expect(function() { + utils.validateTransport({ + read: stub(), + write: stub() + }); + }).to.not.throw(Error); + }); + + }); + +}); diff --git a/test/version.unit.js b/test/version.unit.js new file mode 100644 index 0000000..7499277 --- /dev/null +++ b/test/version.unit.js @@ -0,0 +1,19 @@ +'use strict'; + +const { expect } = require('chai'); +const version = require('../lib/version'); + + +describe('@module kadence/version', function() { + + describe('@function toString', function() { + + it('should return the version string', function() { + expect(version.toString()).to.equal( + `kadence v${version.software} protocol v${version.protocol}` + ); + }); + + }); + +}); diff --git a/test/wallet.unit.js b/test/wallet.unit.js new file mode 100644 index 0000000..e69de29