diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..8b0d8c3 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,40 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], + "parserOptions": { + "ecmaVersion": 2020, + "project": "tsconfig.json", + "sourceType": "module" + }, + "ignorePatterns": [ "dist" ], + "rules": { + "brace-style": [ "warn" ], + "camelcase": [ "warn" ], + "comma-dangle": [ "error" ], + "curly": [ "warn", "all" ], + "dot-notation": "warn", + "eqeqeq": "warn", + "indent": [ "warn", 2, { "SwitchCase": 1 } ], + "linebreak-style": [ "warn", "unix" ], + "lines-between-class-members": [ "warn", "always", { "exceptAfterSingleLine": true } ], + "max-len": [ "warn", 170 ], + "no-await-in-loop": [ "warn" ], + "no-console": [ "warn" ], + "prefer-arrow-callback": [ "warn" ], + "quotes": [ "warn", "double", { "avoidEscape": true } ], + "semi": [ "warn", "always" ], + "sort-imports": [ "warn" ], + "sort-keys": [ "warn" ], + "sort-vars": [ "warn" ], + "@typescript-eslint/explicit-function-return-type": [ "warn" ], + "@typescript-eslint/explicit-module-boundary-types": [ "warn" ], + "@typescript-eslint/no-explicit-any": [ "warn" ], + "@typescript-eslint/no-non-null-assertion": [ "warn" ], + "@typescript-eslint/no-this-alias": [ "warn" ] + } +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..e6709c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Support Request +about: Report a bug or request help. Please read the documentation first before creating a support request. +title: '' +assignees: '' + +--- + + + + +**Describe The Problem:** + + +**To Reproduce:** + + +**Logs:** + + +``` +Show the myQ API logs here. +Remove any sensitive information. +``` + +**Screenshots:** + + + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..99d680b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +blank_issues_enabled: false + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..500344c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature Request +about: Suggest an idea for an enhancement. +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe:** + + +**Describe the solution you'd like:** + + +**Describe alternatives you've considered:** + + +**Additional context:** + + + diff --git a/.github/auto-merge.yml b/.github/auto-merge.yml new file mode 100644 index 0000000..768361c --- /dev/null +++ b/.github/auto-merge.yml @@ -0,0 +1,5 @@ +# Merge all dependencies as long within ${TARGET} scope (defined in workflows/dependabot-automerge.yml). +# +- match: + dependency_type: all + update_type: semver:minor diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1b0e18b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,36 @@ +# Query daily for npm dependency updates. +# +version: 2 + +updates: + + # Enable version updates for github-actions. + - package-ecosystem: "github-actions" + + # Look for ".github/workflows" in the "root" directory. + directory: "/" + + # Check for updated GitHub Actions every weekday. + schedule: + interval: "daily" + + # Allow up to ten pull requests to be generated at any one time. + open-pull-requests-limit: 10 + + # Enable version updates for npm. + - package-ecosystem: "npm" + + # Look for "package.json" and "package-lock.json" files in the "root" directory. + directory: "/" + + # Check the npm registry for updates every weekday. + schedule: + interval: "daily" + + # Allow up to ten pull requests to be generated at any one time. + open-pull-requests-limit: 10 + + # Ignore certain dependency updates. + ignore: + # Ignore node-fetch updates for now due to the breaking change in module management. + - dependency-name: "node-fetch" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8fa51cc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +# Continuous integration - validate builds when commits are made, and publish when releases are created. +# +name: "Continuous Integration" + +# Run the build on all push, pull request, and release creation events. +on: + pull_request: + push: + release: + types: [ created ] + +jobs: + + # Run a validation build on LTS versions of node. + validate-build: + + # Build only if we've received a push event. + if: github.event_name == 'push' + + # Create the build matrix for all the environments we're validating against. + strategy: + matrix: + node-version: [ 12.x, 14.x ] + os: [ ubuntu-latest ] + + # Specify the environments we're going to build in. + runs-on: ${{ matrix.os }} + + # Execute the build activities. + steps: + - name: Checkout the repository. + uses: actions/checkout@v2 + + - name: Setup the node ${{ matrix.node-version }} environment. + uses: actions/setup-node@v2.2.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Build and install the package with a clean slate. + run: | + npm ci + npm run build --if-present + env: + CI: true + + # Publish the release to the NPM registry. + publish-npm: + + # Publish only if we've received a release event and the tag starts with "v" (aka v1.2.3). + if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') + + # Specify the environment we're going to build in. + runs-on: ubuntu-latest + + # Execute the build and publish activities. + steps: + - name: Checkout the repository. + uses: actions/checkout@v2 + + - name: Setup the node environment. + uses: actions/setup-node@v2.2.0 + with: + + # Use the oldest node LTS version that we support. + node-version: '12.x' + + # Use the NPM registry. + registry-url: 'https://registry.npmjs.org/' + + - name: Install the package with a clean slate. + run: npm ci + + - name: Publish the package to NPM. + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.npm_token }} diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml new file mode 100644 index 0000000..4bbc336 --- /dev/null +++ b/.github/workflows/dependabot-automerge.yml @@ -0,0 +1,17 @@ +# Automerge dependency updates identified by dependabot. +# +name: Automerge Dependabot Version Updates + +on: + pull_request_target: + +jobs: + auto-merge: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - uses: actions/checkout@v2 + - uses: ahmadnassri/action-dependabot-auto-merge@v2 + with: + target: minor + github-token: ${{ secrets.UPDATES_TOKEN }} diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml new file mode 100644 index 0000000..d56920a --- /dev/null +++ b/.github/workflows/issue-stale.yml @@ -0,0 +1,27 @@ +# Close stale issues after a defined period of time. +# +name: Close Stale Issues + +on: + issues: + types: [reopened] + schedule: + - cron: "*/60 * * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - name: Autoclose stale issues. + uses: actions/stale@v3 + with: + days-before-close: 2 + days-before-stale: 7 + exempt-issue-labels: 'discussion,help wanted,long running' + exempt-pr-labels: 'awaiting-approval,work-in-progress' + remove-stale-when-updated: true + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-label: 'stale' + stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' + stale-pr-label: 'stale' + stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' diff --git a/.github/workflows/issue-validate.yml b/.github/workflows/issue-validate.yml new file mode 100644 index 0000000..c5d3972 --- /dev/null +++ b/.github/workflows/issue-validate.yml @@ -0,0 +1,18 @@ +# Close issues that don't conform to the issue templates. +# +name: Close Non-Conforming Issues + +on: + issues: + types: [opened] + +jobs: + autoclose: + runs-on: ubuntu-latest + steps: + - name: Autoclose issues that don't follow the issue templates. + uses: roots/issue-closer@v1.1 + with: + issue-close-message: "@${issue.user.login} - this issue is being automatically closed because it does not follow either the feature request or bug report issue template. The issue templates have been designed to help in the troubleshooting (or feature request) process. Please use them and populate it as completely as possible to streamline troubleshooting or feature request discussions." + issue-pattern: "Describe alternatives you've considered|Describe The Problem" + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3de77c --- /dev/null +++ b/.gitignore @@ -0,0 +1,126 @@ +# Ignore compiled code +dist + +# Ignore npmrc. +.npmrc + +# Ignore macOS attribute files. +.DS_Store + +# ------------- Defaults ------------- # + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..23d3b2b --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +# Ignore everything by default. +* + +# Include the following. +!LICENSE.md +!README.md +!config.schema.json +!dist/** +!package.json diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..8de9799 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,16 @@ +Internet Systems Consortium license +=================================== + +Copyright (c) `2017-2021`, `HJD https://github.com/hjdhjd` + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2756f97 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ + +
+ +[![myQ: A modern implementation of the myQ API for Liftmaster and Chamberlain devices](https://raw.githubusercontent.com/hjdhjd/myq/main/myQ.svg)](https://github.com/hjdhjd/myq) + +# myQ + +[![Downloads](https://img.shields.io/npm/dt/myq?color=%235EB5E5&logo=icloud&logoColor=%23FFFFFF&style=for-the-badge)](https://www.npmjs.com/package/myq) +[![Version](https://img.shields.io/npm/v/myq?color=%235EB5E5&label=myQ&logoColor=%23FFFFFF&style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBzdHlsZT0iZmlsbDojRkZGRkZGIiBkPSJNMjMuOTkzIDkuODE2TDEyIDIuNDczbC00LjEyIDIuNTI0VjIuNDczSDQuMTI0djQuODE5TC4wMDQgOS44MTZsMS45NjEgMy4yMDIgMi4xNi0xLjMxNXY5LjgyNmgxNS43NDl2LTkuODI2bDIuMTU5IDEuMzE1IDEuOTYtMy4yMDIiLz48L3N2Zz4K)](https://www.npmjs.com/package/myq) + +## myQ: A modern implementation of the myQ API for Liftmaster and Chamberlain devices. +
+
+ +`myq` is a library that enables you to connect to and communicate with [myQ-enabled devices](https://myq.com). myQ-enabled devices include many garage door openers made primarily by Liftmaster, Chamberlain, and Craftsman, but includes other brands as well. You can determine if your garage door or other device is myQ-enabled by checking the [myQ compatibility check tool](https://www.myq.com/myq-compatibility) on the [myQ](https://www.myq.com) website. + +## Why use this library for myQ support? +In short - because I use it every day to support a very popular [Homebridge](https://homebridge.io) plugin named [homebridge-myq](https://www.npmjs.com/package/homebridge-myq) that I maintain. I have been occasionally asked if I would consider packaging the core API library separately from the plugin so that other open source projects can take advantage of the work that's been done here to understand and decode the myQ API. + +In addition, this implementation is unique: it's the first complete open source implementation of the latest myQ API, v6. The v6 API is quite different in significant ways, including a shift to OAuth-based authentication that's clearly the path the myQ API is moving toward in the future. Additionally, v6 brings other advantages besides leveraging modern OAuth semantics, such as making devices shared across accounts available, which has previously been unavailable in prior myQ API versions. + +### How you can contribute and make this library even better +This implementation is largely feature complete. It doesn't support myQ locks or cameras, but may do so in time, though contributions are always welcome. + +The myQ API is undocumented and implementing a library like this one is the result of many hours of trial and error as well as community support. This work stands on the shoulders of other myQ API projects out there and this project attempts to contribute back to that community base of knowledge to further improve myQ support for everyone in the ecosystem. + +### Features +- Full access to the myQ devices JSON. +- The ability to retrieve the status of any supported myQ device. +- The ability to open or close a supported garage door. +- The ability to turn on or off a supported lamp. + +## Changelog +* [Changelog](https://github.com/hjdhjd/myq/blob/main/docs/Changelog.md): changes and release history of this library. + +## Installation +To use this library in Node, install it from the command line: + +```sh +npm install myq +``` + +## Documentation + +If you'd like to see all this in action in a well-documented, real-world example, please take a good look at my [homebridge-myq](https://github.com/hjdhjd/homebridge-myq) project. It relies heavily on this library for the core functionality it provides. + +### myQ(email: string, password: string [, log: myQLogging]) +Initialize the myQ API and create a login instance using `email` and `password` to connect. `log` is an optional parameter that enables you to customize the type of logging that can be generated, including debug logging. If `log` isn't specified, the myQ API will default to logging to the console. + +### refreshDevices() +Request that the myQ library refresh state and device information for all the devices associated with the currently logged in account. There are failsafes in place to ensure it can't be called more than once every two seconds in order to prevent overtaxing the myQ API and potentially lockout an account. + +`refreshDevices()` must be called at least once, immediately after instantiating the API in order to populate the list of myQ devices. + +Returns: `true` if successful, `false` otherwise. + +### myQ.devices[] +The devices property maintains the list of all known myQ devices. It is an array of `myQDevice` objects, and you can look through [myq-types.ts](https://github.com/hjdhjd/myq/blob/main/src/myq-types.ts) for a sense of what's contained in a `myQDevice` object. + +This property is refreshed each time `refreshDevices()` is called. + +### execute(device: myQDevice, command: string) +Execute a command on a given myQ device. Valid values for `command`: + + * Garage doors: `open` and `close` + * Lamps: `on` and `off` + +Returns: `true` if successful, `false` otherwise. + +### getDevice(serial: string) +Get the details of a specific device identified by the serial number `serial` in the myQ device list. In practice, I rarely use this, and I suspect most people won't either, in favor of walking the entire myQ device list which is what most people want to do most of the time. + +Returns: `myQDevice` if found, or `null`. + +### getDeviceName(device: myQDevice) +Given `device`, returns a nicely formatted device string suitable for logging information or end users. + +Returns: a string representing the device name, model, and serial number, if available. + +### getHwInfo(serial: string) +Get the model information of a device identified by the serial number `serial`. myQ devices have a specific serial number pattern, and you can use it to deduce the model information of a particular device. + +Returns: `myQHwInfo` if found, or `null` + +## Library Development Dashboard +This is mostly of interest to the true developer nerds amongst us. + +[![License](https://img.shields.io/npm/l/myq?color=%230559C9&logo=open%20source%20initiative&logoColor=%23FFFFFF&style=for-the-badge)](https://github.com/hjdhjd/myq/blob/main/LICENSE.md) +[![Build Status](https://img.shields.io/github/workflow/status/hjdhjd/myq/Continuous%20Integration?color=%230559C9&logo=github-actions&logoColor=%23FFFFFF&style=for-the-badge)](https://github.com/hjdhjd/myq/actions?query=workflow%3A%22Continuous+Integration%22) +[![Dependencies](https://img.shields.io/librariesio/release/npm/myq?color=%230559C9&logo=dependabot&style=for-the-badge)](https://libraries.io/npm/myq) +[![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/hjdhjd/myq/latest?color=%230559C9&logo=github&sort=semver&style=for-the-badge)](https://github.com/hjdhjd/myq/commits/main) diff --git a/myQ.svg b/myQ.svg new file mode 100644 index 0000000..dae348d --- /dev/null +++ b/myQ.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..516b7a8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1604 @@ +{ + "name": "myq", + "version": "6.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "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" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + } + } + }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "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 + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "@types/node": { + "version": "16.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz", + "integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "4.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.1.tgz", + "integrity": "sha512-UDqhWmd5i0TvPLmbK5xY3UZB0zEGseF+DHPghZ37Sb83Qd3p8ujhvAtkU4OF46Ka5Pm5kWvFIx0cCTBFKo0alA==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.31.1", + "@typescript-eslint/scope-manager": "4.31.1", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.1.tgz", + "integrity": "sha512-NtoPsqmcSsWty0mcL5nTZXMf7Ei0Xr2MT8jWjXMVgRK0/1qeQ2jZzLFUh4QtyJ4+/lPUyMw5cSfeeME+Zrtp9Q==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.31.1", + "@typescript-eslint/types": "4.31.1", + "@typescript-eslint/typescript-estree": "4.31.1", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.31.1.tgz", + "integrity": "sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "4.31.1", + "@typescript-eslint/types": "4.31.1", + "@typescript-eslint/typescript-estree": "4.31.1", + "debug": "^4.3.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.31.1.tgz", + "integrity": "sha512-N1Uhn6SqNtU2XpFSkD4oA+F0PfKdWHyr4bTX0xTj8NRx1314gBDRL1LUuZd5+L3oP+wo6hCbZpaa1in6SwMcVQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.31.1", + "@typescript-eslint/visitor-keys": "4.31.1" + } + }, + "@typescript-eslint/types": { + "version": "4.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.1.tgz", + "integrity": "sha512-kixltt51ZJGKENNW88IY5MYqTBA8FR0Md8QdGbJD2pKZ+D5IvxjTYDNtJPDxFBiXmka2aJsITdB1BtO1fsgmsQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.1.tgz", + "integrity": "sha512-EGHkbsUvjFrvRnusk6yFGqrqMBTue5E5ROnS5puj3laGQPasVUgwhrxfcgkdHNFECHAewpvELE1Gjv0XO3mdWg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.31.1", + "@typescript-eslint/visitor-keys": "4.31.1", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.1.tgz", + "integrity": "sha512-PCncP8hEqKw6SOJY+3St4LVtoZpPPn+Zlpm7KW5xnviMhdqcsBty4Lsg4J/VECpJjw1CkROaZhH4B8M1OfnXTQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.31.1", + "eslint-visitor-keys": "^2.0.0" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "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 + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "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" + } + }, + "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" + } + }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=" + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", + "integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==", + "requires": { + "array-filter": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.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": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + } + }, + "css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==" + }, + "data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" + }, + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "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" + } + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "requires": { + "webidl-conversions": "^5.0.0" + } + }, + "domhandler": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", + "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "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==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "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 + } + } + }, + "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 + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "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 + } + } + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "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.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "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==", + "dev": true + }, + "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==", + "dev": true + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "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==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fetch-blob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.1.tgz", + "integrity": "sha512-Uf+gxPCe1hTOFXwkxYyckn8iUSk6CFXGy5VENZKifovUTZC9eUODWSBhOBS7zICGrAetKzdwLMr85KhIcePMAQ==" + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", + "dev": true + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "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" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz", + "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + }, + "dependencies": { + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "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": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "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==" + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==" + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "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==", + "dev": true + }, + "is-generator-function": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.9.tgz", + "integrity": "sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A==" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typed-array": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz", + "integrity": "sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==", + "requires": { + "available-typed-arrays": "^1.0.0", + "es-abstract": "^1.17.4", + "foreach": "^2.0.5", + "has-symbols": "^1.0.1" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "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" + } + }, + "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==", + "dev": true + }, + "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": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "dependencies": { + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "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" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node-fetch": { + "version": "3.0.0-beta.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0-beta.9.tgz", + "integrity": "sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==", + "requires": { + "data-uri-to-buffer": "^3.0.1", + "fetch-blob": "^2.1.1" + } + }, + "node-html-parser": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-4.1.4.tgz", + "integrity": "sha512-Uk9zI1IhGrPTfUyhRvImKGZspsqJ0ss6aRysGfkDuPDPfLisMlInYT/3FCydsC2UamzPPhpOax9xbTVUsO6n8Q==", + "requires": { + "css-select": "^4.1.3", + "he": "1.2.0" + } + }, + "nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "requires": { + "boolbase": "^1.0.0" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "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" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "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==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pkce-challenge": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-2.2.0.tgz", + "integrity": "sha512-Ly0Y0OwhtG2N1ynk5ruqoyJxkrWhAPmvdRk0teiLh9Dp2+J4URKpv1JSKWD0j1Sd+QCeiwO9lTl0EjmrB2jWeA==" + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "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 + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "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==" + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "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 + }, + "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" + } + }, + "table": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", + "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-typed-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", + "integrity": "sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==", + "requires": { + "available-typed-arrays": "^1.0.2", + "es-abstract": "^1.17.5", + "foreach": "^2.0.5", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.1", + "is-typed-array": "^1.1.3" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..097da3f --- /dev/null +++ b/package.json @@ -0,0 +1,58 @@ +{ + "name": "myq", + "type": "commonjs", + "version": "6.0.0", + "displayName": "myQ API", + "description": "A nearly complete and modern implementation of the Liftmaster / Chamberlain myQ API.", + "author": { + "name": "HJD", + "url": "https://github.com/hjdhjd" + }, + "homepage": "https://github.com/hjdhjd/myq#readme", + "license": "ISC", + "repository": { + "type": "git", + "url": "git://github.com/hjdhjd/myq.git" + }, + "bugs": { + "url": "https://github.com/hjdhjd/myq/issues" + }, + "keywords": [ + "chamberlain", + "craftsman", + "door", + "garage", + "garage door", + "liftmaster", + "myq", + "remote" + ], + "engines": { + "node": ">=12" + }, + "main": "dist/index.js", + "scripts": { + "build": "rimraf ./dist && tsc", + "clean": "rimraf ./dist", + "lint": "eslint src/**.ts", + "postpublish": "npm run clean", + "prepublishOnly": "npm run lint && npm run build", + "test": "eslint src/**.ts", + "watch": "npm run build && npm link && nodemon" + }, + "devDependencies": { + "@types/node": "^16.9.2", + "@typescript-eslint/eslint-plugin": "^4.31.1", + "@typescript-eslint/parser": "^4.31.1", + "eslint": "^7.32.0", + "rimraf": "^3.0.2", + "typescript": "^4.4.3" + }, + "dependencies": { + "domexception": "^2.0.1", + "node-fetch": "3.0.0-beta.9", + "node-html-parser": "^4.1.4", + "pkce-challenge": "^2.2.0", + "util": "^0.12.4" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3d2a608 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,7 @@ +/* Copyright(C) 2017-2021, HJD (https://github.com/hjdhjd). All rights reserved. + * + * index.ts: myQ API registration. + */ +export * from "./myq-api"; +export * from "./myq-types"; + diff --git a/src/myq-api.ts b/src/myq-api.ts new file mode 100644 index 0000000..1126afb --- /dev/null +++ b/src/myq-api.ts @@ -0,0 +1,767 @@ +/* Copyright(C) 2017-2021, HJD (https://github.com/hjdhjd). All rights reserved. + * + * myq-api.ts: Our myQ API implementation. + */ +import { MYQ_API_CLIENT_ID, MYQ_API_CLIENT_SECRET, MYQ_API_REDIRECT_URI } from "./settings"; +import fetch, { FetchError, Headers, RequestInfo, RequestInit, Response, isRedirect } from "node-fetch"; +import { myQAccount, myQDevice, myQDeviceList, myQHwInfo, myQToken } from "./myq-types"; +import { myQLogging } from "./myq-logging"; +import { parse } from "node-html-parser"; +import pkceChallenge from "pkce-challenge"; +import util from "util"; + +/* + * The myQ API is undocumented, non-public, and has been derived largely through + * reverse engineering the official app, myQ website, and trial and error. + * + * This project stands on the shoulders of the other myQ projects out there that have + * done much of the heavy lifting of decoding the API. + * + * Starting with v6 of the myQ API, myQ now uses OAuth 2.0 + PKCE to authenticate users and + * provide access tokens for future API calls. In order to successfully use the API, we need + * to first authenticate to the myQ API using OAuth, get the access token, and use that for + * future API calls. + * + * On the plus side, the myQ application identifier and HTTP user agent - previously pain + * points for the community when they get seemingly randomly changed or blacklisted - are + * no longer required. + * + * For those familiar with prior versions of the API, v6 does not represent a substantial + * change outside of the shift in authentication type and slightly different endpoint + * semantics. The largest non-authentication-related change relate to how commands are + * sent to the myQ API to execute actions such as opening and closing a garage door, and + * even those changes are relatively minor. + * + * The myQ API is clearly evolving and will continue to do so. So what's good about v6 of + * the API? A few observations that will be explored with time and lots of experimentation + * by the community: + * + * - It seems possible to use guest accounts to now authenticate to myQ. + * - Cameras seem to be more directly supported. + * - Locks seem to be more directly supported. + * + * Overall, the workflow to using the myQ API should still feel familiar: + * + * 1. Login to the myQ API and acquire an OAuth access token. + * 2. Enumerate the list of myQ devices, including gateways and openers. myQ devices like + * garage openers or lights are associated with gateways. While you can have multiple + * gateways in a home, a more typical setup would be one gateway per home, and one or + * more devices associated with that gateway. + * 3. To check status of myQ devices, we periodically poll to get updates on specific + * devices. + * + * Those are the basics and gets us up and running. There are further API calls that + * allow us to open and close openers, lights, and other devices, as well as periodically + * poll for status updates. + * + * That last part is key. Since there is no way that we know of to monitor status changes + * in real time, we have to resort to polling the myQ API regularly to see if something + * has happened that we're interested in (e.g. a garage door opening or closing). It + * would be great if a monitor API existed to inform us when changes occur, but alas, + * it either doesn't exist or hasn't been discovered yet. + */ + +export class myQApi { + public devices!: myQDevice[]; + private accessToken: string | null; + private refreshInterval: number; + private refreshToken: string; + private tokenScope: string; + private accessTokenTimestamp!: number; + private email: string; + private password: string; + private accounts: string[]; + private headers: Headers; + private log: myQLogging; + private lastAuthenticateCall!: number; + private lastRefreshDevicesCall!: number; + + // Initialize this instance with our login information. + constructor(email: string, password: string, log?: myQLogging) { + + // If we didn't get passed a logging parameter, by default we log to the console. + if(!log) { + log = { + /* eslint-disable no-console */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + debug: (message: string, ...parameters: unknown[]): void => { /* No debug logging by default. */ }, + error: (message: string, ...parameters: unknown[]): void => console.error(util.format(message, ...parameters)), + info: (message: string, ...parameters: unknown[]): void => console.log(util.format(message, ...parameters)), + warn: (message: string, ...parameters: unknown[]): void => console.log(util.format(message, ...parameters)) + /* eslint-enable no-console */ + }; + } + + this.accessToken = null; + this.accounts = []; + this.email = email; + this.headers = new Headers(); + this.log = log; + this.password = password; + this.refreshInterval = 0; + this.refreshToken = ""; + this.tokenScope = ""; + + // The myQ API v6 doesn't seem to require an HTTP user agent to be set - so we don't. + this.headers.set("User-Agent", "null"); + } + + // Transmit the PKCE challenge and retrieve the myQ OAuth authorization page to prepare to login. + private async oauthGetAuthPage(codeChallenge: string): Promise { + + const authEndpoint = new URL("https://partner-identity.myq-cloud.com/connect/authorize"); + + // Set the client identifier. + authEndpoint.searchParams.set("client_id", "IOS_CGI_MYQ"); + + // Set the PKCE code challenge. + authEndpoint.searchParams.set("code_challenge", codeChallenge); + + // Set the PKCE code challenge method. + authEndpoint.searchParams.set("code_challenge_method", "S256"); + + // Set the redirect URI to the myQ app. + authEndpoint.searchParams.set("redirect_uri", "com.myqops://ios"); + + // Set the response type. + authEndpoint.searchParams.set("response_type", "code"); + + // Set the scope. + authEndpoint.searchParams.set("scope", "MyQ_Residential offline_access"); + + // Send the PKCE challenge and let's begin the login process. + const response = await this.fetch(authEndpoint.toString(), { + headers: { "User-Agent": "null" }, + redirect: "follow" + }, true); + + if(!response) { + this.log.error("myQ API: Unable to access the OAuth authorization endpoint. Will retry later."); + return null; + } + + return response; + } + + // Login to the myQ API, using the retrieved authorization page. + private async oauthLogin(authPage: Response): Promise { + + // Grab the cookie for the OAuth sequence. We need to deal with spurious additions to the cookie that gets returned by the myQ API. + const cookie = this.trimSetCookie(authPage.headers.raw()["set-cookie"]); + + // Parse the myQ login page and grab what we need. + const htmlText = await authPage.text(); + const loginPageHtml = parse(htmlText); + const requestVerificationToken = loginPageHtml.querySelector("input[name=__RequestVerificationToken]")?.getAttribute("value") as string; + + if(!requestVerificationToken) { + this.log.error("myQ API: Unable to complete OAuth login. The verification token could not be retrieved. Will retry later."); + return null; + } + + // Set the login info. + const loginBody = new URLSearchParams({ "Email": this.email, "Password": this.password, "__RequestVerificationToken": requestVerificationToken }); + + // Login and we're done. + const response = await this.fetch(authPage.url, { + body: loginBody.toString(), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Cookie": cookie, + "User-Agent": "null" + }, + method: "POST", + redirect: "manual" + }, true); + + // An error occurred and we didn't get a good response. + if(!response) { + this.log.error("myQ API: Unable to complete OAuth login. Ensure your username and password are correct. Will retry later."); + return null; + } + + // If we don't have the full set of cookies we expect, the user probably gave bad login information. + if(response.headers.raw()["set-cookie"].length < 2) { + this.log.error("myQ API: Invalid myQ credentials given. Check your login and password. Will retry later."); + return null; + } + + return response; + } + + // Intercept the OAuth login response to adjust cookie headers before sending on it's way. + private async oauthRedirect(loginResponse: Response): Promise { + + // Get the location for the redirect for later use. + const redirectUrl = loginResponse.headers.get("location") as string; + + // Cleanup the cookie so we can complete the login process by removing spurious additions + // to the cookie that gets returned by the myQ API. + const cookie = this.trimSetCookie(loginResponse.headers.raw()["set-cookie"]); + + // Execute the redirect with the cleaned up cookies and we're done. + const response = await this.fetch(redirectUrl, { + headers: { + "Cookie": cookie, + "User-Agent": "null" + }, + redirect: "manual" + }, true); + + if(!response) { + this.log.error("myQ API: Unable to complete the OAuth login redirect. Will retry later."); + return null; + } + + return response; + } + + // Get a new OAuth access token. + private async getOAuthToken(): Promise { + + // Generate the OAuth PKCE challenge required for the myQ API. + const pkce = pkceChallenge(); + + // Call the myQ authorization endpoint using our PKCE challenge to get the web login page. + let response = await this.oauthGetAuthPage(pkce.code_challenge); + + if(!response) { + return null; + } + + // Attempt to login. + response = await this.oauthLogin(response); + + if(!response) { + return null; + } + + // Intercept the redirect back to the myQ iOS app. + response = await this.oauthRedirect(response); + + if(!response) { + return null; + } + + // Parse the redirect URL to extract the PKCE verification code and scope. + const redirectUrl = new URL(response.headers.get("location") ?? ""); + + // Create the request to get our access and refresh tokens. + const requestBody = new URLSearchParams({ + "client_id": MYQ_API_CLIENT_ID, + "client_secret": Buffer.from(MYQ_API_CLIENT_SECRET, "base64").toString(), + "code": redirectUrl.searchParams.get("code") as string, + "code_verifier": pkce.code_verifier, + "grant_type": "authorization_code", + "redirect_uri": MYQ_API_REDIRECT_URI, + "scope": redirectUrl.searchParams.get("scope") as string + }); + + // Now we execute the final login redirect that will validate the PKCE challenge and + // return our access and refresh tokens. + response = await this.fetch("https://partner-identity.myq-cloud.com/connect/token", { + body: requestBody.toString(), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "null" + }, + method: "POST" + }, true); + + if(!response) { + this.log.error("myQ API: Unable to acquire an OAuth access token. Will retry later."); + return null; + } + + // Grab the token JSON. + const token = await response.json() as myQToken; + this.refreshInterval = token.expires_in; + this.refreshToken = token.refresh_token; + this.tokenScope = redirectUrl.searchParams.get("scope") ?? "" ; + + // Refresh our tokens at seven minutes before expiration as a failsafe. + this.refreshInterval -= 420; + + // Ensure we never try to refresh more frequently than every five minutes. + if(this.refreshInterval < 300) { + this.refreshInterval = 300; + } + + // Return the access token in cookie-ready form: "Bearer ...". + return token.token_type + " " + token.access_token; + } + + // Refresh our OAuth access token. + private async refreshOAuthToken(): Promise { + + // Create the request to refresh tokens. + const requestBody = new URLSearchParams({ + "client_id": MYQ_API_CLIENT_ID, + "client_secret": Buffer.from(MYQ_API_CLIENT_SECRET, "base64").toString(), + "grant_type": "refresh_token", + "redirect_uri": MYQ_API_REDIRECT_URI, + "refresh_token": this.refreshToken, + "scope": this.tokenScope + }); + + // Execute the refresh token request. + const response = await this.fetch("https://partner-identity.myq-cloud.com/connect/token", { + body: requestBody.toString(), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "null" + }, + method: "POST" + }, true); + + if(!response) { + return false; + } + + // Grab the refresh token JSON. + const token = await response.json() as myQToken; + this.accessToken = token.token_type + " " + token.access_token; + this.accessTokenTimestamp = Date.now(); + this.refreshInterval = token.expires_in; + this.refreshToken = token.refresh_token; + this.tokenScope = token.scope ?? this.tokenScope; + + // Refresh our tokens at seven minutes before expiration as a failsafe. + this.refreshInterval -= 420; + + // Ensure we never try to refresh more frequently than every five minutes. + if(this.refreshInterval < 300) { + this.refreshInterval = 300; + } + + // Update our authorization header. + this.headers.set("Authorization", this.accessToken); + + this.log.debug("myQ API: Successfully refreshed the myQ API access token."); + + // We're done. + return true; + } + + // Log us into myQ and get an access token. + private async acquireAccessToken(): Promise { + + let firstConnection = true; + const now = Date.now(); + + // Reset the API call time. + this.lastAuthenticateCall = now; + + // Clear out tokens from prior connections. + if(this.accessToken) { + firstConnection = false; + this.accessToken = null; + this.accounts = []; + } + + // Login to the myQ API and get an OAuth access token for our session. + const token = await this.getOAuthToken(); + + if(!token) { + return false; + } + + // On initial plugin startup, let the user know we've successfully connected. + if(firstConnection) { + this.log.info("myQ API: Successfully connected to the myQ API."); + } else { + this.log.debug("myQ API: Successfully reacquired a myQ API access token."); + } + + this.accessToken = token; + this.accessTokenTimestamp = now; + + // Add the token to our headers that we will use for subsequent API calls. + this.headers.set("Authorization", this.accessToken); + + // Grab our account information for subsequent calls. + if(!(await this.getAccounts())) { + this.accessToken = null; + this.accounts = []; + return false; + } + + // Success. + return true; + } + + // Refresh the myQ access token, if needed. + private async refreshAccessToken(): Promise { + + const now = Date.now(); + + // We want to throttle how often we call this API to no more than once every 2 minutes. + if((now - this.lastAuthenticateCall) < (2 * 60 * 1000)) { + return (this.accounts.length && this.accessToken) ? true : false; + } + + // If we don't have a access token yet, acquire one. + if(!this.accounts.length || !this.accessToken) { + return await this.acquireAccessToken(); + } + + // Is it time to refresh? If not, we're good for now. + if((now - this.accessTokenTimestamp) < (this.refreshInterval * 1000)) { + return true; + } + + // Try refreshing our existing access token before resorting to acquiring a new one. + if(await this.refreshOAuthToken()) { + return true; + } + + this.log.error("myQ API: Unable to refresh our access token. " + + "This error can usually be safely ignored and will be resolved by acquiring a new access token."); + + // Now generate a new access token. + if(!(await this.acquireAccessToken())) { + return false; + } + + return true; + } + + // Get the list of myQ devices associated with an account. + public async refreshDevices(): Promise { + + const now = Date.now(); + + // We want to throttle how often we call this API as a failsafe. If we call it more + // than once every two seconds or so, bad things can happen on the myQ side leading + // to potential account lockouts. The author definitely learned this one the hard way. + if(this.lastRefreshDevicesCall && ((now - this.lastRefreshDevicesCall) < (2 * 1000))) { + this.log.debug("myQ API: throttling refreshDevices API call. Using cached data from the past two seconds."); + + return this.devices ? true : false; + } + + // Reset the API call time. + this.lastRefreshDevicesCall = now; + + // Validate and potentially refresh our access token. + if(!(await this.refreshAccessToken())) { + return false; + } + + // Update our account information, to see if we've added or removed access to any other devices. + if(!(await this.getAccounts())) { + this.accessToken = null; + this.accounts = []; + return false; + } + + const newDeviceList = []; + + // Loop over all the accounts we know about. + for(const accountId of this.accounts) { + + // Get the list of device information for this account. + // eslint-disable-next-line no-await-in-loop + const response = await this.fetch("https://devices.myq-cloud.com/api/v5.2/Accounts/" + accountId + "/Devices"); + + if(!response) { + + this.log.error("myQ API: Unable to update device status from the myQ API. Acquiring a new access token and retrying later."); + this.accessToken = null; + this.accounts = []; + return false; + } + + // Now let's get our account information. + // eslint-disable-next-line no-await-in-loop + const data = await response.json() as myQDeviceList; + + this.log.debug(util.inspect(data, { colors: true, depth: 10, sorted: true })); + + newDeviceList.push(...data.items); + } + + // Notify the user about any new devices that we've discovered. + if(newDeviceList) { + + for(const newDevice of newDeviceList) { + + // We already know about this device. + if(this.devices?.some((x: myQDevice) => x.serial_number === newDevice.serial_number)) { + continue; + } + + // We've discovered a new device. + this.log.info("myQ API: Discovered device family %s: %s.", newDevice.device_family, this.getDeviceName(newDevice)); + + } + } + + // Notify the user about any devices that have disappeared. + if(this.devices) { + + for(const existingDevice of this.devices) { + + // This device still is visible. + if(newDeviceList?.some((x: myQDevice) => x.serial_number === existingDevice.serial_number)) { + continue; + } + + // We've had a device disappear. + this.log.info("myQ API: Removed device family %s: %s.", existingDevice.device_family, this.getDeviceName(existingDevice)); + + } + + } + + // Save the updated list of devices. + this.devices = newDeviceList; + + return true; + } + + // Execute an action on a myQ device. + public async execute(device: myQDevice, command: string): Promise { + + // Validate and potentially refresh our access token. + if(!(await this.refreshAccessToken())) { + return false; + } + + let response; + + // Ensure we cann the right endpoint to execute commands depending on device family. + if(device.device_family === "lamp") { + + // Execute a command on a lamp device. + response = await this.fetch("https://account-devices-lamp.myq-cloud.com/api/v5.2/Accounts/" + device.account_id + + "/lamps/" + device.serial_number + "/" + command, { method: "PUT" }); + } else { + + // By default, we assume we're targeting a garage door opener. + response = await this.fetch("https://account-devices-gdo.myq-cloud.com/api/v5.2/Accounts/" + device.account_id + + "/door_openers/" + device.serial_number + "/" + command, { method: "PUT" }); + } + + // Check for errors. + if(!response) { + + this.log.error("myQ API: Unable to send the command to myQ servers. Acquiring a new access token."); + this.accessToken = null; + this.accounts = []; + return false; + } + + return true; + } + + // Get our myQ account information. + private async getAccounts(): Promise { + + // Get the account information. + const response = await this.fetch("https://accounts.myq-cloud.com/api/v6.0/accounts"); + + if(!response) { + this.log.error("myQ API: Unable to retrieve account information. Will retry later."); + return false; + } + + // Now let's get our account information. + const data = await response.json() as myQAccount; + + this.log.debug(util.inspect(data, { colors: true, depth: 10, sorted: true })); + + // No account information returned. + if(!data?.accounts) { + this.log.error("myQ API: Unable to retrieve account information from the myQ API."); + return false; + } + + // Save all the account identifiers we know about for later use. + this.accounts = data.accounts.map(x => x.id); + + return true; + } + + // Get the details of a specific device in the myQ device list. + public getDevice(serial: string): myQDevice | null { + + // Check to make sure we have fresh information from myQ. If it's less than a minute + // old, it looks good to us. + if(!this.devices || !this.lastRefreshDevicesCall || ((Date.now() - this.lastRefreshDevicesCall) > (60 * 1000))) { + + return null; + } + + // If we've got no serial number, we're done here. + if(serial.length <= 0) { + + return null; + } + + // Convert to upper case before searching for it. + serial = serial.toUpperCase(); + + // Iterate through the list and find the device that matches the serial number we seek. + return this.devices.find(x => x.serial_number?.toUpperCase() === serial) ?? null; + } + + // Utility to generate a nicely formatted device string. + public getDeviceName(device: myQDevice): string { + + // A completely enumerated device will appear as: + // DeviceName [DeviceBrand] (serial number: Serial, gateway: GatewaySerial). + let deviceString = device.name; + const hwInfo = this.getHwInfo(device.serial_number); + + if(hwInfo) { + deviceString += " [" + hwInfo.brand + " " + hwInfo.product + "]"; + } + + if(device.serial_number) { + deviceString += " (serial number: " + device.serial_number; + + if(device.parent_device_id) { + deviceString += ", gateway: " + device.parent_device_id; + } + + deviceString += ")"; + } + + return deviceString; + } + + // Return device manufacturer and model information based on the serial number, if we can. + public getHwInfo(serial: string): myQHwInfo { + + // We only know about gateway devices and not individual openers, so we can only decode those. + // According to Liftmaster, here's how you can decode what device you're using: + // + // The MyQ serial number for the Wi-Fi GDO, MyQ Home Bridge, MyQ Smart Garage Hub, + // MyQ Garage (Wi-Fi Hub) and Internet Gateway is 12 characters long. The first two characters, + // typically "GW", followed by 2 characters that are decoded according to the table below to + // identify the device type and brand, with the remaining 8 characters representing the serial number. + const HwInfo: {[index: string]: myQHwInfo} = { + "00": { brand: "Chamberlain", product: "Ethernet Gateway" }, + "01": { brand: "Liftmaster", product: "Ethernet Gateway" }, + "02": { brand: "Craftsman", product: "Ethernet Gateway" }, + "03": { brand: "Chamberlain", product: "WiFi Hub" }, + "04": { brand: "Liftmaster", product: "WiFi Hub" }, + "05": { brand: "Craftsman", product: "WiFi Hub" }, + "0A": { brand: "Chamberlain", product: "WiFi GDO AC" }, + "0B": { brand: "Liftmaster", product: "WiFi GDO AC" }, + "0C": { brand: "Craftsman", product: "WiFi GDO AC" }, + "0D": { brand: "myQ Replacement Logic Board", product: "WiFi GDO AC" }, + "0E": { brand: "Chamberlain", product: "WiFi GDO AC 3/4 HP" }, + "0F": { brand: "Liftmaster", product: "WiFi GDO AC 3/4 HP" }, + "10": { brand: "Craftsman", product: "WiFi GDO AC 3/4 HP" }, + "11": { brand: "myQ Replacement Logic Board", product: "WiFi GDO AC 3/4 HP" }, + "12": { brand: "Chamberlain", product: "WiFi GDO DC 1.25 HP" }, + "13": { brand: "Liftmaster", product: "WiFi GDO DC 1.25 HP" }, + "14": { brand: "Craftsman", product: "WiFi GDO DC 1.25 HP" }, + "15": { brand: "myQ Replacement Logic Board", product: "WiFi GDO DC 1.25 HP" }, + "20": { brand: "Chamberlain", product: "myQ Home Bridge" }, + "21": { brand: "Liftmaster", product: "myQ Home Bridge" }, + "23": { brand: "Chamberlain", product: "Smart Garage Hub" }, + "24": { brand: "Liftmaster", product: "Smart Garage Hub" }, + "27": { brand: "Liftmaster", product: "WiFi Wall Mount Opener" }, + "28": { brand: "Liftmaster Commercial", product: "WiFi Wall Mount Operator" }, + "80": { brand: "Liftmaster EU", product: "Ethernet Gateway" }, + "81": { brand: "Chamberlain EU", product: "Ethernet Gateway" } + }; + + if(serial?.length < 4) { + return undefined as unknown as myQHwInfo; + } + + // Use the third and fourth characters as indices into the hardware matrix. Admittedly, + // we don't have a way to resolve the first two characters to ensure we are matching + // against the right category of devices. + return HwInfo[serial[2] + serial[3]]; + } + + // Utility function to return the relevant portions of the cookies used in the login process. + private trimSetCookie(setCookie: string[]): string { + + // We need to strip spurious additions to the cookie that gets returned by the myQ API. + return setCookie.map(x => x.split(";")[0]).join("; "); + } + + // Utility to let us streamline error handling and return checking from the myQ API. + private async fetch(url: RequestInfo, options: RequestInit = {}, overrideHeaders = false, isRetry = false): Promise { + + let response: Response; + + // Set our headers. + if(!overrideHeaders) { + options.headers = this.headers; + } + + try { + response = await fetch(url, options); + + // Bad username and password. + if(response.status === 401) { + + this.log.error("myQ API: Invalid myQ credentials given. Check your login and password."); + return null; + } + + // Some other unknown error occurred. + if(!response.ok && !isRedirect(response.status)) { + + this.log.error("myQ API: %s Error: %s %s", url, response.status, response.statusText); + return null; + } + + return response; + + } catch(error) { + + if(error instanceof FetchError) { + + switch(error.code) { + + case "ECONNREFUSED": + + this.log.error("myQ API: Connection refused."); + break; + + case "ECONNRESET": + + // Retry on connection reset, but no more than once. + if(!isRetry) { + + this.log.debug("myQ API: Connection has been reset. Retrying the API action."); + return this.fetch(url, options, overrideHeaders, true); + } + + this.log.error("myQ API: Connection has been reset."); + + break; + + case "ENOTFOUND": + + this.log.error("myQ API: Hostname or IP address not found."); + break; + + case "UNABLE_TO_VERIFY_LEAF_SIGNATURE": + + this.log.error("myQ API: Unable to verify the myQ TLS security certificate."); + break; + + default: + + this.log.error(error.message); + } + + } else { + + this.log.error("Unknown fetch error: %s", error); + + } + + return null; + } + } +} diff --git a/src/myq-logging.ts b/src/myq-logging.ts new file mode 100644 index 0000000..1c1f3d0 --- /dev/null +++ b/src/myq-logging.ts @@ -0,0 +1,13 @@ +/* Copyright(C) 2017-2021, HJD (https://github.com/hjdhjd). All rights reserved. + * + * myq-logging.ts: Logging support for the myQ library. + */ + +// Logging support, borrowed from Homebridge. +export interface myQLogging { + + debug(message: string, ...parameters: unknown[]): void; + error(message: string, ...parameters: unknown[]): void; + info(message: string, ...parameters: unknown[]): void; + warn(message: string, ...parameters: unknown[]): void; +} diff --git a/src/myq-types.ts b/src/myq-types.ts new file mode 100644 index 0000000..8a8fbe8 --- /dev/null +++ b/src/myq-types.ts @@ -0,0 +1,174 @@ +/* Copyright(C) 2017-2021, HJD (https://github.com/hjdhjd). All rights reserved. + * + * myq-types.ts: Type definitions for myQ. + */ + +// A complete description of the myQ authentication JSON. +/* eslint-disable camelcase */ +interface myQTokenInterface { + + access_token: string, + expires_in: number, + refresh_token: string, + scope: string, + token_type: string +} +/* eslint-enable camelcase */ + +// A complete description of the myQ account JSON. +/* eslint-disable camelcase */ +export interface myQAccountInterface { + + accounts: { + created_by: string, + id: string, + max_users: { + co_owner: number, + guest: number + }, + name: string + }[] +} +/* eslint-enable camelcase */ + +// A complete description of the myQ account profile JSON. +// This is currently unused and documented here primarily for reference. +/* eslint-disable camelcase */ +export interface myQProfileInterface { + + address: { + address_line1: string, + address_line2: string, + city: string, + country: { + is_eea_country: boolean, + name: string + }, + postal_code: string, + state: string + }, + analytics_id: string, + culture_code: string, + diagnostics_opt_in: boolean, + email: string, + first_name: string, + last_name: string, + mailing_list_opt_in: boolean, + phone_number: string, + timezone: string, + user_id: string +} +/* eslint-enable camelcase */ + +// A complete description of the myQ device list JSON. +export interface myQDeviceListInterface { + + count: number, + href: string, + items: myQDevice[] +} + +// A semi-complete description of the myQ device JSON. +/* eslint-disable camelcase */ +export interface myQDeviceInterface { + + account_id: string, + created_date: string, + device_family: string, + device_model: string, + device_platform: string, + device_type: string, + href: string, + name: string, + parent_device_id: string, + serial_number: string, + state: { + attached_work_light_error_present: boolean, + aux_relay_behavior: string, + aux_relay_delay: string, + close: string, + command_channel_report_status: boolean, + control_from_browser: boolean, + door_ajar_interval: string, + door_state: string, + dps_low_battery_mode: boolean, + firmware_version: string, + gdo_lock_connected: boolean, + homekit_capable: boolean, + homekit_enabled: boolean, + invalid_credential_window: string, + invalid_shutout_period: string, + is_unattended_close_allowed: boolean, + is_unattended_open_allowed: boolean, + lamp_state: string, + lamp_subtype: string, + last_event: string, + last_status: string, + last_update: string, + learn: string, + learn_mode: boolean, + light_state: string, + links: { + events: string, + stream: string + } + max_invalid_attempts: number, + online: boolean, + online_change_time: string, + open: string, + passthrough_interval: string, + pending_bootload_abandoned: boolean, + physical_devices: [], + report_ajar: boolean, + report_forced: boolean, + rex_fires_door: boolean, + servers: string, + updated_date: string, + use_aux_relay: boolean + } +} +/* eslint-enable camelcase */ + +// Hardware device information reference. +export interface myQHwInfoInterface { + + brand: string, + product: string +} + +// Plugin configuration options. +export interface myQOptionsInterface { + + activeRefreshDuration: number, + activeRefreshInterval: number, + debug: boolean, + email: string, + mqttTopic: string, + mqttUrl: string, + name: string, + options: string[], + password: string, + refreshInterval: number +} + +// We use types instead of interfaces here because we can more easily set the entire thing as readonly. +// Unfortunately, interfaces can't be quickly set as readonly in TypeScript without marking each and +// every property as readonly along the way. +export type myQAccount = Readonly; +export type myQDeviceList = Readonly; +export type myQProfile = Readonly; +export type myQToken = Readonly; +export type myQDevice = Readonly; +export type myQHwInfo = Readonly; +export type myQOptions = Readonly; + +/* + * // List all the door types we know about. For future use... + * const myQDoorTypes = [ + * "commercialdooropener", + * "garagedooropener", + * "gate", + * "virtualgaragedooropener", + * "wifigaragedooropener" + * ]; + */ diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..8d1f459 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,13 @@ +/* Copyright(C) 2017-2021, HJD (https://github.com/hjdhjd). All rights reserved. + * + * settings.ts: Settings and constants for myq. + */ + +// myQ OAuth client identifier. +export const MYQ_API_CLIENT_ID = "IOS_CGI_MYQ"; + +// myQ OAuth client secret. +export const MYQ_API_CLIENT_SECRET = "VUQ0RFhuS3lQV3EyNUJTdw=="; + +// myQ OAuth redict URI. +export const MYQ_API_REDIRECT_URI = "com.myqops://ios"; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c4562be --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "lib": [ + "ES2015", + "ES2016", + "ES2017", + "ES2018", + "ES2019", + "ES2020", + "dom" + ], + "sourceMap": true, + "rootDir": "src", + "outDir": "dist", + "declaration": true, + + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "src" + ] +}