diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..95ef3e6 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +!/bin/.*.ts +!/tools/.*.ts \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index baba800..7414408 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,11 +3,12 @@ "es6": true, "node": true }, - "extends": "eslint:recommended", + "parser": "@typescript-eslint/parser", "parserOptions": { - "sourceType": "script", - "ecmaVersion": 8 + "project": "./tsconfig.json" }, + "plugins": ["@typescript-eslint"], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "prettier/@typescript-eslint"], "rules": { "no-shadow": ["error", { "allow": ["err"] }], "handle-callback-err": "error", @@ -16,6 +17,8 @@ "consistent-return": ["error", {"treatUndefinedAsUnspecified": true}], "no-buffer-constructor": "error", "prefer-const": ["error", {"destructuring": "all"}], - "eqeqeq": ["error", "always", {"null": "ignore"}] + "eqeqeq": ["error", "always", {"null": "ignore"}], + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-use-before-define": ["error", { "functions": false }] } } diff --git a/.gitignore b/.gitignore index ca2c62c..daf3df3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ *~ node_modules +dist debug -config/auth.js config/config.json +config/config-ui.json config/cwmp.key config/cwmp.crt config/cwmp.ca-bundle @@ -13,3 +14,5 @@ config/fs.key config/fs.crt config/fs.ca-bundle config/ext +bin/.*.ts +tools/.*.ts \ No newline at end of file diff --git a/.npmignore b/.npmignore index abca22c..6474b2f 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,6 @@ *~ .npmignore -config/auth.js config/config.json config/cwmp.key config/cwmp.crt @@ -17,4 +16,10 @@ config/ext debug/* !debug/.gitkeep +dist .eslintrc.json +.eslintignore +ava.config.js +tsconfig.json +bin/.* +tools/.* diff --git a/ava.config.js b/ava.config.js new file mode 100644 index 0000000..46f7c73 --- /dev/null +++ b/ava.config.js @@ -0,0 +1,6 @@ +export default { + "compileEnhancements": false, + "extensions": ["ts"], + "require": ["esm", "ts-node/register/transpile-only"] +}; + diff --git a/bin/genieacs-cwmp b/bin/genieacs-cwmp index 01c6ec1..8c5f01b 100755 --- a/bin/genieacs-cwmp +++ b/bin/genieacs-cwmp @@ -1,9 +1,113 @@ -#!/usr/bin/env node -var path = require('path'); -var fs = require('fs'); +#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only +import * as path from "path"; +import * as fs from "fs"; -var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..'); -process.chdir(dir); +process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); -var cluster = require(path.resolve(dir, 'lib/cluster')); -cluster.start('cwmp'); +import * as config from "../lib/config"; +import * as logger from "../lib/logger"; +import * as cluster from "../lib/cluster"; +import * as server from "../lib/server"; +import * as cwmp from "../lib/cwmp"; +import * as db from "../lib/db"; +import * as extensions from "../lib/extensions"; +import * as cache from "../lib/cache"; +import { version as VERSION } from "../package.json"; + +logger.init("cwmp", VERSION); + +const SERVICE_ADDRESS = config.get("CWMP_INTERFACE"); +const SERVICE_PORT = config.get("CWMP_PORT"); + +function exitWorkerGracefully(): void { + db.disconnect(); + cache.disconnect(); + extensions.killAll(); + cluster.worker.disconnect(); +} + +function exitWorkerUngracefully(): void { + extensions.killAll(() => { + process.exit(1); + }); +} + +if (!cluster.worker) { + const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES"); + + logger.info({ + message: `genieacs-cwmp starting`, + pid: process.pid, + version: VERSION + }); + + cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); + + process.on("SIGINT", () => { + logger.info({ + message: "Received signal SIGINT, exiting", + pid: process.pid + }); + + cluster.stop(); + }); + + process.on("SIGTERM", () => { + logger.info({ + message: "Received signal SIGTERM, exiting", + pid: process.pid + }); + + cluster.stop(); + }); +} else { + let ssl; + if (config.get("CWMP_SSL")) { + ssl = { + key: path.resolve("" + config.get("CONFIG_DIR"), "cwmp.key"), + cert: path.resolve("" + config.get("CONFIG_DIR"), "cwmp.crt"), + ca: path.resolve("" + config.get("CONFIG_DIR"), "cwmp.ca-bundle") + }; + } + + process.on("uncaughtException", err => { + if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return; + logger.error({ + message: "Uncaught exception", + exception: err, + pid: process.pid + }); + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGINT", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGTERM", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + db.connect(err => { + if (err) throw err; + cache.connect(err => { + if (err) throw err; + server.start( + SERVICE_PORT, + SERVICE_ADDRESS, + ssl, + cwmp.listener, + cwmp.onConnection + ); + }); + }); +} diff --git a/lib/extension-wrapper.js b/bin/genieacs-ext old mode 100644 new mode 100755 similarity index 81% rename from lib/extension-wrapper.js rename to bin/genieacs-ext index 7e27443..a634fcc --- a/lib/extension-wrapper.js +++ b/bin/genieacs-ext @@ -1,3 +1,5 @@ +#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only + /** * Copyright 2013-2018 Zaid Abdulla * @@ -16,18 +18,19 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; + +import { Fault } from "../lib/types"; const jobs = new Set(); const fileName = process.argv[2]; let script; -function errorToFault(err) { +function errorToFault(err: Error): Fault { if (!err) return null; if (!err.name) return { code: "ext", message: `${err}` }; - const fault = { + const fault: Fault = { code: `ext.${err.name}`, message: err.message, detail: { @@ -37,13 +40,17 @@ function errorToFault(err) { }; if (err.stack) { - fault.detail.stack = err.stack; + fault.detail["stack"] = err.stack; // Trim the stack trace - const stackTrimIndex = fault.detail.stack.match( - /\s+at\s[^\s]+\s\(.*extension-wrapper\.js:.+\)/ + const stackTrimIndex = fault.detail["stack"].match( + /\s+at\s[^\s]+\s\(.*genieacs-ext:.+\)/ ); - if (stackTrimIndex) - fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index); + if (stackTrimIndex) { + fault.detail["stack"] = fault.detail["stack"].slice( + 0, + stackTrimIndex.index + ); + } } return fault; diff --git a/bin/genieacs-fs b/bin/genieacs-fs index c9c7592..1a268b4 100755 --- a/bin/genieacs-fs +++ b/bin/genieacs-fs @@ -1,9 +1,103 @@ -#!/usr/bin/env node -var path = require('path'); -var fs = require('fs'); +#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only +import * as path from "path"; +import * as fs from "fs"; -var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..'); -process.chdir(dir); +process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); -var cluster = require(path.resolve(dir, 'lib/cluster')); -cluster.start('fs'); +import * as config from "../lib/config"; +import * as logger from "../lib/logger"; +import * as cluster from "../lib/cluster"; +import * as server from "../lib/server"; +import { listener } from "../lib/fs"; +import * as db from "../lib/db"; +import * as cache from "../lib/cache"; +import { version as VERSION } from "../package.json"; + +logger.init("fs", VERSION); + +const SERVICE_ADDRESS = config.get("FS_INTERFACE"); +const SERVICE_PORT = config.get("FS_PORT"); + +function exitWorkerGracefully(): void { + db.disconnect(); + cache.disconnect(); + cluster.worker.disconnect(); +} + +function exitWorkerUngracefully(): void { + process.exit(1); +} + +if (!cluster.worker) { + const WORKER_COUNT = config.get("FS_WORKER_PROCESSES"); + + logger.info({ + message: `genieacs-fs starting`, + pid: process.pid, + version: VERSION + }); + + cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); + + process.on("SIGINT", () => { + logger.info({ + message: "Received signal SIGINT, exiting", + pid: process.pid + }); + + cluster.stop(); + }); + + process.on("SIGTERM", () => { + logger.info({ + message: "Received signal SIGTERM, exiting", + pid: process.pid + }); + + cluster.stop(); + }); +} else { + let ssl; + if (config.get("FS_SSL")) { + ssl = { + key: path.resolve("" + config.get("CONFIG_DIR"), "fs.key"), + cert: path.resolve("" + config.get("CONFIG_DIR"), "fs.crt"), + ca: path.resolve("" + config.get("CONFIG_DIR"), "fs.ca-bundle") + }; + } + + process.on("uncaughtException", err => { + if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return; + logger.error({ + message: "Uncaught exception", + exception: err, + pid: process.pid + }); + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGINT", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGTERM", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + db.connect(err => { + if (err) throw err; + cache.connect(err => { + if (err) throw err; + server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener); + }); + }); +} diff --git a/bin/genieacs-nbi b/bin/genieacs-nbi index 98ab7cf..e737d1b 100755 --- a/bin/genieacs-nbi +++ b/bin/genieacs-nbi @@ -1,9 +1,107 @@ -#!/usr/bin/env node -var path = require('path'); -var fs = require('fs'); +#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only +import * as path from "path"; +import * as fs from "fs"; -var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..'); -process.chdir(dir); +process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); -var cluster = require(path.resolve(dir, 'lib/cluster')); -cluster.start('nbi'); +import * as config from "../lib/config"; +import * as logger from "../lib/logger"; +import * as cluster from "../lib/cluster"; +import * as server from "../lib/server"; +import { listener } from "../lib/nbi"; +import * as db from "../lib/db"; +import * as extensions from "../lib/extensions"; +import * as cache from "../lib/cache"; +import { version as VERSION } from "../package.json"; + +logger.init("nbi", VERSION); + +const SERVICE_ADDRESS = config.get("NBI_INTERFACE"); +const SERVICE_PORT = config.get("NBI_PORT"); + +function exitWorkerGracefully(): void { + db.disconnect(); + cache.disconnect(); + extensions.killAll(); + cluster.worker.disconnect(); +} + +function exitWorkerUngracefully(): void { + extensions.killAll(() => { + process.exit(1); + }); +} + +if (!cluster.worker) { + const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES"); + + logger.info({ + message: `genieacs-nbi starting`, + pid: process.pid, + version: VERSION + }); + + cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); + + process.on("SIGINT", () => { + logger.info({ + message: "Received signal SIGINT, exiting", + pid: process.pid + }); + + cluster.stop(); + }); + + process.on("SIGTERM", () => { + logger.info({ + message: "Received signal SIGTERM, exiting", + pid: process.pid + }); + + cluster.stop(); + }); +} else { + let ssl; + if (config.get("NBI_SSL")) { + ssl = { + key: path.resolve("" + config.get("CONFIG_DIR"), "nbi.key"), + cert: path.resolve("" + config.get("CONFIG_DIR"), "nbi.crt"), + ca: path.resolve("" + config.get("CONFIG_DIR"), "nbi.ca-bundle") + }; + } + + process.on("uncaughtException", err => { + if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return; + logger.error({ + message: "Uncaught exception", + exception: err, + pid: process.pid + }); + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGINT", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGTERM", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + db.connect(err => { + if (err) throw err; + cache.connect(err => { + if (err) throw err; + server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener); + }); + }); +} diff --git a/bin/genieacs-ui b/bin/genieacs-ui new file mode 100755 index 0000000..88225de --- /dev/null +++ b/bin/genieacs-ui @@ -0,0 +1,115 @@ +#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only +import * as path from "path"; +import * as fs from "fs"; + +process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); + +import * as config from "../lib/config"; +import * as logger from "../lib/logger"; +import * as cluster from "../lib/cluster"; +import * as server from "../lib/server"; +import { listener } from "../lib/ui"; +import * as db from "../lib/ui/db"; +import * as db2 from "../lib/db"; +import * as cache from "../lib/cache"; +import { version as VERSION } from "../package.json"; + +logger.init("ui", VERSION); + +const SERVICE_ADDRESS = config.get("UI_INTERFACE"); +const SERVICE_PORT = config.get("UI_PORT"); + +function exitWorkerGracefully(): void { + db.disconnect(); + db2.disconnect(); + cache.disconnect(); + cluster.worker.disconnect(); +} + +function exitWorkerUngracefully(): void { + process.exit(1); +} + +if (!cluster.worker) { + const WORKER_COUNT = config.get("UI_WORKER_PROCESSES"); + + logger.info({ + message: `genieacs-ui starting`, + pid: process.pid, + version: VERSION + }); + + cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); + + process.on("SIGINT", () => { + logger.info({ + message: "Received signal SIGINT, exiting", + pid: process.pid + }); + + cluster.stop(); + }); + + process.on("SIGTERM", () => { + logger.info({ + message: "Received signal SIGTERM, exiting", + pid: process.pid + }); + + cluster.stop(); + }); +} else { + let ssl; + if (config.get("UI_SSL")) { + ssl = { + key: path.resolve("" + config.get("CONFIG_DIR"), "ui.key"), + cert: path.resolve("" + config.get("CONFIG_DIR"), "ui.crt"), + ca: path.resolve("" + config.get("CONFIG_DIR"), "ui.ca-bundle") + }; + } + + let stopping = false; + + process.on("uncaughtException", err => { + if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return; + logger.error({ + message: "Uncaught exception", + exception: err, + pid: process.pid + }); + stopping = true; + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGINT", () => { + stopping = true; + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGTERM", () => { + stopping = true; + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + const _listener = (req, res): void => { + if (stopping) res.setHeader("Connection", "close"); + listener(req, res); + }; + + db2.connect(err => { + if (err) throw err; + cache.connect(err => { + if (err) throw err; + server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener); + }); + }); +} diff --git a/build/build.ts b/build/build.ts new file mode 100644 index 0000000..832f68f --- /dev/null +++ b/build/build.ts @@ -0,0 +1,284 @@ +import * as path from "path"; +import * as fs from "fs"; +import { promisify } from "util"; +import { rollup, WarningHandler } from "rollup"; +import rollupReplace from "rollup-plugin-replace"; +import rollupJson from "rollup-plugin-json"; +import typescript from "rollup-plugin-typescript"; +import { terser } from "rollup-plugin-terser"; +import webpack from "webpack"; +import postcss from "postcss"; +import postcssImport from "postcss-import"; +import postcssCssNext from "postcss-cssnext"; +import cssnano from "cssnano"; + +const MODE = process.env["NODE_ENV"] || "production"; + +const BUILD_METADATA = new Date() + .toISOString() + .split(".")[0] + .replace(/[^0-9]/g, ""); + +const INPUT_DIR = path.resolve(__dirname, ".."); +const OUTPUT_DIR = path.resolve(__dirname, "../dist"); + +const externals = [ + "path", + "fs", + "cluster", + "os", + "http", + "https", + "zlib", + "crypto", + "mongodb", + "libxmljs", + "vm", + "later", + "parsimmon", + "seedrandom", + "querystring", + "child_process", + "dgram", + "url", + "koa", + "koa-router", + "koa-compress", + "koa-bodyparser", + "koa-jwt", + "koa-static", + "jsonwebtoken", + "stream", + "mithril", + "parsimmon", + "codemirror", + "codemirror/mode/javascript/javascript", + "codemirror/mode/yaml/yaml" +]; + +function rmDirSync(dirPath): void { + if (!fs.existsSync(dirPath)) return; + const files = fs.readdirSync(dirPath); + + for (const file of files) { + const filePath = `${dirPath}/${file}`; + if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath); + else rmDirSync(filePath); + } + fs.rmdirSync(dirPath); +} + +async function init(): Promise { + // Delete any old output directory + rmDirSync(OUTPUT_DIR); + + // Create output directory layout + fs.mkdirSync(OUTPUT_DIR); + fs.mkdirSync(OUTPUT_DIR + "/bin"); + fs.mkdirSync(OUTPUT_DIR + "/config"); + fs.mkdirSync(OUTPUT_DIR + "/debug"); + fs.mkdirSync(OUTPUT_DIR + "/public"); + fs.mkdirSync(OUTPUT_DIR + "/tools"); + + // Create package.json + const packageJson = JSON.parse( + fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString() + ); + delete packageJson["devDependencies"]; + packageJson["scripts"] = { + install: packageJson["scripts"].install, + configure: packageJson["scripts"].configure + }; + packageJson["version"] = `${packageJson["version"]}+${BUILD_METADATA}`; + fs.writeFileSync( + path.resolve(OUTPUT_DIR, "package.json"), + JSON.stringify(packageJson, null, 4) + ); +} + +async function copyStatic(): Promise { + const files = [ + "LICENSE", + "README.md", + "CHANGELOG.md", + "npm-shrinkwrap.json", + "config/config-sample.json", + "config/ext-sample.js", + "public/logo.svg", + "public/favicon.png" + ]; + + for (const file of files) { + fs.copyFileSync( + path.resolve(INPUT_DIR, file), + path.resolve(OUTPUT_DIR, file) + ); + } +} + +async function generateCss(): Promise { + const cssInPath = path.resolve(INPUT_DIR, "ui/css/app.css"); + const cssOutPath = path.resolve(OUTPUT_DIR, "public/app.css"); + const cssIn = fs.readFileSync(cssInPath); + const cssOut = await postcss([ + postcssImport, + postcssCssNext({ warnForDuplicates: false }), + cssnano + ]).process(cssIn, { from: cssInPath, to: cssOutPath }); + fs.writeFileSync(cssOutPath, cssOut.css); +} + +async function generateToolsJs(): Promise { + for (const bin of ["configure-ui", "dump-data-model"]) { + const inputFile = path.resolve(INPUT_DIR, `tools/${bin}`); + const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`); + const bundle = await rollup({ + input: inputFile, + external: externals, + acorn: { + allowHashBang: true + }, + plugins: [ + rollupReplace({ + delimiters: ["", ""], + "#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": "" + }), + typescript({ + tsconfig: "./tsconfig.json", + include: [`tools/${bin}`, "lib/**/*.ts"] + }), + MODE === "production" ? terser() : null + ] + }); + + await bundle.write({ + format: "cjs", + preferConst: true, + banner: "#!/usr/bin/env node", + file: outputFile + }); + + // Mark as executable + const mode = fs.statSync(outputFile).mode; + fs.chmodSync(outputFile, mode | 73); + } +} + +async function generateBackendJs(): Promise { + for (const bin of [ + "genieacs-cwmp", + "genieacs-ext", + "genieacs-nbi", + "genieacs-fs", + "genieacs-ui" + ]) { + const inputFile = path.resolve(INPUT_DIR, `bin/${bin}`); + const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`); + const bundle = await rollup({ + input: inputFile, + external: externals, + acorn: { + allowHashBang: true + }, + treeshake: { + propertyReadSideEffects: false, + pureExternalModules: true + }, + plugins: [ + rollupReplace({ + delimiters: ["", ""], + "#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": "" + }), + rollupJson({ preferConst: true }), + { + resolveId: (importee, importer) => { + if (importee.endsWith("/package.json")) { + const p = path.resolve(path.dirname(importer), importee); + if (p === path.resolve(INPUT_DIR, "package.json")) + return path.resolve(OUTPUT_DIR, "package.json"); + } + return null; + } + }, + typescript({ + tsconfig: "./tsconfig.json", + include: [`bin/${bin}`, "lib/**/*.ts"] + }), + MODE === "production" ? terser() : null + ] + }); + + await bundle.write({ + format: "cjs", + preferConst: true, + banner: "#!/usr/bin/env node", + file: outputFile + }); + + // Mark as executable + const mode = fs.statSync(outputFile).mode; + fs.chmodSync(outputFile, mode | 73); + } +} + +async function generateFrontendJs(): Promise { + const inputFile = path.resolve(INPUT_DIR, "ui/app.ts"); + const outputFile = path.resolve(OUTPUT_DIR, "public/app.js"); + + const bundle = await rollup({ + input: inputFile, + external: externals, + plugins: [ + rollupJson({ preferConst: true }), + typescript({ tsconfig: "./tsconfig.json" }) + ], + inlineDynamicImports: true, + treeshake: { + propertyReadSideEffects: false, + pureExternalModules: true + }, + onwarn: ((warning, warn) => { + // Ignore circular dependency warnings + if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning); + }) as WarningHandler + }); + + await bundle.write({ + preferConst: true, + format: "esm", + file: outputFile + }); + + const webpackConf = { + mode: MODE, + entry: outputFile, + resolve: { + aliasFields: ["module"] + }, + output: { + path: path.resolve(OUTPUT_DIR, "public"), + filename: "app.js" + } + }; + + const stats = await promisify(webpack)(webpackConf); + process.stdout.write(stats.toString({ colors: true }) + "\n"); +} + +init() + .then(() => { + Promise.all([ + copyStatic(), + generateCss(), + generateToolsJs(), + generateBackendJs(), + generateFrontendJs() + ]) + .then(() => {}) + .catch(err => { + process.stderr.write(err.stack + "\n"); + }); + }) + .catch(err => { + process.stderr.write(err.stack + "\n"); + }); diff --git a/config/auth-sample.js b/config/auth-sample.js deleted file mode 100644 index aeb48e9..0000000 --- a/config/auth-sample.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; - -function connectionRequest(deviceId, url, username, password, callback) { - return callback(username || deviceId, password || ""); -} - -exports.connectionRequest = connectionRequest; diff --git a/config/config-sample.json b/config/config-sample.json index 9925c9f..51251e3 100644 --- a/config/config-sample.json +++ b/config/config-sample.json @@ -8,5 +8,8 @@ "FS_INTERFACE" : "0.0.0.0", "FS_PORT" : 7567, "FS_HOSTNAME" : "acs.example.com", + "UI_INTERFACE" : "0.0.0.0", + "UI_PORT" : 3000, + "UI_JWT_SECRET": "", "DEBUG" : false } diff --git a/lib/api-functions.js b/lib/api-functions.js deleted file mode 100644 index 7ccf014..0000000 --- a/lib/api-functions.js +++ /dev/null @@ -1,380 +0,0 @@ -/** - * Copyright 2013-2018 Zaid Abdulla - * - * This file is part of GenieACS. - * - * GenieACS is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * GenieACS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with GenieACS. If not, see . - */ -"use strict"; - -const crypto = require("crypto"); -const dgram = require("dgram"); -const URL = require("url"); -const http = require("http"); -const config = require("./config"); -const db = require("./db"); -const common = require("./common"); -const auth = require("./auth"); -const cache = require("./cache"); - -function udpConReq(address, un, key, callback) { - if (!address) return false; - - let [host, port] = address.split(":", 2); - - if (!port) { - port = 80; - } else { - port = parseInt(port); - if (isNaN(port)) return false; - } - - const ts = Math.trunc(Date.now() / 1000); - const id = Math.trunc(Math.random() * 4294967295); - const cn = crypto.randomBytes(8).toString("hex"); - const sig = crypto - .createHmac("sha1", key) - .update(`${ts}${id}${un}${cn}`) - .digest("hex"); - const uri = `http://${address}?ts=${ts}&id=${id}&un=${un}&cn=${cn}&sig=${sig}`; - const message = Buffer.from( - `GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n` - ); - - const client = dgram.createSocket({ type: "udp4", reuseAddr: true }); - const UDP_CONNECTION_REQUEST_PORT = config.get("UDP_CONNECTION_REQUEST_PORT"); - - if (UDP_CONNECTION_REQUEST_PORT) - // When a device is NAT'ed, the UDP Connection Request must originate from - // the same address and port used by the STUN server, in order to traverse - // the firewall. This does require that the Genieacs NBI and STUN server - // are allowed to bind to the same address and port. The STUN server needs - // to open its UDP port with the SO_REUSEADDR option, allowing the NBI to - // also bind to the same port. - client.bind({ port: UDP_CONNECTION_REQUEST_PORT, exclusive: true }); - - let count = 3; - const func = err => { - if (err || --count <= 0) { - client.close(); - return void callback(err); - } - client.send(message, 0, message.length, port, host, func); - }; - client.send(message, 0, message.length, port, host, func); - return true; -} - -function httpConReq( - url, - username, - password, - allowBasicAuth, - timeout, - callback -) { - const options = URL.parse(url); - if (options.protocol !== "http:") { - return void callback( - new Error("Invalid connection request URL or protocol") - ); - } - options.agent = new http.Agent({ maxSockets: 1 }); - - function statusToError(statusCode) { - switch (statusCode) { - case 200: - case 204: - return null; - case 401: - return new Error("Incorrect connection request credentials"); - case 0: - return new Error("Device is offline"); - default: - return new Error(`Unexpected response code from device: ${statusCode}`); - } - } - - const request = http - .get(options, response => { - if (response.statusCode === 401 && response.headers["www-authenticate"]) { - const authHeader = auth.parseAuthHeader( - response.headers["www-authenticate"] - ); - if (authHeader.method === "Basic") { - if (!allowBasicAuth) { - request.abort(); - return void callback( - new Error("Basic HTTP authentication not allowed") - ); - } - options.headers = { - Authorization: auth.basic(username || "", password || "") - }; - } else if (authHeader.method === "Digest") { - options.headers = { - Authorization: auth.digest( - username || "", - password || "", - options.path, - "GET", - null, - authHeader - ) - }; - } - - const req = http - .get(options, res => { - if (res.statusCode === 0) { - // Workaround for some devices unexpectedly closing the connection - const req2 = http - .get(options, res2 => { - callback(statusToError(res2.statusCode)); - res2.resume(); - }) - .on("error", () => { - req2.abort(); - callback(statusToError(0)); - }) - .on("socket", socket => { - socket.setTimeout(timeout); - socket.on("timeout", () => { - req2.abort(); - }); - }); - } else { - callback(statusToError(res.statusCode)); - } - res.resume(); - }) - .on("error", () => { - req.abort(); - callback(statusToError(0)); - }) - .on("socket", socket => { - socket.setTimeout(timeout); - socket.on("timeout", () => { - req.abort(); - }); - }); - } else { - callback(statusToError(response.statusCode)); - } - // No listener for data so emit resume - response.resume(); - }) - .on("error", () => { - request.abort(); - callback(statusToError(0)); - }) - .on("socket", socket => { - socket.setTimeout(timeout); - socket.on("timeout", () => { - request.abort(); - }); - }); -} - -function connectionRequest(deviceId, callback) { - const CONNECTION_REQUEST_TIMEOUT = config.get( - "CONNECTION_REQUEST_TIMEOUT", - deviceId - ); - const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = config.get( - "CONNECTION_REQUEST_ALLOW_BASIC_AUTH", - deviceId - ); - const options = { - projection: { - "Device.ManagementServer.ConnectionRequestURL._value": 1, - "Device.ManagementServer.UDPConnectionRequestAddress._value": 1, - "Device.ManagementServer.ConnectionRequestUsername._value": 1, - "Device.ManagementServer.ConnectionRequestPassword._value": 1, - "InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value": 1, - "InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value": 1, - "InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value": 1, - "InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1 - } - }; - - db.devicesCollection.findOne({ _id: deviceId }, options, (err, device) => { - if (err) return void callback(err); - if (!device) return void callback(new Error("No such device")); - - let managementServer, - connectionRequestUrl, - udpConnectionRequestAddress, - username, - password; - if (device.Device) - // TR-181 data model - managementServer = device.Device.ManagementServer; - // TR-098 data model - else managementServer = device.InternetGatewayDevice.ManagementServer; - - if (managementServer.ConnectionRequestURL) - connectionRequestUrl = managementServer.ConnectionRequestURL._value; - if (managementServer.UDPConnectionRequestAddress) { - udpConnectionRequestAddress = - managementServer.UDPConnectionRequestAddress._value; - } - if (managementServer.ConnectionRequestUsername) - username = managementServer.ConnectionRequestUsername._value; - if (managementServer.ConnectionRequestPassword) - password = managementServer.ConnectionRequestPassword._value; - - const conReq = () => { - const udpSent = udpConReq( - udpConnectionRequestAddress, - username, - password, - err => { - if (err) throw err; - } - ); - - httpConReq( - connectionRequestUrl, - username, - password, - CONNECTION_REQUEST_ALLOW_BASIC_AUTH, - CONNECTION_REQUEST_TIMEOUT, - err => { - if (udpSent) return void callback(); - callback(err); - } - ); - }; - - if (config.auth && config.auth.connectionRequest) { - // Callback is optional for backward compatibility - if (config.auth.connectionRequest.length > 4) { - return void config.auth.connectionRequest( - deviceId, - connectionRequestUrl, - username, - password, - (u, p) => { - username = u; - password = p; - conReq(); - } - ); - } - [username, password] = config.auth.connectionRequest( - deviceId, - connectionRequestUrl, - username, - password - ); - } - conReq(); - }); -} - -function watchTask(deviceId, taskId, timeout, callback) { - setTimeout(() => { - db.tasksCollection.findOne( - { _id: taskId }, - { projection: { _id: 1 } }, - (err, task) => { - if (err) return void callback(err); - - if (!task) return void callback(null, "completed"); - - const q = { _id: `${deviceId}:task_${taskId}` }; - db.faultsCollection.findOne( - q, - { projection: { _id: 1 } }, - (err, fault) => { - if (err) return void callback(err); - - if (fault) return void callback(null, "fault"); - - if ((timeout -= 500) <= 0) return void callback(null, "timeout"); - - watchTask(deviceId, taskId, timeout, callback); - } - ); - } - ); - }, 500); -} - -function sanitizeTask(task, callback) { - task.timestamp = new Date(task.timestamp || Date.now()); - if (task.expiry) { - if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry)) - task.expiry = new Date(task.expiry); - else task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000); - } - - callback(task); -} - -function insertTasks(tasks, callback) { - if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE) tasks = [tasks]; - else if (!tasks || tasks.length === 0) - return void callback(null, tasks || []); - - let counter = tasks.length; - for (const task of tasks) { - sanitizeTask(task, t => { - if (t.uniqueKey) { - db.tasksCollection.deleteOne( - { device: t.device, uniqueKey: t.uniqueKey }, - () => {} - ); - } - - if (--counter === 0) db.tasksCollection.insertMany(tasks, callback); - }); - } -} - -function deleteDevice(deviceId, callback) { - db.tasksCollection.deleteMany({ device: deviceId }, err => { - if (err) return void callback(err); - db.devicesCollection.deleteOne({ _id: deviceId }, err => { - if (err) return void callback(err); - db.faultsCollection.deleteMany( - { - _id: { - $regex: `^${common.escapeRegExp(deviceId)}\\:` - } - }, - err => { - if (err) return void callback(err); - db.operationsCollection.deleteMany( - { - _id: { - $regex: `^${common.escapeRegExp(deviceId)}\\:` - } - }, - err => { - if (err) return void callback(err); - cache.del(`${deviceId}_tasks_faults_operations`, callback); - } - ); - } - ); - }); - }); -} - -exports.connectionRequest = connectionRequest; -exports.watchTask = watchTask; -exports.insertTasks = insertTasks; -exports.deleteDevice = deleteDevice; diff --git a/lib/api-functions.ts b/lib/api-functions.ts new file mode 100644 index 0000000..0e19bf7 --- /dev/null +++ b/lib/api-functions.ts @@ -0,0 +1,234 @@ +/** + * Copyright 2013-2018 Zaid Abdulla + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ + +import * as db from "./db"; +import * as common from "./common"; +import * as cache from "./cache"; +import { + getCurrentSnapshot, + getConfig, + getConfigExpression +} from "./local-cache"; +import { + httpConnectionRequest, + udpConnectionRequest +} from "./connection-request"; +import { Expression } from "./types"; + +export function connectionRequest(deviceId, callback): void { + const options = { + projection: { + _deviceId: 1, + "Device.ManagementServer.ConnectionRequestURL._value": 1, + "Device.ManagementServer.UDPConnectionRequestAddress._value": 1, + "Device.ManagementServer.ConnectionRequestUsername._value": 1, + "Device.ManagementServer.ConnectionRequestPassword._value": 1, + "InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value": 1, + "InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value": 1, + "InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value": 1, + "InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1 + } + }; + + db.devicesCollection.findOne({ _id: deviceId }, options, (err, device) => { + if (err) return void callback(err); + if (!device) return void callback(new Error("No such device")); + + let managementServer, + connectionRequestUrl, + udpConnectionRequestAddress, + username, + password; + if (device.Device) + // TR-181 data model + managementServer = device.Device.ManagementServer; + // TR-098 data model + else managementServer = device.InternetGatewayDevice.ManagementServer; + + if (managementServer.ConnectionRequestURL) + connectionRequestUrl = managementServer.ConnectionRequestURL._value; + if (managementServer.UDPConnectionRequestAddress) { + udpConnectionRequestAddress = + managementServer.UDPConnectionRequestAddress._value; + } + if (managementServer.ConnectionRequestUsername) + username = managementServer.ConnectionRequestUsername._value; + if (managementServer.ConnectionRequestPassword) + password = managementServer.ConnectionRequestPassword._value; + + const context = { + id: device["_id"], + serialNumber: device["_deviceId"]["SerialNumber"], + productClass: device["_deviceId"]["ProductClass"], + oui: device["_deviceId"]["OUI"], + username: username || "", + password: password || "" + }; + + getCurrentSnapshot() + .then(snapshot => { + const now = Date.now(); + const UDP_CONNECTION_REQUEST_PORT = +getConfig( + snapshot, + "cwmp.udpConnectionRequestAuth", + context, + now + ); + const CONNECTION_REQUEST_TIMEOUT = +getConfig( + snapshot, + "cwmp.connectionRequestTimeout", + context, + now + ); + const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig( + snapshot, + "cwmp.connectionRequestAllowBasicAuth", + context, + now + ); + let authExp: Expression = getConfigExpression( + snapshot, + "cwmp.connectionRequestAuth" + ); + + if (authExp === undefined) { + authExp = [ + "FUNC", + "AUTH", + ["PARAM", "username"], + ["PARAM", "password"] + ] as Expression; + } + + let udpProm; + if (udpConnectionRequestAddress) { + udpProm = udpConnectionRequest( + udpConnectionRequestAddress, + authExp, + db.configCollection, + UDP_CONNECTION_REQUEST_PORT + ); + } + + httpConnectionRequest( + connectionRequestUrl, + authExp, + context, + CONNECTION_REQUEST_ALLOW_BASIC_AUTH, + CONNECTION_REQUEST_TIMEOUT + ) + .then(callback) + .catch(err => { + if (udpProm) return void udpProm.then(callback).catch(callback); + callback(err); + }); + }) + .catch(callback); + }); +} + +export function watchTask(deviceId, taskId, timeout, callback): void { + setTimeout(() => { + db.tasksCollection.findOne( + { _id: taskId }, + { projection: { _id: 1 } }, + (err, task) => { + if (err) return void callback(err); + + if (!task) return void callback(null, "completed"); + + const q = { _id: `${deviceId}:task_${taskId}` }; + db.faultsCollection.findOne( + q, + { projection: { _id: 1 } }, + (err, fault) => { + if (err) return void callback(err); + + if (fault) return void callback(null, "fault"); + + if ((timeout -= 500) <= 0) return void callback(null, "timeout"); + + watchTask(deviceId, taskId, timeout, callback); + } + ); + } + ); + }, 500); +} + +function sanitizeTask(task, callback): void { + task.timestamp = new Date(task.timestamp || Date.now()); + if (task.expiry) { + if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry)) + task.expiry = new Date(task.expiry); + else task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000); + } + + callback(task); +} + +export function insertTasks(tasks, callback): void { + if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE) tasks = [tasks]; + else if (!tasks || tasks.length === 0) + return void callback(null, tasks || []); + + let counter = tasks.length; + for (const task of tasks) { + sanitizeTask(task, t => { + if (t.uniqueKey) { + db.tasksCollection.deleteOne( + { device: t.device, uniqueKey: t.uniqueKey }, + () => {} + ); + } + + if (--counter === 0) db.tasksCollection.insertMany(tasks, callback); + }); + } +} + +export function deleteDevice(deviceId, callback): void { + db.tasksCollection.deleteMany({ device: deviceId }, err => { + if (err) return void callback(err); + db.devicesCollection.deleteOne({ _id: deviceId }, err => { + if (err) return void callback(err); + db.faultsCollection.deleteMany( + { + _id: { + $regex: `^${common.escapeRegExp(deviceId)}\\:` + } + }, + err => { + if (err) return void callback(err); + db.operationsCollection.deleteMany( + { + _id: { + $regex: `^${common.escapeRegExp(deviceId)}\\:` + } + }, + err => { + if (err) return void callback(err); + cache.del(`${deviceId}_tasks_faults_operations`, callback); + } + ); + } + ); + }); + }); +} diff --git a/lib/auth.js b/lib/auth.ts similarity index 74% rename from lib/auth.js rename to lib/auth.ts index 6f1c04a..2312393 100644 --- a/lib/auth.js +++ b/lib/auth.ts @@ -16,11 +16,10 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const crypto = require("crypto"); +import { createHash, randomBytes, pbkdf2 } from "crypto"; -function parseAuthHeader(authHeader) { +export function parseAuthHeader(authHeader): {} { authHeader = authHeader.trim(); const method = authHeader.split(" ", 1)[0]; const res = { method: method }; @@ -53,11 +52,18 @@ function parseAuthHeader(authHeader) { return res; } -function basic(username, password) { +export function basic(username, password): string { return "Basic " + Buffer.from(`${username}:${password}`).toString("base64"); } -function digest(username, password, uri, httpMethod, body, authHeader) { +export function digest( + username, + password, + uri, + httpMethod, + body, + authHeader +): string { const cnonce = "0a4f113b"; const nc = "00000001"; let qop; @@ -68,7 +74,7 @@ function digest(username, password, uri, httpMethod, body, authHeader) { else qop = authHeader.qop; } - let ha1 = crypto.createHash("md5"); + const ha1 = createHash("md5"); ha1 .update(username) .update(":") @@ -76,19 +82,19 @@ function digest(username, password, uri, httpMethod, body, authHeader) { .update(":") .update(password); // TODO support "MD5-sess" algorithm directive - ha1 = ha1.digest("hex"); + const ha1d = ha1.digest("hex"); - let ha2 = crypto.createHash("md5"); + const ha2 = createHash("md5"); ha2 .update(httpMethod) .update(":") .update(uri); if (qop === "auth-int") ha2.update(":").update(body); - ha2 = ha2.digest("hex"); + const ha2d = ha2.digest("hex"); - const hash = crypto.createHash("md5"); + const hash = createHash("md5"); hash - .update(ha1) + .update(ha1d) .update(":") .update(authHeader.nonce); if (qop) { @@ -100,7 +106,7 @@ function digest(username, password, uri, httpMethod, body, authHeader) { .update(":") .update(qop); } - hash.update(":").update(ha2); + hash.update(":").update(ha2d); let authString = `Digest username="${username}"`; authString += `,realm="${authHeader.realm}"`; @@ -114,6 +120,20 @@ function digest(username, password, uri, httpMethod, body, authHeader) { return authString; } -exports.parseAuthHeader = parseAuthHeader; -exports.basic = basic; -exports.digest = digest; +export function generateSalt(length): Promise { + return new Promise((resolve, reject) => { + randomBytes(length, (err, rand) => { + if (err) return void reject(err); + resolve(rand.toString("hex")); + }); + }); +} + +export function hashPassword(pass, salt): Promise { + return new Promise((resolve, reject) => { + pbkdf2(pass, salt, 10000, 128, "sha512", (err, hash) => { + if (err) return void reject(err); + resolve(hash.toString("hex")); + }); + }); +} diff --git a/lib/cache.js b/lib/cache.js deleted file mode 100644 index 617fd41..0000000 --- a/lib/cache.js +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Copyright 2013-2018 Zaid Abdulla - * - * This file is part of GenieACS. - * - * GenieACS is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * GenieACS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with GenieACS. If not, see . - */ -"use strict"; - -const config = require("./config"); - -const MAX_CACHE_TTL = config.get("MAX_CACHE_TTL"); - -let redisClient; -let mongoClient; -let mongoCollection; -let mongoTimeOffset = 0; - -function connect(callback) { - const REDIS_HOST = config.get("REDIS_HOST"); - - if (REDIS_HOST) { - const redis = require("redis"); - const REDIS_PORT = config.get("REDIS_PORT"); - const REDIS_DB = config.get("REDIS_DB"); - redisClient = redis.createClient(REDIS_PORT, REDIS_HOST); - redisClient.select(REDIS_DB, err => { - exports.get = redisGet; - exports.set = redisSet; - exports.del = redisDel; - exports.pop = redisPop; - exports.lock = redisLock; - callback(err); - }); - } else { - const mongodb = require("mongodb"); - const MONGODB_CONNECTION_URL = config.get("MONGODB_CONNECTION_URL"); - - mongodb.MongoClient.connect( - MONGODB_CONNECTION_URL, - { useNewUrlParser: true }, - (err, client) => { - if (err) return void callback(err); - - const db = client.db(); - mongoClient = client; - mongoCollection = db.collection("cache"); - mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }); - - const now = Date.now(); - db.command({ hostInfo: 1 }, (err, res) => { - if (err) return void callback(err); - mongoTimeOffset = res.system.currentTime.getTime() - now; - exports.get = mongoGet; - exports.set = mongoSet; - exports.del = mongoDel; - exports.pop = mongoPop; - exports.lock = mongoLock; - callback(); - }); - } - ); - } -} - -function disconnect() { - if (redisClient) redisClient.quit(); - if (mongoClient) mongoClient.close(); -} - -function redisGet(key, callback) { - if (Array.isArray(key)) redisClient.mget(key, callback); - else redisClient.get(key, callback); -} - -function redisDel(key, callback) { - redisClient.del(key, callback); -} - -function redisSet(key, value, ttl, callback) { - if (!callback && typeof v === "function") { - callback = ttl; - ttl = null; - } - - if (!ttl) ttl = MAX_CACHE_TTL; - - redisClient.setex(key, ttl, value, callback); -} - -function redisPop(key, callback) { - const POP_SCRIPT = - 'local v=redis.call("get",KEYS[1]);redis.call("del",KEYS[1]);return v;'; - redisClient.eval(POP_SCRIPT, 1, key, callback); -} - -function redisLock(lockName, ttl, callback) { - const UNLOCK_SCRIPT = - 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end'; - const EXTEND_SCRIPT = - 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end'; - const token = Math.random() - .toString(36) - .slice(2); - - function unlockOrExtend(extendTtl) { - if (!extendTtl) { - redisClient.eval(UNLOCK_SCRIPT, 1, lockName, token, (err, res) => { - if (err || !res) throw err || new Error("Lock expired"); - }); - } else { - redisClient.eval( - EXTEND_SCRIPT, - 1, - lockName, - token, - extendTtl, - (err, res) => { - if (err || !res) throw err || new Error("Lock expired"); - } - ); - } - } - - redisClient.set(lockName, token, "NX", "EX", ttl, (err, res) => { - if (err || res) return void callback(err, unlockOrExtend); - - setTimeout(() => { - redisLock(lockName, ttl, callback); - }, 200); - }); -} - -function mongoGet(key, callback) { - const expire = new Date(Date.now() - mongoTimeOffset); - if (Array.isArray(key)) { - mongoCollection.find({ _id: { $in: key } }).toArray((err, res) => { - if (err) return void callback(err); - - const indices = {}; - key.forEach((v, i) => { - indices[v] = i; - }); - - const values = []; - res.forEach(r => { - if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"]; - }); - callback(null, values); - }); - } else { - mongoCollection.findOne({ _id: { $in: [key] } }, (err, res) => { - if (err || !res) return void callback(err); - - if (res["expire"] > expire) return void callback(null, res["value"]); - - callback(); - }); - } -} - -function mongoDel(key, callback) { - if (Array.isArray(key)) - mongoCollection.deleteMany({ _id: { $in: key } }, callback); - else mongoCollection.deleteOne({ _id: key }, callback); -} - -function mongoSet(key, value, ttl, callback) { - if (!callback && typeof v === "function") { - callback = ttl; - ttl = null; - } - - if (!ttl) ttl = MAX_CACHE_TTL; - - const expire = new Date(Date.now() - mongoTimeOffset + ttl * 1000); - mongoCollection.replaceOne( - { _id: key }, - { _id: key, value: value, expire: expire }, - { upsert: true }, - callback - ); -} - -function mongoPop(key, callback) { - mongoCollection.findOneAndDelete({ _id: key }, (err, res) => { - if (err || !res["value"]) return void callback(err); - - if (res["value"]["expire"] > new Date(Date.now() - mongoTimeOffset)) - return void callback(null, res["value"]["value"]); - - callback(); - }); -} - -function mongoLock(lockName, ttl, callback) { - const token = Math.random() - .toString(36) - .slice(2); - - function unlockOrExtend(extendTtl) { - if (!extendTtl) { - mongoCollection.deleteOne({ _id: lockName, value: token }, (err, res) => { - if (err || res["result"]["n"] !== 1) - throw err || new Error("Lock expired"); - }); - } else { - const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000); - mongoCollection.updateOne( - { _id: lockName, value: token }, - { expire: expire }, - (err, res) => { - if (err || res["result"]["n"] !== 1) - throw err | new Error("Lock expired"); - } - ); - } - } - - const expireTest = new Date(Date.now() - mongoTimeOffset); - const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000); - - mongoCollection.updateOne( - { _id: lockName, expire: { $lte: expireTest } }, - { $set: { value: token, expire: expireSet } }, - { upsert: true }, - err => { - if (err && err.code === 11000) { - return setTimeout(() => { - mongoLock(lockName, ttl, callback); - }, 200); - } - - return callback(err, unlockOrExtend); - } - ); -} - -exports.connect = connect; -exports.disconnect = disconnect; diff --git a/lib/cache.ts b/lib/cache.ts new file mode 100644 index 0000000..c9ec7c7 --- /dev/null +++ b/lib/cache.ts @@ -0,0 +1,178 @@ +/** + * Copyright 2013-2018 Zaid Abdulla + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ + +import { MongoClient } from "mongodb"; +import * as config from "./config"; + +const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL"); + +let mongoClient; +let mongoCollection; +let mongoTimeOffset = 0; + +export function connect(callback): void { + const MONGODB_CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL"); + + MongoClient.connect( + MONGODB_CONNECTION_URL, + { useNewUrlParser: true }, + (err, client) => { + if (err) return void callback(err); + + const db = client.db(); + mongoClient = client; + mongoCollection = db.collection("cache"); + mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }); + + const now = Date.now(); + db.command({ hostInfo: 1 }, (err, res) => { + if (err) return void callback(err); + mongoTimeOffset = res.system.currentTime.getTime() - now; + callback(); + }); + } + ); +} + +export function disconnect(): void { + if (mongoClient) mongoClient.close(); +} + +export function get(key, callback): void { + const expire = new Date(Date.now() - mongoTimeOffset); + if (Array.isArray(key)) { + mongoCollection.find({ _id: { $in: key } }).toArray((err, res) => { + if (err) return void callback(err); + + const indices = {}; + key.forEach((v, i) => { + indices[v] = i; + }); + + const values = []; + res.forEach(r => { + if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"]; + }); + callback(null, values); + }); + } else { + mongoCollection.findOne({ _id: { $in: [key] } }, (err, res) => { + if (err || !res) return void callback(err); + + if (res["expire"] > expire) return void callback(null, res["value"]); + + callback(); + }); + } +} + +export function del(key, callback): void { + if (Array.isArray(key)) + mongoCollection.deleteMany({ _id: { $in: key } }, callback); + else mongoCollection.deleteOne({ _id: key }, callback); +} + +export function set( + key: string, + value: string | number, + callback: (err?: Error) => void +): void; +export function set( + key: string, + value: string | number, + ttl: number, + callback: (err?: Error) => void +): void; +export function set( + key: string, + value: string | number, + ttl: number | ((err?: Error) => void), + callback?: (err?: Error) => void +): void { + if (!callback && typeof ttl === "function") { + callback = ttl; + ttl = null; + } + + if (!ttl) ttl = MAX_CACHE_TTL; + + const expire = new Date( + Date.now() - mongoTimeOffset + (ttl as number) * 1000 + ); + mongoCollection.replaceOne( + { _id: key }, + { _id: key, value: value, expire: expire }, + { upsert: true }, + callback + ); +} + +export function pop(key, callback): void { + mongoCollection.findOneAndDelete({ _id: key }, (err, res) => { + if (err || !res["value"]) return void callback(err); + + if (res["value"]["expire"] > new Date(Date.now() - mongoTimeOffset)) + return void callback(null, res["value"]["value"]); + + callback(); + }); +} + +export function lock(lockName, ttl, callback): void { + const token = Math.random() + .toString(36) + .slice(2); + + function unlockOrExtend(extendTtl): void { + if (!extendTtl) { + mongoCollection.deleteOne({ _id: lockName, value: token }, (err, res) => { + if (err || res["result"]["n"] !== 1) + throw err || new Error("Lock expired"); + }); + } else { + const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000); + mongoCollection.updateOne( + { _id: lockName, value: token }, + { expire: expire }, + (err, res) => { + if (err || res["result"]["n"] !== 1) + throw err || new Error("Lock expired"); + } + ); + } + } + + const expireTest = new Date(Date.now() - mongoTimeOffset); + const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000); + + mongoCollection.updateOne( + { _id: lockName, expire: { $lte: expireTest } }, + { $set: { value: token, expire: expireSet } }, + { upsert: true }, + err => { + if (err && err.code === 11000) { + return setTimeout(() => { + lock(lockName, ttl, callback); + }, 200); + } + + return callback(err, unlockOrExtend); + } + ); +} diff --git a/lib/cluster.js b/lib/cluster.js deleted file mode 100644 index e1a7a15..0000000 --- a/lib/cluster.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright 2013-2018 Zaid Abdulla - * - * This file is part of GenieACS. - * - * GenieACS is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * GenieACS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with GenieACS. If not, see . - */ -"use strict"; - -const cluster = require("cluster"); -const logger = require("./logger"); -const config = require("./config"); - -const VERSION = require("../package.json").version; - -let respawnTimestamp = 0; -let tooManyCrashesTimestamp = 0; - -function getDependencyVersions() { - const dependencies = [`node@${process.versions.node}`]; - try { - const npmls = JSON.parse( - require("child_process").execSync("npm ls --prod --json --depth 0") - ); - - for (const k in npmls.dependencies) - dependencies.push(`${k}@${npmls.dependencies[k].version}`); - } catch (err) { - // Continue regardless of error - } - - return dependencies.join(","); -} - -function getConfig() { - const conf = []; - const keys = Object.keys(process.env); - for (const k of keys) { - if (k.startsWith("GENIEACS_")) { - const name = k.slice(9); - const value = process.env[k]; - const def = config.getDefault(name); - if (def == null || value !== `${def}`) conf.push(`${name}=${value}`); - } - } - - return conf.join(","); -} - -function restartWorker(worker, code, signal) { - const msg = { - message: "Worker died", - pid: worker.process.pid - }; - - if (code != null) msg.exitCode = code; - - if (signal != null) msg.signal = signal; - - logger.error(msg); - - const now = Date.now(); - respawnTimestamp = Math.max(now, respawnTimestamp + 2000); - if (respawnTimestamp === now) { - tooManyCrashesTimestamp = now; - cluster.fork(); - return; - } - - if (now - tooManyCrashesTimestamp > 60000) { - process.exitCode = 1; - cluster.removeListener("exit", restartWorker); - for (const pid in cluster.workers) cluster.workers[pid].kill(); - - logger.error({ - message: "Too many crashes, exiting", - pid: process.pid - }); - return; - } - - setTimeout(() => { - if (process.exitCode) return; - cluster.fork(); - }, respawnTimestamp - now); -} - -function start(service) { - logger.init(service, VERSION); - const SERVICE_ADDRESS = config.get(`${service.toUpperCase()}_INTERFACE`); - const SERVICE_PORT = config.get(`${service.toUpperCase()}_PORT`); - - logger.info({ - message: `GenieACS (genieacs-${service}) starting`, - pid: process.pid, - version: VERSION, - dependencies: getDependencyVersions(), - config: getConfig() - }); - - cluster.on("listening", (worker, address) => { - if ( - (address.addressType === 4 || address.addressType === 6) && - address.address === SERVICE_ADDRESS && - address.port === SERVICE_PORT - ) { - logger.info({ - message: "Worker listening", - pid: worker.process.pid, - address: address.address, - port: address.port - }); - } - }); - - cluster.on("exit", restartWorker); - - process.on("SIGINT", () => { - logger.info({ - message: "Received signal SIGINT, exiting", - pid: process.pid - }); - - cluster.removeListener("exit", restartWorker); - }); - - process.on("SIGTERM", () => { - logger.info({ - message: "Received signal SIGTERM, exiting", - pid: process.pid - }); - - cluster.removeListener("exit", restartWorker); - for (const pid in cluster.workers) cluster.workers[pid].kill(); - }); - - cluster.setupMaster({ - exec: "lib/server", - args: [service] - }); - - let workerCount = config.get(`${service.toUpperCase()}_WORKER_PROCESSES`); - - if (!workerCount) workerCount = Math.max(2, require("os").cpus().length); - - for (let i = 0; i < workerCount; ++i) cluster.fork(); -} - -process.on("uncaughtException", err => { - logger.error({ - message: "Uncaught exception in master process, exiting", - exception: err, - pid: process.pid - }); - process.exitCode = 1; - cluster.removeListener("exit", restartWorker); - for (const pid in cluster.workers) cluster.workers[pid].kill(); -}); - -exports.start = start; diff --git a/lib/cluster.ts b/lib/cluster.ts new file mode 100644 index 0000000..c26f02d --- /dev/null +++ b/lib/cluster.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2013-2018 Zaid Abdulla + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ + +import cluster from "cluster"; +import { cpus } from "os"; +import * as logger from "./logger"; + +let respawnTimestamp = 0; +let tooManyCrashesTimestamp = 0; + +function restartWorker(worker, code, signal): void { + const msg = { + message: "Worker died", + pid: worker.process.pid, + exitCode: null, + signal: null + }; + + if (code != null) msg.exitCode = code; + + if (signal != null) msg.signal = signal; + + logger.error(msg); + + const now = Date.now(); + respawnTimestamp = Math.max(now, respawnTimestamp + 2000); + if (respawnTimestamp === now) { + tooManyCrashesTimestamp = now; + cluster.fork(); + return; + } + + if (now - tooManyCrashesTimestamp > 60000) { + process.exitCode = 1; + cluster.removeListener("exit", restartWorker); + for (const pid in cluster.workers) cluster.workers[pid].kill(); + + logger.error({ + message: "Too many crashes, exiting", + pid: process.pid + }); + return; + } + + setTimeout(() => { + if (process.exitCode) return; + cluster.fork(); + }, respawnTimestamp - now); +} + +export function start(workerCount, servicePort, serviceAddress): void { + cluster.on("listening", (worker, address) => { + if ( + (address.addressType === 4 || address.addressType === 6) && + address.address === serviceAddress && + address.port === servicePort + ) { + logger.info({ + message: "Worker listening", + pid: worker.process.pid, + address: address.address, + port: address.port + }); + } + }); + + cluster.on("exit", restartWorker); + + if (!workerCount) workerCount = Math.max(2, cpus().length); + + for (let i = 0; i < workerCount; ++i) cluster.fork(); +} + +export function stop(): void { + cluster.removeListener("exit", restartWorker); + for (const pid in cluster.workers) cluster.workers[pid].kill(); +} + +export const worker = cluster.worker; diff --git a/lib/common.js b/lib/common.js deleted file mode 100644 index 15a3609..0000000 --- a/lib/common.js +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Copyright 2013-2018 Zaid Abdulla - * - * This file is part of GenieACS. - * - * GenieACS is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * GenieACS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with GenieACS. If not, see . - */ -"use strict"; - -const UNDEFINED_TYPE = "[object Undefined]"; -const NULL_TYPE = "[object Null]"; -const BOOLEAN_TYPE = "[object Boolean]"; -const NUMBER_TYPE = "[object Number]"; -const STRING_TYPE = "[object String]"; -const ARRAY_TYPE = "[object Array]"; -const OBJECT_TYPE = "[object Object]"; -const REGEXP_TYPE = "[object RegExp]"; -const DATE_TYPE = "[object Date]"; - -const typeOf = obj => Object.prototype.toString.call(obj); - -function generateDeviceId(deviceIdStruct) { - // Escapes everything except alphanumerics and underscore - function esc(str) { - return str.replace(/[^A-Za-z0-9_]/g, chr => { - const buf = Buffer.from(chr, "utf8"); - let rep = ""; - for (const b of buf) rep += "%" + b.toString(16).toUpperCase(); - return rep; - }); - } - - // Guaranteeing globally unique id as defined in TR-069 - if (deviceIdStruct["ProductClass"]) { - return ( - esc(deviceIdStruct["OUI"]) + - "-" + - esc(deviceIdStruct["ProductClass"]) + - "-" + - esc(deviceIdStruct["SerialNumber"]) - ); - } - return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]); -} - -function parseAlias(pattern, start, res) { - const aliases = []; - let i = start; - while (i < pattern.length && pattern[i] !== "]") { - const alias = []; - let j = (i = parsePath(pattern, i, alias) + 1); - while (pattern[j] !== "]" && pattern[j] !== ",") { - if (pattern[j] === '"' && i === j) { - ++j; - while (pattern[j] !== '"' || pattern[j - 1] === "\\") { - if (++j >= pattern.length) - throw new Error("Invalid alias expression"); - } - } - if (++j >= pattern.length) throw new Error("Invalid alias expression"); - } - - let value = pattern.slice(i, j).trim(); - i = j; - if (value[0] === '"') { - try { - value = JSON.parse(value); - } catch (error) { - throw new Error("Invalid alias expression"); - } - } - - alias.push(value); - aliases.push(alias); - if (pattern[i] === ",") ++i; - } - - // Sort to ensure identical expressions have idential string representation - function srt(a, b) { - const jMax = Math.min(a.length, b.length); - for (let j = 0; j < jMax; j += 2) { - const kMax = Math.min(a[j].length, b[j].length); - for (let k = 0; k < kMax; ++k) { - if (Array.isArray(a[j][k])) { - if (Array.isArray(b[j][k])) return srt(a[j][k], b[j][k]); - else if (b[j][k] == null) return -1; - else return 1; - } else if (a[j][k] == null) { - if (b[j][k] == null) return 0; - else return 1; - } else if (b[j][k] == null || Array.isArray(b[j][k])) { - return -1; - } else if (a[j][k] > b[j][k]) { - return 1; - } else if (a[j][k] < b[j][k]) { - return -1; - } - } - - if (a[j].length > b[j].length) return -1; - else if (a[j].length < b[j].length) return 1; - - if (a[j + 1] > b[j + 1]) return -1; - else if (a[j + 1] < b[j + 1]) return 1; - } - - if (a.length > b.length) return -1; - else if (a.length < b.length) return 1; - - return 0; - } - - aliases.sort(srt); - res.push([].concat.apply([], aliases)); - return i; -} - -function parsePath(pattern, start, res) { - const path = []; - path.wildcard = 0; - path.alias = 0; - let i = start || 0; - - // Colon separator is needed for parseAlias - if (i < pattern.length && pattern[i] !== ":") { - for (;;) { - if (pattern[i] === "[") { - path.alias |= 1 << path.length; - i = parseAlias(pattern, i + 1, path) + 1; - } else { - const j = i; - while (i < pattern.length && pattern[i] !== ":" && pattern[i] !== ".") - ++i; - - const n = pattern.slice(j, i); - if (n === "*") path.wildcard |= 1 << path.length; - path.push(n); - } - - if (i >= pattern.length || pattern[i] === ":") break; - else if (pattern[i] !== ".") throw new Error("Invalid alias expression"); - - ++i; - } - } - - Object.freeze(path); - - if (res == null) return path; - - res.push(path); - return i; -} - -function addPathMeta(path) { - if (path.alias != null && path.wildcard != null) return path; - - path.alias = 0; - path.wildcard = 0; - - for (const [i, p] of path.entries()) { - if (Array.isArray(p)) { - path.alias |= 1 << i; - for (let j = 0; j < p.length; j += 2) addPathMeta(p[j]); - } else if (p === "*") { - path.wildcard |= 1 << i; - } - } - - Object.freeze(path); - return path; -} - -// Source: http://stackoverflow.com/a/6969486 -function escapeRegExp(str) { - return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); -} - -exports.UNDEFINED_TYPE = UNDEFINED_TYPE; -exports.NULL_TYPE = NULL_TYPE; -exports.BOOLEAN_TYPE = BOOLEAN_TYPE; -exports.NUMBER_TYPE = NUMBER_TYPE; -exports.STRING_TYPE = STRING_TYPE; -exports.ARRAY_TYPE = ARRAY_TYPE; -exports.OBJECT_TYPE = OBJECT_TYPE; -exports.DATE_TYPE = DATE_TYPE; -exports.REGEXP_TYPE = REGEXP_TYPE; - -exports.typeOf = typeOf; -exports.generateDeviceId = generateDeviceId; -exports.parsePath = parsePath; -exports.addPathMeta = addPathMeta; -exports.escapeRegExp = escapeRegExp; diff --git a/lib/common.ts b/lib/common.ts new file mode 100644 index 0000000..2752ae5 --- /dev/null +++ b/lib/common.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2013-2018 Zaid Abdulla + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ + +export const UNDEFINED_TYPE = "[object Undefined]"; +export const NULL_TYPE = "[object Null]"; +export const BOOLEAN_TYPE = "[object Boolean]"; +export const NUMBER_TYPE = "[object Number]"; +export const STRING_TYPE = "[object String]"; +export const ARRAY_TYPE = "[object Array]"; +export const OBJECT_TYPE = "[object Object]"; +export const REGEXP_TYPE = "[object RegExp]"; +export const DATE_TYPE = "[object Date]"; + +export const typeOf = (obj): string => Object.prototype.toString.call(obj); + +export function generateDeviceId(deviceIdStruct): string { + // Escapes everything except alphanumerics and underscore + function esc(str): string { + return str.replace(/[^A-Za-z0-9_]/g, chr => { + const buf = Buffer.from(chr, "utf8"); + let rep = ""; + for (const b of buf) rep += "%" + b.toString(16).toUpperCase(); + return rep; + }); + } + + // Guaranteeing globally unique id as defined in TR-069 + if (deviceIdStruct["ProductClass"]) { + return ( + esc(deviceIdStruct["OUI"]) + + "-" + + esc(deviceIdStruct["ProductClass"]) + + "-" + + esc(deviceIdStruct["SerialNumber"]) + ); + } + return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]); +} + +// Source: http://stackoverflow.com/a/6969486 +export function escapeRegExp(str): string { + return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); +} diff --git a/lib/common/authorizer.ts b/lib/common/authorizer.ts new file mode 100644 index 0000000..a399c49 --- /dev/null +++ b/lib/common/authorizer.ts @@ -0,0 +1,93 @@ +import pass from "./validators/pass"; +import test from "./validators/test"; +import { PermissionSet } from "../types"; + +export const validators = { pass, test }; + +export default class Authorizer { + private permissionSets: PermissionSet[]; + private validatorCache: WeakMap< + object, + (mutationType, mutation, any) => boolean + >; + private hasAccessCache: Map; + + public constructor(permissionSets) { + this.permissionSets = permissionSets; + this.validatorCache = new WeakMap(); + this.hasAccessCache = new Map(); + } + + public hasAccess(resourceType, access): boolean { + const cacheKey = `${resourceType}-${access}`; + if (this.hasAccessCache.has(cacheKey)) + return this.hasAccessCache.get(cacheKey); + + let has = false; + for (const permissionSet of this.permissionSets) { + for (const perm of permissionSet) { + if (perm[resourceType]) { + if (perm[resourceType].access >= access) { + has = true; + break; + } + } + } + } + + this.hasAccessCache.set(cacheKey, has); + return has; + } + + public getValidator( + resourceType, + resource + ): (mutationType: string, mutation: any, args: any) => boolean { + if (this.validatorCache.has(resource)) + return this.validatorCache.get(resource); + + let funcs = {}; + + for (const permissionSet of this.permissionSets) { + for (const perm of permissionSet) { + if (perm[resourceType]) { + if (perm[resourceType].access >= 3) { + if (perm[resourceType].validate) { + for (const [k, v] of Object.entries(perm[resourceType].validate)) + funcs[k] = v; + } + } else { + funcs = {}; + } + } + } + } + + const validator = ( + mutationType: string, + mutation: any, + any: any + ): boolean => { + let valid = false; + for (const [k, v] of Object.entries(funcs)) { + if (v) { + const res = validators[k]( + resourceType, + resource, + mutationType, + mutation, + any + ); + + if (res > 0) valid = true; + else if (res < 0) return false; + } + } + + return valid; + }; + + this.validatorCache.set(resource, validator); + return validator; + } +} diff --git a/lib/common/expression-cnf.ts b/lib/common/expression-cnf.ts new file mode 100644 index 0000000..43f80ad --- /dev/null +++ b/lib/common/expression-cnf.ts @@ -0,0 +1,266 @@ +import { map, parseLikePattern } from "./expression-parser"; +import { Expression } from "../types"; + +type CNF = number[][]; + +function or(cnf1: CNF, cnf2: CNF): CNF { + if (cnf1.length === 0) return cnf2; + else if (cnf2.length === 0) return cnf1; + + if (cnf1.length > cnf2.length) { + const c = cnf1; + cnf1 = cnf2; + cnf2 = c; + } + + const res = []; + for (let i = 0; i < cnf1.length; ++i) + for (let j = i; j < cnf2.length; ++j) res.push(cnf1[i].concat(cnf2[j])); + + return res; +} + +function likePatternIncludes(pat1, pat2, idx1 = 0, idx2 = 0): boolean { + while (idx1 < pat1.length && idx2 < pat2.length) { + if (pat1[idx1] === "\\%") { + for (let i = idx2; i <= pat2.length; ++i) + if (likePatternIncludes(pat1, pat2, idx1 + 1, i)) return true; + return false; + } else if (pat2[idx2] === "\\%") { + if (pat1[idx1] !== "\\%") return false; + } else if (pat1[idx1] === "\\_") { + // Ignore + } else if (pat2[idx2] === "\\_") { + return false; + } else if (pat2[idx2] !== pat1[idx1]) { + return false; + } + ++idx1; + ++idx2; + } + + if (idx1 === pat1.length && idx2 === pat2.length) return true; + return false; +} + +export function booleanCnf(exp: Expression): { vars: number; clauses: CNF } { + const ranges = new Map(); + const likePatterns = new Map(); + map(exp, e => { + if (!Array.isArray(e)) return e; + if (e[0] === "LIKE" || e[0] === "NOT LIKE") { + if ( + typeof e[2] === "string" && + (e.length >= 4 || (e[3] != null && !Array.isArray(e[3]))) + ) { + const key = JSON.stringify(e[1]); + let s = likePatterns.get(key); + if (!s) likePatterns.set(key, (s = new Map())); + s.set(`${e[3] || ""}:${e[2]}`, parseLikePattern(e[2], e[3])); + } + } else if ([">", ">=", "<", "<=", "=", "<>"].includes(e[0])) { + if (!Array.isArray(e[2])) { + const key = `${JSON.stringify(e[1])}:${typeof e[2]}`; + let s = ranges.get(key); + if (!s) ranges.set(key, (s = new Set())); + s.add(e[2]); + } else if (!Array.isArray(e[1])) { + const key = `${JSON.stringify(e[2])}:${typeof e[1]}`; + let s = ranges.get(key); + if (!s) ranges.set(key, (s = new Set())); + s.add(e[2]); + } + } + return e; + }); + + for (const [k, v] of ranges) { + if (k.endsWith(":number")) { + ranges.set( + k, + Array.from(v as number[]).sort((a: number, b: number) => a - b) + ); + } else { + ranges.set(k, Array.from(v).sort()); + } + } + + const expressions: Map = new Map(); + const mutuallyExclusive: Map> = new Map(); + + function getVariable(lhs, op, rhs = ""): number { + const key = `${lhs}:${op}:${rhs}`; + let f = expressions.get(key); + if (!f) { + expressions.set(key, (f = expressions.size + 1)); + let m = mutuallyExclusive.get(lhs); + if (!m) mutuallyExclusive.set(lhs, (m = new Set())); + m.add(f); + if (rhs || op !== "null") m.add(getVariable(lhs, "null")); + } + return f; + } + + function getComparisonVariables( + lhs, + from, + fromInclusive, + to, + toInclusive + ): number[] { + const type = from != null ? typeof from : typeof to; + const range = ranges.get(`${lhs}:${type}`); + const vars = []; + + if (from == null) vars.push(`${lhs}:--${range[0]}`); + + for (let i = 0; i < range.length; ++i) { + if ( + i > 0 && + (from == null || range[i] > from) && + (to == null || range[i] <= to) + ) + vars.push(`${lhs}:${range[i - 1]}--${range[i]}`); + + if ( + (range[i] > from && range[i] < to) || + (range[i] === from && fromInclusive) || + (range[i] === to && toInclusive) + ) + vars.push(`${lhs}:${range[i]}`); + } + + if (to == null) vars.push(`${lhs}:${range[range.length - 1]}--`); + + return vars.map(key => { + let f = expressions.get(key); + if (!f) { + expressions.set(key, (f = expressions.size + 1)); + let m = mutuallyExclusive.get(lhs); + if (!m) mutuallyExclusive.set(lhs, (m = new Set())); + m.add(f); + } + return f; + }); + } + + function getLikeVariables(lhs, pat, esc): number[] { + const vars = []; + const pats = likePatterns.get(lhs); + pat = pats.get(`${esc || ""}:${pat}`); + for (const p of pats.values()) { + if (pat === p || likePatternIncludes(pat, p)) + vars.push(`${lhs}:like:${JSON.stringify(p)}`); + } + + return vars.map(key => { + let f = expressions.get(key); + if (!f) expressions.set(key, (f = expressions.size + 1)); + return f; + }); + } + + interface ComparisonOperators { + [name: string]: (v: string) => [string, boolean, string, boolean]; + } + + const comparisonOperators: ComparisonOperators = { + "=": v => [v, true, v, true], + ">": v => [v, false, null, null], + ">=": v => [v, true, null, null], + "<": v => [null, null, v, false], + "<=": v => [null, null, v, true] + }; + + function recursive(clause, negate): CNF { + let op = clause[0]; + + if ((op === "AND" && !negate) || (op === "OR" && negate)) { + let res = []; + for (let i = 1; i < clause.length; ++i) + res = res.concat(recursive(clause[i], negate)); + return res; + } else if ((op === "OR" && !negate) || (op === "AND" && negate)) { + let res = []; + for (let i = 1; i < clause.length; ++i) + res = or(res, recursive(clause[i], negate)); + return res; + } else if (op === "NOT") { + return recursive(clause[1], !negate); + } else if (op === "<>") { + const vars = getComparisonVariables( + JSON.stringify(clause[1]), + ...comparisonOperators["="](clause[2]) + ); + if (!negate) return vars.map(x => [0 - x]); + else return [vars]; + } else if (op === "IS NULL") { + const v = getVariable(JSON.stringify(clause[1]), "null"); + if (negate) return [[0 - v]]; + else return [[v]]; + } else if (op === "IS NOT NULL") { + const v = getVariable(JSON.stringify(clause[1]), "null"); + if (!negate) return [[0 - v]]; + else return [[v]]; + } else if (op in comparisonOperators) { + if (!Array.isArray(clause[2])) { + const vars = getComparisonVariables( + JSON.stringify(clause[1]), + ...comparisonOperators[op](clause[2]) + ); + if (negate) return vars.map(x => [0 - x]); + else return [vars]; + } else if (!Array.isArray(clause[1])) { + op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op; + const vars = getComparisonVariables( + JSON.stringify(clause[2]), + ...comparisonOperators[op](clause[1]) + ); + if (negate) return vars.map(x => [0 - x]); + else return [vars]; + } else { + let lhs = JSON.stringify(clause[1]); + let rhs = JSON.stringify(clause[2]); + // For consistency + if (rhs > lhs) { + const t = rhs; + rhs = lhs; + lhs = t; + op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op; + } + + const v = getVariable(lhs, op, rhs); + if (negate) return [[0 - v]]; + else return [[v]]; + } + } else if ( + (op === "LIKE" || op === "NOT LIKE") && + !Array.isArray(clause[2]) && + clause[2] != null && + (clause.length >= 4 || clause[3] != null) + ) { + if (op === "NOT LIKE") negate = !negate; + const vars = getLikeVariables( + JSON.stringify(clause[1]), + clause[2], + clause[3] + ); + if (negate) return vars.map(x => [0 - x]); + else return [vars]; + } else { + const v = getVariable(JSON.stringify(clause), ""); + if (negate) return [[0 - v]]; + else return [[v]]; + } + } + + const cnf = recursive(exp, false); + + for (const m of mutuallyExclusive.values()) { + const ar: number[] = Array.from(m); + for (let i = 0; i < ar.length; ++i) + for (let j = i + 1; j < ar.length; ++j) cnf.push([0 - ar[i], 0 - ar[j]]); + } + + return { vars: expressions.size, clauses: cnf }; +} diff --git a/lib/common/expression-parser.ts b/lib/common/expression-parser.ts new file mode 100644 index 0000000..4da1afa --- /dev/null +++ b/lib/common/expression-parser.ts @@ -0,0 +1,378 @@ +import * as parsimmon from "parsimmon"; +import { Expression } from "../types"; + +// Turn escaped characters into real ones (e.g. "\\n" becomes "\n"). +function interpretEscapes(str): string { + const escapes = { + b: "\b", + f: "\f", + n: "\n", + r: "\r", + t: "\t" + }; + return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => { + const type = escape.charAt(0); + const hex = escape.slice(1); + if (type === "u") return String.fromCharCode(parseInt(hex, 16)); + + if (escapes.hasOwnProperty(type)) return escapes[type]; + + return type; + }); +} + +export function map(exp, callback): Expression { + if (!Array.isArray(exp)) return callback(exp); + + let clone; + for (let i = 1; i < exp.length; ++i) { + const sub = map(exp[i], callback); + if (sub !== exp[i]) { + clone = clone || exp.slice(); + clone[i] = sub; + } + } + + return callback(clone || exp); +} + +function binaryLeft(operatorsParser, nextParser): parsimmon.Parser<{}> { + return parsimmon.seqMap( + nextParser, + parsimmon.seq(operatorsParser, nextParser).many(), + (first, rest) => + rest.reduce((acc, ch) => { + const [op, another] = ch; + if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]); + if (Array.isArray(another) && op === another[0]) + return [op, acc].concat(another.slice(1)); + return [op, acc, another]; + }, first) + ); +} + +const lang = parsimmon.createLanguage({ + ComparisonOperator: function() { + return parsimmon + .alt( + parsimmon.string(">="), + parsimmon.string("<>"), + parsimmon.string("<="), + parsimmon.string("="), + parsimmon.string(">"), + parsimmon.string("<") + ) + .skip(parsimmon.optWhitespace); + }, + LikeOperator: function() { + return parsimmon + .alt( + parsimmon + .regexp(/like/i) + .result("LIKE") + .desc("LIKE"), + parsimmon + .regexp(/not\s+like/i) + .result("NOT LIKE") + .desc("NOT LIKE") + ) + .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) + .skip(parsimmon.optWhitespace); + }, + IsNullOperator: function() { + return parsimmon + .alt( + parsimmon + .regexp(/is\s+null/i) + .result("IS NULL") + .desc("IS NULL"), + parsimmon + .regexp(/is\s+not\s+null/i) + .result("IS NOT NULL") + .desc("IS NOT NULL") + ) + .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) + .skip(parsimmon.optWhitespace); + }, + NotOperator: function() { + return parsimmon + .regexp(/not/i) + .result("NOT") + .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) + .skip(parsimmon.optWhitespace) + .desc("NOT"); + }, + AndOperator: function() { + return parsimmon + .regexp(/and/i) + .result("AND") + .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) + .skip(parsimmon.optWhitespace) + .desc("AND"); + }, + OrOperator: function() { + return parsimmon + .regexp(/or/i) + .result("OR") + .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) + .skip(parsimmon.optWhitespace) + .desc("OR"); + }, + Parameter: function(r) { + return parsimmon + .alt( + parsimmon.regexp(/[a-zA-Z0-9_.*-]+/), + r.Expression.wrap( + parsimmon.string("{").skip(parsimmon.optWhitespace), + parsimmon.string("}") + ) + ) + .atLeast(1) + .map(x => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]]) + .skip(parsimmon.optWhitespace) + .desc("parameter"); + }, + StringValueSql: function() { + return parsimmon + .regexp(/'([^']*)'/, 1) + .atLeast(1) + .skip(parsimmon.optWhitespace) + .map(s => s.join("'")) + .desc("string"); + }, + StringValueJs: function() { + return parsimmon + .regexp(/"((?:\\.|.)*?)"/, 1) + .skip(parsimmon.optWhitespace) + .map(interpretEscapes) + .desc("string"); + }, + NumberValue: function() { + return parsimmon + .regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/) + .skip(parsimmon.optWhitespace) + .map(Number) + .desc("number"); + }, + BooleanValue: function() { + return parsimmon + .alt( + parsimmon + .regexp(/true/i) + .result(true) + .desc("TRUE"), + parsimmon + .regexp(/false/i) + .result(false) + .desc("FALSE") + ) + .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) + .skip(parsimmon.optWhitespace); + }, + NullValue: function() { + return parsimmon + .regexp(/null/i) + .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) + .skip(parsimmon.optWhitespace) + .result(null) + .desc("NULL"); + }, + FuncValue: function(r) { + return parsimmon.seqMap( + parsimmon + .regexp(/([a-zA-Z0-9_]+)/, 1) + .skip(parsimmon.optWhitespace) + .desc("function"), + r.ValueExpression.sepBy( + parsimmon.string(",").skip(parsimmon.optWhitespace) + ).wrap( + parsimmon.string("(").skip(parsimmon.optWhitespace), + parsimmon.string(")").skip(parsimmon.optWhitespace) + ), + (f, args) => ["FUNC", f.toUpperCase()].concat(args) + ); + }, + Value: function(r) { + return parsimmon.alt( + r.NullValue, + r.BooleanValue, + r.NumberValue, + r.StringValueSql, + r.StringValueJs, + r.FuncValue + ); + }, + ValueExpression: function(r) { + return binaryLeft( + parsimmon.string("||").skip(parsimmon.optWhitespace), + binaryLeft( + parsimmon + .alt(parsimmon.string("+"), parsimmon.string("-")) + .skip(parsimmon.optWhitespace), + binaryLeft( + parsimmon + .alt(parsimmon.string("*"), parsimmon.string("/")) + .skip(parsimmon.optWhitespace), + parsimmon.alt( + r.Value, + r.Parameter, + r.Expression.wrap( + parsimmon.string("(").skip(parsimmon.optWhitespace), + parsimmon.string(")").skip(parsimmon.optWhitespace) + ) + ) + ) + ) + ); + }, + Comparison: function(r) { + return parsimmon.alt( + parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]), + parsimmon.seqMap( + r.ValueExpression, + r.ComparisonOperator, + r.ValueExpression, + (p, o, v) => [o, p, v] + ), + parsimmon.seqMap( + r.ValueExpression, + r.LikeOperator, + r.ValueExpression.skip( + parsimmon + .regexp(/escape/i) + .result("ESCAPE") + .skip(parsimmon.whitespace) + .desc("ESCAPE") + ), + r.ValueExpression, + (a, b, c, d) => [b, a, c, d] + ), + parsimmon.seqMap( + r.ValueExpression, + r.LikeOperator, + r.ValueExpression, + (a, b, c) => [b, a, c] + ) + ); + }, + Expression: function(r) { + function unary(operatorsParser, nextParser): parsimmon.Parser<{}> { + return parsimmon.seq(operatorsParser, nextParser).or(nextParser); + } + + return binaryLeft( + r.OrOperator, + binaryLeft( + r.AndOperator, + unary(r.NotOperator, r.Comparison.or(r.ValueExpression)) + ) + ).trim(parsimmon.optWhitespace); + } +}); + +export function parse(str): Expression { + if (!str) return null; + return lang.Expression.tryParse(str); +} + +export function stringify(exp, level = 0): string { + if (!Array.isArray(exp)) return JSON.stringify(exp); + + const opLevels = { + OR: 10, + AND: 11, + NOT: 12, + "=": 20, + "<>": 20, + ">": 20, + ">=": 20, + "<": 20, + "<=": 20, + LIKE: 20, + "NOT LIKE": 20, + "IS NULL": 20, + "IS NOT NULL": 20, + "||": 30, + "+": 31, + "-": 31, + "*": 32, + "/": 32 + }; + + const op = exp[0].toUpperCase(); + + function wrap(e): string { + if (opLevels[op] <= level) return `(${e})`; + else return e; + } + + if (op === "FUNC") { + return wrap( + `${exp[1]}(${exp + .slice(2) + .map(e => stringify(e)) + .join(", ")})` + ); + } else if (op === "PARAM") { + if (typeof exp[1] === "string") { + return wrap(exp[1]); + } else if (Array.isArray(exp[1]) && exp[1][0] === "||") { + return wrap( + exp[1] + .slice(1) + .map(p => { + if (typeof p === "string") return p; + else return `{${stringify(p)}}`; + }) + .join("") + ); + } else { + return wrap(`{${stringify(exp[1])}}`); + } + } else if (op === "IS NULL" || op === "IS NOT NULL") { + return wrap(`${stringify(exp[1], opLevels[op])} ${op}`); + } else if (op === "LIKE" || op === "NOT LIKE") { + if (exp[3]) { + return wrap( + `${stringify(exp[1], opLevels[op])} ${op} ${stringify( + exp[2], + opLevels[op] + )} ESCAPE ${stringify(exp[3], opLevels[op])}` + ); + } else { + return wrap( + `${stringify(exp[1], opLevels[op])} ${op} ${stringify( + exp[2], + opLevels[op] + )}` + ); + } + } else if (op in opLevels) { + const parts = exp.slice(1).map((e, i) => { + return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0)); + }); + + if (op === "NOT") return wrap(`${op} ${parts[0]}`); + else return wrap(parts.join(` ${op} `)); + } else { + throw new Error(`Unrecognized operator ${exp[0]}`); + } +} + +export function parseLikePattern(pat, esc): string[] { + const chars = pat.split(""); + + for (let i = 0; i < chars.length; ++i) { + const c = chars[i]; + if (c === esc) { + chars[i] = chars[i + 1] || ""; + chars[i + 1] = ""; + } else if (c === "_") { + chars[i] = "\\_"; + } else if (c === "%") { + chars[i] = "\\%"; + while (chars[i + 1] === "%") chars[++i] = ""; + } + } + return chars.filter(c => c); +} diff --git a/lib/common/expression.ts b/lib/common/expression.ts new file mode 100644 index 0000000..d2611e4 --- /dev/null +++ b/lib/common/expression.ts @@ -0,0 +1,250 @@ +import { map, parseLikePattern } from "./expression-parser"; +import { booleanCnf } from "./expression-cnf"; +import { naiveDpll } from "./sat-solver"; +import { Expression } from "../types"; + +const isArray = Array.isArray; + +const regExpCache = new WeakMap(); + +const REDUCE_SKIP = {}; +function reduce(exp, callback): Expression { + let loop = true; + while (loop) { + loop = false; + for (let i = 2; i < exp.length; ++i) { + const res = callback(exp[i - 1], exp[i], i - 2); + if (res !== REDUCE_SKIP) { + loop = true; + exp = exp.slice(); + exp.splice(i - 1, 2, res); + } + } + } + if (exp.length === 2) return exp[1]; + return exp; +} + +export function likePatternToRegExp(pat, esc = "", flags = ""): RegExp { + const convChars = { + "-": "\\-", + "/": "\\/", + "\\": "\\/", + "^": "\\^", + $: "\\$", + "*": "\\*", + "+": "\\+", + "?": "\\?", + ".": "\\.", + "(": "\\(", + ")": "\\)", + "|": "\\|", + "[": "\\[", + "]": "\\]", + "{": "\\{", + "}": "\\}", + "\\%": ".*", + "\\_": "." + }; + let chars = parseLikePattern(pat, esc); + if (!chars.length) return new RegExp("^$", flags); + chars = chars.map(c => convChars[c] || c); + chars[0] = chars[0] === ".*" ? "" : "^" + chars[0]; + const l = chars.length - 1; + chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$"; + return new RegExp(chars.join(""), flags); +} + +export function evaluate( + exp, + obj, + now: number, + cb?: Function +): string | number | boolean | null; +export function evaluate(exp, obj?, now?: number, cb?: Function): Expression; +export function evaluate(exp, obj?, now?: number, cb?: Function): Expression { + function getRegExp(pat, esc): RegExp { + const k = `${esc || ""}:${pat}`; + let c = regExpCache.get(exp); + if (!c) regExpCache.set(exp, (c = {})); + if (!c[k]) c[k] = likePatternToRegExp(pat, esc); + return c[k]; + } + + return map(exp, e => { + if (cb) e = cb(e); + if (!isArray(e)) return e; + + if (e[0] === "FUNC") { + if (e[1] === "NOW") { + if (now) return now; + } else if (e[1] === "UPPER") { + if (e[2] == null) return null; + if (!isArray(e[2])) return `${e[2]}`.toUpperCase(); + } else if (e[1] === "LOWER") { + if (e[2] == null) return null; + if (!isArray(e[2])) return `${e[2]}`.toLowerCase(); + } + } else if (e[0] === "PARAM") { + if (e[1] == null) return null; + if (obj && !isArray(e[1])) { + let v = obj[e[1]]; + if (v == null) return null; + if (typeof v === "object") v = v.value ? v.value[0] : null; + return v; + } + } else if (e[0] === "AND") { + return reduce(e, (a, b) => { + if (!isArray(a)) return a ? b : a; + if (!isArray(b)) return b ? a : b; + return REDUCE_SKIP; + }); + } else if (e[0] === "OR") { + return reduce(e, (a, b) => { + if (!isArray(a)) return a ? a : b; + if (!isArray(b)) return b ? b : a; + return REDUCE_SKIP; + }); + } else if (e[0] === "NOT") { + if (!isArray(e[1])) return !e[1]; + else if (e[1][0] === "NOT") return e[1][1]; + } else if (e[0] === "IS NULL") { + if (isArray(e[1])) return e; + else if (e[1] == null) return true; + else return null; + } else if (e[0] === "IS NOT NULL") { + if (isArray(e[1])) return e; + else if (e[1] != null) return true; + else return null; + } else if (e[0] === "LIKE") { + if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e; + else if (e[1] == null || e[2] == null || (e.length >= 4 && e[3] == null)) + return null; + + const r = getRegExp(e[2], e[3]); + return r.test(e[1]); + } else if (e[0] === "NOT LIKE") { + if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e; + else if (e[1] == null || e[2] == null || (e.length >= 4 && e[3] == null)) + return null; + const r = getRegExp(e[2], e[3]); + return !r.test(e[1]); + } else if (e[0] === "=") { + if (isArray(e[1]) || isArray(e[2])) return e; + if (e[1] == null || e[2] == null) return null; + return e[1] === e[2]; + } else if (e[0] === "<>") { + if (isArray(e[1]) || isArray(e[2])) return e; + if (e[1] == null || e[2] == null) return null; + return e[1] !== e[2]; + } else if (e[0] === ">") { + if (isArray(e[1]) || isArray(e[2])) return e; + if (e[1] == null || e[2] == null) return null; + return e[1] > e[2]; + } else if (e[0] === ">=") { + if (isArray(e[1]) || isArray(e[2])) return e; + if (e[1] == null || e[2] == null) return null; + return e[1] >= e[2]; + } else if (e[0] === "<") { + if (isArray(e[1]) || isArray(e[2])) return e; + if (e[1] == null || e[2] == null) return null; + return e[1] < e[2]; + } else if (e[0] === "<=") { + if (isArray(e[1]) || isArray(e[2])) return e; + if (e[1] == null || e[2] == null) return null; + return e[1] <= e[2]; + } else if (e[0] === "*") { + return reduce(e, (a, b) => { + if (!isArray(a) && !isArray(b)) { + if (a == null || b == null) return null; + return a * b; + } + return REDUCE_SKIP; + }); + } else if (e[0] === "/") { + return reduce(e, (a, b, i) => { + if (!isArray(a) && !isArray(b)) { + if (a == null || b == null) return null; + return i === 0 ? a / b : a * b; + } + return REDUCE_SKIP; + }); + } else if (e[0] === "+") { + return reduce(e, (a, b) => { + if (!isArray(a) && !isArray(b)) { + if (a == null || b == null) return null; + return a + b; + } + return REDUCE_SKIP; + }); + } else if (e[0] === "-") { + return reduce(e, (a, b, i) => { + if (!isArray(a) && !isArray(b)) { + if (a == null || b == null) return null; + return i === 0 ? a - b : a + b; + } + return REDUCE_SKIP; + }); + } else if (e[0] === "||") { + return reduce(e, (a, b) => { + if (!isArray(a) && !isArray(b)) { + if (a == null || b == null) return null; + return `${a}${b}`; + } + return REDUCE_SKIP; + }); + } + return e; + }); +} + +export function and(exp1, exp2): Expression { + if (!isArray(exp1)) return exp1 ? exp2 : exp1; + if (!isArray(exp2)) return exp2 ? exp1 : exp2; + + let res: Expression = ["AND"]; + + if (exp1[0] === "AND") res = res.concat(exp1.slice(1)); + else res.push(exp1); + + if (exp2[0] === "AND") res = res.concat(exp2.slice(1)); + else res.push(exp2); + + return res; +} + +export function or(exp1, exp2): Expression { + if (!isArray(exp1)) return exp1 ? exp1 : exp2; + if (!isArray(exp2)) return exp2 ? exp2 : exp1; + + let res: Expression = ["OR"]; + + if (exp1[0] === "OR") res = res.concat(exp1.slice(1)); + else res.push(exp1); + + if (exp2[0] === "OR") res = res.concat(exp2.slice(1)); + else res.push(exp2); + + return res; +} + +export function not(exp): Expression { + if (isArray(exp) && exp[0] === "NOT") return exp[1]; + return ["NOT", exp]; +} + +export function subset(exp1, exp2): boolean { + const e = evaluate(["NOT", ["OR", ["NOT", exp1], exp2]]); + if (!isArray(e)) return !e; + const { vars, clauses } = booleanCnf(e); + return !naiveDpll(clauses, vars); +} + +export function extractParams(exp): string[] { + const params = new Set(); + map(exp, e => { + if (isArray(e) && e[0] === "PARAM") params.add(e[1]); + return e; + }); + return Array.from(params); +} diff --git a/lib/common/memoize.ts b/lib/common/memoize.ts new file mode 100644 index 0000000..f7d984e --- /dev/null +++ b/lib/common/memoize.ts @@ -0,0 +1,43 @@ +let cache1 = new Map(); +let cache2 = new Map(); +const keys = new WeakMap(); + +function getKey(obj): string { + if (obj === null) return "null"; + else if (obj === undefined) return "undefined"; + + const t = typeof obj; + if (t === "number" || t === "boolean" || t === "string") return `${t}:${obj}`; + if (t !== "function" && t !== "object") + throw new Error(`Cannot memoize ${t} arguments`); + + let k = keys.get(obj); + if (!k) { + const rnd = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER); + k = `${t}:${rnd.toString(36)}`; + keys.set(obj, k); + } + return k; +} + +export default function memoize(func): Function { + return (...args) => { + const key = JSON.stringify(args.map(getKey)) + getKey(func); + + if (cache1.has(key)) return cache1.get(key); + + let r; + if (cache2.has(key)) r = cache2.get(key); + else r = func(...args); + cache1.set(key, r); + return r; + }; +} + +const interval = setInterval(() => { + cache2 = cache1; + cache1 = new Map(); +}, 120000); + +// Don't hold Node.js process +if (interval.unref) interval.unref(); diff --git a/lib/path-set.js b/lib/common/path-set.ts similarity index 70% rename from lib/path-set.js rename to lib/common/path-set.ts index aa773be..4edad4c 100644 --- a/lib/path-set.js +++ b/lib/common/path-set.ts @@ -16,19 +16,24 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -class PathSet { - constructor() { +import Path from "./path"; + +export default class PathSet { + private lengthIndex: Set[]; + private fragmentIndex: Map>[]; + + public constructor() { this.lengthIndex = []; this.fragmentIndex = []; } - get depth() { + public get depth(): number { return this.lengthIndex.length; } - add(path) { + public add(path: Path): Path { + if (path.alias) throw new Error("PathSet does not support aliased paths"); const p = this.get(path); if (p) return p; @@ -41,43 +46,36 @@ class PathSet { const lengthIndex = this.lengthIndex[path.length]; lengthIndex.add(path); - let wildcard = 0; for (let i = 0; i < path.length; ++i) { - const fragment = path[i]; - if (fragment === "*") wildcard = wildcard | (1 << i); - + const fragment = path.segments[i] as string; const fragmentIndex = this.fragmentIndex[i]; let fragmentIndexSet = fragmentIndex.get(fragment); if (!fragmentIndexSet) { - fragmentIndexSet = new Set(); + fragmentIndexSet = new Set(); fragmentIndex.set(fragment, fragmentIndexSet); } fragmentIndexSet.add(path); } - if (path.wildcard == null) { - path.alias = 0; - path.wildcard = wildcard; - Object.freeze(path); - } return path; } - get(pattern) { - const lengthIndex = this.lengthIndex[pattern.length]; + public get(path: Path): Path { + if (path.alias) throw new Error("PathSet does not support aliased paths"); + const lengthIndex = this.lengthIndex[path.length]; if (!lengthIndex || !lengthIndex.size) return null; - if (lengthIndex.has(pattern)) return pattern; + if (lengthIndex.has(path)) return path; const sets = [lengthIndex]; - for (let i = 0; i < pattern.length; ++i) { + for (let i = 0; i < path.length; ++i) { const fragmentIndex = this.fragmentIndex[i]; if (!fragmentIndex || !fragmentIndex.size) return null; - const fragment = pattern[i]; + const fragment = path.segments[i] as string; const fragmentIndexSet = fragmentIndex.get(fragment); if (!fragmentIndexSet) return null; @@ -88,34 +86,40 @@ class PathSet { sets.sort((a, b) => a.size - b.size); const smallestSet = sets.shift(); - for (let path of smallestSet) { + for (let p of smallestSet) { for (const s of sets) { - if (!s.has(path)) { - path = null; + if (!s.has(p)) { + p = null; break; } } - if (path) return path; + if (p) return p; } return null; } - find(pattern, superset = false, subset = false, depth = pattern.length) { + public find( + path: Path, + superset: boolean = false, + subset: boolean = false, + depth: number = path.length + ): Path[] { + if (path.alias) throw new Error("PathSet does not support aliased paths"); const res = []; const groups = [[]]; - for (let i = pattern.length; i <= depth; ++i) + for (let i = path.length; i <= depth; ++i) if (this.lengthIndex[i]) groups[0].push(this.lengthIndex[i]); if (!groups[0].length) return res; - for (let i = 0; i < pattern.length; ++i) { + for (let i = 0; i < path.length; ++i) { const fragmentIndex = this.fragmentIndex[i]; if (!fragmentIndex) return res; - const fragment = pattern[i]; + const fragment = path.segments[i] as string; if (fragment === "*" && subset) continue; @@ -140,26 +144,24 @@ class PathSet { const smallestGroup = groups.shift(); for (const set of smallestGroup) { - for (let path of set) { + for (let p of set) { for (const group of groups) { let found = false; for (const s of group) { - if (s.has(path)) { + if (s.has(p)) { found = true; break; } } if (!found) { - path = null; + p = null; break; } } - if (path) res.push(path); + if (p) res.push(p); } } return res; } } - -module.exports = PathSet; diff --git a/lib/common/path.ts b/lib/common/path.ts new file mode 100644 index 0000000..60fd16f --- /dev/null +++ b/lib/common/path.ts @@ -0,0 +1,189 @@ +/** + * Copyright 2013-2019 Zaid Abdulla + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ + +type Segments = (string | Alias)[]; +type Alias = [Path, string][]; + +let cache1 = new Map(); +let cache2 = new Map(); + +export default class Path { + public readonly segments: Segments; + public readonly wildcard: number; + public readonly alias: number; + protected _string: string; + + protected static parseAlias( + pattern: string, + index: number + ): { index: number; alias: Alias } { + const alias: Alias = []; + while (index < pattern.length && pattern[index] !== "]") { + const res = Path.parsePath(pattern, index); + let j = (index = res.index + 1); + while (pattern[j] !== "]" && pattern[j] !== ",") { + if (pattern[j] === '"' && index === j) { + ++j; + while (pattern[j] !== '"' || pattern[j - 1] === "\\") { + if (++j >= pattern.length) + throw new Error("Invalid alias expression"); + } + } + if (++j >= pattern.length) throw new Error("Invalid alias expression"); + } + + let value = pattern.slice(index, j).trim(); + index = j; + if (value[0] === '"') { + try { + value = JSON.parse(value); + } catch (error) { + throw new Error("Invalid alias expression"); + } + } + + alias.push([new Path(res.segments, res.wildcard, res.alias), value]); + if (pattern[index] === ",") ++index; + } + + // Ensure identical expressions have idential string representation + alias.sort((a, b) => { + if (a[0].toString() > b[0].toString()) return 1; + else if (a[0].toString() < b[0].toString()) return -1; + else if (a[1] > b[1]) return 1; + else if (a[1] < b[1]) return -1; + else return 0; + }); + + Object.freeze(alias); + return { index, alias }; + } + + protected static parsePath( + pattern: string, + index: number + ): { index: number; segments: Segments; wildcard: number; alias: number } { + const segments = []; + let wildcard = 0; + let alias = 0; + // Colon separator is needed for parseAlias + if (index < pattern.length && pattern[index] !== ":") { + for (;;) { + if (pattern[index] === "[") { + const res = Path.parseAlias(pattern, index + 1); + index = res.index + 1; + alias |= 1 << segments.length; + segments.push(res.alias); + } else { + const j = index; + while ( + index < pattern.length && + pattern[index] !== ":" && + pattern[index] !== "." + ) + ++index; + const s = pattern.slice(j, index).trim(); + if (s === "*") wildcard |= 1 << segments.length; + segments.push(s); + } + + if (index >= pattern.length || pattern[index] === ":") break; + else if (pattern[index] !== ".") + throw new Error("Invalid alias expression"); + ++index; + } + } + + Object.freeze(segments); + return { index, segments, wildcard, alias }; + } + + protected constructor(segments: Segments, wildcard: number, alias: number) { + this.segments = segments; + this.wildcard = wildcard; + this.alias = alias; + this._string = null; + } + + public static parse(str: string): Path { + let path = cache1.get(str); + if (!path) { + path = cache2.get(str); + if (!path) { + const res = Path.parsePath(str, 0); + path = new Path(res.segments, res.wildcard, res.alias); + } + cache1.set(str, path); + } + return path; + } + + public get length(): number { + return this.segments.length; + } + + public toString(): string { + if (this._string == null) { + this._string = this.segments + .map(s => { + if (Array.isArray(s)) { + const parts = s.map( + al => `${al[0].toString()}:${JSON.stringify(al[1])}` + ); + return `[${parts.join(",")}]`; + } + return s; + }) + .join("."); + } + return this._string; + } + + public slice(start: number = 0, end: number = this.segments.length): Path { + if (start < 0) start = Math.max(0, this.segments.length + start); + if (end < 0) end = Math.max(0, this.segments.length + end); + if (start >= end) return new Path([], 0, 0); + const segments = this.segments.slice(start, end); + const mask = (1 << (end - start)) - 1; + const wildcard = (this.wildcard >> start) & mask; + const alias = (this.alias >> start) & mask; + return new Path(segments, wildcard, alias); + } + + public concat(path: Path): Path { + const segments = this.segments.concat(path.segments); + const wildcard = this.wildcard | (path.wildcard << this.segments.length); + const alias = this.alias | (path.alias << this.segments.length); + return new Path(segments, wildcard, alias); + } + + public stripAlias(): Path { + if (!this.alias) return this; + const segments = this.segments.map(s => (Array.isArray(s) ? "*" : s)); + return new Path(segments, this.wildcard | this.alias, 0); + } +} + +const interval = setInterval(() => { + cache2 = cache1; + cache1 = new Map(); +}, 120000); + +// Don't hold Node.js process +if (interval.unref) interval.unref(); diff --git a/lib/common/sat-solver.ts b/lib/common/sat-solver.ts new file mode 100644 index 0000000..0e94062 --- /dev/null +++ b/lib/common/sat-solver.ts @@ -0,0 +1,175 @@ +/** + * Naive DPLL solver for propositional logic adapted from + * https://github.com/tammet/logictools + * + * Origianl code by Tanel Tammet released under the MIT license. + * + * Check whether a clause set is satisfiable or contradictory, using a naive + * implementation of the dpll (Davis-Putnam-Logemann-Loveland) procedure + * combining truth table search with the resolution-like unit propagation. + * + * The standard pure-literal rule is not implemented in this naive version, + * since it is not really crucial for the algorithm. + * + * The code is very similar to the pure table solver, except we use a + * unit-propagating, derived-varlist-returing satisfiableAt. + */ + +/* eslint-disable @typescript-eslint/no-use-before-define */ + +function satisfiableAt(clauses, varVals, varnr, val): boolean { + if (varnr !== 0) varVals[varnr] = val; + + const propRes = unitPropagate(clauses, varVals); + if (propRes === 1) { + varVals[varnr] = 0; + return true; + } + + // if result false (-1) unitPropagate restores the varVals state itself + if (propRes === -1) { + varVals[varnr] = 0; + return false; + } + + // find next unassigned var to split + let nextVar = 0; + for (let i = 1; i < varVals.length; i++) { + if (varVals[i] === 0) { + nextVar = i; + break; + } + } + + if (nextVar === 0) throw new Error("Error in satisfiableAt"); + + if ( + satisfiableAt(clauses, varVals, nextVar, 1) || + satisfiableAt(clauses, varVals, nextVar, -1) + ) + // not necessary to restore split var: search is halted anyway + return true; + + // restore split var + varVals[nextVar] = 0; + + // restore derived vars + for (let i = 0; i < propRes.length; i++) varVals[propRes[i]] = 0; + + return false; +} + +/** + * Check whether clauses are satisfiable under the varVals assignment, without + * search. + * + * Iterate through all clauses and check whether + * + * - all literals in the clause are assigned false: undo derived units (set + * values in varVals to 0) and return false immediately + * + * - a clause contains a literal assigned true: skip further actions on this + * clause + * + * - a single literal in the clause is unassigned, all the rest are assigned + * false: derive this literal as a unit clause. + * + * In case at least one literal is derived during the process above, iterate + * the whole process until no more new literals are derived. + * + * Returns: + * + * 1: all clauses were assigned true, hence the clause set is satisfiable. + * The whole search will terminate after that. + * + * -1: clause set is contradictory. Derived units have been restored. + * + * a list of derived variables: the status of the clause set is + * undetermined. The values of variables on the list will be restored (set to + * 0) during backtracking. + */ +function unitPropagate(clauses, varVals): number[] | 1 | -1 { + // all vars with a value derived in the forthcoming main loop + const derivedVars = []; + + // the main loop is run, deriving new unit clauses, until either: + // - the clause set is detected to be unsatisfiable + // - the clause set is detected to be satisfiable + // - there are no more derived unit clauses + + for (;;) { + let allClausesFound = true; // set to false if we find an undetermined clause + const derivedQueue = []; // derived units (literals) during one iteration + for (let i = 0; i < clauses.length; i++) { + const clause = clauses[i]; + let clauseVal = 0; + let unassignedCount = 0; // count unassigned vars in clause + let unassignedLit = 0; // 0 means none found for this clause + + for (let j = 0; j < clause.length; j++) { + const lit = clause[j]; + let nr, polarity; + if (lit < 0) { + nr = 0 - lit; + polarity = -1; + } else { + nr = lit; + polarity = 1; + } + + if (varVals[nr] === polarity) { + clauseVal = 1; + break; + } else if (varVals[nr] === 0) { + unassignedCount++; + unassignedLit = lit; + } + } + + if (clauseVal === 1) continue; // clause is subsumed + + // clause is not subsumed by varVals + if (unassignedCount === 0) { + for (let j = 0; j < derivedVars.length; j++) + varVals[derivedVars[j]] = 0; // restore derived + return -1; + } else if (unassignedCount === 1) { + // unassignedLit is a derived literal + derivedQueue.push(unassignedLit); + } + + if (unassignedCount !== 0) allClausesFound = false; + } + + // if all clauses were subsumed or all the variables in a clause had a value + // no need to restore varVals: whole search finished + if (allClausesFound) return 1; + + // in case there are any derived units present, store to varVals and + // iterate the main loop + if (derivedQueue.length === 0) break; + + // some units were derived: iterate the main loop + for (let j = 0; j < derivedQueue.length; j++) { + const lit = derivedQueue[j]; + let nr, polarity; + if (lit < 0) { + nr = 0 - lit; + polarity = -1; + } else { + nr = lit; + polarity = 1; + } + varVals[nr] = polarity; + derivedVars.push(nr); + } + } + + return derivedVars; +} + +export function naiveDpll(clauses, maxvarnr): boolean { + // variable values are 0 if not set, 1 if positive, -1 if negative + const varVals = new Int32Array(maxvarnr + 1); + return satisfiableAt(clauses, varVals, 0, 0); +} diff --git a/lib/common/validators/pass.ts b/lib/common/validators/pass.ts new file mode 100644 index 0000000..f532102 --- /dev/null +++ b/lib/common/validators/pass.ts @@ -0,0 +1,3 @@ +export default function pass(): number { + return 1; +} diff --git a/lib/common/validators/test.ts b/lib/common/validators/test.ts new file mode 100644 index 0000000..0732976 --- /dev/null +++ b/lib/common/validators/test.ts @@ -0,0 +1,12 @@ +export default function test(resourceType, resource): number { + if (resourceType === "devices") { + if (resource["Tags.test"] && resource["Tags.test"].value[0]) return 1; + } else if (resourceType === "presets") { + if (resource["_id"].startsWith("test-")) + // TODO check "test" tag too + return 1; + } else if (resourceType === "provisions") { + if (resource["_id"].startsWith("test-")) return 1; + } + return -1; +} diff --git a/lib/config.js b/lib/config.ts similarity index 87% rename from lib/config.js rename to lib/config.ts index dcc47eb..8bdb2a5 100644 --- a/lib/config.js +++ b/lib/config.ts @@ -16,9 +16,9 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const path = require("path"); +import { resolve } from "path"; +import { readFileSync } from "fs"; const options = { CONFIG_DIR: { type: "path", default: "config" }, @@ -26,9 +26,6 @@ const options = { type: "string", default: "mongodb://127.0.0.1/genieacs" }, - REDIS_PORT: { type: "int", default: 6379 }, - REDIS_HOST: { type: "string", default: "" }, - REDIS_DB: { type: "int", default: 0 }, CWMP_WORKER_PROCESSES: { type: "int", default: 0 }, CWMP_PORT: { type: "int", default: 7547 }, @@ -52,6 +49,14 @@ const options = { FS_LOG_FILE: { type: "path", default: "" }, FS_ACCESS_LOG_FILE: { type: "path", default: "" }, + UI_WORKER_PROCESSES: { type: "int", default: 0 }, + UI_PORT: { type: "int", default: 3000 }, + UI_INTERFACE: { type: "string", default: "0.0.0.0" }, + UI_SSL: { type: "bool", default: false }, + UI_LOG_FILE: { type: "path", default: "" }, + UI_ACCESS_LOG_FILE: { type: "path", default: "" }, + UI_JWT_SECRET: { type: "string", default: "" }, + UDP_CONNECTION_REQUEST_PORT: { type: "int", default: 0 }, DOWNLOAD_TIMEOUT: { type: "int", default: 3600 }, @@ -84,9 +89,9 @@ const options = { DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 } }; -const allConfig = {}; +const allConfig: { [name: string]: string | number } = {}; -function setConfig(name, value, commandLineArgument) { +function setConfig(name, value, commandLineArgument = false): boolean { if (allConfig[name] != null) return true; // For compatibility with v1.0 @@ -113,7 +118,7 @@ function setConfig(name, value, commandLineArgument) { if (name === "FS_IP" || name === "fs-ip") setConfig("FS_HOSTNAME", value); - function cast(val, type) { + function cast(val, type): string | number | boolean { switch (type) { case "int": return Number(val); @@ -127,7 +132,7 @@ function setConfig(name, value, commandLineArgument) { return String(val); case "path": if (!val) return ""; - return path.resolve(val); + return resolve(val); default: return null; } @@ -175,7 +180,11 @@ for (const [k, v] of Object.entries(process.env)) setConfig("CONFIG_DIR", options["CONFIG_DIR"]["default"]); // Configuration file -const configFile = require(path.resolve(allConfig.CONFIG_DIR, "config")); +const configFile = JSON.parse( + readFileSync( + resolve(allConfig.CONFIG_DIR as string, "config.json") + ).toString() +); for (const [k, v] of Object.entries(configFile)) { if (!setConfig(k, v)) // Pass as environment variable to be accessable by extensions @@ -186,7 +195,10 @@ for (const [k, v] of Object.entries(configFile)) { for (const [k, v] of Object.entries(options)) if (v["default"] != null) setConfig(k, v["default"]); -function get(optionName, deviceId) { +export function get( + optionName: string, + deviceId?: string +): string | number | boolean { if (!deviceId) return allConfig[optionName]; optionName = `${optionName}-${deviceId}`; @@ -214,22 +226,12 @@ function get(optionName, deviceId) { return null; } -function getDefault(optionName) { +export function getDefault(optionName): string | number | boolean { const option = options[optionName]; if (!option) return null; let val = option["default"]; - if (val && option.type === "path") val = path.resolve(val); + if (val && option.type === "path") val = resolve(val); return val; } - -// Load authentication scripts -try { - exports.auth = require(path.resolve(allConfig.CONFIG_DIR, "auth")); -} catch (error) { - // No auth.js exists -} - -exports.get = get; -exports.getDefault = getDefault; diff --git a/lib/connection-request.ts b/lib/connection-request.ts new file mode 100644 index 0000000..00874d7 --- /dev/null +++ b/lib/connection-request.ts @@ -0,0 +1,198 @@ +/** + * Copyright 2013-2019 Zaid Abdulla + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ + +import * as crypto from "crypto"; +import * as dgram from "dgram"; +import { parse } from "url"; +import * as http from "http"; +import { evaluate } from "./common/expression"; +import { Expression } from "./types"; +import * as auth from "./auth"; + +function extractAuth( + exp: Expression, + context: {}, + now: number, + dflt: any +): [string, string, Expression] { + let username, password; + exp = evaluate(exp, context, now, e => { + if (!username && Array.isArray(e) && e[0] === "FUNC" && e[1] === "AUTH") { + if (!Array.isArray(e[2]) && !Array.isArray(e[3])) { + username = e[2]; + password = e[3]; + } + return dflt; + } + return e; + }); + + return [username, password, exp]; +} + +function httpGet( + options: {}, + timeout +): Promise<{ statusCode: number; headers: {} }> { + return new Promise((resolve, reject) => { + const req = http + .get(options, res => { + res.resume(); + resolve({ statusCode: res.statusCode, headers: res.headers }); + }) + .on("error", () => { + req.abort(); + reject(new Error("Device is offline")); + }) + .on("socket", socket => { + socket.setTimeout(timeout); + socket.on("timeout", () => { + req.abort(); + }); + }); + }); +} + +export async function httpConnectionRequest( + address: string, + authExp: Expression, + context: {}, + allowBasicAuth: boolean, + timeout: number +): Promise { + const now = Date.now(); + const options: http.RequestOptions = parse(address); + if (options.protocol !== "http:") + throw new Error("Invalid connection request URL or protocol"); + + options.agent = new http.Agent({ + maxSockets: 1, + keepAlive: true + }); + + let authHeader: {}; + let username: string; + let password: string; + + while (!authHeader || (username != null && password != null)) { + let opts = options; + if (authHeader) { + if (authHeader["method"] === "Basic") { + if (!allowBasicAuth) + throw new Error("Basic HTTP authentication not allowed"); + + opts = Object.assign( + { + headers: { + Authorization: auth.basic(username || "", password || "") + } + }, + options + ); + } else if (authHeader["method"] === "Digest") { + opts = Object.assign( + { + headers: { + Authorization: auth.digest( + username, + password, + options.path, + "GET", + null, + authHeader + ) + } + }, + options + ); + } else { + throw new Error("Unrecognized auth method"); + } + } + + let res = await httpGet(opts, timeout); + + // Workaround for some devices unexpectedly closing the connection + if (res.statusCode === 0 && authHeader) res = await httpGet(opts, timeout); + if (res.statusCode === 0) throw new Error("Device is offline"); + if (res.statusCode === 200 || res.statusCode === 204) return; + + if (res.statusCode === 401 && res.headers["www-authenticate"]) { + authHeader = auth.parseAuthHeader(res.headers["www-authenticate"]); + [username, password, authExp] = extractAuth(authExp, context, now, false); + } else { + throw new Error( + `Unexpected response code from device: ${res.statusCode}` + ); + } + } + throw new Error("Incorrect connection request credentials"); +} + +export async function udpConnectionRequest( + address: string, + authExp: Expression, + context: {}, + sourcePort: number = 0 +): Promise { + const [host, portStr] = address.split(":", 2); + const port = portStr ? parseInt(portStr) : 80; + const now = Date.now(); + + const client = dgram.createSocket({ type: "udp4", reuseAddr: true }); + // When a device is NAT'ed, the UDP Connection Request must originate from + // the same address and port used by the STUN server, in order to traverse + // the firewall. This does require that the Genieacs NBI and STUN server + // are allowed to bind to the same address and port. The STUN server needs + // to open its UDP port with the SO_REUSEADDR option, allowing the NBI to + // also bind to the same port. + if (sourcePort) client.bind({ port: sourcePort, exclusive: true }); + + let username: string; + let password: string; + + [username, password, authExp] = extractAuth(authExp, context, now, null); + if (username == null) username = ""; + if (password == null) password = ""; + while (username != null && password != null) { + const ts = Math.trunc(now / 1000); + const id = Math.trunc(Math.random() * 4294967295); + const cn = crypto.randomBytes(8).toString("hex"); + const sig = crypto + .createHmac("sha1", password) + .update(`${ts}${id}${username}${cn}`) + .digest("hex"); + const uri = `http://${address}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`; + const message = Buffer.from( + `GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n` + ); + + for (let i = 0; i < 3; ++i) { + await new Promise((resolve, reject) => { + client.send(message, 0, message.length, port, host, (err: Error) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + [username, password, authExp] = extractAuth(authExp, context, now, null); + } + client.close(); +} diff --git a/lib/cwmp.js b/lib/cwmp.ts similarity index 82% rename from lib/cwmp.js rename to lib/cwmp.ts index 3842735..e8cd9a5 100644 --- a/lib/cwmp.js +++ b/lib/cwmp.ts @@ -16,25 +16,36 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const zlib = require("zlib"); -const crypto = require("crypto"); -const fs = require("fs"); -const config = require("./config"); -const common = require("./common"); -const soap = require("./soap"); -const session = require("./session"); -const query = require("./query"); -const device = require("./device"); -const cache = require("./cache"); -const localCache = require("./local-cache"); -const db = require("./db"); -const logger = require("./logger"); -const scheduling = require("./scheduling"); +import * as zlib from "zlib"; +import * as crypto from "crypto"; +import * as fs from "fs"; +import { Socket } from "net"; +import * as config from "./config"; +import * as common from "./common"; +import * as soap from "./soap"; +import * as session from "./session"; +import { evaluate, extractParams } from "./common/expression"; +import * as device from "./device"; +import * as cache from "./cache"; +import * as localCache from "./local-cache"; +import * as db from "./db"; +import * as logger from "./logger"; +import * as scheduling from "./scheduling"; +import Path from "./common/path"; +import { + SessionContext, + AcsRequest, + SetAcsRequest, + SessionFault, + Operation, + Fault +} from "./types"; const MAX_CYCLES = 4; -const MAX_CONCURRENT_REQUESTS = config.get("MAX_CONCURRENT_REQUESTS"); +const MAX_CONCURRENT_REQUESTS = +config.get("MAX_CONCURRENT_REQUESTS"); + +const currentSessions = new WeakMap(); const stats = { concurrentRequests: 0, @@ -43,7 +54,7 @@ const stats = { initiatedSessions: 0 }; -function throwError(err, httpResponse) { +function throwError(err, httpResponse?): never { if (httpResponse) { currentSessions.delete(httpResponse.connection); httpResponse.writeHead(500, { Connection: "close" }); @@ -53,11 +64,15 @@ function throwError(err, httpResponse) { throw err; } -function writeResponse(sessionContext, res, close) { +function writeResponse( + sessionContext: SessionContext, + res, + close = false +): void { // Close connection after last request in session if (close) res.headers["Connection"] = "close"; - if (config.get("DEBUG", sessionContext.deviceId)) { + if (sessionContext.debug) { const dump = `# RESPONSE ${new Date(Date.now())}\n` + JSON.stringify(res.headers) + @@ -69,7 +84,7 @@ function writeResponse(sessionContext, res, close) { }); } - function finish() { + function finish(): void { stats.concurrentRequests -= 1; if (sessionContext.httpRequest.connection.destroyed) { logger.accessError({ @@ -130,7 +145,19 @@ function writeResponse(sessionContext, res, close) { } } -function recordFault(sessionContext, fault, provisions, channels) { +function recordFault( + sessionContext: SessionContext, + fault: Fault, + provisions, + channels +): void; +function recordFault(sessionContext: SessionContext, fault: Fault): void; +function recordFault( + sessionContext: SessionContext, + fault: Fault, + provisions?, + channels? +): void { if (!provisions) { provisions = sessionContext.provisions; channels = sessionContext.channels; @@ -141,7 +168,10 @@ function recordFault(sessionContext, fault, provisions, channels) { const provs = sessionContext.faults[channel] ? sessionContext.faults[channel].provisions : []; - faults[channel] = Object.assign({ provisions: provs }, fault); + faults[channel] = Object.assign( + { provisions: provs, timestamp: sessionContext.timestamp }, + fault + ) as SessionFault; if (channel.startsWith("task_")) { const taskId = channel.slice(5); for (const t of sessionContext.tasks) @@ -185,7 +215,7 @@ function recordFault(sessionContext, fault, provisions, channels) { session.clearProvisions(sessionContext); } -function inform(sessionContext, rpc) { +function inform(sessionContext, rpc): void { session.inform(sessionContext, rpc.cpeRequest, (err, acsResponse) => { if (err) return void throwError(err, sessionContext.httpResponse); @@ -195,7 +225,12 @@ function inform(sessionContext, rpc) { cwmpVersion: sessionContext.cwmpVersion }); - const cookiesPath = config.get("COOKIES_PATH", sessionContext.deviceId); + const cookiesPath = localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.cookiesPath", + sessionContext.configContext + ); + if (cookiesPath) { res.headers["Set-Cookie"] = `session=${ sessionContext.sessionId @@ -208,7 +243,7 @@ function inform(sessionContext, rpc) { }); } -function transferComplete(sessionContext, rpc) { +function transferComplete(sessionContext, rpc): void { session.transferComplete( sessionContext, rpc.cpeRequest, @@ -244,8 +279,8 @@ function transferComplete(sessionContext, rpc) { ); } -// Append providions and remove duplicates -function appendProvisions(original, toAppend) { +// Append provisions and remove duplicates +function appendProvisions(original, toAppend): boolean { let modified = false; const stringified = new WeakMap(); @@ -276,7 +311,7 @@ function appendProvisions(original, toAppend) { return modified; } -function applyPresets(sessionContext) { +function applyPresets(sessionContext: SessionContext): void { const deviceData = sessionContext.deviceData; const presets = localCache.getPresets(sessionContext.cacheSnapshot); @@ -284,7 +319,11 @@ function applyPresets(sessionContext) { const blackList = {}; let whiteList = null; let whiteListProvisions = null; - const RETRY_DELAY = config.get("RETRY_DELAY", sessionContext.deviceId); + const RETRY_DELAY = +localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.retryDelay", + sessionContext.configContext + ); if (sessionContext.faults) { for (const [channel, fault] of Object.entries(sessionContext.faults)) { @@ -309,13 +348,13 @@ function applyPresets(sessionContext) { deviceData.attributes.revision = 1; const deviceEvents = {}; - for (const p of deviceData.paths.find(["Events", "*"], false, true)) { + for (const p of deviceData.paths.find(Path.parse("Events.*"), false, true)) { const attrs = deviceData.attributes.get(p); if (attrs && attrs.value && attrs.value[1][0] >= sessionContext.timestamp) - deviceEvents[p[1]] = true; + deviceEvents[p.segments[1] as string] = true; } - const parameters = {}; + const parameters: { [name: string]: Path } = {}; const filteredPresets = []; for (const preset of presets) { @@ -345,11 +384,8 @@ function applyPresets(sessionContext) { } filteredPresets.push(preset); - for (const k of Object.keys(preset.precondition)) { - sessionContext.channels[preset.channel] = 0; - const p = k.split(/([^a-zA-Z0-9\-_.].*)/, 1)[0]; - parameters[p] = common.parsePath(p); - } + for (const k of extractParams(preset.precondition)) + parameters[k] = Path.parse(k); } const declarations = Object.values(parameters).map(v => ({ @@ -380,7 +416,7 @@ function applyPresets(sessionContext) { const parameterValues = {}; for (const [k, v] of Object.entries(parameters)) { const unpacked = device.unpack(deviceData, v); - if (!unpacked[0]) continue; + if (!unpacked.length) continue; const attrs = deviceData.attributes.get(unpacked[0]); if (attrs && attrs.value && attrs.value[1]) parameterValues[k] = attrs.value[1][0]; @@ -390,8 +426,9 @@ function applyPresets(sessionContext) { session.addProvisions(sessionContext, whiteList, whiteListProvisions); const appendProvisionsToFaults = {}; + const now = Date.now(); for (const p of filteredPresets) { - if (query.testFilter(parameterValues, p.precondition)) { + if (evaluate(p.precondition, parameterValues, now)) { if (blackList[p.channel] === 2) { appendProvisionsToFaults[p.channel] = ( appendProvisionsToFaults[p.channel] || [] @@ -469,7 +506,7 @@ function applyPresets(sessionContext) { ); } -function nextRpc(sessionContext) { +function nextRpc(sessionContext: SessionContext): void { session.rpcRequest(sessionContext, null, (err, fault, id, acsRequest) => { if (err) return void throwError(err, sessionContext.httpResponse); @@ -595,7 +632,7 @@ function nextRpc(sessionContext) { }); } -function endSession(sessionContext, callback) { +function endSession(sessionContext: SessionContext, callback): void { let saveCache = sessionContext.cacheUntil != null; let counter = 3; @@ -705,24 +742,29 @@ function endSession(sessionContext, callback) { if ((counter -= 2) === 1) callback(null, sessionContext.new); } -function sendAcsRequest(sessionContext, id, acsRequest) { +function sendAcsRequest( + sessionContext: SessionContext, + id?: string, + acsRequest?: AcsRequest +): void { if (!acsRequest) return void writeResponse(sessionContext, soap.response(null), true); if (acsRequest.name === "Download") { - acsRequest.fileSize = 0; - if (!acsRequest.url) { + const downloadRequest = acsRequest as SetAcsRequest; + downloadRequest.fileSize = 0; + if (!downloadRequest.url) { const FS_PORT = config.get("FS_PORT"); const FS_HOSTNAME = config.get("FS_HOSTNAME"); const FS_SSL = config.get("FS_SSL"); - acsRequest.url = FS_SSL ? "https://" : "http://"; - acsRequest.url += FS_HOSTNAME; - if (FS_PORT !== 80) acsRequest.url += ":" + FS_PORT; - acsRequest.url += "/" + encodeURI(acsRequest.fileName); + downloadRequest.url = FS_SSL ? "https://" : "http://"; + downloadRequest.url += FS_HOSTNAME; + if (FS_PORT !== 80) downloadRequest.url += ":" + FS_PORT; + downloadRequest.url += "/" + encodeURI(downloadRequest.fileName); const files = localCache.getFiles(sessionContext.cacheSnapshot); - if (files[acsRequest.fileName]) - acsRequest.fileSize = files[acsRequest.fileName].length; + if (files[downloadRequest.fileName]) + downloadRequest.fileSize = files[downloadRequest.fileName].length; } } @@ -742,7 +784,11 @@ function sendAcsRequest(sessionContext, id, acsRequest) { writeResponse(sessionContext, res); } -function getSession(connection, sessionId, callback) { +function getSession( + connection, + sessionId, + callback: (err?: Error, sessionContext?: SessionContext) => void +): void { const sessionContext = currentSessions.get(connection); if (sessionContext) { currentSessions.delete(connection); @@ -759,10 +805,8 @@ function getSession(connection, sessionId, callback) { }, 100); } -const currentSessions = new WeakMap(); - // When socket closes, store active sessions in cache -function onConnection(socket) { +export function onConnection(socket): void { // The property remoteAddress may be undefined after the connection is // closed, unless we read it at least once (caching?) socket.remoteAddress; @@ -831,7 +875,29 @@ setInterval(() => { stats.initiatedSessions = 0; }, 10000).unref(); -function getDueTasksAndFaultsAndOperations(deviceId, timestamp, callback) { +interface GetDueTasksAndFaultsAndOperationsCallback { + ( + err: null, + tasks: any[], + faults: { [channel: string]: SessionFault }, + operations: { [commandKey: string]: Operation }, + ttl: number + ): void; + + ( + err: Error, + tasks?: null, + faults?: null, + operations?: null, + ttl?: null + ): void; +} + +function getDueTasksAndFaultsAndOperations( + deviceId, + timestamp, + callback: GetDueTasksAndFaultsAndOperationsCallback +): void { cache.get(`${deviceId}_tasks_faults_operations`, (err, res) => { if (err) return void callback(err); @@ -841,7 +907,8 @@ function getDueTasksAndFaultsAndOperations(deviceId, timestamp, callback) { null, res.tasks || [], res.faults || {}, - res.operations || {} + res.operations || {}, + 0 ); } @@ -890,8 +957,12 @@ function cacheDueTasksAndFaultsAndOperations( operations, cacheUntil, callback -) { - const v = {}; +): void { + const v = { + tasks: null, + faults: null, + operations: null + }; if (tasks.length) v.tasks = tasks; if (Object.keys(faults).length) v.faults = faults; if (Object.keys(operations).length) v.operations = operations; @@ -908,7 +979,7 @@ function cacheDueTasksAndFaultsAndOperations( ); } -function processRequest(sessionContext, rpc) { +function processRequest(sessionContext, rpc): void { if (rpc.cpeRequest) { if (rpc.cpeRequest.name === "Inform") { logger.accessInfo({ @@ -987,7 +1058,7 @@ function processRequest(sessionContext, rpc) { } } -function listener(httpRequest, httpResponse) { +export function listener(httpRequest, httpResponse): void { stats.totalRequests += 1; if (httpRequest.method !== "POST") { @@ -1055,14 +1126,14 @@ function listener(httpRequest, httpResponse) { offset += chunk.length; } - function parsedRpc(sessionContext, rpc, parseWarnings) { + function parsedRpc(sessionContext, rpc, parseWarnings): void { for (const w of parseWarnings) { w.sessionContext = sessionContext; w.rpc = rpc; logger.accessWarn(w); } - if (config.get("DEBUG", sessionContext.deviceId)) { + if (sessionContext.debug) { const dump = `# REQUEST ${new Date(Date.now())}\n` + JSON.stringify(httpRequest.headers) + @@ -1134,7 +1205,7 @@ function listener(httpRequest, httpResponse) { if (sessionContext) { if ( (rpc.cpeRequest && rpc.cpeRequest.name === "Inform") || - !sessionContext.rpcRequest ^ !(rpc.cpeResponse || rpc.cpeFault) + !sessionContext.rpcRequest !== !(rpc.cpeResponse || rpc.cpeFault) ) { logger.accessError({ message: "Bad session state", @@ -1165,48 +1236,72 @@ function listener(httpRequest, httpResponse) { stats.initiatedSessions += 1; const deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId); - session.init( - deviceId, - rpc.cwmpVersion, - rpc.sessionTimeout || config.get("SESSION_TIMEOUT", deviceId), - (err, _sessionContext) => { - if (err) return void throwError(err, httpResponse); + localCache.getCurrentSnapshot((err, cacheSnapshot) => { + if (err) return void throwError(err, httpResponse); - _sessionContext.httpRequest = httpRequest; - _sessionContext.httpResponse = httpResponse; - _sessionContext.sessionId = crypto.randomBytes(8).toString("hex"); - httpRequest.connection.setTimeout(_sessionContext.timeout * 1000); + const configContext = { + id: deviceId, + serialNumber: rpc.cpeRequest.deviceId["SerialNumber"], + productClass: rpc.cpeRequest.deviceId["ProductClass"], + oui: rpc.cpeRequest.deviceId["OUI"] + }; - getDueTasksAndFaultsAndOperations( - deviceId, - _sessionContext.timestamp, - (err, dueTasks, faults, operations, cacheUntil) => { - if (err) return void throwError(err, httpResponse); - - _sessionContext.tasks = dueTasks; - _sessionContext.operations = operations; - _sessionContext.cacheUntil = cacheUntil; - _sessionContext.faults = faults; - _sessionContext.retries = {}; - for (const [k, v] of Object.entries(_sessionContext.faults)) { - if (v.expiry >= _sessionContext.timestamp) { - // Delete expired faults - delete _sessionContext.faults[k]; - if (!_sessionContext.faultsTouched) - _sessionContext.faultsTouched = {}; - _sessionContext.faultsTouched[k] = true; - } else { - _sessionContext.retries[k] = v.retries; - } - } - parsedRpc(_sessionContext, rpc, parseWarnings); - } + const sessionTimeout = + rpc.sessionTimeout || + localCache.getConfig( + cacheSnapshot, + "cwmp.sessionTimeout", + configContext ); - } - ); + + session.init( + deviceId, + rpc.cwmpVersion, + sessionTimeout, + (err, _sessionContext) => { + if (err) return void throwError(err, httpResponse); + + _sessionContext.cacheSnapshot = cacheSnapshot; + _sessionContext.configContext = configContext; + _sessionContext.debug = !!localCache.getConfig( + cacheSnapshot, + "cwmp.debug", + configContext + ); + + _sessionContext.httpRequest = httpRequest; + _sessionContext.httpResponse = httpResponse; + _sessionContext.sessionId = crypto.randomBytes(8).toString("hex"); + httpRequest.connection.setTimeout(_sessionContext.timeout * 1000); + + getDueTasksAndFaultsAndOperations( + deviceId, + _sessionContext.timestamp, + (err, dueTasks, faults, operations, cacheUntil) => { + if (err) return void throwError(err, httpResponse); + + _sessionContext.tasks = dueTasks; + _sessionContext.operations = operations; + _sessionContext.cacheUntil = cacheUntil; + _sessionContext.faults = faults; + _sessionContext.retries = {}; + for (const [k, v] of Object.entries(_sessionContext.faults)) { + if (v.expiry >= _sessionContext.timestamp) { + // Delete expired faults + delete _sessionContext.faults[k]; + if (!_sessionContext.faultsTouched) + _sessionContext.faultsTouched = {}; + _sessionContext.faultsTouched[k] = true; + } else { + _sessionContext.retries[k] = v.retries; + } + } + parsedRpc(_sessionContext, rpc, parseWarnings); + } + ); + } + ); + }); }); }); } - -exports.listener = listener; -exports.onConnection = onConnection; diff --git a/lib/db.js b/lib/db.ts similarity index 66% rename from lib/db.js rename to lib/db.ts index 2f701e8..01acf48 100644 --- a/lib/db.js +++ b/lib/db.ts @@ -16,13 +16,15 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const mongodb = require("mongodb"); -const config = require("./config"); -const common = require("./common"); +import { MongoClient, ObjectID } from "mongodb"; +import { get } from "./config"; +import { escapeRegExp } from "./common"; +import { parse } from "./common/expression-parser"; +import { DeviceData, Attributes } from "./types"; +import Path from "./common/path"; -let tasksCollection, +export let tasksCollection, devicesCollection, presetsCollection, objectsCollection, @@ -30,47 +32,49 @@ let tasksCollection, virtualParametersCollection, faultsCollection, filesCollection, - operationsCollection; + operationsCollection, + permissionsCollection, + usersCollection, + configCollection; -function connect(callback) { - mongodb.MongoClient.connect( - config.get("MONGODB_CONNECTION_URL"), +export let client; + +export function connect(callback): void { + MongoClient.connect( + "" + get("MONGODB_CONNECTION_URL"), { useNewUrlParser: true }, - (err, client) => { + (err, _client) => { if (err) return void callback(err); - exports.client = client; + client = _client; const db = client.db(); - exports.tasksCollection = tasksCollection = db.collection("tasks"); + tasksCollection = db.collection("tasks"); tasksCollection.createIndex({ device: 1, timestamp: 1 }); - exports.devicesCollection = devicesCollection = db.collection("devices"); - exports.presetsCollection = presetsCollection = db.collection("presets"); - exports.objectsCollection = objectsCollection = db.collection("objects"); - exports.filesCollection = filesCollection = db.collection("fs.files"); - exports.provisionsCollection = provisionsCollection = db.collection( - "provisions" - ); - exports.virtualParametersCollection = virtualParametersCollection = db.collection( - "virtualParameters" - ); - exports.faultsCollection = faultsCollection = db.collection("faults"); - exports.operationsCollection = operationsCollection = db.collection( - "operations" - ); + devicesCollection = db.collection("devices"); + presetsCollection = db.collection("presets"); + objectsCollection = db.collection("objects"); + filesCollection = db.collection("fs.files"); + provisionsCollection = db.collection("provisions"); + virtualParametersCollection = db.collection("virtualParameters"); + faultsCollection = db.collection("faults"); + operationsCollection = db.collection("operations"); + permissionsCollection = db.collection("permissions"); + usersCollection = db.collection("users"); + configCollection = db.collection("config"); callback(); } ); } -function disconnect() { - if (exports.client) exports.client.close(); +export function disconnect(): void { + if (client) client.close(); } // Optimize projection by removing overlaps // This can modify the object -function optimizeProjection(obj) { +function optimizeProjection(obj: {}): {} { if (obj[""]) return { "": obj[""] }; const keys = Object.keys(obj).sort(); @@ -89,55 +93,66 @@ function optimizeProjection(obj) { return obj; } -function fetchDevice(id, timestamp, patterns, callback) { - const MAX_DEPTH = config.get("MAX_DEPTH", id); +export function fetchDevice( + id: string, + timestamp: number, + patterns: [Path, number][], + callback +): void { + const MAX_DEPTH = +get("MAX_DEPTH", id); - if (!patterns || !patterns.length) patterns = [[[], (1 << MAX_DEPTH) - 1]]; + if (!patterns || !patterns.length) + patterns = [[Path.parse(""), (1 << MAX_DEPTH) - 1]]; let projection = {}; const projectionTree = {}; - function func(path, pats, projTree) { - const children = {}; + function func( + path: string, + pathLength: number, + pats: [Path, number][], + projTree + ): void { + const children: { [fragment: string]: [Path, number][] } = {}; for (const pat of pats) { - const fragment = pat[0][path.length] || "*"; + const fragment = (pat[0].segments[pathLength] as string) || "*"; if (fragment === "*") { - if (pat[1] << path.length) projection[path.join(".")] = 1; + if (pat[1] << pathLength) projection[path.slice(0, -1)] = 1; return; } if (!projTree[fragment]) projTree[fragment] = {}; - if (pat[1] & (1 << path.length)) { - projection[path.concat("_timestamp").join(".")] = 1; - projection[path.concat("_object").join(".")] = 1; - projection[path.concat("_instance").join(".")] = 1; - projection[path.concat([fragment, "_timestamp"]).join(".")] = 1; - projection[path.concat([fragment, "_value"]).join(".")] = 1; - projection[path.concat([fragment, "_type"]).join(".")] = 1; - projection[path.concat([fragment, "_object"]).join(".")] = 1; - projection[path.concat([fragment, "_instance"]).join(".")] = 1; - projection[path.concat([fragment, "_writable"]).join(".")] = 1; - projection[path.concat([fragment, "_orig"]).join(".")] = 1; + if (pat[1] & (1 << pathLength)) { + projection[path + "_timestamp"] = 1; + projection[path + "_object"] = 1; + projection[path + "_instance"] = 1; + projection[path + fragment + "._timestamp"] = 1; + projection[path + fragment + "._value"] = 1; + projection[path + fragment + "._type"] = 1; + projection[path + fragment + "._object"] = 1; + projection[path + fragment + "._instance"] = 1; + projection[path + fragment + "._writable"] = 1; + projection[path + fragment + "._orig"] = 1; } - if (pat[1] >> (path.length + 1)) { + if (pat[1] >> (pathLength + 1)) { if (!children[fragment]) children[fragment] = []; children[fragment].push(pat); } } for (const [k, v] of Object.entries(children)) - func(path.concat(k), v, projTree[k]); + func(path + k + ".", pathLength + 1, v, projTree[k]); } - func([], patterns, projectionTree); + func("", 0, patterns, projectionTree); delete projectionTree["DeviceID"]; delete projectionTree["Events"]; delete projectionTree["Tags"]; - const res = []; - const loaded = []; + const res: [Path, number, Attributes?][] = []; + const loaded: [Path, number][] = []; projection = optimizeProjection(projection); @@ -145,38 +160,47 @@ function fetchDevice(id, timestamp, patterns, callback) { if (k === "" || k === "Events" || k.startsWith("Events.")) { if (k === "" || k === "Events" || k === "Events._writable") { res.push([ - ["Events"], + Path.parse("Events"), timestamp, { object: [timestamp, 1], writable: [timestamp, 0] } ]); - if (k === "Events._writable") loaded.push([["Events"], 1]); + if (k === "Events._writable") loaded.push([Path.parse("Events"), 1]); } if (k === "Events") { projection["_lastInform"] = 1; projection["_lastBoot"] = 1; projection["_lastBootstrap"] = 1; projection["_registered"] = 1; - loaded.push([["Events"], (1 << MAX_DEPTH) - 1]); + loaded.push([Path.parse("Events"), (1 << MAX_DEPTH) - 1]); } else if (k === "Events.Inform._writable" || k === "Events.Inform") { projection["_lastInform"] = 1; - loaded.push([["Events", "Inform"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([Path.parse("Events.Inform"), 1 ^ ((1 << MAX_DEPTH) - 1)]); } else if (k === "Events.1_BOOT._writable" || k === "Events.1_BOOT") { projection["_lastBoot"] = 1; - loaded.push([["Events", "1_BOOT"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([Path.parse("Events.1_BOOT"), 1 ^ ((1 << MAX_DEPTH) - 1)]); } else if ( k === "Events.0_BOOTSTRAP._writable" || k === "Events.0_BOOTSTRAP" ) { projection["_lastBootstrap"] = 1; - loaded.push([["Events", "0_BOOTSTRAP"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("Events.0_BOOTSTRAP"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if ( k === "Events.Registered._writable" || k === "Events.Registered" ) { projection["_registered"] = 1; - loaded.push([["Events", "Registered"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("Events.Registered"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if (k.endsWith("._writable") && k !== "Events._writable") { - loaded.push([k.split(".").slice(0, 2), 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse(k.split(".", 2).join(".")), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } if (k !== "") delete projection[k]; } @@ -184,42 +208,58 @@ function fetchDevice(id, timestamp, patterns, callback) { if (k === "" || k === "DeviceID" || k.startsWith("DeviceID.")) { if (k === "" || k === "DeviceID" || k === "DeviceID._writable") { res.push([ - ["DeviceID"], + Path.parse("DeviceID"), timestamp, { object: [timestamp, 1], writable: [timestamp, 0] } ]); - if (k === "DeviceID._writable") loaded.push([["DeviceID"], 1]); + if (k === "DeviceID._writable") + loaded.push([Path.parse("DeviceID"), 1]); } if (k === "DeviceID") { projection["_id"] = 1; projection["_deviceId"] = 1; - loaded.push([["DeviceID"], (1 << MAX_DEPTH) - 1]); + loaded.push([Path.parse("DeviceID"), (1 << MAX_DEPTH) - 1]); } else if (k === "DeviceID.ID._writable" || k === "DeviceID.ID") { projection["_id"] = 1; - loaded.push([["DeviceID", "ID"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([Path.parse("DeviceID.ID"), 1 ^ ((1 << MAX_DEPTH) - 1)]); } else if ( k === "DeviceID.Manufacturer._writable" || k === "DeviceID.DeManufacturer" ) { projection["_deviceId._Manufacturer"] = 1; - loaded.push([["DeviceID", "Manufacturer"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("DeviceID.Manufacturer"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if ( k === "DeviceID.ProductClass._writable" || k === "DeviceID.ProductClass" ) { projection["_deviceId._ProductClass"] = 1; - loaded.push([["DeviceID", "ProductClass"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("DeviceID.ProductClass"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if (k === "DeviceID.OUI._writable" || k === "DeviceID.OUI") { projection["_deviceId._OUI"] = 1; - loaded.push([["DeviceID", "ProductClass"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("DeviceID.ProductClass"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if ( k === "DeviceID.SerialNumber._writable" || k === "DeviceID.SerialNumber" ) { projection["_deviceId._SerialNumber"] = 1; - loaded.push([["DeviceID", "SerialNumber"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("DeviceID.SerialNumber"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if (k.endsWith("._writable") && k !== "DeviceID._writable") { - loaded.push([k.split(".").slice(0, 2), 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse(k.split(".", 2).join(".")), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } if (k !== "") delete projection[k]; } @@ -227,49 +267,54 @@ function fetchDevice(id, timestamp, patterns, callback) { if (k === "Tags" || k.startsWith("Tags.")) { if (!projection["_tags"]) { projection["_tags"] = 1; - loaded.push([["Tags"], (1 << MAX_DEPTH) - 1]); + loaded.push([Path.parse("Tags"), (1 << MAX_DEPTH) - 1]); } delete projection[k]; } } - const options = {}; + let options; if (!projection[""]) { - if (!Object.keys(projection).length) options.projection = { _id: 1 }; - else options.projection = projection; + if (!Object.keys(projection).length) options = { projection: { _id: 1 } }; + else options = { projection: projection }; } devicesCollection.findOne({ _id: id }, options, (err, device) => { if (err || !device) return void callback(err); - function storeParams(obj, path, ts, descendantsFetched, projTree) { + function storeParams( + obj, + path: string, + pathLength: number, + ts, + descendantsFetched, + projTree + ): void { let thisFetched = false; if (descendantsFetched) { thisFetched = true; - } else if (projection[path.join(".")]) { + } else if (projection[path.slice(0, -1)]) { descendantsFetched = true; if ( - path.length && + pathLength && projection[ - path - .slice(0, -1) - .concat("_timestamp") - .join(".") + path.slice(0, path.lastIndexOf(".", path.length - 2) + 1) + + "_timestamp" ] ) { thisFetched = true; loaded.push([ - path, - ((1 << (path.length - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1) + Path.parse(path.slice(0, -1)), + ((1 << (pathLength - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1) ]); } else { loaded.push([ - path, - ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1) + Path.parse(path.slice(0, -1)), + ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1) ]); } - } else if (projection[path.concat("_writable").join(".")]) { - loaded.push([path, 1 << (path.length - 1)]); + } else if (projection[path + "_writable"]) { + loaded.push([Path.parse(path.slice(0, -1)), 1 << (pathLength - 1)]); thisFetched = true; } @@ -279,7 +324,7 @@ function fetchDevice(id, timestamp, patterns, callback) { if (obj["_instance"] && obj["_object"] == null) obj["_object"] = true; if (thisFetched) { - const attrs = {}; + const attrs: Attributes = {}; let t = obj["_timestamp"] || 1; if (ts > t) t = ts; @@ -295,7 +340,7 @@ function fetchDevice(id, timestamp, patterns, callback) { if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0]; - res.push([path, t, attrs]); + res.push([Path.parse(path.slice(0, -1)), t, attrs]); } for (const [k, v] of Object.entries(obj)) { @@ -303,7 +348,8 @@ function fetchDevice(id, timestamp, patterns, callback) { obj["_object"] = true; storeParams( v, - path.concat(k), + path + k + ".", + pathLength + 1, obj["_timestamp"], descendantsFetched, projTree ? projTree[k] : null @@ -315,14 +361,14 @@ function fetchDevice(id, timestamp, patterns, callback) { if (!descendantsFetched) { if (projTree) { for (const k of Object.keys(projTree)) { - const p = path.concat(k); - loaded.push([p, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)]); - if ((obj["_object"] || !path.length) && obj["_timestamp"]) + const p = Path.parse(path + k); + loaded.push([p, ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1)]); + if ((obj["_object"] || !pathLength) && obj["_timestamp"]) res.push([p, obj["_timestamp"]]); } } - } else if ((obj["_object"] || !path.length) && obj["_timestamp"]) { - res.push([path.concat("*"), obj["_timestamp"]]); + } else if ((obj["_object"] || !pathLength) && obj["_timestamp"]) { + res.push([Path.parse(path + "*"), obj["_timestamp"]]); } } @@ -330,7 +376,7 @@ function fetchDevice(id, timestamp, patterns, callback) { switch (k) { case "_lastInform": res.push([ - ["Events", "Inform"], + Path.parse("Events.Inform"), +v, { object: [+v, 0], @@ -342,7 +388,7 @@ function fetchDevice(id, timestamp, patterns, callback) { break; case "_lastBoot": res.push([ - ["Events", "1_BOOT"], + Path.parse("Events.1_BOOT"), +v, { object: [+v, 0], @@ -354,7 +400,7 @@ function fetchDevice(id, timestamp, patterns, callback) { break; case "_lastBootstrap": res.push([ - ["Events", "0_BOOTSTRAP"], + Path.parse("Events.0_BOOTSTRAP"), +v, { object: [+v, 0], @@ -367,7 +413,7 @@ function fetchDevice(id, timestamp, patterns, callback) { case "_registered": // Use current timestamp for registered event attribute timestamps res.push([ - ["Events", "Registered"], + Path.parse("Events.Registered"), timestamp, { object: [timestamp, 0], @@ -380,12 +426,12 @@ function fetchDevice(id, timestamp, patterns, callback) { case "_id": if (projection[""] || projection["_id"]) { res.push([ - ["DeviceID", "ID"], + Path.parse("DeviceID.ID"), timestamp, { object: [timestamp, 0], writable: [timestamp, 0], - value: [timestamp, [v, "xsd:string"]] + value: [timestamp, [v as string, "xsd:string"]] } ]); } @@ -393,18 +439,18 @@ function fetchDevice(id, timestamp, patterns, callback) { delete device[k]; break; case "_tags": - if (v.length) { + if ((v as string[]).length) { res.push([ - ["Tags"], + Path.parse("Tags"), timestamp, { object: [timestamp, 1], writable: [timestamp, 0] } ]); } - for (let t of v) { + for (let t of v as string[]) { t = t.replace(/[^a-zA-Z0-9-]+/g, "_"); res.push([ - ["Tags", t], + Path.parse("Tags." + t), timestamp, { object: [timestamp, 0], @@ -418,7 +464,7 @@ function fetchDevice(id, timestamp, patterns, callback) { case "_deviceId": if (v["_Manufacturer"] != null) { res.push([ - ["DeviceID", "Manufacturer"], + Path.parse("DeviceID.Manufacturer"), timestamp, { object: [timestamp, 0], @@ -430,7 +476,7 @@ function fetchDevice(id, timestamp, patterns, callback) { if (v["_OUI"] != null) { res.push([ - ["DeviceID", "OUI"], + Path.parse("DeviceID.OUI"), timestamp, { object: [timestamp, 0], @@ -442,7 +488,7 @@ function fetchDevice(id, timestamp, patterns, callback) { if (v["_ProductClass"] != null) { res.push([ - ["DeviceID", "ProductClass"], + Path.parse("DeviceID.ProductClass"), timestamp, { object: [timestamp, 0], @@ -454,7 +500,7 @@ function fetchDevice(id, timestamp, patterns, callback) { if (v["_SerialNumber"] != null) { res.push([ - ["DeviceID", "SerialNumber"], + Path.parse("DeviceID.SerialNumber"), timestamp, { object: [timestamp, 0], @@ -468,50 +514,49 @@ function fetchDevice(id, timestamp, patterns, callback) { } } - storeParams(device, [], 0, false, projectionTree); + storeParams(device, "", 0, 0, false, projectionTree); callback(null, res, loaded); }); } -function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) { +export function saveDevice( + deviceId: string, + deviceData: DeviceData, + isNew: boolean, + sessionTimestamp: number, + callback +): void { const update = { $set: {}, $unset: {}, $addToSet: {}, $pull: {} }; for (const diff of deviceData.timestamps.diff()) { if (diff[0].wildcard !== 1 << (diff[0].length - 1)) continue; if ( - diff[0][0] === "Events" || - diff[0][0] === "DeviceID" || - diff[0][0] === "Tags" + diff[0].segments[0] === "Events" || + diff[0].segments[0] === "DeviceID" || + diff[0].segments[0] === "Tags" ) continue; + const parent = deviceData.paths.get(diff[0].slice(0, -1)); + // Param timestamps may be greater than session timestamp to track revisions if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp; if (diff[2] == null && diff[1] != null) { update["$unset"][ - diff[0] - .slice(0, -1) - .concat("_timestamp") - .join(".") + parent.length ? parent.toString() + "._timestamp" : "_timestamp" ] = 1; - } else if (diff[2] !== diff[1]) { - const parent = deviceData.paths.get(diff[0].slice(0, -1)); + } else { if (parent && (!parent.length || deviceData.attributes.has(parent))) { update["$set"][ - diff[0] - .slice(0, -1) - .concat("_timestamp") - .join(".") + parent.length ? parent.toString() + "._timestamp" : "_timestamp" ] = new Date(diff[2]); } } } for (const diff of deviceData.attributes.diff()) { - if (diff[1] === diff[2]) continue; - const path = diff[0]; const value1 = (((diff[1] || {}).value || [])[1] || [])[0]; const value2 = (((diff[2] || {}).value || [])[1] || [])[0]; @@ -524,11 +569,11 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) { const writable2 = ((diff[2] || {}).writable || [])[1]; const writable1 = ((diff[1] || {}).writable || [])[1]; - switch (path[0]) { + switch (path.segments[0]) { case "Events": - if (diff[0].length === 2 && value2 !== value1) { + if (path.length === 2 && value2 !== value1) { if (!diff[2]) { - switch (path[1]) { + switch (path.segments[1]) { case "Inform": update["$unset"]["_lastInform"] = 1; break; @@ -542,8 +587,8 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) { update["$unset"]["_registered"] = 1; } } else { - const t = new Date(diff[2].value[1][0]); - switch (path[1]) { + const t = new Date(diff[2].value[1][0] as number); + switch (path.segments[1]) { case "Inform": update["$set"]["_lastInform"] = t; break; @@ -563,7 +608,7 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) { case "DeviceID": if (value2 !== value1) { const v = diff[2].value[1][0]; - switch (path[1]) { + switch (path.segments[1]) { case "ID": update["$set"]["_id"] = v; break; @@ -586,21 +631,21 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) { if (value2 != null) { if (!update["$addToSet"]["_tags"]) update["$addToSet"]["_tags"] = { $each: [] }; - update["$addToSet"]["_tags"]["$each"].push(diff[0][1]); + update["$addToSet"]["_tags"]["$each"].push(path.segments[1]); } else { if (!update["$pull"]["_tags"]) { update["$pull"]["_tags"] = { $in: [] }; } - update["$pull"]["_tags"]["$in"].push(diff[0][1]); + update["$pull"]["_tags"]["$in"].push(path.segments[1]); } } break; default: if (!diff[2]) { - update["$unset"][diff[0].join(".")] = 1; + update["$unset"][path.toString()] = 1; continue; } @@ -615,35 +660,38 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) { if (value2 !== value1) { if ( valueType2 === "xsd:dateTime" && - Number.isInteger(value2) + Number.isInteger(value2 as number) ) { - update["$set"][path.concat("_value").join(".")] = new Date( - value2 + update["$set"][path.toString() + "._value"] = new Date( + value2 as number ); } else { - update["$set"][path.concat("_value").join(".")] = value2; + update["$set"][path.toString() + "._value"] = value2; } } if (valueType2 !== valueType1) - update["$set"][path.concat("_type").join(".")] = valueType2; + update["$set"][path.toString() + "._type"] = valueType2; if (valueTimestamp2 !== valueTimestamp1) { - update["$set"][ - path.concat("_timestamp").join(".") - ] = new Date(valueTimestamp2); + update["$set"][path.toString() + "._timestamp"] = new Date( + valueTimestamp2 + ); } break; case "object": - if (!diff[1] || !diff[1].object || object2 !== object1) - update["$set"][path.concat("_object").join(".")] = !!object2; + if (!diff[1] || !diff[1].object || object2 !== object1) { + update["$set"][ + path.length ? path.toString() + "._object" : "_object" + ] = !!object2; + } break; case "writable": if (!diff[1] || !diff[1].writable || writable2 !== writable1) { update["$set"][ - path.concat("_writable").join(".") + path.length ? path.toString() + "._writable" : "_writable" ] = !!writable2; } } @@ -656,10 +704,11 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) { diff[1][attrName][1] != null && (!diff[2] || !diff[2][attrName] || diff[2][attrName][1] == null) ) { - update["$unset"][path.concat(`_${attrName}`).join(".")] = 1; + const p = path.length ? path.toString() + "." : ""; + update["$unset"][`${p}_${attrName}`] = 1; if (attrName === "value") { - update["$unset"][path.concat("_type").join(".")] = 1; - update["$unset"][path.concat("_timestamp").join(".")] = 1; + update["$unset"][p + "_type"] = 1; + update["$unset"][p + "_timestamp"] = 1; } } } @@ -721,9 +770,9 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) { ); } -function getFaults(deviceId, callback) { +export function getFaults(deviceId, callback): void { faultsCollection - .find({ _id: { $regex: `^${common.escapeRegExp(deviceId)}\\:` } }) + .find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } }) .toArray((err, res) => { if (err) return callback(err); @@ -742,7 +791,7 @@ function getFaults(deviceId, callback) { }); } -function saveFault(deviceId, channel, fault, callback) { +export function saveFault(deviceId, channel, fault, callback): void { const id = `${deviceId}:${channel}`; fault = Object.assign({}, fault); fault._id = id; @@ -753,11 +802,11 @@ function saveFault(deviceId, channel, fault, callback) { faultsCollection.replaceOne({ _id: id }, fault, { upsert: true }, callback); } -function deleteFault(deviceId, channel, callback) { +export function deleteFault(deviceId, channel, callback): void { faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` }, callback); } -function getDueTasks(deviceId, timestamp, callback) { +export function getDueTasks(deviceId, timestamp, callback): void { const cur = tasksCollection.find({ device: deviceId }).sort(["timestamp"]); const tasks = []; @@ -779,8 +828,8 @@ function getDueTasks(deviceId, timestamp, callback) { // For API compatibility if (task.name === "download" && task.file) { let q; - if (mongodb.ObjectID.isValid(task.file)) - q = { _id: { $in: [task.file, new mongodb.ObjectID(task.file)] } }; + if (ObjectID.isValid(task.file)) + q = { _id: { $in: [task.file, new ObjectID(task.file)] } }; else q = { _id: task.file }; filesCollection.find(q).toArray((err, res) => { @@ -802,16 +851,16 @@ function getDueTasks(deviceId, timestamp, callback) { ); } -function clearTasks(deviceId, taskIds, callback) { +export function clearTasks(deviceId, taskIds, callback): void { tasksCollection.deleteMany( - { _id: { $in: taskIds.map(id => new mongodb.ObjectID(id)) } }, + { _id: { $in: taskIds.map(id => new ObjectID(id)) } }, callback ); } -function getOperations(deviceId, callback) { +export function getOperations(deviceId, callback): void { operationsCollection - .find({ _id: { $regex: `^${common.escapeRegExp(deviceId)}\\:` } }) + .find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } }) .toArray((err, res) => { if (err) return void callback(err); @@ -830,7 +879,7 @@ function getOperations(deviceId, callback) { }); } -function saveOperation(deviceId, commandKey, operation, callback) { +export function saveOperation(deviceId, commandKey, operation, callback): void { const id = `${deviceId}:${commandKey}`; operation = Object.assign({}, operation); operation._id = id; @@ -846,47 +895,67 @@ function saveOperation(deviceId, commandKey, operation, callback) { ); } -function deleteOperation(deviceId, commandKey, callback) { +export function deleteOperation(deviceId, commandKey, callback): void { operationsCollection.deleteOne( { _id: `${deviceId}:${commandKey}` }, callback ); } -function getPresets(callback) { +export function getPresets(callback): void { presetsCollection.find().toArray(callback); } -function getObjects(callback) { +export function getObjects(callback): void { objectsCollection.find().toArray(callback); } -function getProvisions(callback) { +export function getProvisions(callback): void { provisionsCollection.find().toArray(callback); } -function getVirtualParameters(callback) { +export function getVirtualParameters(callback): void { virtualParametersCollection.find().toArray(callback); } -function getFiles(callback) { +export function getFiles(callback): void { filesCollection.find().toArray(callback); } -exports.connect = connect; -exports.disconnect = disconnect; -exports.fetchDevice = fetchDevice; -exports.saveDevice = saveDevice; -exports.getFaults = getFaults; -exports.saveFault = saveFault; -exports.deleteFault = deleteFault; -exports.clearTasks = clearTasks; -exports.saveOperation = saveOperation; -exports.deleteOperation = deleteOperation; -exports.getPresets = getPresets; -exports.getObjects = getObjects; -exports.getProvisions = getProvisions; -exports.getVirtualParameters = getVirtualParameters; -exports.getFiles = getFiles; -exports.getDueTasks = getDueTasks; -exports.getOperations = getOperations; +export function getConfig(callback): void { + configCollection.find().toArray((err, res) => { + if (err) return void callback(err); + + callback( + null, + res.map(c => ({ + id: c["_id"], + value: parse(c["value"]) + })) + ); + }); +} + +export function getPermissions( + callback: ( + err?: Error, + permissions?: { + role: string; + resource: string; + access: number; + filter: string; + validate: string; + }[] + ) => void +): void { + permissionsCollection.find().toArray(callback); +} + +export function getUsers( + callback: ( + err?: Error, + users?: { _id: string; password: string; salt: string; roles: string }[] + ) => void +): void { + usersCollection.find().toArray(callback); +} diff --git a/lib/default-provisions.js b/lib/default-provisions.ts similarity index 68% rename from lib/default-provisions.js rename to lib/default-provisions.ts index 362be69..fe74590 100644 --- a/lib/default-provisions.js +++ b/lib/default-provisions.ts @@ -16,20 +16,20 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const config = require("./config"); -const common = require("./common"); -const device = require("./device"); -const scheduling = require("./scheduling"); +import Path from "./common/path"; +import * as config from "./config"; +import * as device from "./device"; +import * as scheduling from "./scheduling"; -const MAX_DEPTH = config.get("MAX_DEPTH"); +const MAX_DEPTH = +config.get("MAX_DEPTH"); -function refresh(sessionContext, provision, declarations) { - const path = common.parsePath(provision[1]).slice(); - const l = path.length; - path.length = MAX_DEPTH; - path.fill("*", l); +export function refresh(sessionContext, provision, declarations): boolean { + const segments = Path.parse(provision[1]).segments.slice(); + const l = segments.length; + segments.length = MAX_DEPTH; + segments.fill("*", l); + const path = Path.parse(segments.join(".")); const every = 1000 * (provision[2] || 1); const offset = scheduling.variance(sessionContext.deviceId, every); const t = scheduling.interval(sessionContext.timestamp, every, offset); @@ -48,10 +48,9 @@ function refresh(sessionContext, provision, declarations) { return true; } -function value(sessionContext, provision, declarations) { - const path = common.parsePath(provision[1]); +export function value(sessionContext, provision, declarations): boolean { declarations.push({ - path: path, + path: Path.parse(provision[1]), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -62,11 +61,9 @@ function value(sessionContext, provision, declarations) { return true; } -function tag(sessionContext, provision, declarations) { - const path = ["Tags", provision[1]]; - +export function tag(sessionContext, provision, declarations): boolean { declarations.push({ - path: path, + path: Path.parse(`Tags.${provision[1]}`), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -77,9 +74,9 @@ function tag(sessionContext, provision, declarations) { return true; } -function reboot(sessionContext, provision, declarations) { +export function reboot(sessionContext, provision, declarations): boolean { declarations.push({ - path: ["Reboot"], + path: Path.parse("Reboot"), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -90,9 +87,9 @@ function reboot(sessionContext, provision, declarations) { return true; } -function reset(sessionContext, provision, declarations) { +export function reset(sessionContext, provision, declarations): boolean { declarations.push({ - path: ["FactoryReset"], + path: Path.parse("FactoryReset"), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -103,18 +100,15 @@ function reset(sessionContext, provision, declarations) { return true; } -function download(sessionContext, provision, declarations) { +export function download(sessionContext, provision, declarations): boolean { const alias = [ - ["FileType"], - provision[1] || "", - ["FileName"], - provision[2] || "", - ["TargetFileName"], - provision[3] || "" - ]; + `FileType:${JSON.stringify(provision[1] || "")}`, + `FileName:${JSON.stringify(provision[2] || "")}`, + `TargetFileName:${JSON.stringify(provision[3] || "")}` + ].join(","); declarations.push({ - path: ["Downloads", alias], + path: Path.parse(`Downloads.[${alias}]`), pathGet: 1, pathSet: 1, attrGet: null, @@ -123,7 +117,7 @@ function download(sessionContext, provision, declarations) { }); declarations.push({ - path: ["Downloads", alias, "Download"], + path: Path.parse(`Downloads.[${alias}].Download`), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -134,18 +128,18 @@ function download(sessionContext, provision, declarations) { return true; } -function instances( +export function instances( sessionContext, provision, declarations, startRevision, endRevision -) { +): boolean { let count = Number(provision[2]); if (Number.isNaN(count)) return true; - const path = common.parsePath(provision[1]); + const path = Path.parse(provision[1]); if (provision[2][0] === "+" || provision[2][0] === "-") { declarations.push({ @@ -178,11 +172,3 @@ function instances( return true; } - -exports.refresh = refresh; -exports.value = value; -exports.tag = tag; -exports.reboot = reboot; -exports.reset = reset; -exports.download = download; -exports.instances = instances; diff --git a/lib/device.js b/lib/device.ts similarity index 73% rename from lib/device.js rename to lib/device.ts index 1f7f562..7f4fc3e 100644 --- a/lib/device.js +++ b/lib/device.ts @@ -16,7 +16,9 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; + +import Path from "./common/path"; +import { DeviceData, Declaration, Clear } from "./types"; const CHANGE_FLAGS = { object: 2, @@ -24,7 +26,7 @@ const CHANGE_FLAGS = { value: 8 }; -function parseBool(v) { +function parseBool(v): boolean { v = "" + v; if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true; else if (v === "false" || v === "FALSE" || v === "False" || v === "0") @@ -32,14 +34,16 @@ function parseBool(v) { else return null; } -function sanitizeParameterValue(parameterValue) { +export function sanitizeParameterValue( + parameterValue: [string | number | boolean, string] +): [string | number | boolean, string?] { if (parameterValue[0] != null) { switch (parameterValue[1]) { case "xsd:boolean": if (typeof parameterValue[0] !== "boolean") { - let v = parseBool(parameterValue[0]); - if (v == null) v = "" + parameterValue[0]; - parameterValue[0] = v; + const b = parseBool(parameterValue[0]); + if (b == null) parameterValue[0] = "" + parameterValue[0]; + else parameterValue[0] = b; } break; case "xsd:string": @@ -52,20 +56,22 @@ function sanitizeParameterValue(parameterValue) { case "xsd:int": case "xsd:unsignedInt": if (typeof parameterValue[0] !== "number") { - let v = parseInt(parameterValue[0]); - if (isNaN(v)) v = "" + parameterValue[0]; - parameterValue[0] = v; + const i = parseInt(parameterValue[0] as string); + if (isNaN(i)) parameterValue[0] = "" + parameterValue[0]; + else parameterValue[0] = i; } break; case "xsd:dateTime": if (typeof parameterValue[0] !== "number") { // Don't use parseInt because it reads date string as a number - let v = +parameterValue[0]; - if (isNaN(v)) { - v = Date.parse(parameterValue[0]); - if (isNaN(v)) v = "" + parameterValue[0]; + let i = +parameterValue[0]; + if (isNaN(i)) { + i = Date.parse(parameterValue[0] as string); + if (isNaN(i)) parameterValue[0] = "" + parameterValue[0]; + else parameterValue[0] = i; + } else { + parameterValue[0] = i; } - parameterValue[0] = v; } break; default: @@ -83,85 +89,79 @@ function sanitizeParameterValue(parameterValue) { return parameterValue; } -function getAliasDeclarations(path, timestamp) { - var decs = []; - - function recursive(p, prefix) { - const pattern = prefix.concat(p); - - for (let i = prefix.length; i < pattern.length; ++i) { - if (Array.isArray(pattern[i])) { - const pat = pattern[i]; - pattern[i] = "*"; - for (let j = 0; j < pat.length; j += 2) - recursive(pat[j], pattern.slice(0, i + 1)); - } - } - - decs.push({ - path: pattern, +export function getAliasDeclarations( + path: Path, + timestamp: number, + attrGet = null +): Declaration[] { + const stripped = path.stripAlias(); + let decs: Declaration[] = [ + { + path: stripped, pathGet: timestamp, pathSet: null, - attrGet: prefix.length ? { value: timestamp } : null, + attrGet: attrGet, attrSet: null, defer: true - }); - } + } + ]; - recursive(path, []); + if (path.alias) { + for (const [i, alias] of path.segments.entries()) { + if (Array.isArray(alias)) { + const parent = stripped.slice(0, i + 1); + for (const [p] of alias as [Path, string][]) { + decs = decs.concat( + getAliasDeclarations(parent.concat(p), timestamp, { + value: timestamp + }) + ); + } + } + } + } return decs; } -function unpack(deviceData, path, revision) { - let allMatches = []; +export function unpack( + deviceData: DeviceData, + path: Path, + revision?: number +): Path[] { + let allMatches = [] as Path[]; if (!path.alias) { for (const p of deviceData.paths.find(path, false, true)) if (deviceData.attributes.has(p, revision)) allMatches.push(p); } else { - const wildcardPath = path.slice(); - wildcardPath.wildcard = path.wildcard; - for (let i = 0; i < wildcardPath.length; ++i) { - if (Array.isArray(wildcardPath[i])) { - wildcardPath[i] = "*"; - wildcardPath.wildcard |= 1 << i; - } - } + const wildcardPath = path.stripAlias(); for (const p of deviceData.paths.find(wildcardPath, false, true)) if (deviceData.attributes.has(p, revision)) allMatches.push(p); for (let i = path.length - 1; i >= 0; --i) { - if (Array.isArray(path[i])) { - for (let j = 0; j < path[i].length; j += 2) { - const p = wildcardPath.slice(0, i + 1).concat(path[i][j]); - p.alias = - ((wildcardPath.alias & ((1 << (i + 1)) - 1)) + path[i][j].alias) << - (i + 1); - p.wildcard = - ((wildcardPath.wildcard & ((1 << (i + 1)) - 1)) + - path[i][j].wildcard) << - (i + 1); + if (path.alias & (i << i)) { + for (const [param, val] of path.segments[i] as [Path, string][]) { + const p = wildcardPath.slice(0, i + 1).concat(param); const unpacked = unpack(deviceData, p, revision); - const filtered = []; + const filtered: Path[] = []; for (const up of unpacked) { const attributes = deviceData.attributes.get(up, revision); if ( attributes && attributes.value && attributes.value[1] && - sanitizeParameterValue([ - path[i][j + 1], - attributes.value[1][1] - ])[0] === attributes.value[1][0] + sanitizeParameterValue([val, attributes.value[1][1]])[0] === + attributes.value[1][0] ) { for (let m = 0; m < allMatches.length; ++m) { let k; - for (k = i; k >= 0; --k) if (allMatches[m][k] !== up[k]) break; + for (k = i; k >= 0; --k) + if (allMatches[m].segments[k] !== up.segments[k]) break; if (k < 0) { filtered.push(allMatches[m]); - allMatches[m] = []; + allMatches[m] = null; } } } @@ -172,21 +172,19 @@ function unpack(deviceData, path, revision) { } } - allMatches.sort((a, b) => { - for (let i = 0; i < a.length; ++i) { - if (a === b) continue; + allMatches.sort((p1, p2) => { + for (let i = 0; i < p1.length; ++i) { + const a = p1.segments[i] as string; + const b = p2.segments[i] as string; + if (a !== b) { + // Use numeric sorting for numbers + const ia = parseInt(a); + const ib = parseInt(b); - // Use numeric sorting for numbers - const ia = parseInt(a); - const ib = parseInt(b); - - if (ia.toString() === a && ib.toString() === b) { - a = ia; - b = ib; + if (ia === +a && ib === +b) return ia - ib; + else if (a < b) return -1; + else return 1; } - - if (a < b) return -1; - else return 1; } return 0; }); @@ -194,7 +192,13 @@ function unpack(deviceData, path, revision) { return allMatches; } -function clear(deviceData, path, timestamp, attributes, changeFlags = 0) { +export function clear( + deviceData, + path, + timestamp, + attributes, + changeFlags = 0 +): void { const changeTrackers = {}; timestamp = timestamp || 0; @@ -253,8 +257,8 @@ function clear(deviceData, path, timestamp, attributes, changeFlags = 0) { if (changeTrackers[k] & changeFlags) deviceData.changes.add(k); } -function compareEquality(a, b) { - var t = typeof a; +function compareEquality(a, b): boolean { + const t = typeof a; if ( a === null || a === undefined || @@ -268,7 +272,13 @@ function compareEquality(a, b) { return JSON.stringify(a) === JSON.stringify(b); } -function set(deviceData, path, timestamp, attributes, toClear) { +export function set( + deviceData: DeviceData, + path: Path, + timestamp, + attributes, + toClear?: Clear[] +): Clear[] { path = deviceData.paths.add(path); const currentTimestamp = deviceData.timestamps.get(path); @@ -366,7 +376,12 @@ function set(deviceData, path, timestamp, attributes, toClear) { return toClear; } -function track(deviceData, path, marker, attributes) { +export function track( + deviceData: DeviceData, + path: Path, + marker: string, + attributes? +): void { path = deviceData.paths.add(path); let f = 1; @@ -382,7 +397,7 @@ function track(deviceData, path, marker, attributes) { cur[marker] |= f; } -function clearTrackers(deviceData, tracker) { +export function clearTrackers(deviceData: DeviceData, tracker): void { if (Array.isArray(tracker)) { for (const v of deviceData.trackers.values()) for (const t of tracker) delete v[t]; @@ -392,11 +407,3 @@ function clearTrackers(deviceData, tracker) { deviceData.changes.delete(tracker); } } - -exports.sanitizeParameterValue = sanitizeParameterValue; -exports.getAliasDeclarations = getAliasDeclarations; -exports.unpack = unpack; -exports.set = set; -exports.clear = clear; -exports.track = track; -exports.clearTrackers = clearTrackers; diff --git a/lib/extensions.js b/lib/extensions.ts similarity index 73% rename from lib/extensions.js rename to lib/extensions.ts index 39b1cf2..6ce729e 100644 --- a/lib/extensions.js +++ b/lib/extensions.ts @@ -16,35 +16,41 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const childProcess = require("child_process"); -const crypto = require("crypto"); +import { spawn } from "child_process"; +import * as crypto from "crypto"; +import * as config from "./config"; -const config = require("./config"); - -const TIMEOUT = config.get("EXT_TIMEOUT"); +const TIMEOUT = +config.get("EXT_TIMEOUT"); const processes = {}; -const jobs = {}; +const jobs = new Map(); -function messageHandler(message) { - const func = jobs[message[0]]; +function messageHandler(message): void { + const func = jobs.get(message[0]); if (func) { - delete jobs[message[0]]; + jobs.delete(message[0]); func(null, message[1], message[2]); } } -function run(args, callback) { +export function run(args, callback): boolean { const scriptName = args[0]; + const id = crypto.randomBytes(8).toString("hex"); + jobs.set(id, callback); + if (!processes[scriptName]) { - const p = childProcess.fork("lib/extension-wrapper", [scriptName]); + const p = spawn("bin/genieacs-ext", [scriptName], { + stdio: ["inherit", "inherit", "inherit", "ipc"] + }); processes[scriptName] = p; - p.on("error", () => { + p.on("error", err => { if (processes[scriptName] === p) { + if (jobs.delete(id)) + callback(null, { code: err.name, message: err.message }); + kill(processes[scriptName]); delete processes[scriptName]; } @@ -60,19 +66,17 @@ function run(args, callback) { p.on("message", messageHandler); } - const id = crypto.randomBytes(8).toString("hex"); - jobs[id] = callback; setTimeout(() => { - if (id in jobs) { - delete jobs[id]; + if (jobs.delete(id)) callback(null, { code: "timeout", message: "Extension timed out" }); - } }, TIMEOUT); + if (!processes[scriptName].connected) return false; + return processes[scriptName].send([id, args.slice(1)]); } -function kill(process, callback) { +function kill(process, callback?: () => void): void { if (process.signalCode !== null || process.exitCode !== null) return; const timeToKill = Date.now() + 5000; @@ -91,7 +95,7 @@ function kill(process, callback) { }, 100); } -function killAll(callback) { +export function killAll(callback?): void { let counter = 1; for (const n in processes) { ++counter; @@ -102,6 +106,3 @@ function killAll(callback) { } if (--counter === 0 && callback) callback(); } - -exports.run = run; -exports.killAll = killAll; diff --git a/lib/fs.js b/lib/fs.ts similarity index 81% rename from lib/fs.js rename to lib/fs.ts index 5d19cd7..7ad7698 100644 --- a/lib/fs.js +++ b/lib/fs.ts @@ -16,14 +16,13 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const url = require("url"); -const querystring = require("querystring"); -const mongodb = require("mongodb"); -const db = require("./db"); +import * as url from "url"; +import * as querystring from "querystring"; +import { GridFSBucket } from "mongodb"; +import * as db from "./db"; -function listener(request, response) { +export function listener(request, response): void { const urlParts = url.parse(request.url, true); if (request.method === "GET") { const filename = querystring.unescape(urlParts.pathname.substring(1)); @@ -45,8 +44,8 @@ function listener(request, response) { "Content-Length": file.length }); - const bucket = new mongodb.GridFSBucket(db.client.db()); - const downloadStream = bucket.openDownloadStream(filename); + const bucket = new GridFSBucket(db.client.db()); + const downloadStream = bucket.openDownloadStreamByName(filename); downloadStream.pipe(response); }); } else { @@ -54,5 +53,3 @@ function listener(request, response) { response.end("405 Method Not Allowed"); } } - -exports.listener = listener; diff --git a/lib/gpn-heuristic.js b/lib/gpn-heuristic.ts similarity index 95% rename from lib/gpn-heuristic.js rename to lib/gpn-heuristic.ts index 83c26ed..35d3324 100644 --- a/lib/gpn-heuristic.js +++ b/lib/gpn-heuristic.ts @@ -16,7 +16,6 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; const WILDCARD_MULTIPLIER = 2; const UNDISCOVERED_DEPTH = 7; @@ -28,7 +27,7 @@ const UNDISCOVERED_DEPTH = 7; // flags is an int where its bits mark the segments in the pattern that // need refreshing. Leading 0s indicate that the pattern up to that // point has been discovered. -function estimateGpnCount(gpnPatterns, depth = 0) { +export function estimateGpnCount(gpnPatterns, depth = 0): number { const children = {}; const wildcardChildren = []; let wildcardDiscovered = false; @@ -77,5 +76,3 @@ function estimateGpnCount(gpnPatterns, depth = 0) { return gpnCount; } - -exports.estimateGpnCount = estimateGpnCount; diff --git a/lib/instance-set.js b/lib/instance-set.ts similarity index 81% rename from lib/instance-set.js rename to lib/instance-set.ts index f006b37..17c0376 100644 --- a/lib/instance-set.js +++ b/lib/instance-set.ts @@ -16,22 +16,27 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -class InstanceSet { - constructor() { +interface Instance { + [name: string]: string; +} + +export default class InstanceSet { + private set: Set; + + public constructor() { this.set = new Set(); } - add(instance) { + public add(instance: Instance): void { this.set.add(instance); } - delete(instance) { + public delete(instance: Instance): void { this.set.delete(instance); } - superset(instance) { + public superset(instance: Instance): Instance[] { const res = []; for (const inst of this.set) { let match = true; @@ -67,7 +72,7 @@ class InstanceSet { return res; } - subset(instance) { + public subset(instance: Instance): Instance[] { const res = []; for (const inst of this.set) { @@ -104,25 +109,23 @@ class InstanceSet { return res; } - [Symbol.iterator]() { + public [Symbol.iterator](): IterableIterator { return this.set.values(); } - forEach(callback) { - return this.set.forEach(callback); + public forEach(callback: (instance: Instance) => void): void { + this.set.forEach(callback); } - values() { + public values(): IterableIterator { return this.set.values(); } - clear() { + public clear(): void { this.set.clear(); } - get size() { + public get size(): number { return this.set.size; } } - -module.exports = InstanceSet; diff --git a/lib/local-cache.js b/lib/local-cache.ts similarity index 56% rename from lib/local-cache.js rename to lib/local-cache.ts index 078c7a5..e60c41a 100644 --- a/lib/local-cache.js +++ b/lib/local-cache.ts @@ -16,16 +16,30 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const vm = require("vm"); -const crypto = require("crypto"); +import * as vm from "vm"; +import * as crypto from "crypto"; +import * as config from "./config"; +import * as db from "./db"; +import * as cache from "./cache"; +import { mongoQueryToFilter } from "./mongodb-functions"; +import * as logger from "./logger"; +import * as scheduling from "./scheduling"; +import * as expression from "./common/expression"; +import { parse } from "./common/expression-parser"; +import { Preset, Expression } from "./types"; -const db = require("./db"); -const cache = require("./cache"); -const query = require("./query"); -const logger = require("./logger"); -const scheduling = require("./scheduling"); +interface Permissions { + [role: string]: { + [access: number]: { + [resource: string]: { + access: number; + filter: Expression; + validate: { [validator: string]: boolean }; + }; + }; + }; +} const REFRESH = 3000; const EVICT_TIMEOUT = 60000; @@ -34,7 +48,7 @@ const snapshots = new Map(); let currentSnapshot = null; let nextRefresh = 1; -function computeHash(snapshot) { +function computeHash(snapshot): string { // MD5 hash for presets, provisions, virtual parameters for detecting changes const h = crypto.createHash("md5"); for (const p of snapshot.presets) { @@ -56,10 +70,35 @@ function computeHash(snapshot) { h.update(JSON.stringify(keys)); for (const k of keys) h.update(snapshot.virtualParameters[k].md5); + keys = Object.keys(snapshot.config).sort(); + h.update(JSON.stringify(keys)); + for (const k of keys) h.update(JSON.stringify(snapshot.config[k])); + + keys = Object.keys(snapshot.files).sort(); + h.update(JSON.stringify(keys)); + for (const k of keys) h.update(JSON.stringify(snapshot.files[k])); + + keys = Object.keys(snapshot.users).sort(); + h.update(JSON.stringify(keys)); + for (const k of keys) h.update(JSON.stringify(snapshot.users[k])); + + const roles = Object.keys(snapshot.permissions).sort(); + h.update(JSON.stringify(roles)); + for (const r of roles) { + const levels = Object.keys(snapshot.permissions[r]).sort(); + h.update(JSON.stringify(levels)); + for (const l of levels) { + keys = Object.keys(snapshot.permissions[r][l]).sort(); + h.update(JSON.stringify(keys)); + for (const k of keys) + h.update(JSON.stringify(snapshot.permissions[r][l][k])); + } + } + return h.digest("hex"); } -function flattenObject(src, prefix = "", dst = {}) { +function flattenObject(src, prefix = "", dst = {}): {} { for (const k of Object.keys(src)) { const v = src[k]; if (typeof v === "object" && !Array.isArray(v)) @@ -69,7 +108,7 @@ function flattenObject(src, prefix = "", dst = {}) { return dst; } -function refresh(callback) { +function refresh(callback): void { if (!nextRefresh) { return void setTimeout(() => { refresh(callback); @@ -120,7 +159,7 @@ function refresh(callback) { else return a.weight - b.weight; }); - const presets = []; + const presets = [] as Preset[]; for (const preset of res) { let schedule = null; if (preset.schedule) { @@ -146,9 +185,15 @@ function refresh(callback) { } const events = preset.events || {}; - const precondition = query.convertMongoQueryToFilters( - JSON.parse(preset.precondition) - ); + let precondition; + try { + precondition = parse(preset.precondition); + } catch (error) { + precondition = mongoQueryToFilter( + JSON.parse(preset.precondition) + ); + } + const _provisions = preset.provisions || []; // Generate provisions from the old configuration format @@ -296,13 +341,107 @@ function refresh(callback) { }) ); + promises.push( + new Promise((resolve, reject) => { + db.getPermissions((err, perms) => { + if (err) return void reject(err); + const permissions: Permissions = {}; + for (const p of perms) { + if (!permissions[p.role]) permissions[p.role] = {}; + if (!permissions[p.role][p.access]) + permissions[p.role][p.access] = {}; + + const validate = {}; + permissions[p.role][p.access][p.resource] = { + access: p.access, + filter: parse(p.filter || "true"), + validate: validate + }; + + for (const v of (p.validate || "").split(",")) + validate[v.trim()] = true; + } + + resolve(permissions); + }); + }) + ); + + promises.push( + new Promise((resolve, reject) => { + db.getUsers((err, _users) => { + if (err) return void reject(err); + const users = {}; + for (const user of _users) { + users[user._id] = { + password: user.password, + salt: user.salt, + roles: user.roles.split(",").map(s => s.trim()) + }; + } + resolve(users); + }); + }) + ); + + promises.push( + new Promise((resolve, reject) => { + db.getConfig((err, conf) => { + if (err) return void reject(err); + + conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)); + + const ui = { + filters: {}, + device: {}, + index: {}, + overview: { charts: {}, groups: {} } + }; + const _config = {}; + + for (const c of conf) { + // Evaluate expressions to simplify them + const val = expression.evaluate(c.value); + _config[c.id] = val; + if (c.id.startsWith("ui.")) { + const keys = c.id.split("."); + // remove the first key(ui) + keys.shift(); + let ref = ui; + while (keys.length > 1) { + const k = keys.shift(); + if (typeof ref[k] !== "object") ref[k] = {}; + ref = ref[k]; + } + ref[keys[0]] = val; + } + } + + if (!Object.keys(ui["index"]).length) { + ui["index"] = { + "0": { + label: "ID", + parameter: ["PARAM", "DeviceID.ID"] + } + }; + } + + resolve([_config, ui]); + }); + }) + ); + Promise.all(promises) .then(res => { const snapshot = { presets: res[0], provisions: res[1], virtualParameters: res[2], - files: res[3] + files: res[3], + permissions: res[4], + users: res[5], + config: res[6][0], + ui: res[6][1] }; if (currentSnapshot) { @@ -327,44 +466,128 @@ function refresh(callback) { }); } -function getCurrentSnapshot(callback) { - if (Date.now() < nextRefresh) return void callback(null, currentSnapshot); - refresh(err => { - callback(err, currentSnapshot); +interface GetCurrentSnapshotCallback { + (err: null, currentSnapshot: string): void; + (err: Error, currentSnapshot?: null): void; +} + +export function getCurrentSnapshot(callback: GetCurrentSnapshotCallback); +export function getCurrentSnapshot(): Promise; +export function getCurrentSnapshot( + callback?: GetCurrentSnapshotCallback +): Promise | void { + if (Date.now() < nextRefresh) { + if (callback) return callback(null, currentSnapshot); + return Promise.resolve(currentSnapshot); + } + + if (callback) { + return refresh(err => { + callback(err, currentSnapshot); + }); + } + + return new Promise((resolve, reject) => { + refresh(err => { + if (err) return void reject(err); + resolve(currentSnapshot); + }); }); } -function hasSnapshot(hash) { +export function hasSnapshot(hash): boolean { return snapshots.has(hash); } -function getPresets(snapshotKey) { +export function getPresets(snapshotKey): Preset[] { const snapshot = snapshots.get(snapshotKey); if (!snapshot) throw new Error("Cache snapshot does not exist"); return snapshot.presets; } -function getProvisions(snapshotKey) { +export function getProvisions( + snapshotKey +): { [name: string]: { md5: string; script: vm.Script } } { const snapshot = snapshots.get(snapshotKey); if (!snapshot) throw new Error("Cache snapshot does not exist"); return snapshot.provisions; } -function getVirtualParameters(snapshotKey) { +export function getVirtualParameters( + snapshotKey +): { [name: string]: { md5: string; script: vm.Script } } { const snapshot = snapshots.get(snapshotKey); if (!snapshot) throw new Error("Cache snapshot does not exist"); return snapshot.virtualParameters; } -function getFiles(snapshotKey) { +export function getFiles( + snapshotKey +): { [name: string]: { length: number; md5: string; contentType: string } } { const snapshot = snapshots.get(snapshotKey); if (!snapshot) throw new Error("Cache snapshot does not exist"); return snapshot.files; } -exports.getCurrentSnapshot = getCurrentSnapshot; -exports.hasSnapshot = hasSnapshot; -exports.getPresets = getPresets; -exports.getProvisions = getProvisions; -exports.getVirtualParameters = getVirtualParameters; -exports.getFiles = getFiles; +export function getConfig( + snapshotKey, + key, + context?, + now? +): string | number | boolean | null { + const snapshot = snapshots.get(snapshotKey); + if (!snapshot) throw new Error("Cache snapshot does not exist"); + + const oldOpts = { + "cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT", + "cwmp.debug": "DEBUG", + "cwmp.retryDelay": "RETRY_DELAY", + "cwmp.sessionTimeout": "SESSION_TIMEOUT", + "cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT", + "cwmp.gpnNextLevel": "GPN_NEXT_LEVEL", + "cwmp.gpvBatchSize": "GPV_BATCH_SIZE", + "cwmp.cookiesPath": "COOKIES_PATH", + "cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS", + "cwmp.booleanLiteral": "BOOLEAN_LITERAL", + "cwmp.connectionRequestAllowBasicAuth": + "CONNECTION_REQUEST_ALLOW_BASIC_AUTH", + "cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS", + "cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD", + "cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT" + }; + + if (!(key in snapshot.config)) { + if (key in oldOpts) + return config.get(oldOpts[key], context ? context.id : null); + else return null; + } + return expression.evaluate(snapshot.config[key], context, now || Date.now()); +} + +export function getConfigExpression(snapshotKey, key): Expression { + const snapshot = snapshots.get(snapshotKey); + if (!snapshot) throw new Error("Cache snapshot does not exist"); + + return snapshot.config[key]; +} + +export function getUsers(snapshotKey): {} { + const snapshot = snapshots.get(snapshotKey); + if (!snapshot) throw new Error("Cache snapshot does not exist"); + + return snapshot.users; +} + +export function getPermissions(snapshotKey): {} { + const snapshot = snapshots.get(snapshotKey); + if (!snapshot) throw new Error("Cache snapshot does not exist"); + + return snapshot.permissions; +} + +export function getUiConfig(snapshotKey): {} { + const snapshot = snapshots.get(snapshotKey); + if (!snapshot) throw new Error("Cache snapshot does not exist"); + + return snapshot.ui; +} diff --git a/lib/logger.js b/lib/logger.ts similarity index 84% rename from lib/logger.js rename to lib/logger.ts index a0eaf91..9c755e0 100644 --- a/lib/logger.js +++ b/lib/logger.ts @@ -16,32 +16,46 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const fs = require("fs"); -const os = require("os"); +import * as fs from "fs"; +import * as os from "os"; -const config = require("./config"); +import * as config from "./config"; const REOPEN_EVERY = 60000; const LOG_FORMAT = config.get("LOG_FORMAT"); const ACCESS_LOG_FORMAT = config.get("ACCESS_LOG_FORMAT") || LOG_FORMAT; -const defaultMeta = {}; +const defaultMeta: { [name: string]: any } = {}; let LOG_SYSTEMD = false; let ACCESS_LOG_SYSTEMD = false; let LOG_FILE, ACCESS_LOG_FILE; +declare global { + /* eslint-disable-next-line @typescript-eslint/no-namespace */ + namespace NodeJS { + export interface WritableStream { + fd?: number; + } + } +} + +declare module "fs" { + interface WriteStream { + fd?: number; + } +} + let logStream = fs.createWriteStream(null, { fd: process.stderr.fd }); let logStat = fs.fstatSync(logStream.fd); let accessLogStream = fs.createWriteStream(null, { fd: process.stdout.fd }); let accessLogStat = fs.fstatSync(accessLogStream.fd); // Reopen if original files have been moved (e.g. logrotate) -function reopen() { +function reopen(): void { let counter = 1; if (LOG_FILE) { @@ -90,7 +104,7 @@ function reopen() { setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); } -function init(service, version) { +export function init(service, version): void { defaultMeta.hostname = os.hostname(); defaultMeta.pid = process.pid; defaultMeta.name = `genieacs-${service}`; @@ -115,9 +129,7 @@ function init(service, version) { const JOURNAL_STREAM = process.env["JOURNAL_STREAM"]; if (JOURNAL_STREAM) { - let [dev, inode] = JOURNAL_STREAM.split(":"); - dev = parseInt(dev); - inode = parseInt(inode); + const [dev, inode] = JOURNAL_STREAM.split(":").map(parseInt); LOG_SYSTEMD = logStat.dev === dev && logStat.ino === inode; ACCESS_LOG_SYSTEMD = @@ -129,12 +141,12 @@ function init(service, version) { setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); } -function close() { +export function close(): void { accessLogStream.end(); logStream.end(); } -function flatten(details) { +export function flatten(details): {} { if (details.sessionContext) { details.deviceId = details.sessionContext.deviceId; details.remoteAddress = @@ -184,10 +196,21 @@ function flatten(details) { delete details.fault; } + // For genieacs-ui + if (details.context) { + details.remoteAddress = details.context.request.ip; + if (details.context.state.user) + details.user = details.context.state.user.username; + delete details.context; + } + + for (const [k, v] of Object.entries(details)) + if (v == null) delete details[k]; + return details; } -function formatJson(details, systemd) { +function formatJson(details, systemd): string { if (systemd) { let severity = ""; if (details.severity === "info") severity = "<6>"; @@ -200,19 +223,22 @@ function formatJson(details, systemd) { return `${JSON.stringify(flatten(details))}${os.EOL}`; } -function formatSimple(details, systemd) { +function formatSimple(details, systemd): string { flatten(details); let remote = ""; if (details.remoteAddress) { if (details.deviceId) remote = `${details.remoteAddress} ${details.deviceId}: `; + else if (details.user) + remote = `${details.user}@${details.remoteAddress}: `; else remote = `${details.remoteAddress}: `; } const keys = Object.keys(details); const skip = { deviceId: 1, + user: 1, remoteAddress: 1, severity: 1, timestamp: 1, @@ -241,7 +267,7 @@ function formatSimple(details, systemd) { }${meta}${os.EOL}`; } -function log(details) { +function log(details): void { details.timestamp = new Date().toISOString(); if (LOG_FORMAT === "json") { details = Object.assign({}, defaultMeta, details); @@ -251,22 +277,22 @@ function log(details) { } } -function info(details) { +export function info(details): void { details.severity = "info"; log(details); } -function warn(details) { +export function warn(details): void { details.severity = "warn"; log(details); } -function error(details) { +export function error(details): void { details.severity = "error"; log(details); } -function accessLog(details) { +export function accessLog(details): void { details.timestamp = new Date().toISOString(); if (ACCESS_LOG_FORMAT === "json") { Object.assign(details, defaultMeta); @@ -276,27 +302,17 @@ function accessLog(details) { } } -function accessInfo(details) { +export function accessInfo(details): void { details.severity = "info"; accessLog(details); } -function accessWarn(details) { +export function accessWarn(details): void { details.severity = "warn"; accessLog(details); } -function accessError(details) { +export function accessError(details): void { details.severity = "error"; accessLog(details); } - -exports.init = init; -exports.close = close; -exports.flatten = flatten; -exports.info = info; -exports.warn = warn; -exports.error = error; -exports.accessInfo = accessInfo; -exports.accessWarn = accessWarn; -exports.accessError = accessError; diff --git a/lib/mongodb-functions.ts b/lib/mongodb-functions.ts new file mode 100644 index 0000000..cd00614 --- /dev/null +++ b/lib/mongodb-functions.ts @@ -0,0 +1,609 @@ +import { ObjectID } from "mongodb"; +import { map, parse, stringify } from "./common/expression-parser"; +import { likePatternToRegExp, evaluate } from "./common/expression"; +import { Expression, Fault, Task } from "./types"; + +const isArray = Array.isArray; + +export function processDeviceFilter(filter): Expression { + return map(filter, exp => { + if (!isArray(exp)) return exp; + + if (exp[0] === "PARAM") { + const p = exp[1]; + if (p === "DeviceID.ID") return ["PARAM", "_id"]; + else if (p === "DeviceID") return ["PARAM", "_deviceId"]; + else if (p.startsWith("DeviceID.")) + return ["PARAM", "_deviceId._" + p.slice(9)]; + else if (p === "Tags") return ["PARAM", "_tags"]; + else if (p === "tag") return ["PARAM", "_tags"]; + else if (p === "Events.Inform") return ["PARAM", "_lastInform"]; + else if (p === "Events.Registered") return ["PARAM", "_registered"]; + else if (p === "Events.0_BOOTSTRAP") return ["PARAM", "_lastBootstrap"]; + else if (p === "Events.1_BOOT") return ["PARAM", "_lastBoot"]; + } else if ( + isArray(exp[1]) && + exp[1][0] === "PARAM" && + exp[1][1].startsWith("Tags.") + ) { + const t = exp[1][1].slice(5); + if (exp[0] === "IS NULL") return ["<>", ["PARAM", "_tags"], t]; + else if (exp[0] === "IS NOT NULL") return ["=", ["PARAM", "_tags"], t]; + else if (exp[0] === "=" && exp[2] === true) + return ["=", ["PARAM", "_tags"], t]; + } else if ( + ["=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"].includes(exp[0]) + ) { + let e = map(exp, ee => { + if ( + isArray(ee) && + ee[0] === "PARAM" && + typeof ee[1] === "string" && + !ee[1].startsWith("_") + ) + return ["PARAM", `${ee[1]}._value`]; + return ee; + }); + + if (typeof e[2] === "number") { + const alt = (e as any[]).slice(); + alt[2] = new Date(e[2]); + e = ["OR", e, alt]; + } + return e; + } + return exp; + }); +} + +export function processTasksFilter(filter): Expression { + return map(filter, exp => { + if (!isArray(exp)) return exp; + if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { + const e = exp.slice(); + if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectID(e[2]); + else if (e[1][0] === "PARAM" && e[1][1] === "timestamp") + e[2] = new Date(e[2]); + else if (e[1][0] === "PARAM" && e[1][1] === "expiry") + e[2] = new Date(e[2]); + return e; + } + return exp; + }); +} + +export function processFaultsFilter(filter): Expression { + return map(filter, exp => { + if (!isArray(exp)) return exp; + if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { + const e = exp.slice(); + if (e[1][0] === "PARAM" && e[1][1] === "timestamp") e[2] = new Date(e[2]); + else if (e[1][0] === "PARAM" && e[1][1] === "expiry") + e[2] = new Date(e[2]); + return e; + } + return exp; + }); +} + +export function filterToMongoQuery(exp: Expression): {} { + const ops = { + OR: 0, + AND: 0, + NOT: 0, + "=": 1, + "<>": 1, + ">": 1, + ">=": 1, + "<": 1, + "<=": 1, + LIKE: 1, + "NOT LIKE": 1, + "IS NULL": 1, + "IS NOT NULL": 1 + }; + + function recursive(filter, negate, res = {}): {} { + const op = filter[0]; + + if (ops[op] === 0) { + for (let i = 1; i < filter.length; ++i) { + if (!isArray(filter[i]) || ops[filter[i][0]] == null) + throw new Error(`Invalid expression in ${op} clause`); + } + + if ((!negate && op === "AND") || (negate && op === "OR")) { + res["$and"] = res["$and"] || []; + for (let i = 1; i < filter.length; ++i) + res["$and"].push(recursive(filter[i], negate)); + } else if ((!negate && op === "OR") || (negate && op === "AND")) { + res["$or"] = res["$or"] || []; + + for (let i = 1; i < filter.length; ++i) + res["$or"].push(recursive(filter[i], negate)); + } else if (op === "NOT") { + recursive(filter[1], !negate, res); + } + } else if (ops[op] === 1) { + if (isArray(filter[2])) { + throw new Error(`Right operand of ${op} clause is not a primitive`); + } else if ( + ["LIKE", "NOT LIKE"].includes(op) && + isArray(filter[1]) && + filter[1][0] === "FUNC" && + ["UPPER", "LOWER"].includes(filter[1][1]) + ) { + if ( + (filter[1][1] === "UPPER" && filter[2] !== filter[2].toUpperCase()) || + (filter[1][1] === "LOWER" && filter[2] !== filter[2].toLowerCase()) + ) { + throw new Error( + `Cannot compare ${ + filter[1][1] + }() against non-${filter[1][1].toLowerCase()}case pattern` + ); + } + } else if (!isArray(filter[1]) || filter[1][0] !== "PARAM") { + throw new Error(`Left operand of ${op} clause is not a parameter`); + } + + if (op === "=") { + const param = filter[1][1]; + const p = (res[param] = res[param] || {}); + if (negate) p["$ne"] = filter[2]; + else p["$eq"] = filter[2]; + } else if (op === "<>") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$ne"] = filter[2]; + if (param !== "_tags") p["$exists"] = true; + } else if (op === ">") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$gt"] = filter[2]; + } else if (op === ">=") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$gte"] = filter[2]; + } else if (op === "<") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$lt"] = filter[2]; + } else if (op === "<=") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$lte"] = filter[2]; + } else if (op === "IS NULL") { + const param = filter[1][1]; + res[param] = { $exists: negate }; + } else if (op === "IS NOT NULL") { + const param = filter[1][1]; + res[param] = { $exists: !negate }; + } else { + if (op === "NOT LIKE") negate = !negate; + let param; + let flags; + if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") { + param = filter[1][2][1]; + flags = "i"; + } else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") { + param = filter[1][2][1]; + flags = "i"; + } else { + param = filter[1][1]; + flags = ""; + } + const r = likePatternToRegExp(filter[2], filter[3], flags); + if (negate) res[param] = { $not: r }; + else res[param] = r; + } + } else { + throw new Error(`Unrecognized operator ${op}`); + } + + return res; + } + + const _exp = evaluate(exp, null, Date.now()); + + if (!isArray(_exp)) { + if (_exp === true) return {}; + throw new Error("Primitives are not valid queries"); + } + + return recursive(_exp, false); +} + +export function processDeviceProjection(projection: {}): {} { + if (!projection) return projection; + const p = {}; + for (const [k, v] of Object.entries(projection)) { + if (k === "DeviceID.ID") { + p["_id"] = 1; + } else if (k.startsWith("DeviceID")) { + p["_deviceId._SerialNumber"] = v; + p["_deviceId._OUI"] = v; + p["_deviceId._ProductClass"] = v; + p["_deviceId._Manufacturer"] = v; + } else if (k.startsWith("Tags")) { + p["_tags"] = v; + } else if (k.startsWith("Events")) { + p["_lastInform"] = v; + p["_registered"] = v; + p["_lastBoot"] = v; + p["_lastBootstrap"] = v; + } else { + p[k] = v; + } + } + + return p; +} + +export function processDeviceSort(sort: {}): {} { + if (!sort) return sort; + const s = {}; + for (const [k, v] of Object.entries(sort)) { + if (k === "DeviceID.ID") s["_id"] = v; + else if (k.startsWith("DeviceID.")) s[`_deviceId._${k.slice(9)}`] = v; + else if (k === "Events.Inform") s["_lastInform"] = v; + else if (k === "Events.Registered") s["_registered"] = v; + else if (k === "Events.1_BOOT") s["_lastBoot"] = v; + else if (k === "Events.0_BOOTSTRAP") s["_lastBootstrap"] = v; + else s[`${k}._value`] = v; + } + + return s; +} + +function parseDate(d): number | string { + const n = +d; + return isNaN(n) ? "" + d : n; +} + +interface FlatDevice { + [param: string]: { + object: boolean; + objectTimestamp: number; + writable: boolean; + writableTimestamp: number; + value: [string | number | boolean, string]; + valueTimestamp: number; + }; +} + +export function flattenDevice(device): FlatDevice { + function recursive(input, root, output, timestamp): void { + for (const [name, tree] of Object.entries(input)) { + if (!root) { + if (name === "_lastInform") { + output["Events.Inform"] = { + value: [parseDate(tree), "xsd:dateTime"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_registered") { + output["Events.Registered"] = { + value: [parseDate(tree), "xsd:dateTime"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_lastBoot") { + output["Events.1_BOOT"] = { + value: [parseDate(tree), "xsd:dateTime"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_lastBootstrap") { + output["Events.0_BOOTSTRAP"] = { + value: [parseDate(tree), "xsd:dateTime"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_id") { + output["DeviceID.ID"] = { + value: [tree, "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_deviceId") { + output["DeviceID.Manufacturer"] = { + value: [tree["_Manufacturer"], "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + output["DeviceID.OUI"] = { + value: [tree["_OUI"], "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + output["DeviceID.ProductClass"] = { + value: [tree["_ProductClass"], "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + output["DeviceID.SerialNumber"] = { + value: [tree["_SerialNumber"], "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_tags") { + output["Tags"] = { + writable: true, + writableTimestamp: timestamp, + object: true, + objectTimestamp: timestamp + }; + + for (const t of tree as string[]) { + output[`Tags.${t}`] = { + value: [true, "xsd:boolean"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } + } + } + + if (name.startsWith("_")) continue; + + let childrenTimestamp = timestamp; + + if (!root) childrenTimestamp = +(input["_timestamp"] || 1); + else if (+input["_timestamp"] > timestamp) + childrenTimestamp = +input["_timestamp"]; + + const attrs = {}; + if (tree["_value"] != null) { + attrs["value"] = [ + tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"], + tree["_type"] + ]; + attrs["valueTimestamp"] = +(tree["_timestamp"] || childrenTimestamp); + attrs["object"] = false; + attrs["objectTimestamp"] = childrenTimestamp; + } else if (tree["_object"] != null) { + attrs["object"] = tree["_object"]; + attrs["objectTimestamp"] = childrenTimestamp; + } + + if (tree["_writable"] != null) { + attrs["writable"] = tree["_writable"]; + attrs["writableTimestamp"] = childrenTimestamp; + } + + const r = root ? `${root}.${name}` : name; + output[r] = attrs; + + if (attrs["object"] || tree["object"] == null) + recursive(tree, r, output, childrenTimestamp); + } + } + + const newDevice = {}; + const timestamp = new Date(device["_lastInform"] || 1).getTime(); + recursive(device, "", newDevice, timestamp); + return newDevice; +} + +export function flattenFault(fault): Fault { + const f = Object.assign({}, fault); + if (f.timestamp) f.timestamp = +f.timestamp; + if (f.expiry) f.expiry = +f.expiry; + return f; +} + +export function flattenTask(task): Task { + const t = Object.assign({}, task); + t._id = "" + t._id; + if (t.timestamp) t.timestamp = +t.timestamp; + if (t.expiry) t.expiry = +t.expiry; + return t; +} + +export function mongoQueryToFilter(query): Expression { + function recursive(_query): Expression { + const expressions: Expression[] = []; + for (const [k, v] of Object.entries(_query)) { + if (k[0] === "$") { + if (k === "$and") { + const and: Expression = ["AND"]; + for (const vv of Object.values(v)) and.push(recursive(vv)); + expressions.push(and); + } else if (k === "$or") { + const or: Expression = ["OR"]; + for (const vv of Object.values(v)) or.push(recursive(vv)); + expressions.push(or); + } else { + throw new Error(`Operator ${k} not supported`); + } + } else if (k === "_tags") { + if (typeof v === "object") { + if (isArray(v)) throw new Error(`Invalid type`); + + if (v.hasOwnProperty("$ne")) + expressions.push(["IS NULL", ["PARAM", `Tags.${v["$ne"]}`]]); + else if (v.hasOwnProperty("$eq")) + expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v["$eq"]}`]]); + else throw new Error(`Invalid tag query`); + } else { + expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]); + } + } else if (k.startsWith("Tags.")) { + let exists: boolean; + if (typeof v === "boolean") exists = v; + else if (v.hasOwnProperty("$eq")) exists = !!v["$eq"]; + else if (v.hasOwnProperty("$ne")) exists = !v["$ne"]; + else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"]; + else throw new Error(`Invalid tag query`); + + expressions.push([exists ? "IS NOT NULL" : "IS NULL", ["PARAM", k]]); + } else if (typeof v === "object") { + if (isArray(v)) throw new Error(`Invalid type`); + + const exps: Expression[] = []; + for (const [kk, vv] of Object.entries(v)) { + let op; + switch (kk) { + case "$eq": + op = "="; + break; + case "$ne": + op = "<>"; + break; + case "$lt": + op = "<"; + break; + case "$lte": + op = "<="; + break; + case "$gt": + op = ">"; + break; + case "$gte": + op = ">="; + break; + default: + throw new Error(`Operator ${kk} not supported`); + } + exps.push([op, ["PARAM", k], vv]); + } + if (exps.length === 1) return exps[0]; + const and: Expression = ["AND"]; + expressions.push(and.concat(exps)); + } else { + expressions.push(["=", ["PARAM", k], v]); + } + } + if (expressions.length === 1) return expressions[0]; + const and: Expression = ["AND"]; + return and.concat(expressions); + } + + // empty filter + if (!Object.keys(query).length) return true; + + return recursive(query); +} + +export function flattenPreset(preset): {} { + const p = Object.assign({}, preset); + if (p.precondition) { + try { + // Try parse to check expression validity + parse(p.precondition); + } catch (error) { + p.precondition = mongoQueryToFilter(JSON.parse(p.precondition)); + p.precondition = p.precondition.length ? stringify(p.precondition) : ""; + } + } + + if (p.events) { + const e = []; + for (const [k, v] of Object.entries(p.events)) e.push(v ? k : `-${k}`); + p.events = e.join(", "); + } + + const provision = p.configurations[0]; + if ( + p.configurations.length !== 1 || + provision.type !== "provision" || + !provision.name || + !provision.name.length + ) + throw new Error("Invalid preset provision"); + p.provision = provision.name; + p.provisionArgs = provision.args + ? JSON.stringify(provision.args).slice(1, -1) + : ""; + delete p.configurations; + return p; +} + +export function flattenFile(file): {} { + const f = {}; + f["_id"] = file["_id"]; + if (file.metadata) { + f["metadata.fileType"] = file.metadata.fileType || ""; + f["metadata.oui"] = file.metadata.oui || ""; + f["metadata.productClass"] = file.metadata.productClass || ""; + f["metadata.version"] = file.metadata.version || ""; + } + return f; +} + +export function preProcessPreset(data): {} { + const preset = Object.assign({}, data); + + if (!preset.precondition) preset.precondition = ""; + // Try parse to check expression validity + parse(preset.precondition); + + preset.weight = parseInt(preset.weight) || 0; + + const events = {}; + if (preset.events) { + for (let e of preset.events.split(",")) { + let v = true; + e = e.trim(); + if (e.startsWith("-")) { + v = false; + e = e.slice(1).trim(); + } + if (e) events[e] = v; + } + } + + preset.events = events; + + if (!preset.provision) throw new Error("Invalid preset provision"); + + const configuration = { + type: "provision", + name: preset.provision, + args: null + }; + + if (preset.provisionArgs) + configuration.args = JSON.parse(`[${preset.provisionArgs}]`); + + delete preset.provision; + delete preset.provisionArgs; + preset.configurations = [configuration]; + return preset; +} diff --git a/lib/nbi.js b/lib/nbi.ts similarity index 93% rename from lib/nbi.js rename to lib/nbi.ts index e63395a..84ea65f 100644 --- a/lib/nbi.js +++ b/lib/nbi.ts @@ -16,20 +16,18 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const url = require("url"); -const mongodb = require("mongodb"); -const querystring = require("querystring"); -const vm = require("vm"); -const childProcess = require("child_process"); -const config = require("./config"); -const db = require("./db"); -const query = require("./query"); -const apiFunctions = require("./api-functions"); -const cache = require("./cache"); - -const VERSION = require("../package.json").version; +import * as url from "url"; +import * as mongodb from "mongodb"; +import * as querystring from "querystring"; +import * as vm from "vm"; +import { exec } from "child_process"; +import * as config from "./config"; +import * as db from "./db"; +import * as query from "./query"; +import * as apiFunctions from "./api-functions"; +import * as cache from "./cache"; +import { version as VERSION } from "../package.json"; const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/; const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/; @@ -44,7 +42,7 @@ const PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-_%]+)\/?$/; const VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-_%]+)\/?$/; const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/; -function throwError(err, httpResponse) { +function throwError(err, httpResponse?): never { if (httpResponse) { httpResponse.writeHead(500, { Connection: "close" }); httpResponse.end(`${err.name}: ${err.message}`); @@ -52,17 +50,20 @@ function throwError(err, httpResponse) { throw err; } -function listener(request, response) { +export function listener(request, response): void { const chunks = []; let bytes = 0; response.setHeader("GenieACS-Version", VERSION); - request.addListener("data", chunk => { - chunks.push(chunk); - bytes += chunk.length; - }); + request.addListener( + "data", + (chunk): void => { + chunks.push(chunk); + bytes += chunk.length; + } + ); - function getBody() { + function getBody(): Buffer { // Write all chunks into a Buffer const body = Buffer.allocUnsafe(bytes); let offset = 0; @@ -81,7 +82,7 @@ function listener(request, response) { PRESETS_REGEX.exec(urlParts.pathname)[1] ); if (request.method === "PUT") { - const preset = JSON.parse(body); + const preset = JSON.parse(body.toString()); preset._id = presetName; db.presetsCollection.replaceOne( { _id: presetName }, @@ -118,7 +119,7 @@ function listener(request, response) { OBJECTS_REGEX.exec(urlParts.pathname)[1] ); if (request.method === "PUT") { - const object = JSON.parse(body); + const object = JSON.parse(body.toString()); object._id = objectName; db.objectsCollection.replaceOne( { _id: objectName }, @@ -321,7 +322,7 @@ function listener(request, response) { DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1] ); if (body.length) { - const task = JSON.parse(body); + const task = JSON.parse(body.toString()); task.device = deviceId; apiFunctions.insertTasks(task, err => { if (err) return void throwError(err, response); @@ -341,7 +342,7 @@ function listener(request, response) { const taskTimeout = (urlParts.query.timeout && - parseInt(urlParts.query.timeout)) || + parseInt(urlParts.query.timeout as string)) || config.get("DEVICE_ONLINE_THRESHOLD", deviceId); apiFunctions.watchTask( @@ -500,15 +501,18 @@ function listener(request, response) { const uploadStream = bucket.openUploadStreamWithId(filename, filename, { metadata: metadata }); - uploadStream.end(body, err => { - if (err) return void throwError(err, response); + uploadStream.on("error", err => { + throwError(err, response); + }); + + uploadStream.end(body, () => { response.writeHead(201); response.end(); }); } else if (request.method === "DELETE") { const bucket = new mongodb.GridFSBucket(db.client.db()); - bucket.delete(filename, err => { + bucket.delete((filename as unknown) as mongodb.ObjectId, err => { if (err) return void throwError(err, response); response.writeHead(200); @@ -520,7 +524,7 @@ function listener(request, response) { } } else if (PING_REGEX.test(urlParts.pathname)) { const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]); - childProcess.exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => { + exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => { if (err) { response.writeHead(404, { "Cache-Control": "no-cache" }); response.end(`${err.name}: ${err.message}`); @@ -578,7 +582,7 @@ function listener(request, response) { let q = {}; if (urlParts.query.query) { try { - q = JSON.parse(urlParts.query.query); + q = JSON.parse(urlParts.query.query as string); } catch (err) { response.writeHead(400); response.end(`${err.name}: ${err.message}`); @@ -607,14 +611,14 @@ function listener(request, response) { let projection = null; if (urlParts.query.projection) { projection = {}; - for (const p of urlParts.query.projection.split(",")) + for (const p of (urlParts.query.projection as string).split(",")) projection[p.trim()] = 1; } const cur = collection.find(q, { projection: projection }); if (urlParts.query.sort) { - const s = JSON.parse(urlParts.query.sort); + const s = JSON.parse(urlParts.query.sort as string); const sort = {}; for (const [k, v] of Object.entries(s)) { if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices") @@ -625,9 +629,11 @@ function listener(request, response) { cur.sort(sort); } - if (urlParts.query.skip) cur.skip(parseInt(urlParts.query.skip)); + if (urlParts.query.skip) + cur.skip(parseInt(urlParts.query.skip as string)); - if (urlParts.query.limit) cur.limit(parseInt(urlParts.query.limit)); + if (urlParts.query.limit) + cur.limit(parseInt(urlParts.query.limit as string)); cur.count(false, (err, total) => { if (err) return void throwError(err); @@ -661,5 +667,3 @@ function listener(request, response) { } }); } - -exports.listener = listener; diff --git a/lib/query.js b/lib/query.ts similarity index 60% rename from lib/query.js rename to lib/query.ts index 8abd9f5..da3220b 100644 --- a/lib/query.js +++ b/lib/query.ts @@ -16,11 +16,10 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const common = require("./common"); +import * as common from "./common"; -function stringToRegexp(input, flags) { +function stringToRegexp(input, flags?): RegExp | false { if (input.indexOf("*") === -1) return false; let output = input.replace(/[[\]\\^$.|?+()]/, "\\$&"); @@ -34,7 +33,7 @@ function stringToRegexp(input, flags) { return new RegExp(output, flags); } -function normalize(input) { +function normalize(input): any { if (common.typeOf(input) === common.STRING_TYPE) { const vals = [input]; const m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input); @@ -53,7 +52,7 @@ function normalize(input) { return input; } -function expandValue(value) { +export function expandValue(value): any[] { if (common.typeOf(value) === common.ARRAY_TYPE) { let a = []; for (const j of value) a = a.concat(expandValue(j)); @@ -89,7 +88,7 @@ function expandValue(value) { return objs; } -function permute(param, val) { +function permute(param, val): any[] { const conditions = []; const values = expandValue(val); @@ -104,12 +103,12 @@ function permute(param, val) { return conditions; } -function expand(query) { +export function expand(query): {} { const newQuery = {}; for (const [k, v] of Object.entries(query)) { if (k[0] === "$") { // Operator - newQuery[k] = v.map(e => expand(e)); + newQuery[k] = (v as any[]).map(e => expand(e)); } else { const conditions = permute(k, v); if (conditions.length > 1) { @@ -130,7 +129,7 @@ function expand(query) { return newQuery; } -function testExpressions(params, expressions, lop) { +function testExpressions(params, expressions, lop): boolean { for (const f of expressions) { const res = test(params, f); switch (lop) { @@ -157,7 +156,7 @@ function testExpressions(params, expressions, lop) { } } -function test(params, query) { +export function test(params, query): boolean { let res; for (const [k, v] of Object.entries(query)) { if (k.charAt(0) === "$") { @@ -170,7 +169,8 @@ function test(params, query) { // TODO comparing array to regex, array to array, and object to object res = value.indexOf(v) !== -1; } else { - if (common.typeOf(v) === common.REGEXP_TYPE) res = v.test(value); + if (common.typeOf(v) === common.REGEXP_TYPE) + res = (v as RegExp).test(value); else res = v === value; } } else { @@ -216,141 +216,11 @@ function test(params, query) { return true; } -function matchType(src, dst) { - const type = typeof src; - if (type === "string") { - return String(dst); - } else if (type === "number") { - if (+dst === parseFloat(dst)) return +dst; - else if (!isNaN(Date.parse(dst))) return Date.parse(dst); - } else if (type === "boolean") { - const v = String(dst) - .trim() - .toLowerCase(); - if (v === "true" || v === "1") return true; - else if (v === "false" || v === "0") return false; - } - return dst; -} - -function testFilter(obj, filter) { - for (const [k, v] of Object.entries(filter)) { - const [param, op] = k.split(/([^a-zA-Z0-9\-_.].*)/, 2); - const val = matchType(obj[param], v); - switch (op) { - case "=": - case undefined: - if (obj[param] !== val) return false; - break; - case ">": - if (!(obj[param] > val)) return false; - break; - case ">=": - if (!(obj[param] >= val)) return false; - break; - case "<": - if (!(obj[param] < val)) return false; - break; - case "<=": - if (!(obj[param] <= val)) return false; - break; - case "!": - if (obj[param] === val) return false; - break; - default: - throw new Error(`Unrecognized operator ${op}`); - } - } - return true; -} - -function convertMongoQueryToFilters(query, filters) { - filters = filters || {}; - for (let [k, v] of Object.entries(query)) { - if (k[0] === "$") { - if (k === "$and") - for (const vv of v) convertMongoQueryToFilters(vv, filters); - else throw new Error(`Operator ${k} not supported`); - } else if (k === "_tags") { - if (common.typeOf(v) === common.OBJECT_TYPE) { - for (let [kk, vv] of Object.entries(v)) { - vv = vv.replace(/[^a-zA-Z0-9-]+/g, "_"); - if (kk === "$ne") filters[`Tags.${vv}!`] = true; - else throw new Error(`Operator ${kk} not supported`); - } - } else { - v = v.replace(/[^a-zA-Z0-9-]+/g, "_"); - filters[`Tags.${v}`] = true; - } - } else { - switch (k) { - case "_id": - k = "DeviceID.ID"; - break; - case "_deviceId._Manufacturer": - k = "DeviceID.Manufacturer"; - break; - case "_deviceId._OUI": - k = "DeviceID.OUI"; - break; - case "_deviceId._ProductClass": - k = "DeviceID.ProductClass"; - break; - case "_deviceId._SerialNumber": - k = "DeviceID.SerialNumber"; - break; - case "_lastInform": - k = "Events.Inform"; - break; - case "_lastBootstrap": - k = "Events.0_BOOTSTRAP"; - break; - case "_lastBoot": - k = "Events.1_BOOT"; - break; - case "_registered": - k = "Events.Registered"; - } - - if (common.typeOf(v) === common.OBJECT_TYPE) { - for (const [kk, vv] of Object.entries(v)) { - switch (kk) { - case "$eq": - filters[k] = vv; - break; - case "$ne": - filters[`${k}!`] = vv; - break; - case "$lt": - filters[`${k}<`] = vv; - break; - case "$lte": - filters[`${k}<=`] = vv; - break; - case "$gt": - filters[`${k}>`] = vv; - break; - case "$gte": - filters[`${k}>=`] = vv; - break; - default: - throw new Error(`Oprator ${kk} not supported`); - } - } - } else { - filters[k] = v; - } - } - } - - return filters; -} - -function sanitizeQueryTypes(query, types) { +export function sanitizeQueryTypes(query, types): {} { for (const [k, v] of Object.entries(query)) { if (k[0] === "$") { // Logical operator - for (const vv of v) sanitizeQueryTypes(vv, types); + for (const vv of v as any[]) sanitizeQueryTypes(vv, types); } else if (k in types) { if (common.typeOf(v) === common.OBJECT_TYPE) { for (const [kk, vv] of Object.entries(v)) { @@ -383,9 +253,3 @@ function sanitizeQueryTypes(query, types) { return query; } - -exports.expand = expand; -exports.test = test; -exports.sanitizeQueryTypes = sanitizeQueryTypes; -exports.convertMongoQueryToFilters = convertMongoQueryToFilters; -exports.testFilter = testFilter; diff --git a/lib/sandbox.js b/lib/sandbox.ts similarity index 83% rename from lib/sandbox.js rename to lib/sandbox.ts index fe796c5..f1cbb36 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.ts @@ -16,17 +16,15 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const vm = require("vm"); - -const seedrandom = require("seedrandom"); - -const common = require("./common"); -const device = require("./device"); -const extensions = require("./extensions"); -const logger = require("./logger"); -const scheduling = require("./scheduling"); +import * as vm from "vm"; +import seedrandom from "seedrandom"; +import * as device from "./device"; +import * as extensions from "./extensions"; +import * as logger from "./logger"; +import * as scheduling from "./scheduling"; +import Path from "./common/path"; +import { Fault, SessionContext } from "./types"; // Used for throwing to exit user script and commit const COMMIT = Symbol(); @@ -41,7 +39,7 @@ const context = vm.createContext(); let state; const runningExtensions = new WeakMap(); -function runExtension(sessionContext, key, extCall, callback) { +function runExtension(sessionContext, key, extCall, callback): void { let re = runningExtensions.get(sessionContext); if (!re) { re = {}; @@ -62,13 +60,23 @@ function runExtension(sessionContext, key, extCall, callback) { } class SandboxDate { - constructor() { - if (arguments.length) return new Date(...arguments); + public constructor( + ...argumentList: [ + number?, + number?, + number?, + number?, + number?, + number?, + number? + ] + ) { + if (argumentList.length) return new Date(...argumentList); return new Date(state.sessionContext.timestamp); } - static now(intervalOrCron, variance) { + public static now(intervalOrCron, variance): number { let t = state.sessionContext.timestamp; if (typeof intervalOrCron === "number") { @@ -93,7 +101,7 @@ class SandboxDate { } } -function random() { +function random(): number { if (!state.rng) state.rng = seedrandom(state.sessionContext.deviceId); return state.rng(); @@ -104,7 +112,7 @@ random.seed = function(s) { }; class ParameterWrapper { - constructor(path, attributes, unpacked, unpackedRevision) { + public constructor(path: Path, attributes, unpacked?, unpackedRevision?) { for (const attrName of attributes) { Object.defineProperty(this, attrName, { get: function() { @@ -148,7 +156,7 @@ class ParameterWrapper { if (!unpacked.length) return UNDEFINED; - return unpacked[0].join("."); + return unpacked[0].toString(); } }); @@ -189,13 +197,17 @@ class ParameterWrapper { } } -function declare(path, timestamps, values) { +function declare( + path: string, + timestamps: { [attr: string]: number }, + values: { [attr: string]: any } +): ParameterWrapper { state.uncommitted = true; if (!timestamps) timestamps = {}; if (!values) values = {}; - const parsedPath = common.parsePath(path); + const parsedPath = Path.parse(path); const declaration = { path: parsedPath, @@ -237,14 +249,14 @@ function declare(path, timestamps, values) { return new ParameterWrapper(parsedPath, attrs); } -function clear(path, timestamp, attributes) { +function clear(path: string, timestamp: number, attributes?): void { state.uncommitted = true; if (state.revision === state.maxRevision) - state.clear.push([common.parsePath(path), timestamp, attributes]); + state.clear.push([Path.parse(path), timestamp, attributes]); } -function commit() { +function commit(): void { ++state.revision; state.uncommitted = false; @@ -258,7 +270,7 @@ function commit() { } } -function ext() { +function ext(): any { ++state.extCounter; const extCall = Array.from(arguments).map(String); const key = `${state.revision}: ${JSON.stringify(extCall)}`; @@ -270,19 +282,19 @@ function ext() { throw EXT; } -function log(msg, meta) { +function log(msg: string, meta: {}): void { if (state.revision === state.maxRevision && state.extCounter >= 0) { const details = Object.assign({}, meta, { sessionContext: state.sessionContext, message: `Script: ${msg}` }); - delete details.hostname; - delete details.pid; - delete details.name; - delete details.version; - delete details.deviceId; - delete details.remoteAddress; + delete details["hostname"]; + delete details["pid"]; + delete details["name"]; + delete details["version"]; + delete details["deviceId"]; + delete details["remoteAddress"]; logger.accessInfo(details); } @@ -300,12 +312,12 @@ context.random = random; vm.runInContext("Math.random = random;", context); delete context.random; -function errorToFault(err) { +function errorToFault(err: Error): Fault { if (!err) return null; if (!err.name) return { code: "script", message: `${err}` }; - const fault = { + const fault: Fault = { code: `script.${err.name}`, message: err.message, detail: { @@ -315,27 +327,31 @@ function errorToFault(err) { }; if (err.stack) { - fault.detail.stack = err.stack; + fault.detail["stack"] = err.stack; // Trim the stack trace at the self-executing anonymous wrapper function - const stackTrimIndex = fault.detail.stack.match( + const stackTrimIndex = fault.detail["stack"].match( /\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/ ); - if (stackTrimIndex) - fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index); + if (stackTrimIndex) { + fault.detail["stack"] = fault.detail["stack"].slice( + 0, + stackTrimIndex.index + ); + } } return fault; } -function run( - script, +export function run( + script: vm.Script, globals, - sessionContext, - startRevision, - maxRevision, + sessionContext: SessionContext, + startRevision: number, + maxRevision: number, callback, extCounter = 0 -) { +): void { state = { sessionContext: sessionContext, revision: startRevision, @@ -416,5 +432,3 @@ function run( } } } - -exports.run = run; diff --git a/lib/scheduling.js b/lib/scheduling.ts similarity index 80% rename from lib/scheduling.js rename to lib/scheduling.ts index 4329ac0..a9293ec 100644 --- a/lib/scheduling.js +++ b/lib/scheduling.ts @@ -16,13 +16,11 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const crypto = require("crypto"); +import * as crypto from "crypto"; +import * as later from "later"; -const later = require("later"); - -function md532(str) { +function md532(str): number { const digest = crypto .createHash("md5") .update(str) @@ -35,22 +33,22 @@ function md532(str) { ); } -function variance(deviceId, vrnc) { +export function variance(deviceId, vrnc): number { return (md532(deviceId) >>> 0) % vrnc; } -function interval(timestamp, intrvl, offset = 0) { +export function interval(timestamp, intrvl, offset = 0): number { return Math.trunc((timestamp + offset) / intrvl) * intrvl - offset; } -function parseCron(cronExp) { +export function parseCron(cronExp): any { const parts = cronExp.trim().split(/\s+/); if (parts.length === 5) parts.unshift("*"); return later.schedule(later.parse.cron(parts.join(" "), true)); } -function cron(timestamp, schedule, offset = 0) { +export function cron(timestamp, schedule, offset = 0): number[] { // TODO later.js doesn't throw erorr if expression is invalid! const ret = [0, 0]; @@ -62,8 +60,3 @@ function cron(timestamp, schedule, offset = 0) { return ret; } - -exports.variance = variance; -exports.interval = interval; -exports.parseCron = parseCron; -exports.cron = cron; diff --git a/lib/server.js b/lib/server.js deleted file mode 100644 index 64be8d8..0000000 --- a/lib/server.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright 2013-2018 Zaid Abdulla - * - * This file is part of GenieACS. - * - * GenieACS is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * GenieACS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with GenieACS. If not, see . - */ -"use strict"; - -const service = process.argv[2]; - -if (service == null) throw new Error("Missing argument cwmp, fs, or nbi"); - -const cluster = require("cluster"); -const path = require("path"); -const fs = require("fs"); -const http = require("http"); -const https = require("https"); -const logger = require("./logger"); -logger.init(service, require("../package.json").version); -const config = require("./config"); -const db = require("./db"); -const cache = require("./cache"); -const extensions = require("./extensions"); - -const networkInterface = config.get(`${service.toUpperCase()}_INTERFACE`); -const port = config.get(`${service.toUpperCase()}_PORT`); -const useHttps = config.get(`${service.toUpperCase()}_SSL`); - -const { listener, onConnection } = require(`./${service}`); - -let server; - -function closeServer(timeout, callback) { - if (!server) return void callback(); - - setTimeout(() => { - if (!callback) return; - - // Ignore HTTP requests from connection that may still be open - server.removeListener("request", listener); - server.setTimeout(1); - - const cb = callback; - callback = null; - setTimeout(cb, 1000); - }, timeout).unref(); - - server.close(() => { - if (!callback) return; - - const cb = callback; - callback = null; - cb(); - }); -} - -function exit() { - setTimeout(() => { - extensions.killAll(() => { - process.exit(1); - }); - }, 30000).unref(); - - closeServer(20000, () => { - db.disconnect(); - cache.disconnect(); - extensions.killAll(); - if (cluster.worker) cluster.worker.disconnect(); - }); -} - -process.on("uncaughtException", err => { - logger.error({ - message: "Uncaught exception", - exception: err, - pid: process.pid - }); - exit(); -}); - -if (useHttps) { - const httpsKey = path.resolve(config.get("CONFIG_DIR"), `${service}.key`); - const httpsCert = path.resolve(config.get("CONFIG_DIR"), `${service}.crt`); - const httpsCa = path.resolve( - config.get("CONFIG_DIR"), - `${service}.ca-bundle` - ); - const options = { - key: fs.readFileSync(httpsKey), - cert: fs.readFileSync(httpsCert) - }; - - try { - // Use intermediate certificates if available - options.ca = fs - .readFileSync(httpsCa) - .toString() - .match(/-+BEGIN CERTIFICATE-+[0-9a-zA-Z+\-/=\s]+?-+END CERTIFICATE-+/g); - } catch (error) { - // No intermediate certificate - } - server = https.createServer(options, listener); - if (onConnection != null) server.on("secureConnection", onConnection); -} else { - server = http.createServer(listener); - if (onConnection != null) server.on("connection", onConnection); -} - -db.connect(err => { - if (err) throw err; - - cache.connect(err => { - if (err) throw err; - - server.listen(port, networkInterface); - }); -}); - -process.on("SIGINT", exit); -process.on("SIGTERM", exit); diff --git a/lib/server.ts b/lib/server.ts new file mode 100644 index 0000000..95440d1 --- /dev/null +++ b/lib/server.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2013-2018 Zaid Abdulla + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ + +import * as fs from "fs"; +import * as http from "http"; +import * as https from "https"; + +let server: http.Server | https.Server; +let listener: (...args) => void; + +function closeServer(timeout, callback): void { + if (!server) return void callback(); + + setTimeout(() => { + if (!callback) return; + + // Ignore HTTP requests from connection that may still be open + server.removeListener("request", listener); + server.setTimeout(1); + + const cb = callback; + callback = null; + setTimeout(cb, 1000); + }, timeout).unref(); + + server.close(() => { + if (!callback) return; + + const cb = callback; + callback = null; + cb(); + }); +} + +export function start( + port, + networkInterface, + ssl, + _listener, + onConnection? +): void { + listener = _listener; + + if (ssl) { + const options = { + key: fs.readFileSync(ssl.key), + cert: fs.readFileSync(ssl.cert), + ca: null + }; + + try { + // Use intermediate certificates if available + options.ca = fs + .readFileSync(ssl.ca) + .toString() + .match(/-+BEGIN CERTIFICATE-+[0-9a-zA-Z+\-/=\s]+?-+END CERTIFICATE-+/g); + } catch (error) { + // No intermediate certificate + } + server = https.createServer(options, listener); + if (onConnection != null) server.on("secureConnection", onConnection); + } else { + server = http.createServer(listener); + if (onConnection != null) server.on("connection", onConnection); + } + + server.listen(port, networkInterface); +} + +export function stop(): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("Could not close server in a timely manner")); + }, 30000).unref(); + closeServer(20000, resolve); + }); +} diff --git a/lib/session.js b/lib/session.ts similarity index 80% rename from lib/session.js rename to lib/session.ts index 074bd2e..5981d56 100644 --- a/lib/session.js +++ b/lib/session.ts @@ -16,25 +16,37 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const config = require("./config"); -const common = require("./common"); -const db = require("./db"); -const device = require("./device"); -const sandbox = require("./sandbox"); -const localCache = require("./local-cache"); -const PathSet = require("./path-set"); -const VersionedMap = require("./versioned-map"); -const InstanceSet = require("./instance-set"); -const defaultProvisions = require("./default-provisions"); -const gpnHeuristic = require("./gpn-heuristic"); +import * as config from "./config"; +import * as db from "./db"; +import * as device from "./device"; +import * as sandbox from "./sandbox"; +import * as localCache from "./local-cache"; +import * as defaultProvisions from "./default-provisions"; +import { estimateGpnCount } from "./gpn-heuristic"; +import Path from "./common/path"; +import PathSet from "./common/path-set"; +import VersionedMap from "./versioned-map"; +import InstanceSet from "./instance-set"; +import { + Attributes, + SessionContext, + DeviceData, + VirtualParameterDeclaration, + GetAcsRequest, + SetAcsRequest, + AttributeTimestamps, + AttributeValues, + Fault, + Declaration, + Clear, + CpeResponse, + CpeFault, + CpeSetResponse, + CpeGetResponse +} from "./types"; -// Multiply by two because every iteration is two -// phases: read and update -const MAX_ITERATIONS = config.get("MAX_COMMIT_ITERATIONS") * 2; - -function initDeviceData() { +function initDeviceData(): DeviceData { return { paths: new PathSet(), timestamps: new VersionedMap(), @@ -45,9 +57,14 @@ function initDeviceData() { }; } -function init(deviceId, cwmpVersion, timeout, callback) { +export function init( + deviceId: string, + cwmpVersion: string, + timeout: number, + callback: (err: Error, sessionContext: SessionContext) => void +): void { const timestamp = Date.now(); - const sessionContext = { + const sessionContext: SessionContext = { timestamp: timestamp, deviceId: deviceId, deviceData: initDeviceData(), @@ -64,14 +81,10 @@ function init(deviceId, cwmpVersion, timeout, callback) { declarations: [] }; - localCache.getCurrentSnapshot((err, cacheSnapshot) => { - if (err) return void callback(err); - sessionContext.cacheSnapshot = cacheSnapshot; - callback(null, sessionContext); - }); + callback(null, sessionContext); } -function loadParameters(sessionContext, callback) { +function loadParameters(sessionContext: SessionContext, callback): void { if (!sessionContext.toLoad || !sessionContext.toLoad.size) return void callback(); @@ -87,7 +100,10 @@ function loadParameters(sessionContext, callback) { // Device not available in database, mark as new sessionContext.new = true; loaded = [ - [[], (1 << config.get("MAX_DEPTH", sessionContext.deviceId)) - 1] + [ + Path.parse(""), + (1 << +config.get("MAX_DEPTH", sessionContext.deviceId)) - 1 + ] ]; parameters = []; } @@ -112,7 +128,7 @@ function loadParameters(sessionContext, callback) { ); } -function generateRpcId(sessionContext) { +function generateRpcId(sessionContext: SessionContext): string { return ( sessionContext.timestamp.toString(16) + ("0" + sessionContext.cycle.toString(16)).slice(-2) + @@ -120,11 +136,11 @@ function generateRpcId(sessionContext) { ); } -function inform(sessionContext, rpcReq, callback) { +export function inform(sessionContext: SessionContext, rpcReq, callback): void { const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; - const params = [ + const params: [Path, number, Attributes][] = [ [ - ["DeviceID", "Manufacturer"], + Path.parse("DeviceID.Manufacturer"), timestamp, { object: [timestamp, 0], @@ -134,7 +150,7 @@ function inform(sessionContext, rpcReq, callback) { ], [ - ["DeviceID", "OUI"], + Path.parse("DeviceID.OUI"), timestamp, { object: [timestamp, 0], @@ -144,7 +160,7 @@ function inform(sessionContext, rpcReq, callback) { ], [ - ["DeviceID", "ProductClass"], + Path.parse("DeviceID.ProductClass"), timestamp, { object: [timestamp, 0], @@ -154,7 +170,7 @@ function inform(sessionContext, rpcReq, callback) { ], [ - ["DeviceID", "SerialNumber"], + Path.parse("DeviceID.SerialNumber"), timestamp, { object: [timestamp, 0], @@ -165,7 +181,7 @@ function inform(sessionContext, rpcReq, callback) { ]; for (const p of rpcReq.parameterList) { - const path = common.parsePath(p[0]); + const path = Path.parse(p[0]); params.push([ path, timestamp, @@ -177,7 +193,7 @@ function inform(sessionContext, rpcReq, callback) { } params.push([ - ["Events", "Inform"], + Path.parse("Events.Inform"), timestamp, { object: [timestamp, 0], @@ -188,7 +204,7 @@ function inform(sessionContext, rpcReq, callback) { for (const e of rpcReq.event) { params.push([ - ["Events", e.replace(/\s+/g, "_")], + Path.parse(`Events.${e.replace(/\s+/g, "_")}`), timestamp, { object: [timestamp, 0], @@ -199,7 +215,7 @@ function inform(sessionContext, rpcReq, callback) { } // Preload DeviceID params - loadPath(sessionContext, ["DeviceID", "*"]); + loadPath(sessionContext, Path.parse("DeviceID.*")); for (const p of params) loadPath(sessionContext, p[0]); @@ -208,7 +224,7 @@ function inform(sessionContext, rpcReq, callback) { if (sessionContext.new) { params.push([ - ["DeviceID", "ID"], + Path.parse("DeviceID.ID"), timestamp, { object: [timestamp, 0], @@ -217,7 +233,7 @@ function inform(sessionContext, rpcReq, callback) { } ]); params.push([ - ["Events", "Registered"], + Path.parse("Events.Registered"), timestamp, { object: [timestamp, 0], @@ -233,7 +249,7 @@ function inform(sessionContext, rpcReq, callback) { let toClear = null; for (const p of params) { // Don't need to clear wildcards for Events - if (p[0][0] === "Events") { + if (p[0].segments[0] === "Events") { device.set(sessionContext.deviceData, p[0], p[1], p[2]); } else { toClear = device.set( @@ -252,7 +268,11 @@ function inform(sessionContext, rpcReq, callback) { }); } -function transferComplete(sessionContext, rpcReq, callback) { +export function transferComplete( + sessionContext: SessionContext, + rpcReq, + callback +): void { const revision = (sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1; sessionContext.deviceData.timestamps.revision = revision; @@ -286,7 +306,7 @@ function transferComplete(sessionContext, rpcReq, callback) { ); } - loadPath(sessionContext, ["Downloads", instance, "*"]); + loadPath(sessionContext, Path.parse(`Downloads.${instance}.*`)); loadParameters(sessionContext, err => { if (err) return void callback(err); @@ -296,11 +316,9 @@ function transferComplete(sessionContext, rpcReq, callback) { let p; - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastDownload" - ]); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastDownload`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -309,11 +327,9 @@ function transferComplete(sessionContext, rpcReq, callback) { toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastFileType" - ]); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastFileType`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -322,11 +338,9 @@ function transferComplete(sessionContext, rpcReq, callback) { toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastFileName" - ]); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastFileName`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -335,11 +349,9 @@ function transferComplete(sessionContext, rpcReq, callback) { toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastTargetFileName" - ]); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastTargetFileName`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -348,11 +360,9 @@ function transferComplete(sessionContext, rpcReq, callback) { toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "StartTime" - ]); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.StartTime`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -361,11 +371,9 @@ function transferComplete(sessionContext, rpcReq, callback) { toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "CompleteTime" - ]); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.CompleteTime`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -380,8 +388,12 @@ function transferComplete(sessionContext, rpcReq, callback) { }); } -function revertDownloadParameters(sessionContext, instance, callback) { - loadPath(sessionContext, ["Downloads", instance, "*"]); +function revertDownloadParameters( + sessionContext: SessionContext, + instance, + callback +): void { + loadPath(sessionContext, Path.parse(`Downloads.${instance}.*`)); loadParameters(sessionContext, err => { if (err) return void callback(err); @@ -390,19 +402,15 @@ function revertDownloadParameters(sessionContext, instance, callback) { let p; - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastDownload" - ]); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastDownload`) + ); const lastDownload = sessionContext.deviceData.attributes.get(p); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "Download" - ]); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.Download`) + ); const toClear = device.set(sessionContext.deviceData, p, timestamp, { value: [ @@ -418,7 +426,10 @@ function revertDownloadParameters(sessionContext, instance, callback) { }); } -function timeoutOperations(sessionContext, callback) { +export function timeoutOperations( + sessionContext: SessionContext, + callback +): void { const revision = (sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1; sessionContext.deviceData.timestamps.revision = revision; @@ -435,7 +446,11 @@ function timeoutOperations(sessionContext, callback) { } const DOWNLOAD_TIMEOUT = - config.get("DOWNLOAD_TIMEOUT", sessionContext.deviceId) * 1000; + +localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.downloadTimeout", + sessionContext.configContext + ) * 1000; if (sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT) { delete sessionContext.operations[commandKey]; @@ -464,7 +479,20 @@ function timeoutOperations(sessionContext, callback) { if ((counter -= 2) === 1) callback(null, faults, operations); } -function addProvisions(sessionContext, channel, provisions) { +export function addProvisions( + sessionContext: SessionContext, + channel, + provisions +): void { + // Multiply by two because every iteration is two + // phases: read and update + const MAX_ITERATIONS = + +localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.maxCommitIterations", + sessionContext.configContext + ) * 2; + delete sessionContext.syncState; delete sessionContext.rpcRequest; sessionContext.declarations = []; @@ -508,7 +536,16 @@ function addProvisions(sessionContext, channel, provisions) { } } -function clearProvisions(sessionContext) { +export function clearProvisions(sessionContext: SessionContext): void { + // Multiply by two because every iteration is two + // phases: read and update + const MAX_ITERATIONS = + +localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.maxCommitIterations", + sessionContext.configContext + ) * 2; + if (sessionContext.revisions[sessionContext.revisions.length - 1] > 0) { sessionContext.deviceData.timestamps.collapse(1); sessionContext.deviceData.attributes.collapse(1); @@ -532,12 +569,18 @@ function clearProvisions(sessionContext) { } function runProvisions( - sessionContext, - provisions, - startRevision, - endRevision, - callback -) { + sessionContext: SessionContext, + provisions: any[][], + startRevision: number, + endRevision: number, + callback: ( + err: Error, + fault?: Fault, + ret?: any, + decs?: Declaration[], + toClear?: Clear[] + ) => void +): void { let done = true; let allDeclarations = []; let allClear = []; @@ -597,12 +640,18 @@ function runProvisions( } function runVirtualParameters( - sessionContext, - provisions, - startRevision, - endRevision, - callback -) { + sessionContext: SessionContext, + provisions: any[][], + startRevision: number, + endRevision: number, + callback: ( + err: Error, + fault?: Fault, + ret?: any, + decs?: Declaration[], + toClear?: Clear[] + ) => void +): void { let done = true; const virtualParameterUpdates = []; let allDeclarations = []; @@ -646,7 +695,11 @@ function runVirtualParameters( return void (counter = 0); } - const ret = {}; + const ret: { + writable?: number; + value?: [string | number | boolean, string?]; + } = {}; + if (_returnValue.writable != null) { ret.writable = +!!_returnValue.writable; } else if ( @@ -744,7 +797,10 @@ function runVirtualParameters( } } -function runDeclarations(sessionContext, declarations) { +function runDeclarations( + sessionContext: SessionContext, + declarations: Declaration[] +): VirtualParameterDeclaration[] { if (!sessionContext.syncState) { sessionContext.syncState = { refreshAttributes: { @@ -754,7 +810,7 @@ function runDeclarations(sessionContext, declarations) { value: new Set() }, spv: new Map(), - gpn: new Set(), + gpn: new Set(), gpnPatterns: new Map(), tags: new Map(), virtualParameterDeclarations: [], @@ -763,19 +819,21 @@ function runDeclarations(sessionContext, declarations) { downloadsToDelete: new Set(), downloadsToCreate: new InstanceSet(), downloadsValues: new Map(), - downloadsDownload: new Map() + downloadsDownload: new Map(), + reboot: 0, + factoryReset: 0 }; } - const allDeclareTimestamps = new Map(); - const allDeclareAttributeTimestamps = new Map(); - const allDeclareAttributeValues = new Map(); + const allDeclareTimestamps = new Map(); + const allDeclareAttributeTimestamps = new Map(); + const allDeclareAttributeValues = new Map(); const allVirtualParameters = localCache.getVirtualParameters( sessionContext.cacheSnapshot ); - function mergeAttributeTimestamps(p, attrs) { + function mergeAttributeTimestamps(p: Path, attrs: AttributeTimestamps): void { let cur = allDeclareAttributeTimestamps.get(p); if (!cur) { allDeclareAttributeTimestamps.set(p, attrs); @@ -787,7 +845,11 @@ function runDeclarations(sessionContext, declarations) { } } - function mergeAttributeValues(p, attrs, defer) { + function mergeAttributeValues( + p: Path, + attrs: AttributeValues, + defer: boolean + ): void { let cur = allDeclareAttributeValues.get(p); if (!cur) { if (!defer) allDeclareAttributeValues.set(p, attrs); @@ -798,23 +860,29 @@ function runDeclarations(sessionContext, declarations) { } for (const declaration of declarations) { - let path = common.addPathMeta(declaration.path); - let unpacked; + let path = declaration.path; + let unpacked: Path[]; - if ((path.alias | path.wildcard) & 1 || path[0] === "VirtualParameters") { - sessionContext.deviceData.paths.add(["VirtualParameters"]); + if ( + (path.alias | path.wildcard) & 1 || + path.segments[0] === "VirtualParameters" + ) { + sessionContext.deviceData.paths.add(Path.parse("VirtualParameters")); if ((path.alias | path.wildcard) & 2) { - sessionContext.deviceData.paths.add(["VirtualParameters", "*"]); - for (const k of Object.keys(allVirtualParameters)) - sessionContext.deviceData.paths.add(["VirtualParameters", k]); + sessionContext.deviceData.paths.add(Path.parse("VirtualParameters.*")); + for (const k of Object.keys(allVirtualParameters)) { + sessionContext.deviceData.paths.add( + Path.parse(`VirtualParameters.${k}`) + ); + } } } - if ((path.alias | path.wildcard) & 1 || path[0] === "Reboot") - sessionContext.deviceData.paths.add(["Reboot"]); + if ((path.alias | path.wildcard) & 1 || path.segments[0] === "Reboot") + sessionContext.deviceData.paths.add(Path.parse("Reboot")); - if ((path.alias | path.wildcard) & 1 || path[0] === "FactoryReset") - sessionContext.deviceData.paths.add(["FactoryReset"]); + if ((path.alias | path.wildcard) & 1 || path.segments[0] === "FactoryReset") + sessionContext.deviceData.paths.add(Path.parse("FactoryReset")); if (path.alias) { const aliasDecs = device.getAliasDeclarations( @@ -882,16 +950,14 @@ function runDeclarations(sessionContext, declarations) { minInstances = maxInstances = declaration.pathSet; } - let parent = common.addPathMeta(path.slice(0, -1)); + let parent = path.slice(0, -1); let keys; - if (Array.isArray(path[path.length - 1])) { + if (Array.isArray(path.segments[path.length - 1])) { keys = {}; - for (let i = 0; i < path[path.length - 1].length; i += 2) { - keys[path[path.length - 1][i].join(".")] = - path[path.length - 1][i + 1]; - } - } else if (path[path.length - 1] === "*") { + for (const [p, v] of path.segments[path.length - 1]) + keys[p.toString()] = v; + } else if (path.segments[path.length - 1] === "*") { keys = {}; } @@ -917,14 +983,13 @@ function runDeclarations(sessionContext, declarations) { sessionContext.deviceData, parent ); - for (let par of parentsUnpacked) { - par = sessionContext.deviceData.paths.add(par); + for (const par of parentsUnpacked) { processInstances( sessionContext, par, device.unpack( sessionContext.deviceData, - common.addPathMeta(par.concat([path[par.length]])) + par.concat(path.slice(-1)) ), keys, minInstances, @@ -944,7 +1009,11 @@ function runDeclarations(sessionContext, declarations) { ); } -function rpcRequest(sessionContext, _declarations, callback) { +export function rpcRequest( + sessionContext: SessionContext, + _declarations: Declaration[], + callback +): void { if (sessionContext.rpcRequest != null) { return void callback( null, @@ -1080,6 +1149,15 @@ function rpcRequest(sessionContext, _declarations, callback) { }); } + // Multiply by two because every iteration is two + // phases: read and update + const MAX_ITERATIONS = + +localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.maxCommitIterations", + sessionContext.configContext + ) * 2; + if (sessionContext.iteration >= MAX_ITERATIONS * (sessionContext.cycle + 1)) { return void callback(null, { code: "too_many_commits", @@ -1135,11 +1213,11 @@ function rpcRequest(sessionContext, _declarations, callback) { return false; } else if (declaration[0].length === 2) { - if (declaration[0][1] === "*") { + if (declaration[0].segments[1] === "*") { for (const k of Object.keys(allVirtualParameters)) { toClear = device.set( sessionContext.deviceData, - ["VirtualParameters", k], + Path.parse(`VirtualParameters.${k}`), timestamp, { object: [timestamp, 0] @@ -1155,7 +1233,9 @@ function rpcRequest(sessionContext, _declarations, callback) { toClear ); return false; - } else if (allVirtualParameters[declaration[0][1]]) { + } else if ( + allVirtualParameters[declaration[0].segments[1] as string] + ) { // Avoid setting on every inform as "exist" timestamp // is not saved in DB if (!sessionContext.deviceData.attributes.has(declaration[0])) { @@ -1249,12 +1329,15 @@ function rpcRequest(sessionContext, _declarations, callback) { if (index == null) { index = 0; for (const p of sessionContext.deviceData.paths.find( - ["Downloads", "*"], + Path.parse("Downloads.*"), false, true )) { - if (+p[1] > index && sessionContext.deviceData.attributes.has(p)) - index = +p[1]; + if ( + +p.segments[1] > index && + sessionContext.deviceData.attributes.has(p) + ) + index = +p.segments[1]; } } @@ -1262,7 +1345,7 @@ function rpcRequest(sessionContext, _declarations, callback) { toClear = device.set( sessionContext.deviceData, - ["Downloads"], + Path.parse("Downloads"), timestamp, { object: [timestamp, 1], writable: [timestamp, 1] }, toClear @@ -1270,7 +1353,7 @@ function rpcRequest(sessionContext, _declarations, callback) { toClear = device.set( sessionContext.deviceData, - ["Downloads", "" + index], + Path.parse(`Downloads.${index}`), timestamp, { object: [timestamp, 1], writable: [timestamp, 1] }, toClear @@ -1304,7 +1387,7 @@ function rpcRequest(sessionContext, _declarations, callback) { for (const [k, v] of Object.entries(params)) { toClear = device.set( sessionContext.deviceData, - ["Downloads", `${index}`, k], + Path.parse(`Downloads.${index}.${k}`), timestamp, { object: [timestamp, 0], @@ -1317,7 +1400,7 @@ function rpcRequest(sessionContext, _declarations, callback) { toClear = device.set( sessionContext.deviceData, - ["Downloads", `${index}`, "*"], + Path.parse(`Downloads.${index}.*`), timestamp, null, toClear @@ -1335,7 +1418,7 @@ function rpcRequest(sessionContext, _declarations, callback) { toClear ); for (const p of sessionContext.syncState.downloadsValues.keys()) { - if (p[1] === instance[1]) + if (p.segments[1] === instance.segments[1]) sessionContext.syncState.downloadsValues.delete(p); } } @@ -1422,7 +1505,7 @@ function rpcRequest(sessionContext, _declarations, callback) { toClear = device.set( sessionContext.deviceData, - ["VirtualParameters", vparams[i][0]], + Path.parse(`VirtualParameters.${vparams[i][0]}`), timestamp, vpu, toClear @@ -1435,7 +1518,7 @@ function rpcRequest(sessionContext, _declarations, callback) { }); } -function generateGetRpcRequest(sessionContext) { +function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { const syncState = sessionContext.syncState; if (!syncState) return null; @@ -1504,10 +1587,12 @@ function generateGetRpcRequest(sessionContext) { syncState.refreshAttributes.writable.clear(); if (syncState.gpn.size) { - const GPN_NEXT_LEVEL = config.get( - "GPN_NEXT_LEVEL", - sessionContext.deviceId + const GPN_NEXT_LEVEL = localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.gpnNextLevel", + sessionContext.configContext ); + const paths = Array.from(syncState.gpn.keys()).sort( (a, b) => b.length - a.length ); @@ -1535,7 +1620,7 @@ function generateGetRpcRequest(sessionContext) { const v = syncState.gpnPatterns.get(p); if (v) patterns.push([p, (v >> path.length) << path.length]); } - est = gpnHeuristic.estimateGpnCount(patterns); + est = estimateGpnCount(patterns); } if (est < Math.pow(2, Math.max(0, 8 - path.length))) { @@ -1554,24 +1639,26 @@ function generateGetRpcRequest(sessionContext) { return { name: "GetParameterNames", - parameterPath: path.concat("").join("."), + parameterPath: path.length ? path.toString() + "." : "", nextLevel: nextLevel }; } } if (syncState.refreshAttributes.value.size) { - const GPV_BATCH_SIZE = config.get( - "GPV_BATCH_SIZE", - sessionContext.deviceId + const GPV_BATCH_SIZE = localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.gpvBatchSize", + sessionContext.configContext ); - const parameterNames = []; + + const parameterNames: string[] = []; for (const path of syncState.refreshAttributes.value) { syncState.refreshAttributes.value.delete(path); // Need to check in case param is deleted or changed to object const attrs = sessionContext.deviceData.attributes.get(path); if (attrs && attrs.object && attrs.object[1] === 0) { - parameterNames.push(path); + parameterNames.push(path.toString()); if (parameterNames.length >= GPV_BATCH_SIZE) break; } } @@ -1579,14 +1666,14 @@ function generateGetRpcRequest(sessionContext) { if (parameterNames.length) { return { name: "GetParameterValues", - parameterNames: parameterNames.map(p => p.join(".")) + parameterNames: parameterNames }; } } return null; } -function generateSetRpcRequest(sessionContext) { +function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest { const syncState = sessionContext.syncState; if (!syncState) return null; @@ -1598,7 +1685,7 @@ function generateSetRpcRequest(sessionContext) { if (instance && sessionContext.deviceData.attributes.has(instance)) { return { name: "DeleteObject", - objectName: instance.concat("").join(".") + objectName: instance.toString() + "." }; } } @@ -1611,7 +1698,7 @@ function generateSetRpcRequest(sessionContext) { instances.delete(instance); return { name: "AddObject", - objectName: param.concat("").join("."), + objectName: param.toString() + ".", instanceValues: instance, next: "getInstanceKeys" }; @@ -1620,23 +1707,31 @@ function generateSetRpcRequest(sessionContext) { } // Set values - const GPV_BATCH_SIZE = config.get("GPV_BATCH_SIZE", sessionContext.deviceId); - const DATETIME_MILLISECONDS = config.get( - "DATETIME_MILLISECONDS", - sessionContext.deviceId - ); - const BOOLEAN_LITERAL = config.get( - "BOOLEAN_LITERAL", - sessionContext.deviceId + const GPV_BATCH_SIZE = localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.gpvBatchSize", + sessionContext.configContext ); - const parameterValues = []; + const DATETIME_MILLISECONDS = !!localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.datetimeMilliseconds", + sessionContext.configContext + ); + + const BOOLEAN_LITERAL = !!localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.booleanLiteral", + sessionContext.configContext + ); + + const parameterValues: [Path, string | number | boolean, string][] = []; for (const [k, v] of syncState.spv) { syncState.spv.delete(k); const attrs = sessionContext.deviceData.attributes.get(k); const curVal = attrs.value ? attrs.value[1] : null; if (curVal && attrs.writable && attrs.writable[1]) { - const val = v.slice(); + const val = v.slice() as [string | number | boolean, string]; if (!val[1]) val[1] = curVal[1]; device.sanitizeParameterValue(val); @@ -1658,7 +1753,14 @@ function generateSetRpcRequest(sessionContext) { if (parameterValues.length) { return { name: "SetParameterValues", - parameterList: parameterValues.map(p => [p[0].join("."), p[1], p[2]]), + parameterList: parameterValues.map( + p => + [p[0].toString(), p[1], p[2]] as [ + string, + string | number | boolean, + string + ] + ), DATETIME_MILLISECONDS: DATETIME_MILLISECONDS, BOOLEAN_LITERAL: BOOLEAN_LITERAL }; @@ -1669,32 +1771,34 @@ function generateSetRpcRequest(sessionContext) { const attrs = deviceData.attributes.get(p); if (!(attrs && attrs.value && t <= attrs.value[1][0])) { const fileTypeAttrs = deviceData.attributes.get( - deviceData.paths.get(p.slice(0, -1).concat("FileType")) + deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType"))) ); const fileNameAttrs = deviceData.attributes.get( - deviceData.paths.get(p.slice(0, -1).concat("FileName")) + deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileName"))) ); const targetFileNameAttrs = deviceData.attributes.get( - deviceData.paths.get(p.slice(0, -1).concat("TargetFileName")) + deviceData.paths.get( + p.slice(0, -1).concat(Path.parse("TargetFileName")) + ) ); return { name: "Download", commandKey: generateRpcId(sessionContext), - instance: p[1], + instance: p.segments[1] as string, fileType: fileTypeAttrs ? fileTypeAttrs.value - ? fileTypeAttrs.value[1][0] + ? (fileTypeAttrs.value[1][0] as string) : null : null, fileName: fileNameAttrs ? fileNameAttrs.value - ? fileNameAttrs.value[1][0] + ? (fileNameAttrs.value[1][0] as string) : null : null, targetFileName: targetFileNameAttrs ? targetFileNameAttrs.value - ? targetFileNameAttrs.value[1][0] + ? (targetFileNameAttrs.value[1][0] as string) : null : null }; @@ -1703,7 +1807,7 @@ function generateSetRpcRequest(sessionContext) { // Reboot if (syncState.reboot) { - const p = sessionContext.deviceData.paths.get(["Reboot"]); + const p = sessionContext.deviceData.paths.get(Path.parse("Reboot")); const attrs = p ? sessionContext.deviceData.attributes.get(p) : null; if (!(attrs && attrs.value && attrs.value[1][0] >= syncState.reboot)) { delete syncState.reboot; @@ -1713,7 +1817,7 @@ function generateSetRpcRequest(sessionContext) { // Factory reset if (syncState.factoryReset) { - const p = sessionContext.deviceData.paths.get(["FactoryReset"]); + const p = sessionContext.deviceData.paths.get(Path.parse("FactoryReset")); const attrs = p ? sessionContext.deviceData.attributes.get(p) : null; if ( !(attrs && attrs.value && attrs.value[1][0] >= syncState.factoryReset) @@ -1727,9 +1831,15 @@ function generateSetRpcRequest(sessionContext) { } function generateGetVirtualParameterProvisions( - sessionContext, - virtualParameterDeclarations -) { + sessionContext: SessionContext, + virtualParameterDeclarations: VirtualParameterDeclaration[] +): [ + string, + AttributeTimestamps, + AttributeValues, + AttributeTimestamps, + AttributeValues +][] { let provisions; if (virtualParameterDeclarations) { for (const declaration of virtualParameterDeclarations) { @@ -1753,7 +1863,7 @@ function generateGetVirtualParameterProvisions( if (Object.keys(dec).length) { if (!provisions) provisions = []; provisions.push([ - declaration[0][1], + declaration[0].segments[1], dec, {}, currentTimestamps, @@ -1767,9 +1877,15 @@ function generateGetVirtualParameterProvisions( } function generateSetVirtualParameterProvisions( - sessionContext, - virtualParameterDeclarations -) { + sessionContext: SessionContext, + virtualParameterDeclarations: VirtualParameterDeclaration[] +): [ + string, + AttributeTimestamps, + AttributeValues, + AttributeTimestamps, + AttributeValues +][] { let provisions; if (virtualParameterDeclarations) { for (const declaration of virtualParameterDeclarations) { @@ -1782,7 +1898,10 @@ function generateSetVirtualParameterProvisions( attrs.value && attrs.value[1] != null ) { - const val = declaration[2].value.slice(); + const val = declaration[2].value.slice() as [ + string | number | boolean, + string + ]; if (val[1] == null) val[1] = attrs.value[1][1]; device.sanitizeParameterValue(val); @@ -1797,7 +1916,7 @@ function generateSetVirtualParameterProvisions( } provisions.push([ - declaration[0][1], + declaration[0].segments[1], {}, { value: val }, currentTimestamps, @@ -1813,25 +1932,31 @@ function generateSetVirtualParameterProvisions( } function processDeclarations( - sessionContext, + sessionContext: SessionContext, allDeclareTimestamps, - allDeclareAttributeTimestamps, - allDeclareAttributeValues -) { + allDeclareAttributeTimestamps: Map, + allDeclareAttributeValues: Map +): VirtualParameterDeclaration[] { const deviceData = sessionContext.deviceData; const syncState = sessionContext.syncState; - const root = sessionContext.deviceData.paths.add([]); - const paths = deviceData.paths.find([], false, true, 99); - paths.sort((a, b) => - a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard + const root = sessionContext.deviceData.paths.add(Path.parse("")); + const paths = deviceData.paths.find(root, false, true, 99); + paths.sort( + (a, b): number => + a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard ); - const virtualParameterDeclarations = []; + const virtualParameterDeclarations = [] as VirtualParameterDeclaration[]; - function func(leafParam, leafIsObject, leafTimestamp, _paths) { + function func( + leafParam: Path, + leafIsObject: number, + leafTimestamp: number, + _paths: Path[] + ): void { const currentPath = _paths[0]; - const children = new Map(); + const children = new Map(); let declareTimestamp = 0; let declareAttributeTimestamps; let declareAttributeValues; @@ -1843,12 +1968,12 @@ function processDeclarations( for (const path of _paths) { if (path.length > currentPath.length) { - const fragment = path[currentPath.length]; + const fragment = path.segments[currentPath.length] as string; let child = children.get(fragment); if (!child) { if (path.length > currentPath.length + 1) { // This is to ensure we don't descend more than one step at a time - const p = common.addPathMeta(path.slice(0, currentPath.length + 1)); + const p = path.slice(0, currentPath.length + 1); child = [p]; } else { child = []; @@ -1904,7 +2029,11 @@ function processDeclarations( leafTimestamp = Math.max(leafTimestamp, currentTimestamp); } - switch (currentPath[0] !== "*" ? currentPath[0] : leafParam[0]) { + switch ( + currentPath.segments[0] !== "*" + ? currentPath.segments[0] + : leafParam.segments[0] + ) { case "Reboot": if (currentPath.length === 1) { if (declareAttributeValues && declareAttributeValues.value) @@ -1924,13 +2053,10 @@ function processDeclarations( declareAttributeValues && declareAttributeValues.value ) { - syncState.tags.set( - currentPath, - device.sanitizeParameterValue([ - declareAttributeValues.value[0], - "xsd:boolean" - ])[0] - ); + syncState.tags.set(currentPath, device.sanitizeParameterValue([ + declareAttributeValues.value[0], + "xsd:boolean" + ])[0] as boolean); } break; @@ -1945,7 +2071,7 @@ function processDeclarations( declareAttributeValues && declareAttributeValues.value ) { - if (currentPath[2] === "Download") { + if (currentPath.segments[2] === "Download") { syncState.downloadsDownload.set( currentPath, declareAttributeValues.value[0] @@ -2018,7 +2144,7 @@ function processDeclarations( syncState.refreshAttributes.object.add(leafParam); if (leafIsObject == null) { const f = - ((1 << syncState.gpnPatterns.length) - 1) ^ + ((1 << syncState.gpnPatterns.size) - 1) ^ ((1 << leafParam.length) - 1); syncState.gpnPatterns.set( currentPath, @@ -2087,7 +2213,11 @@ function processDeclarations( return virtualParameterDeclarations; } -function loadPath(sessionContext, path, depth) { +function loadPath( + sessionContext: SessionContext, + path: Path, + depth?: number +): boolean { depth = depth || (1 << path.length) - 1; if (sessionContext.new || !depth) return true; @@ -2095,7 +2225,8 @@ function loadPath(sessionContext, path, depth) { // Trim trailing wildcards let trimWildcard = path.length; - while (trimWildcard && path[trimWildcard - 1] === "*") --trimWildcard; + while (trimWildcard && path.segments[trimWildcard - 1] === "*") + --trimWildcard; if (trimWildcard < path.length) path = path.slice(0, trimWildcard); @@ -2123,16 +2254,17 @@ function loadPath(sessionContext, path, depth) { } function processInstances( - sessionContext, - parent, - parameters, - keys, - minInstances, - maxInstances, - defer -) { + sessionContext: SessionContext, + parent: Path, + parameters: Path[], + keys: {}, + minInstances: number, + maxInstances: number, + defer: boolean +): void { + parent = sessionContext.deviceData.paths.add(parent); let instancesToCreate, instancesToDelete; - if (parent[0] === "Downloads") { + if (parent.segments[0] === "Downloads") { if (parent.length !== 1) return; instancesToDelete = sessionContext.syncState.downloadsToDelete; instancesToCreate = sessionContext.syncState.downloadsToCreate; @@ -2182,10 +2314,14 @@ function processInstances( } } -function clear(sessionContext, toClear, callback) { +function clear( + sessionContext: SessionContext, + toClear: Clear[], + callback +): void { if (!toClear || !toClear.length) return void callback(); - const MAX_DEPTH = config.get("MAX_DEPTH", sessionContext.deviceId); + const MAX_DEPTH = +config.get("MAX_DEPTH", sessionContext.deviceId); for (const c of toClear) { if (c[1]) { @@ -2215,13 +2351,19 @@ function clear(sessionContext, toClear, callback) { }); } -function rpcResponse(sessionContext, id, rpcRes, callback) { +export function rpcResponse( + sessionContext: SessionContext, + id: string, + _rpcRes: CpeResponse, + callback +): void { if (id !== generateRpcId(sessionContext)) return void callback(new Error("Request ID not recognized")); ++sessionContext.rpcCount; - const rpcReq = sessionContext.rpcRequest; + const rpcRes = _rpcRes as CpeGetResponse & CpeSetResponse; + const rpcReq = sessionContext.rpcRequest as GetAcsRequest & SetAcsRequest; if (!rpcReq.next) { sessionContext.rpcRequest = null; @@ -2237,32 +2379,34 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { if (!parameterNames.length) { sessionContext.rpcRequest = null; } else { - sessionContext.rpcRequest = { + const r: GetAcsRequest = { name: "GetParameterValues", parameterNames: parameterNames, next: "setInstanceKeys", instanceValues: instanceValues }; + sessionContext.rpcRequest = r; } } else if (rpcReq.next === "setInstanceKeys") { - const parameterList = []; + const parameterList: [string, string | number | boolean, string][] = []; for (const p of rpcRes.parameterList) { if (p[1] !== rpcReq.instanceValues[p[0]]) { - parameterList.push( - [p[0]].concat( - device.sanitizeParameterValue([rpcReq.instanceValues[p[0]], p[2]]) - ) - ); + const v = device.sanitizeParameterValue([ + rpcReq.instanceValues[p[0]], + p[2] + ]); + parameterList.push([p[0], v[0], v[1]]); } } if (!parameterList.length) { sessionContext.rpcRequest = null; } else { - sessionContext.rpcRequest = { + const r: SetAcsRequest = { name: "SetParameterValues", parameterList: parameterList }; + sessionContext.rpcRequest = r; } } @@ -2273,7 +2417,11 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { sessionContext.deviceData.timestamps.revision = revision; sessionContext.deviceData.attributes.revision = revision; - let toClear, root, missing, params; + let toClear, + root: Path, + missing, + params: [Path, number, Attributes?][], + wildcardPath: Path; switch (rpcRes.name) { case "GetParameterValuesResponse": @@ -2286,7 +2434,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { for (const p of rpcRes.parameterList) { toClear = device.set( sessionContext.deviceData, - common.parsePath(p[0]), + Path.parse(p[0]), timestamp, { object: [timestamp, 0], value: [timestamp, p.slice(1)] }, toClear @@ -2302,10 +2450,11 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { } if (rpcReq.parameterPath.endsWith(".")) - root = common.parsePath(rpcReq.parameterPath.slice(0, -1)); - else root = common.parsePath(rpcReq.parameterPath); + root = Path.parse(rpcReq.parameterPath.slice(0, -1)); + else root = Path.parse(rpcReq.parameterPath); - params = [[root.concat("*"), timestamp]]; + wildcardPath = Path.parse("*"); + params = [[root.concat(wildcardPath), timestamp]]; // Some clients don't report all ancestors explicitly missing = {}; @@ -2317,8 +2466,9 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { if (p[0].endsWith(".")) { missing[p[0].slice(0, -1)] |= 1; - const path = common.parsePath(p[0].slice(0, -1)); - if (!rpcReq.nextLevel) params.push([path.concat("*"), timestamp]); + const path = Path.parse(p[0].slice(0, -1)); + if (!rpcReq.nextLevel) + params.push([path.concat(wildcardPath), timestamp]); params.push([ path, @@ -2328,7 +2478,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { } else { missing[p[0]] |= 1; params.push([ - common.parsePath(p[0]), + Path.parse(p[0]), timestamp, { object: [timestamp, 0], writable: [timestamp, p[1] ? 1 : 0] } ]); @@ -2338,13 +2488,13 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { for (const [k, v] of Object.entries(missing)) { if (v === 0) { // TODO consider showing a warning - const path = common.parsePath(k); + const path = Path.parse(k); params.push([ path, timestamp, { object: [timestamp, 1], writable: [timestamp, 0] } ]); - params.push([path.concat("*"), timestamp]); + params.push([path.concat(wildcardPath), timestamp]); } } @@ -2355,15 +2505,15 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { params.sort((a, b) => { let al = a[0].length; let bl = b[0].length; - if (b[0][bl - 1] === "*") bl *= -1; - if (a[0][al - 1] === "*") al *= -1; + if (b[0].segments[bl - 1] === "*") bl *= -1; + if (a[0].segments[al - 1] === "*") al *= -1; return bl - al; }); if (rpcReq.nextLevel) { loadPath(sessionContext, root, (1 << (root.length + 1)) - 1); } else { - const MAX_DEPTH = config.get("MAX_DEPTH", sessionContext.deviceId); + const MAX_DEPTH = +config.get("MAX_DEPTH", sessionContext.deviceId); loadPath(sessionContext, root, (1 << MAX_DEPTH) - 1); } @@ -2380,7 +2530,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { "VirtualParameters", "Downloads" ]) { - const p = sessionContext.deviceData.paths.get([n]); + const p = sessionContext.deviceData.paths.get(Path.parse(n)); if (p && sessionContext.deviceData.attributes.has(p)) sessionContext.deviceData.timestamps.set(p, timestamp); } @@ -2409,7 +2559,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { for (const p of rpcReq.parameterList) { toClear = device.set( sessionContext.deviceData, - common.parsePath(p[0]), + Path.parse(p[0]), timestamp + 1, { object: [timestamp + 1, 0], @@ -2424,7 +2574,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { case "AddObjectResponse": toClear = device.set( sessionContext.deviceData, - common.parsePath(rpcReq.objectName + rpcRes.instanceNumber), + Path.parse(rpcReq.objectName + rpcRes.instanceNumber), timestamp + 1, { object: [timestamp + 1, 1] }, toClear @@ -2433,7 +2583,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { case "DeleteObjectResponse": toClear = device.set( sessionContext.deviceData, - common.parsePath(rpcReq.objectName.slice(0, -1)), + Path.parse(rpcReq.objectName.slice(0, -1)), timestamp + 1, null, toClear @@ -2442,7 +2592,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { case "RebootResponse": toClear = device.set( sessionContext.deviceData, - common.parsePath("Reboot"), + Path.parse("Reboot"), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, toClear @@ -2451,7 +2601,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { case "FactoryResetResponse": toClear = device.set( sessionContext.deviceData, - common.parsePath("FactoryReset"), + Path.parse("FactoryReset"), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, toClear @@ -2460,7 +2610,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { case "DownloadResponse": toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "Download"], + Path.parse(`Download.${rpcReq.instance}.Download`), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, toClear @@ -2469,7 +2619,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { if (rpcRes.status === 0) { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "LastDownload"], + Path.parse(`Downloads.${rpcReq.instance}.LastDownload`), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] @@ -2479,7 +2629,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "LastFileType"], + Path.parse(`Downloads.${rpcReq.instance}.LastFileType`), timestamp + 1, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, toClear @@ -2487,7 +2637,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "LastFileName"], + Path.parse(`Downloads.${rpcReq.instance}.LastFileName`), timestamp + 1, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, toClear @@ -2495,7 +2645,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "LastTargetFileName"], + Path.parse(`Downloads.${rpcReq.instance}.LastTargetFileName`), timestamp + 1, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, toClear @@ -2503,7 +2653,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "StartTime"], + Path.parse(`Downloads.${rpcReq.instance}.StartTime`), timestamp + 1, { value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] }, toClear @@ -2511,7 +2661,7 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "CompleteTime"], + Path.parse(`Downloads.${rpcReq.instance}.CompleteTime`), timestamp + 1, { value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] }, toClear @@ -2548,8 +2698,13 @@ function rpcResponse(sessionContext, id, rpcRes, callback) { clear(sessionContext, toClear, callback); } -function rpcFault(sessionContext, id, faultResponse, callback) { - const rpcReq = sessionContext.rpcRequest; +export function rpcFault( + sessionContext: SessionContext, + id: string, + faultResponse: CpeFault, + callback: (err: Error, fault?: Fault) => void +): void { + const rpcReq = sessionContext.rpcRequest as GetAcsRequest & SetAcsRequest; delete sessionContext.syncState; delete sessionContext.rpcRequest; ++sessionContext.rpcCount; @@ -2562,51 +2717,48 @@ function rpcFault(sessionContext, id, faultResponse, callback) { sessionContext.deviceData.timestamps.revision = revision; sessionContext.deviceData.attributes.revision = revision; - let toClear; + let toClear: Clear[]; if (rpcReq.name === "GetParameterNames") { if (rpcReq.parameterPath) { toClear = [ - [rpcReq.parameterPath.replace(/\.$/, "").split("."), timestamp] + [Path.parse(rpcReq.parameterPath.replace(/\.$/, "")), timestamp] ]; } } else if (rpcReq.name === "GetParameterValues") { - toClear = rpcReq.parameterNames.map(p => [ - p.replace(/\.$/, "").split("."), - timestamp - ]); + toClear = rpcReq.parameterNames.map( + p => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear + ); } else if (rpcReq.name === "SetParameterValues") { - toClear = rpcReq.parameterList.map(p => [ - p[0].replace(/\.$/, "").split("."), - timestamp - ]); + toClear = rpcReq.parameterList.map( + p => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear + ); } else if (rpcReq.name === "AddObject") { - toClear = [[rpcReq.objectName.replace(/\.$/, "").split("."), timestamp]]; + toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]]; } else if (rpcReq.name === "DeleteObject") { - toClear = [[rpcReq.objectName.replace(/\.$/, "").split("."), timestamp]]; + toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]]; } if (toClear) return void clear(sessionContext, toClear, callback); } - const fault = { + const fault: Fault = { code: `cwmp.${faultResponse.detail.faultCode}`, message: faultResponse.detail.faultString, - detail: faultResponse.detail, - timestamp: sessionContext.timestamp + detail: faultResponse.detail }; callback(null, fault); } -function deserialize(sessionContextString, callback) { +export function deserialize(sessionContextString, callback): void { const sessionContext = JSON.parse(sessionContextString); for (const decs of sessionContext.declarations) - for (const d of decs) common.addPathMeta(d.path); + for (const d of decs) d.path = Path.parse(d.path); const deviceData = initDeviceData(); for (const r of sessionContext.deviceData) { - const path = deviceData.paths.add(r[0]); + const path = deviceData.paths.add(Path.parse(r[0])); if (r[1]) deviceData.loaded.set(path, r[1]); @@ -2625,43 +2777,34 @@ function deserialize(sessionContextString, callback) { }); } -function serialize(sessionContext, callback) { +export function serialize(sessionContext: SessionContext, callback): void { const deviceData = []; for (const path of sessionContext.deviceData.paths.find( - [], + Path.parse(""), false, false, 99 )) { - const e = [path]; - e[1] = sessionContext.deviceData.loaded.get(path) || 0; - e[2] = sessionContext.deviceData.trackers.get(path) || null; - e[3] = sessionContext.deviceData.timestamps.getRevisions(path) || null; - e[4] = sessionContext.deviceData.attributes.getRevisions(path) || null; + const e = [ + path.toString(), + sessionContext.deviceData.loaded.get(path) || 0, + sessionContext.deviceData.trackers.get(path) || null, + sessionContext.deviceData.timestamps.getRevisions(path) || null, + sessionContext.deviceData.attributes.getRevisions(path) || null + ]; deviceData.push(e); } - sessionContext = Object.assign({}, sessionContext); - sessionContext.deviceData = deviceData; - delete sessionContext.syncState; - delete sessionContext.toLoad; - delete sessionContext.httpRequest; - delete sessionContext.httpResponse; + const jsonSessionContext = Object.assign({}, sessionContext, { + deviceData: deviceData, + syncState: null, + toLoad: null, + httpRequest: null, + httpResponse: null + }); - const sessionContextString = JSON.stringify(sessionContext); + const sessionContextString = JSON.stringify(jsonSessionContext); callback(null, sessionContextString); } - -exports.init = init; -exports.timeoutOperations = timeoutOperations; -exports.inform = inform; -exports.transferComplete = transferComplete; -exports.addProvisions = addProvisions; -exports.clearProvisions = clearProvisions; -exports.rpcRequest = rpcRequest; -exports.rpcResponse = rpcResponse; -exports.rpcFault = rpcFault; -exports.serialize = serialize; -exports.deserialize = deserialize; diff --git a/lib/soap.js b/lib/soap.ts similarity index 83% rename from lib/soap.js rename to lib/soap.ts index 2c8b0a6..c4a963b 100644 --- a/lib/soap.js +++ b/lib/soap.ts @@ -16,13 +16,23 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const libxmljs = require("libxmljs"); +import { parseXml, Document, Element, ParserOptions } from "libxmljs"; +import * as config from "./config"; +import { version as VERSION } from "../package.json"; +import { + CpeResponse, + CpeGetResponse, + CpeSetResponse, + InformRequest, + AcsResponse, + CpeRequest, + FaultStruct, + SpvFault, + CpeFault +} from "./types"; -const config = require("./config"); - -const SERVER_NAME = `GenieACS/${require("../package.json").version}`; +const SERVER_NAME = `GenieACS/${VERSION}`; const NAMESPACES = { "1.0": { @@ -62,21 +72,36 @@ const NAMESPACES = { } }; -const LIBXMLJS_PARSE_OPTIONS = { +const LIBXMLJS_PARSE_OPTIONS: ParserOptions = { nocdata: true, - recover: config.get("XML_RECOVER"), - ignore_enc: config.get("XML_IGNORE_ENC") + recover: !!config.get("XML_RECOVER"), + /* eslint-disable-next-line @typescript-eslint/camelcase */ + ignore_enc: !!config.get("XML_IGNORE_ENC") }; -const LIBXMLJS_SAVE_OPTIONS = { - format: config.get("XML_FORMAT"), +interface WriterOptions { + declaration?: boolean; + format?: boolean; + selfCloseEmpty?: boolean; + whitespace?: boolean; + type?: "XML" | "HTML" | "XHTML"; +} + +declare module "libxmljs" { + interface Document { + toString(formatting: boolean | WriterOptions): string; + } +} + +const LIBXMLJS_SAVE_OPTIONS: WriterOptions = { + format: !!config.get("XML_FORMAT"), declaration: !config.get("XML_NO_DECL"), selfCloseEmpty: !config.get("XML_NO_EMPTY") }; let warnings; -function parseBool(v) { +function parseBool(v): boolean { v = "" + v; if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true; else if (v === "false" || v === "FALSE" || v === "False" || v === "0") @@ -84,7 +109,7 @@ function parseBool(v) { else return null; } -function event(xml) { +function event(xml): string[] { return xml .childNodes() .filter(n => n.name() === "EventStruct") @@ -97,7 +122,7 @@ function event(xml) { ); } -function parameterInfoList(xml) { +function parameterInfoList(xml): [string, boolean][] { return xml .childNodes() .filter(e => e.name() === "ParameterInfoStruct") @@ -128,7 +153,9 @@ function parameterInfoList(xml) { }); } -function parameterValueList(xml) { +function parameterValueList( + xml +): [string, string | number | boolean, string][] { return xml .childNodes() .filter(e => e.name() === "ParameterValueStruct") @@ -181,13 +208,13 @@ function parameterValueList(xml) { }); } -function GetParameterNames(xml, methodRequest) { +function GetParameterNames(xml, methodRequest): void { const el = xml.node("cwmp:GetParameterNames"); el.node("ParameterPath").text(methodRequest.parameterPath); el.node("NextLevel").text(+methodRequest.nextLevel); } -function GetParameterNamesResponse(xml) { +function GetParameterNamesResponse(xml): CpeGetResponse { return { name: "GetParameterNamesResponse", parameterList: parameterInfoList( @@ -196,7 +223,7 @@ function GetParameterNamesResponse(xml) { }; } -function GetParameterValues(xml, methodRequest) { +function GetParameterValues(xml, methodRequest): void { const el = xml.node("cwmp:GetParameterValues").node("ParameterNames"); el.attr({ "soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]` @@ -205,7 +232,7 @@ function GetParameterValues(xml, methodRequest) { for (const p of methodRequest.parameterNames) el.node("string").text(p); } -function GetParameterValuesResponse(xml) { +function GetParameterValuesResponse(xml): CpeGetResponse { return { name: "GetParameterValuesResponse", parameterList: parameterValueList( @@ -214,7 +241,7 @@ function GetParameterValuesResponse(xml) { }; } -function SetParameterValues(xml, methodRequest) { +function SetParameterValues(xml, methodRequest): void { const el = xml.node("cwmp:SetParameterValues"); const paramList = el.node("ParameterList"); paramList.attr({ @@ -244,7 +271,7 @@ function SetParameterValues(xml, methodRequest) { el.node("ParameterKey").text(methodRequest.parameterKey || ""); } -function SetParameterValuesResponse(xml) { +function SetParameterValuesResponse(xml): CpeSetResponse { return { name: "SetParameterValuesResponse", status: parseInt( @@ -256,13 +283,13 @@ function SetParameterValuesResponse(xml) { }; } -function AddObject(xml, methodRequest) { +function AddObject(xml, methodRequest): void { const el = xml.node("cwmp:AddObject"); el.node("ObjectName").text(methodRequest.objectName); el.node("ParameterKey").text(methodRequest.parameterKey || ""); } -function AddObjectResponse(xml) { +function AddObjectResponse(xml): CpeSetResponse { let instanceNumber, status; for (const c of xml.childNodes()) { switch (c.name()) { @@ -282,13 +309,13 @@ function AddObjectResponse(xml) { }; } -function DeleteObject(xml, methodRequest) { +function DeleteObject(xml, methodRequest): void { const el = xml.node("cwmp:DeleteObject"); el.node("ObjectName").text(methodRequest.objectName); el.node("ParameterKey").text(methodRequest.parameterKey || ""); } -function DeleteObjectResponse(xml) { +function DeleteObjectResponse(xml): CpeSetResponse { return { name: "DeleteObjectResponse", status: parseInt( @@ -300,28 +327,28 @@ function DeleteObjectResponse(xml) { }; } -function Reboot(xml, methodRequest) { +function Reboot(xml, methodRequest): void { const el = xml.node("cwmp:Reboot"); el.node("CommandKey").text(methodRequest.commandKey || ""); } -function RebootResponse() { +function RebootResponse(): CpeSetResponse { return { name: "RebootResponse" }; } -function FactoryReset(xml) { +function FactoryReset(xml): void { xml.node("cwmp:FactoryReset"); } -function FactoryResetResponse() { +function FactoryResetResponse(): CpeSetResponse { return { name: "FactoryResetResponse" }; } -function Download(xml, methodRequest) { +function Download(xml, methodRequest): void { const el = xml.node("cwmp:Download"); el.node("CommandKey").text(methodRequest.commandKey || ""); el.node("FileType").text(methodRequest.fileType); @@ -335,7 +362,7 @@ function Download(xml, methodRequest) { el.node("FailureURL").text(methodRequest.failureUrl || ""); } -function DownloadResponse(xml) { +function DownloadResponse(xml): CpeSetResponse { let status, startTime, completeTime; for (const c of xml.childNodes()) { switch (c.name()) { @@ -359,7 +386,7 @@ function DownloadResponse(xml) { }; } -function Inform(xml) { +function Inform(xml): InformRequest { let retryCount, evnt, parameterList; const deviceId = { Manufacturer: null, @@ -397,18 +424,18 @@ function Inform(xml) { }; } -function InformResponse(xml) { +function InformResponse(xml): void { xml .node("cwmp:InformResponse") .node("MaxEnvelopes") .text(1); } -function GetRPCMethods() { +function GetRPCMethods(): AcsResponse { return { name: "GetRPCMethods" }; } -function GetRPCMethodsResponse(xml, methodResponse) { +function GetRPCMethodsResponse(xml, methodResponse): void { const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList"); el.attr({ "soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]` @@ -417,7 +444,7 @@ function GetRPCMethodsResponse(xml, methodResponse) { for (const m of methodResponse.methodList) el.node("string").text(m); } -function TransferComplete(xml) { +function TransferComplete(xml): AcsResponse { let commandKey, _faultStruct, startTime, completeTime; for (const c of xml.childNodes()) { switch (c.name()) { @@ -445,11 +472,11 @@ function TransferComplete(xml) { }; } -function TransferCompleteResponse(xml) { +function TransferCompleteResponse(xml): void { xml.node("cwmp:TransferCompleteResponse").text(""); } -function RequestDownload(xml) { +function RequestDownload(xml): CpeRequest { return { name: "RequestDownload", fileType: xml @@ -459,12 +486,12 @@ function RequestDownload(xml) { }; } -function RequestDownloadResponse(xml) { +function RequestDownloadResponse(xml): void { xml.node("cwmp:RequestDownloadResponse").text(""); } -function faultStruct(xml) { - let faultCode, faultString, setParameterValuesFault, pn, fc, fs; +function faultStruct(xml): FaultStruct { + let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs; for (const c of xml.childNodes()) { switch (c.name()) { case "FaultCode": @@ -499,7 +526,7 @@ function faultStruct(xml) { return { faultCode, faultString, setParameterValuesFault }; } -function fault(xml) { +function fault(xml): CpeFault { let faultCode, faultString, detail; for (const c of xml.childNodes()) { switch (c.name()) { @@ -518,7 +545,18 @@ function fault(xml) { return { faultCode, faultString, detail }; } -function request(data, cwmpVersion, warn) { +export function request( + data, + cwmpVersion, + warn +): { + id: string; + cwmpVersion: string; + sessionTimeout: number; + cpeRequest?: CpeRequest; + cpeFault?: CpeFault; + cpeResponse?: CpeResponse; +} { warnings = warn; const rpc = { @@ -526,12 +564,13 @@ function request(data, cwmpVersion, warn) { cwmpVersion: cwmpVersion, sessionTimeout: null, cpeRequest: null, - cpeFault: null + cpeFault: null, + cpeResponse: null }; if (!data.length) return rpc; - const xml = libxmljs.parseXml(data, LIBXMLJS_PARSE_OPTIONS); + const xml = parseXml(data, LIBXMLJS_PARSE_OPTIONS); let headerElement, bodyElement; @@ -589,7 +628,7 @@ function request(data, cwmpVersion, warn) { rpc.cpeRequest = Inform(methodElement); break; case "GetRPCMethods": - rpc.cpeRequest = GetRPCMethods(methodElement); + rpc.cpeRequest = GetRPCMethods(); break; case "TransferComplete": rpc.cpeRequest = TransferComplete(methodElement); @@ -613,16 +652,16 @@ function request(data, cwmpVersion, warn) { rpc.cpeResponse = DeleteObjectResponse(methodElement); break; case "RebootResponse": - rpc.cpeResponse = RebootResponse(methodElement); + rpc.cpeResponse = RebootResponse(); break; case "FactoryResetResponse": - rpc.cpeResponse = FactoryResetResponse(methodElement); + rpc.cpeResponse = FactoryResetResponse(); break; case "DownloadResponse": rpc.cpeResponse = DownloadResponse(methodElement); break; case "Fault": - rpc.cpeFault = fault(methodElement, rpc.cwmpVersion); + rpc.cpeFault = fault(methodElement); break; default: throw new Error(`8000 Method not supported ${methodName}`); @@ -632,8 +671,8 @@ function request(data, cwmpVersion, warn) { return rpc; } -function createSoapEnv(cwmpVersion) { - const xml = libxmljs.Document(); +function createSoapEnv(cwmpVersion: string): Element { + const xml = new Document(); const env = xml.node("soap-env:Envelope"); for (const prefix of Object.keys(NAMESPACES[cwmpVersion])) @@ -642,20 +681,20 @@ function createSoapEnv(cwmpVersion) { return env; } -function response(rpc) { +export function response(rpc): { code: number; headers: {}; data: string } { const headers = { Server: SERVER_NAME, SOAPServer: SERVER_NAME }; - if (!rpc) return { code: 204, headers: headers, data: Buffer.allocUnsafe(0) }; + if (!rpc) return { code: 204, headers: headers, data: "" }; const env = createSoapEnv(rpc.cwmpVersion); const header = env.node("soap-env:Header"); header .node("cwmp:ID") .attr({ - "soap-env:mustUnderstand": 1 + "soap-env:mustUnderstand": "1" }) .text(rpc.id); @@ -664,16 +703,16 @@ function response(rpc) { if (rpc.acsResponse) { switch (rpc.acsResponse.name) { case "InformResponse": - InformResponse(body, rpc.acsResponse); + InformResponse(body); break; case "GetRPCMethodsResponse": GetRPCMethodsResponse(body, rpc.acsResponse); break; case "TransferCompleteResponse": - TransferCompleteResponse(body, rpc.acsResponse); + TransferCompleteResponse(body); break; case "RequestDownloadResponse": - RequestDownloadResponse(body, rpc.acsResponse); + RequestDownloadResponse(body); break; default: throw new Error(`Unknown method response type ${rpc.acsResponse.name}`); @@ -699,7 +738,7 @@ function response(rpc) { Reboot(body, rpc.acsRequest); break; case "FactoryReset": - FactoryReset(body, rpc.acsRequest); + FactoryReset(body); break; case "Download": Download(body, rpc.acsRequest); @@ -716,6 +755,3 @@ function response(rpc) { data: env.doc().toString(LIBXMLJS_SAVE_OPTIONS) }; } - -exports.request = request; -exports.response = response; diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..3323ee6 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,311 @@ +import Path from "./common/path"; +import PathSet from "./common/path-set"; +import VersionedMap from "./versioned-map"; +import InstanceSet from "./instance-set"; +import { IncomingMessage, ServerResponse } from "http"; + +export type Expression = string | number | boolean | null | any[]; + +export interface Fault { + code: string; + message: string; + detail?: + | FaultStruct + | { + name: string; + message: string; + stack?: string; + }; +} + +export interface SessionFault extends Fault { + timestamp: number; + provisions: string[][]; + retryNow?: boolean; + precondition?: boolean; + retries?: number; + expiry?: number; +} + +export interface Attributes { + object?: [number, 1 | 0]; + writable?: [number, 1 | 0]; + value?: [number, [string | number | boolean, string]]; +} + +export interface AttributeTimestamps { + object?: number; + writable?: number; + value?: number; +} + +export interface AttributeValues { + object?: boolean; + writable?: boolean; + value?: [string | number | boolean, string?]; +} + +export interface DeviceData { + paths: PathSet; + timestamps: VersionedMap; + attributes: VersionedMap; + loaded: Map; + trackers: Map; + changes: Set; +} + +export type VirtualParameterDeclaration = [ + Path, + { path?: number; object?: number; writable?: number; value?: number }?, + { + path?: [number, number]; + object?: boolean; + writable?: boolean; + value?: [string | number | boolean, string?]; + }? +]; + +export interface SyncState { + refreshAttributes: { + exist: Set; + object: Set; + writable: Set; + value: Set; + }; + spv: Map; + gpn: Set; + gpnPatterns: Map; + tags: Map; + virtualParameterDeclarations: VirtualParameterDeclaration[][]; + instancesToDelete: Map>; + instancesToCreate: Map; + downloadsToDelete: Set; + downloadsToCreate: InstanceSet; + downloadsValues: Map; + downloadsDownload: Map; + reboot: number; + factoryReset: number; +} + +export interface SessionContext { + sessionId?: string; + timestamp: number; + deviceId: string; + deviceData: DeviceData; + cwmpVersion: string; + timeout: number; + provisions: any[]; + channels: { [channel: string]: number }; + virtualParameters: [ + string, + AttributeTimestamps, + AttributeValues, + AttributeTimestamps, + AttributeValues + ][][]; + revisions: number[]; + rpcCount: number; + iteration: number; + cycle: number; + extensionsCache: any; + declarations: Declaration[][]; + faults?: { [channel: string]: SessionFault }; + retries?: { [channel: string]: number }; + cacheSnapshot?: string; + configContext?: { [name: string]: any }; + httpResponse?: ServerResponse; + httpRequest?: IncomingMessage; + faultsTouched?: { [channel: string]: boolean }; + presetCycles?: number; + toLoad?: Map; + new?: boolean; + debug?: boolean; + tasks?: Task[]; + operations?: { [commandKey: string]: Operation }; + cacheUntil?: number; + syncState?: SyncState; + lastActivity?: number; + rpcRequest?: AcsRequest; + operationsTouched?: { [commandKey: string]: 1 | 0 }; + provisionsRet?: any[]; + doneTasks?: string[]; +} + +export interface Task { + _id: string; + name: string; + parameterNames?: string[]; + parameterValues?: [string, string | number | boolean, string?][]; + objectName?: string; + fileType?: string; + fileName?: string; + targetFileName?: string; + expiry?: number; +} + +export interface Operation { + name: string; + timestamp: number; + provisions: string[][]; + channels: { [channel: string]: number }; + retries: { [channel: string]: number }; + args: { + instance: string; + fileType: string; + fileName: string; + targetFileName: string; + }; +} + +export interface AcsRequest { + name: string; + next?: string; +} + +export interface GetAcsRequest extends AcsRequest { + name: "GetParameterNames" | "GetParameterValues"; + objectName?: string; + parameterNames?: string[]; + parameterPath?: string; + nextLevel?: boolean; + instanceValues?: { [name: string]: string }; +} + +export interface SetAcsRequest extends AcsRequest { + name: + | "SetParameterValues" + | "AddObject" + | "DeleteObject" + | "FactoryReset" + | "Reboot" + | "Download"; + parameterList?: [string, string | number | boolean, string][]; + instanceValues?: { [param: string]: string | number | boolean }; + objectName?: string; + DATETIME_MILLISECONDS?: boolean; + BOOLEAN_LITERAL?: boolean; + commandKey?: string; + instance?: string; + fileType?: string; + fileSize?: number; + url?: string; + fileName?: string; + targetFileName?: string; +} + +export interface CpeResponse { + name: string; +} + +export interface SpvFault { + parameterName: string; + faultCode: string; + faultString: string; +} + +export interface FaultStruct { + faultCode: string; + faultString: string; + setParameterValuesFault?: SpvFault[]; +} + +export interface CpeFault { + faultCode: string; + faultString: string; + detail?: FaultStruct; +} + +export interface CpeGetResponse extends CpeResponse { + name: "GetParameterNamesResponse" | "GetParameterValuesResponse"; + parameterList?: + | [string, boolean][] + | [string, string | number | boolean, string][]; +} + +export interface CpeSetResponse extends CpeResponse { + name: + | "SetParameterValuesResponse" + | "AddObjectResponse" + | "DeleteObjectResponse" + | "RebootResponse" + | "FactoryResetResponse" + | "DownloadResponse"; + status?: number; + instanceNumber?: string; + startTime?: number; + completeTime?: number; +} + +export interface CpeRequest { + name: string; + fileType?: string; +} + +export interface InformRequest extends CpeRequest { + name: "Inform"; + deviceId: { + Manufacturer: string; + OUI: string; + ProductClass?: string; + SerialNumber: string; + }; + event: string[]; + retryCount: number; + parameterList: [string, string | number | boolean, string][]; +} + +export interface AcsResponse { + name: string; + commandKey?: string; + faultStruct?: FaultStruct; + startTime?: number; + completeTime?: number; +} + +export interface QueryOptions { + projection?: any; + skip?: number; + limit?: number; + sort?: { + [param: string]: number; + }; +} + +export interface Declaration { + path: Path; + pathGet: number; + pathSet?: [number, number]; + attrGet?: { object?: number; writable?: number; value?: number }; + attrSet?: { + object?: boolean; + writable?: boolean; + value?: [string | number | boolean, string?]; + }; + defer: boolean; +} + +export type Clear = [ + Path, + number, + { object?: number; writable?: number; value?: number }?, + number? +]; + +export interface Preset { + name: string; + channel: string; + schedule?: { md5: string; duration: number; schedule: any }; + events?: { [event: string]: boolean }; + precondition?: {}; + provisions: string[][]; +} + +export type PermissionSet = { + [resource: string]: { + access: number; + validate?: { + [validator: string]: boolean; + }; + filter: Expression; + }; +}[]; diff --git a/lib/ui.ts b/lib/ui.ts new file mode 100644 index 0000000..aee29af --- /dev/null +++ b/lib/ui.ts @@ -0,0 +1,160 @@ +import { Z_SYNC_FLUSH } from "zlib"; +import Koa from "koa"; +import Router from "koa-router"; +import * as jwt from "jsonwebtoken"; +import koaStatic from "koa-static"; +import koaCompress from "koa-compress"; +import koaBodyParser from "koa-bodyparser"; +import koaJwt from "koa-jwt"; +import * as config from "./config"; +import api from "./ui/api"; +import Authorizer from "./common/authorizer"; +import * as logger from "./logger"; +import * as localCache from "./local-cache"; +import { PermissionSet } from "./types"; +import { authSimple } from "./ui/api-functions"; + +declare module "koa" { + interface Request { + body: any; + } +} + +const koa = new Koa(); +const router = new Router(); + +const JWT_SECRET = "" + config.get("UI_JWT_SECRET"); +const JWT_COOKIE = "genieacs-ui-jwt"; + +function getPermissionSets(ctx): PermissionSet[] { + const allPermissions = localCache.getPermissions(ctx.state.configSnapshot); + const permissionSets = ctx.state.user.roles.map(role => + Object.values(allPermissions[role] || {}) + ); + return permissionSets; +} + +koa.on("error", async err => { + throw err; +}); + +koa.use(async (ctx, next) => { + const configSnapshot = await localCache.getCurrentSnapshot(); + ctx.state.configSnapshot = configSnapshot; + return next(); +}); + +koa.use( + koaJwt({ + secret: JWT_SECRET, + passthrough: true, + cookie: JWT_COOKIE + }) +); + +koa.use(async (ctx, next) => { + if (ctx.state.user && ctx.state.user.roles) + ctx.state.authorizer = new Authorizer(getPermissionSets(ctx)); + else ctx.state.authorizer = new Authorizer([]); + + return next(); +}); + +router.post("/login", async ctx => { + if (!JWT_SECRET) { + ctx.status = 500; + ctx.body = "UI_JWT_SECRET is not set"; + logger.error({ message: "UI_JWT_SECRET is not set" }); + return; + } + + const username = ctx.request.body.username; + const password = ctx.request.body.password; + + const log = { + message: "Log in", + context: ctx, + username: username, + method: null + }; + + function success(roles, method): void { + log.method = method; + const token = jwt.sign({ username, roles }, JWT_SECRET); + ctx.cookies.set(JWT_COOKIE, token); + ctx.body = JSON.stringify(token); + logger.accessInfo(log); + } + + function failure(): void { + ctx.status = 400; + ctx.body = "Incorrect username or password"; + log.message += " failed"; + logger.accessWarn(log); + } + + const roles = await authSimple(ctx.state.configSnapshot, username, password); + + if (roles) return void success(roles, "simple"); + + failure(); +}); + +router.post("/logout", async ctx => { + ctx.cookies.set(JWT_COOKIE); // Delete cookie + ctx.body = ""; + + logger.accessInfo({ + message: "Log out", + context: ctx + }); +}); + +koa.use(async (ctx, next) => { + if (ctx.request.type === "application/octet-stream") + ctx.disableBodyParser = true; + + return next(); +}); + +koa.use(koaBodyParser()); +router.use("/api", api.routes(), api.allowedMethods()); + +router.get("/", async ctx => { + let permissionSets = []; + if (ctx.state.user && ctx.state.user.roles) + permissionSets = getPermissionSets(ctx); + + ctx.body = ` + + + GenieACS + + + + + + + + + `; +}); + +koa.use( + koaCompress({ + flush: Z_SYNC_FLUSH + }) +); + +koa.use(router.routes()); +koa.use(koaStatic("./public")); + +export const listener = koa.callback(); diff --git a/lib/ui/api-functions.ts b/lib/ui/api-functions.ts new file mode 100644 index 0000000..a3d13d9 --- /dev/null +++ b/lib/ui/api-functions.ts @@ -0,0 +1,230 @@ +import { exec } from "child_process"; +import { ObjectID } from "mongodb"; +import * as db from "./db"; +import { del } from "../cache"; +import { getUsers } from "../local-cache"; +import { hashPassword } from "../auth"; +import { + insertTasks, + watchTask, + connectionRequest, + deleteDevice +} from "../api-functions"; + +function deleteFault(id): Promise { + const deviceId = id.split(":", 1)[0]; + const channel = id.slice(deviceId.length + 1); + return new Promise((resolve, reject) => { + Promise.all([ + db.deleteFault(id), + channel.startsWith("task_") + ? db.deleteTask(new ObjectID(channel.slice(5))) + : null + ]) + .then(() => { + del(`${deviceId}_tasks_faults_operations`, err => { + err ? reject(err) : resolve(); + }); + }) + .catch(reject); + }); +} + +export function deleteResource(resource, id): Promise { + return new Promise((resolve, reject) => { + let promise; + + switch (resource) { + case "devices": + promise = new Promise((res, rej) => { + deleteDevice(id, err => { + if (err) return void rej(err); + res(); + }); + }); + break; + + case "files": + promise = db.deleteFile(id); + break; + + case "faults": + promise = deleteFault(id); + break; + + case "provisions": + promise = db.deleteProvision(id); + break; + + case "presets": + promise = db.deletePreset(id); + break; + + case "virtualParameters": + promise = db.deleteVirtualParameter(id); + break; + + case "config": + promise = db.deleteConfig(id); + break; + + case "permissions": + promise = db.deletePermission(id); + break; + + case "users": + promise = db.deleteUser(id); + break; + } + + promise + .then(() => { + del("presets_hash", err => { + if (err) return void reject(err); + resolve(); + }); + }) + .catch(reject); + }); +} + +export function postTasks( + deviceId, + tasks, + timeout +): Promise<{ connectionRequest: string; tasks: any[] }> { + return new Promise((resolve, reject) => { + for (const task of tasks) task.device = deviceId; + + insertTasks(tasks, err => { + if (err) return void reject(err); + const statuses = tasks.map(t => { + return { _id: t._id, status: "pending" }; + }); + del(`${deviceId}_tasks_faults_operations`, err => { + if (err) return void reject(err); + connectionRequest(deviceId, err => { + if (err) { + return void resolve({ + connectionRequest: err.message, + tasks: statuses + }); + } + + const sample = tasks[tasks.length - 1]; + + // Waiting for session to finish or timeout + watchTask(deviceId, sample._id, timeout, err => { + if (err) return void reject(err); + + const promises2 = []; + for (const s of statuses) { + promises2.push(db.query("tasks", ["=", ["PARAM", "_id"], s._id])); + promises2.push( + db.query("faults", [ + "=", + ["PARAM", "_id"], + `${deviceId}:task_${s._id}` + ]) + ); + } + + Promise.all(promises2) + .then(res => { + for (const [i, r] of statuses.entries()) { + if (res[i * 2].length === 0) { + r.status = "done"; + } else if (res[i * 2 + 1].length === 1) { + r.status = "fault"; + r.fault = res[i * 2 + 1][0]; + } + db.deleteTask(r._id); + } + + resolve({ connectionRequest: "OK", tasks: statuses }); + }) + .catch(reject); + }); + }); + }); + }); + }); +} + +interface PingResponse { + packetsTransmitted: number; + packetsReceived: number; + packetLoss: string; + min: number; + avg: number; + max: number; + mdev: number; +} + +export function ping(host): Promise { + return new Promise((resolve, reject) => { + exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => { + if (err) return void reject(err); + + const m = stdout.match( + /(\d) packets transmitted, (\d) received, ([\d.%]+) packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/ + ); + if (!m) return void reject(new Error("Could not parse ping response")); + + resolve({ + packetsTransmitted: +m[1], + packetsReceived: +m[2], + packetLoss: m[3], + min: +m[4], + avg: +m[5], + max: +m[6], + mdev: +m[7] + }); + }); + }); +} + +export function putResource(resource, id, data): Promise { + return new Promise((resolve, reject) => { + let promise; + + if (resource === "presets") { + promise = db.putPreset(id, data); + } else if (resource === "provisions") { + promise = db.putProvision(id, data); + } else if (resource === "virtualParameters") { + promise = db.putVirtualParameter(id, data); + } else if (resource === "config") { + promise = db.putConfig(id, data); + } else if (resource === "permissions") { + promise = db.putPermission(id, data); + } else if (resource === "users") { + delete data.password; + delete data.salt; + promise = db.putUser(id, data); + } + + promise + .then(() => { + del("presets_hash", err => { + if (err) return void reject(err); + resolve(); + }); + }) + .catch(reject); + }); +} + +export function authSimple(snapshot, username, password): Promise { + return new Promise((resolve, reject) => { + const users = getUsers(snapshot); + const user = users[username]; + if (!user || !user.password) return void resolve(null); + hashPassword(password, user.salt) + .then(hash => { + if (hash === user.password) resolve(user.roles); + else resolve(null); + }) + .catch(reject); + }); +} diff --git a/lib/ui/api.ts b/lib/ui/api.ts new file mode 100644 index 0000000..75f421b --- /dev/null +++ b/lib/ui/api.ts @@ -0,0 +1,562 @@ +import * as stream from "stream"; +import Router from "koa-router"; +import * as db from "./db"; +import * as apiFunctions from "./api-functions"; +import { evaluate, and, extractParams } from "../common/expression"; +import { parse } from "../common/expression-parser"; +import * as logger from "../logger"; +import { getConfig } from "../local-cache"; +import { QueryOptions, Expression } from "../types"; +import { generateSalt, hashPassword } from "../auth"; +import { del } from "../cache"; + +const router = new Router(); +export default router; + +function logUnauthorizedWarning(log): void { + log.message += " not authorized"; + logger.accessWarn(log); +} + +const RESOURCE_DELETE = 1 << 0; +const RESOURCE_PUT = 1 << 1; + +const RESOURCE_IDS = { + devices: "DeviceID.ID", + presets: "_id", + provisions: "_id", + files: "_id", + virtualParameters: "_id", + config: "_id", + permissions: "_id", + users: "_id", + faults: "_id", + tasks: "_id" +}; + +const resources = { + devices: 0 | RESOURCE_DELETE, + presets: 0 | RESOURCE_DELETE | RESOURCE_PUT, + provisions: 0 | RESOURCE_DELETE | RESOURCE_PUT, + files: 0 | RESOURCE_DELETE, + virtualParameters: 0 | RESOURCE_DELETE | RESOURCE_PUT, + config: 0 | RESOURCE_DELETE | RESOURCE_PUT, + permissions: 0 | RESOURCE_DELETE | RESOURCE_PUT, + users: 0 | RESOURCE_DELETE | RESOURCE_PUT, + faults: 0 | RESOURCE_DELETE, + tasks: 0 +}; + +router.get(`/devices/:id.csv`, async ctx => { + const log = { + message: "Query device (CSV)", + context: ctx, + id: ctx.params.id + }; + + const filter = ["=", ["PARAM", RESOURCE_IDS.devices], ctx.params.id]; + + if (!ctx.state.authorizer.hasAccess("devices", 2)) { + logUnauthorizedWarning(log); + return void (ctx.status = 404); + } + + const res = await db.query("devices", filter); + if (!res[0]) return void (ctx.status = 404); + + ctx.type = "text/csv"; + ctx.attachment( + `device-${ctx.params.id}-${new Date() + .toISOString() + .replace(/[:.]/g, "")}.csv` + ); + + ctx.body = new stream.PassThrough(); + ctx.body.write( + "Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp\n" + ); + + for (const k of Object.keys(res[0]).sort()) { + const p = res[0][k]; + const row = [ + k, + p.object, + p.objectTimestamp, + p.writable, + p.writableTimestamp, + p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "", + p.value != null ? p.value[1] : "", + p.valueTimestamp + ]; + ctx.body.write(row.map(r => (r != null ? r : "")).join(",") + "\n"); + } + ctx.body.end(); + logger.accessInfo(log); +}); + +for (const [resource, flags] of Object.entries(resources)) { + router.head(`/${resource}`, async (ctx, next) => { + let filter: Expression = true; + if (ctx.request.query.filter) filter = parse(ctx.request.query.filter); + + const log = { + message: `Count ${resource}`, + context: ctx, + filter: ctx.request.query.filter, + count: null + }; + + if (!ctx.state.authorizer.hasAccess(resource, 1)) { + logUnauthorizedWarning(log); + return void next(); + } + + // Exclude temporary tasks and faults + if (resource === "tasks" || resource === "faults") { + filter = and(filter, [ + "NOT", + ["<", ["PARAM", "expiry"], Date.now() + 60000] + ]); + } + + const count = await db.count(resource, filter); + + ctx.set("X-Total-Count", `${count}`); + ctx.body = ""; + + log.count = count; + logger.accessInfo(log); + }); + + router.get(`/${resource}`, async (ctx, next) => { + const options: QueryOptions = {}; + let filter: Expression = true; + if (ctx.request.query.filter) filter = parse(ctx.request.query.filter); + if (ctx.request.query.limit) options.limit = +ctx.request.query.limit; + if (ctx.request.query.skip) options.skip = +ctx.request.query.skip; + if (ctx.request.query.sort) + options.sort = JSON.parse(ctx.request.query.sort); + if (ctx.request.query.projection) { + options.projection = ctx.request.query.projection + .split(",") + .reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {}); + } + + const log = { + message: `Query ${resource}`, + context: ctx, + filter: ctx.request.query.filter, + limit: options.limit, + skip: options.skip, + sort: options.sort, + projection: options.projection + }; + + if (!ctx.state.authorizer.hasAccess(resource, 2)) { + logUnauthorizedWarning(log); + return void next(); + } + + // Exclude temporary tasks and faults + if (resource === "tasks" || resource === "faults") { + filter = and(filter, [ + "NOT", + ["<", ["PARAM", "expiry"], Date.now() + 60000] + ]); + } + + ctx.body = new stream.PassThrough(); + ctx.type = "application/json"; + + let c = 0; + ctx.body.write("[\n"); + await db.query(resource, filter, options, obj => { + ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n"); + }); + ctx.body.end("]"); + + logger.accessInfo(log); + }); + + // CSV download + router.get(`/${resource}.csv`, async (ctx, next) => { + const options: QueryOptions = { projection: {} }; + let filter: Expression = true; + if (ctx.request.query.filter) filter = parse(ctx.request.query.filter); + if (ctx.request.query.limit) options.limit = +ctx.request.query.limit; + if (ctx.request.query.skip) options.skip = +ctx.request.query.skip; + if (ctx.request.query.sort) + options.sort = JSON.parse(ctx.request.query.sort); + + const log = { + message: `Query ${resource} (CSV)`, + context: ctx, + filter: ctx.request.query.filter, + limit: options.limit, + skip: options.skip, + sort: options.sort + }; + + if (!ctx.state.authorizer.hasAccess(resource, 2)) { + logUnauthorizedWarning(log); + return void next(); + } + + const columns = JSON.parse(ctx.request.query.columns); + const now = Date.now(); + + for (const [k, v] of Object.entries(columns)) { + const e = evaluate(parse(v), null, now); + columns[k] = e; + for (const p of extractParams(e)) options.projection[p] = 1; + } + + // Exclude temporary tasks and faults + if (resource === "tasks" || resource === "faults") { + filter = and(filter, [ + "NOT", + ["<", ["PARAM", "expiry"], Date.now() + 60000] + ]); + } + + ctx.body = new stream.PassThrough(); + ctx.type = "text/csv"; + ctx.attachment( + `${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv` + ); + + ctx.body.write( + Object.keys(columns).map(k => `"${k.replace(/"/, '""')}"`) + "\n" + ); + await db.query(resource, filter, options, obj => { + const arr = Object.values(columns).map(exp => { + const v = evaluate(exp, obj, null, e => { + if (Array.isArray(e)) { + if (e[0] === "PARAM") { + if (resource === "devices") { + if (e[1] === "Tags") { + const tags = []; + for (const p in obj) + if (p.startsWith("Tags.")) tags.push(p.slice(5)); + + return tags.join(", "); + } + } + } else if (e[0] === "FUNC") { + if (e[1] === "DATE_STRING") { + if (e[2] && !Array.isArray(e[2])) + return new Date(e[2]).toISOString(); + } + } + } + + return e; + }); + + if (Array.isArray(v) || v == null) return ""; + if (typeof v === "string") return `"${v.replace(/"/g, '""')}"`; + return v; + }); + ctx.body.write(arr.join(",") + "\n"); + }); + ctx.body.end(); + + logger.accessInfo(log); + }); + + router.head(`/${resource}/:id`, async (ctx, next) => { + const log = { + message: `Count ${resource}`, + context: ctx, + filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"` + }; + + const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id]; + if (!ctx.state.authorizer.hasAccess(resource, 2)) { + logUnauthorizedWarning(log); + return void next(); + } + + const res = await db.query(resource, filter); + + if (!res.length) return void next(); + + logger.accessInfo(log); + ctx.body = ""; + }); + + router.get(`/${resource}/:id`, async (ctx, next) => { + const log = { + message: `Query ${resource}`, + context: ctx, + filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"` + }; + + const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id]; + if (!ctx.state.authorizer.hasAccess(resource, 2)) { + logUnauthorizedWarning(log); + return void next(); + } + + const res = await db.query(resource, filter); + + if (!res.length) return void next(); + + logger.accessInfo(log); + ctx.body = res[0]; + }); + + // TODO add PUT, PATCH routes + if (flags & RESOURCE_DELETE) { + router.delete(`/${resource}/:id`, async (ctx, next) => { + const log = { + message: `Delete ${resource}`, + context: ctx, + id: ctx.params.id + }; + + const authorizer = ctx.state.authorizer; + const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id]; + if (!authorizer.hasAccess(resource, 3)) { + logUnauthorizedWarning(log); + return void next(); + } + const res = await db.query(resource, filter); + if (!res.length) return void next(); + + const validate = authorizer.getValidator(resource, res[0]); + if (!validate("delete")) { + logUnauthorizedWarning(log); + return void (ctx.status = 403); + } + + await apiFunctions.deleteResource(resource, ctx.params.id); + + logger.accessInfo(log); + + ctx.body = ""; + }); + } + + if (flags & RESOURCE_PUT) { + router.put(`/${resource}/:id`, async (ctx, next) => { + const id = ctx.params.id; + + const log = { + message: `Put ${resource}`, + context: ctx, + id: id + }; + + const authorizer = ctx.state.authorizer; + if (!authorizer.hasAccess(resource, 3)) { + logUnauthorizedWarning(log); + return void next(); + } + + const obj = ctx.request.body; + + const validate = authorizer.getValidator(resource, obj); + if (!validate("put")) { + logUnauthorizedWarning(log); + return void (ctx.status = 403); + } + + try { + await apiFunctions.putResource(resource, id, obj); + } catch (err) { + log.message += " failed"; + logger.accessWarn(log); + ctx.body = err; + return void (ctx.status = 400); + } + + logger.accessInfo(log); + + ctx.body = ""; + }); + } +} + +router.put("/files/:id", async (ctx, next) => { + const resource = "files"; + const id = ctx.params.id; + + const log = { + message: `Upload ${resource}`, + context: ctx, + id: id, + metadata: null + }; + + const authorizer = ctx.state.authorizer; + if (!authorizer.hasAccess(resource, 3)) { + logUnauthorizedWarning(log); + return void next(); + } + + const metadata = { + fileType: ctx.request.headers["metadata.filetype"] || "", + oui: ctx.request.headers["metadata.oui"] || "", + productClass: ctx.request.headers["metadata.productclass"] || "", + version: ctx.request.headers["metadata.version"] || "" + }; + + const validate = authorizer.getValidator(resource, metadata); + if (!validate("put")) { + logUnauthorizedWarning(log); + return void (ctx.status = 403); + } + + await db.putFile(id, metadata, ctx.req); + log.metadata = metadata; + logger.accessInfo(log); + + ctx.body = ""; +}); + +router.post("/devices/:id/tasks", async (ctx, next) => { + const log = { + message: "Commit tasks", + context: ctx, + deviceId: ctx.params.id, + tasks: null + }; + + const authorizer = ctx.state.authorizer; + const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id]; + if (!authorizer.hasAccess("devices", 3)) { + logUnauthorizedWarning(log); + return void next(); + } + const devices = await db.query("devices", filter); + if (!devices.length) return void next(); + + const validate = authorizer.getValidator("devices", devices[0]); + for (const t of ctx.request.body) { + if (!validate("task", t)) { + logUnauthorizedWarning(log); + return void (ctx.status = 403); + } + } + + // TODO Pass context and timestamp + const onlineThreshold = getConfig( + ctx.state.configSnapshot, + "cwmp.deviceOnlineThreshold" + ); + + const res = await apiFunctions.postTasks( + ctx.params.id, + ctx.request.body, + onlineThreshold + ); + + log.tasks = res.tasks.map(t => t._id).join(","); + + logger.accessInfo(log); + + ctx.set("Connection-Request", res.connectionRequest); + ctx.body = res.tasks; +}); + +router.post("/devices/:id/tags", async (ctx, next) => { + const log = { + message: "Update tags", + context: ctx, + deviceId: ctx.params.id, + tags: ctx.request.body + }; + + const authorizer = ctx.state.authorizer; + const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id]; + if (!authorizer.hasAccess("devices", 3)) { + logUnauthorizedWarning(log); + return void next(); + } + const res = await db.query("devices", filter); + if (!res.length) return void next(); + + const validate = authorizer.getValidator("devices", res[0]); + if (!validate("tags", ctx.request.body)) { + logUnauthorizedWarning(log); + return void (ctx.status = 403); + } + + await db.updateDeviceTags(ctx.params.id, ctx.request.body); + + logger.accessInfo(log); + + ctx.body = ""; +}); + +router.get("/ping/:host", async ctx => { + try { + ctx.body = await apiFunctions.ping(ctx.params.host); + } catch (err) { + ctx.status = 404; + ctx.response.headers["Cache-Control"] = "no-cache"; + ctx.body = `${err.name}: ${err.message}`; + } +}); + +router.put("/users/:id/password", async (ctx, next) => { + const username = ctx.params.id; + const log = { + message: "Change password", + context: ctx, + username: username + }; + + const authorizer = ctx.state.authorizer; + + if (!ctx.state.user) { + // User not logged in + if ( + !(await apiFunctions.authSimple( + ctx.state.configSnapshot, + username, + ctx.request.body.authPassword + )) + ) { + logUnauthorizedWarning(log); + ctx.status = 401; + ctx.body = "Authentication failed, check your username and password"; + return; + } + } else if (!authorizer.hasAccess("users", 3)) { + logUnauthorizedWarning(log); + return void next(); + } + + const filter = ["=", ["PARAM", RESOURCE_IDS.users], username]; + const res = await db.query("users", filter); + if (!res.length) return void next(); + + const newPassword = ctx.request.body.newPassword; + if (ctx.state.user) { + const validate = authorizer.getValidator("users", res[0]); + if (!validate("password", { password: newPassword })) { + logUnauthorizedWarning(log); + return void (ctx.status = 403); + } + } + + const salt = await generateSalt(64); + const password = await hashPassword(newPassword, salt); + await db.putUser(username, { password, salt }); + + function delCache(): Promise { + return new Promise((resolve, reject) => { + del("presets_hash", err => { + if (err) return void reject(err); + resolve(); + }); + }); + } + + await delCache(); + + logger.accessInfo(log); + ctx.body = ""; +}); diff --git a/lib/ui/db.ts b/lib/ui/db.ts new file mode 100644 index 0000000..d5c3560 --- /dev/null +++ b/lib/ui/db.ts @@ -0,0 +1,375 @@ +import { MongoClient, GridFSBucket } from "mongodb"; +import * as config from "../config"; +import * as mongodbFunctions from "../mongodb-functions"; +import * as expression from "../common/expression"; +import { QueryOptions } from "../types"; + +const CACHE_TTL = 300000; + +let _clientPromise = null; + +const RESOURCE_COLLECTION = { + files: "fs.files" +}; + +function ensureIndexes(client): void { + client + .db() + .collection("cache") + .createIndex({ expire: 1 }, { expireAfterSeconds: 0 }); +} + +function getClient(): Promise { + if (!_clientPromise) { + _clientPromise = new Promise((resolve, reject) => { + const CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL"); + MongoClient.connect( + CONNECTION_URL, + { useNewUrlParser: true }, + (err, client) => { + if (err) return void reject(err); + ensureIndexes(client); + resolve(client); + } + ); + }); + } + + return _clientPromise; +} + +export function cache(key, valueGetter: () => Promise, ttl): Promise { + return new Promise((resolve, reject) => { + getClient() + .then(client => { + const collection = client.db().collection("cache"); + collection.findOne({ _id: key }, (err, doc) => { + if (err) return void reject(err); + if (doc != null) return void resolve(JSON.parse(doc.value) as T); + valueGetter() + .then(res => { + const expire = Date.now() + (ttl || CACHE_TTL); + const cacheDoc = { + _id: key, + value: JSON.stringify(res), + expire: new Date(expire) + }; + collection.updateOne( + { _id: key }, + { $set: cacheDoc }, + { upsert: true }, + err => { + if (err) reject(err); + else resolve(res); + } + ); + }) + .catch(reject); + }); + }) + .catch(reject); + }); +} + +export function query( + resource, + filter, + options?: QueryOptions, + callback?: (doc: any) => void +): Promise { + options = options || {}; + let q; + filter = expression.evaluate(filter, null, Date.now()); + + if (Array.isArray(filter)) { + if (resource === "devices") { + filter = mongodbFunctions.processDeviceFilter(filter); + } else if (resource === "tasks") { + filter = mongodbFunctions.processTasksFilter(filter); + } else if (resource === "faults") { + filter = mongodbFunctions.processFaultsFilter(filter); + } else if (resource === "users") { + // Protect against brute force, and dictionary attacks + const params = expression.extractParams(filter); + if (params.includes("password") || params.includes("salt")) + return Promise.reject(new Error("Invalid users filter")); + } + + q = mongodbFunctions.filterToMongoQuery(filter); + } else if (filter != null && !filter) { + return Promise.resolve([]); + } + + return new Promise((resolve, reject) => { + getClient().then(client => { + const collection = client + .db() + .collection(RESOURCE_COLLECTION[resource] || resource); + const cursor = collection.find(q); + if (options.projection) { + let projection = options.projection; + if (resource === "devices") { + projection = mongodbFunctions.processDeviceProjection( + options.projection + ); + } + + if (resource === "presets") projection.configurations = 1; + cursor.project(projection); + } + + if (resource === "users") cursor.project({ password: 0, salt: 0 }); + + if (options.skip) cursor.skip(options.skip); + if (options.limit) cursor.limit(options.limit); + + if (options.sort) { + let s = Object.entries(options.sort) + .sort((a, b) => Math.abs(b[1]) - Math.abs(a[1])) + .reduce( + (obj, [k, v]) => + Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }), + {} + ); + + if (resource === "devices") s = mongodbFunctions.processDeviceSort(s); + cursor.sort(s); + } + + if (!callback) { + cursor.toArray((err, docs) => { + if (err) return reject(err); + if (resource === "devices") + docs = docs.map(d => mongodbFunctions.flattenDevice(d)); + else if (resource === "faults") + docs = docs.map(d => mongodbFunctions.flattenFault(d)); + else if (resource === "tasks") + docs = docs.map(d => mongodbFunctions.flattenTask(d)); + else if (resource === "presets") + docs = docs.map(d => mongodbFunctions.flattenPreset(d)); + else if (resource === "files") + docs = docs.map(d => mongodbFunctions.flattenFile(d)); + return resolve(docs); + }); + } else { + cursor.forEach( + doc => { + if (resource === "devices") + doc = mongodbFunctions.flattenDevice(doc); + else if (resource === "faults") + doc = mongodbFunctions.flattenFault(doc); + else if (resource === "tasks") + doc = mongodbFunctions.flattenTask(doc); + else if (resource === "presets") + doc = mongodbFunctions.flattenPreset(doc); + else if (resource === "files") + doc = mongodbFunctions.flattenFile(doc); + callback(doc); + }, + err => { + if (err) reject(err); + else resolve(); + } + ); + } + }); + }); +} + +export function count(resource, filter): Promise { + let q; + filter = expression.evaluate(filter, null, Date.now()); + + if (Array.isArray(filter)) { + if (resource === "devices") + filter = mongodbFunctions.processDeviceFilter(filter); + else if (resource === "tasks") + filter = mongodbFunctions.processTasksFilter(filter); + else if (resource === "faults") + filter = mongodbFunctions.processFaultsFilter(filter); + q = mongodbFunctions.filterToMongoQuery(filter); + } else if (filter != null && !filter) { + return Promise.resolve(0); + } + + return new Promise((resolve, reject) => { + getClient().then(client => { + const collection = client + .db() + .collection(RESOURCE_COLLECTION[resource] || resource); + collection.find(q).count((err, c) => { + if (err) reject(err); + else resolve(c); + }); + }); + }); +} + +export function updateDeviceTags(deviceId, tags): Promise { + return new Promise((resolve, reject) => { + const add = []; + const pull = []; + + for (const [tag, onOff] of Object.entries(tags)) { + if (onOff) add.push(tag); + else pull.push(tag); + } + getClient() + .then(client => { + const collection = client.db().collection("devices"); + + const object = {}; + if (add && add.length) object["$addToSet"] = { _tags: { $each: add } }; + if (pull && pull.length) object["$pullAll"] = { _tags: pull }; + collection.updateOne({ _id: deviceId }, object, err => { + if (err) return void reject(err); + resolve(); + }); + }) + .catch(reject); + }); +} + +function putResource(resource, id, object): Promise { + return new Promise((resolve, reject) => { + getClient() + .then(client => { + const collection = client + .db() + .collection(RESOURCE_COLLECTION[resource] || resource); + collection.replaceOne({ _id: id }, object, { upsert: true }, err => { + if (err) return void reject(err); + resolve(); + }); + }) + .catch(reject); + }); +} + +function deleteResource(resource, id): Promise { + return new Promise((resolve, reject) => { + getClient() + .then(client => { + const collection = client + .db() + .collection(RESOURCE_COLLECTION[resource] || resource); + collection.deleteOne({ _id: id }, err => { + if (err) return void reject(err); + resolve(); + }); + }) + .catch(reject); + }); +} + +export function putPreset(id, object): Promise { + object = mongodbFunctions.preProcessPreset(object); + return putResource("presets", id, object); +} + +export function deletePreset(id): Promise { + return deleteResource("presets", id); +} + +export function putProvision(id, object): Promise { + return putResource("provisions", id, object); +} + +export function deleteProvision(id): Promise { + return deleteResource("provisions", id); +} + +export function putVirtualParameter(id, object): Promise { + return putResource("virtualParameters", id, object); +} + +export function deleteVirtualParameter(id): Promise { + return deleteResource("virtualParameters", id); +} + +export function putConfig(id, object): Promise { + return putResource("config", id, object); +} + +export function deleteConfig(id): Promise { + return deleteResource("config", id); +} + +export function putPermission(id, object): Promise { + return putResource("permissions", id, object); +} + +export function deletePermission(id): Promise { + return deleteResource("permissions", id); +} + +export function putUser(id, object): Promise { + return new Promise((resolve, reject) => { + getClient() + .then(client => { + const collection = client.db().collection("users"); + // update instead of replace to keep the password if not set by user + collection.updateOne( + { _id: id }, + { $set: object }, + { upsert: true }, + err => { + if (err) return void reject(err); + resolve(); + } + ); + }) + .catch(reject); + }); +} + +export function deleteUser(id): Promise { + return deleteResource("users", id); +} + +export function putFile(filename, metadata, contentStream): Promise { + return new Promise((resolve, reject) => { + getClient() + .then(client => { + const bucket = new GridFSBucket(client.db()); + const uploadStream = bucket.openUploadStreamWithId(filename, filename, { + metadata: metadata + }); + uploadStream.on("error", reject); + contentStream.on("error", reject); + uploadStream.on("finish", resolve); + contentStream.pipe(uploadStream); + }) + .catch(reject); + }); +} + +export function deleteFile(filename): Promise { + return new Promise((resolve, reject) => { + getClient() + .then(client => { + const bucket = new GridFSBucket(client.db()); + bucket.delete(filename, err => { + if (err) return void reject(err); + resolve(); + }); + }) + .catch(reject); + }); +} + +export function deleteFault(id): Promise { + return deleteResource("faults", id); +} + +export function deleteTask(id): Promise { + return deleteResource("tasks", id); +} + +export function disconnect(): void { + if (_clientPromise) { + _clientPromise.then(client => { + client.close(); + }); + } +} diff --git a/lib/versioned-map.js b/lib/versioned-map.ts similarity index 78% rename from lib/versioned-map.js rename to lib/versioned-map.ts index e93ff14..cd52a1c 100644 --- a/lib/versioned-map.js +++ b/lib/versioned-map.ts @@ -16,44 +16,53 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const NONEXISTENT = new Object(); +const NONEXISTENT = Symbol(); const UNDEFINED = undefined; -class VersionedMap { - constructor() { +interface Revisions { + [rev: number]: V; + delete?: number; +} + +export default class VersionedMap { + private _sizeDiff: number[]; + private _revision: number; + private map: Map; + public dirty: number; + + public constructor() { this._sizeDiff = [0]; this._revision = 0; this.map = new Map(); this.dirty = 0; } - get size() { + public get size(): number { return this.map.size + this._sizeDiff[this.revision]; } - get revision() { + public get revision(): number { return this._revision; } - set revision(rev) { + public set revision(rev) { for (let i = this._sizeDiff.length; i <= rev; ++i) this._sizeDiff[i] = this._sizeDiff[i - 1]; this._revision = rev; } - get(key, rev = this._revision) { + public get(key: K, rev = this._revision): V { const revisions = this.map.get(key); if (!revisions) return UNDEFINED; const v = revisions[Math.min(revisions.length - 1, rev)]; if (v === NONEXISTENT) return UNDEFINED; - return v; + return v as V; } - has(key, rev = this._revision) { + public has(key: K, rev = this._revision): boolean { const revisions = this.map.get(key); if (!revisions) return false; @@ -62,7 +71,7 @@ class VersionedMap { return true; } - set(key, value, rev = this._revision) { + public set(key: K, value: V, rev = this._revision): this { let revisions = this.map.get(key); if (!revisions) { this.dirty |= 1 << rev; @@ -72,7 +81,7 @@ class VersionedMap { for (let i = 0; i < rev; ++i) revisions[i] = NONEXISTENT; revisions[rev] = value; this.map.set(key, revisions); - return true; + return this; } // Can't modify old revisions @@ -86,10 +95,10 @@ class VersionedMap { for (let i = revisions.length; i < rev; ++i) revisions[i] = old; revisions[rev] = value; - return true; + return this; } - delete(key, rev = this._revision) { + public delete(key: K, rev = this._revision): boolean { const revisions = this.map.get(key); if (!revisions) return false; @@ -108,24 +117,24 @@ class VersionedMap { return true; } - getRevisions(key) { + public getRevisions(key: K): Revisions { const revisions = this.map.get(key); if (!revisions) return null; - const res = {}; + const res: Revisions = {}; - let prev = NONEXISTENT; + let prev: V | symbol = NONEXISTENT; for (const [i, v] of revisions.entries()) { if (v === prev) continue; if (v === NONEXISTENT) res.delete |= 1 << i; - else res[i] = v; + else res[i] = v as V; prev = v; } return res; } - setRevisions(key, revisionsObj) { + public setRevisions(key: K, revisionsObj: Revisions): void { const del = revisionsObj.delete || 0; const mutations = Object.keys(revisionsObj).reduce( (acc, cur) => (cur === "delete" ? acc : acc | (1 << +cur)), @@ -134,7 +143,7 @@ class VersionedMap { const revisions = []; - let prev = NONEXISTENT; + let prev: V | symbol = NONEXISTENT; for (let i = 0; mutations >> i; ++i) { let v = prev; if (del & (1 << i)) v = NONEXISTENT; @@ -147,28 +156,28 @@ class VersionedMap { this.map.set(key, revisions); } - getDiff(key) { + public getDiff(key: K): [V, V] { const revisions = this.map.get(key); if (!revisions) return [UNDEFINED, UNDEFINED]; let first = revisions[0]; if (first === NONEXISTENT) first = UNDEFINED; let last = revisions[revisions.length - 1]; if (last === NONEXISTENT) last = UNDEFINED; - return [first, last]; + return [first as V, last as V]; } - *diff() { + public *diff(): IterableIterator<[K, V, V]> { for (const [key, revisions] of this.map) { let first = revisions[0]; let last = revisions[revisions.length - 1]; - if ((first === last) === NONEXISTENT) continue; + if (first === NONEXISTENT && last === NONEXISTENT) continue; if (first === NONEXISTENT) first = UNDEFINED; if (last === NONEXISTENT) last = UNDEFINED; - yield [key, first, last]; + yield [key, first as V, last as V]; } } - collapse(revision) { + public collapse(revision: number): void { if (this._sizeDiff.length <= revision) return; this._sizeDiff[revision] = this._sizeDiff[this._sizeDiff.length - 1]; @@ -189,5 +198,3 @@ class VersionedMap { } } } - -module.exports = VersionedMap; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 964690b..e20e082 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -4,6 +4,48 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@ava/babel-plugin-throws-helper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ava/babel-plugin-throws-helper/-/babel-plugin-throws-helper-3.0.0.tgz", + "integrity": "sha512-mN9UolOs4WX09QkheU1ELkVy2WPnwonlO3XMdN8JF8fQqRVgVTR21xDbvEOUsbwz6Zwjq7ji9yzyjuXqDPalxg==", + "dev": true + }, + "@ava/babel-preset-stage-4": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ava/babel-preset-stage-4/-/babel-preset-stage-4-2.0.0.tgz", + "integrity": "sha512-OWqMYeTSZ16AfLx0Vn0Uj7tcu+uMRlbKmks+DVCFlln7vomVsOtst+Oz+HCussDSFGpE+30VtHAUHLy6pLDpHQ==", + "dev": true, + "requires": { + "@babel/plugin-proposal-async-generator-functions": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", + "@babel/plugin-transform-async-to-generator": "^7.0.0", + "@babel/plugin-transform-dotall-regex": "^7.0.0", + "@babel/plugin-transform-exponentiation-operator": "^7.0.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0" + } + }, + "@ava/babel-preset-transform-test-files": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ava/babel-preset-transform-test-files/-/babel-preset-transform-test-files-4.0.1.tgz", + "integrity": "sha512-D7Z92B8Rgsj35JZveKJGwpUDuBKLiRKH6eyKpNmDHy7TJjr8y3VSDr3bUK+O456F3SkkBXrUihQuMrr39nWQhQ==", + "dev": true, + "requires": { + "@ava/babel-plugin-throws-helper": "^3.0.0", + "babel-plugin-espower": "^3.0.1" + } + }, + "@ava/write-file-atomic": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ava/write-file-atomic/-/write-file-atomic-2.2.0.tgz", + "integrity": "sha512-BTNB3nGbEfJT+69wuqXFr/bQH7Vr7ihx2xGOMNqPgDGhwspoZhiWumDDZNjBy7AScmqS5CELIOGtPVXESyrnDA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, "@babel/code-frame": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", @@ -13,6 +55,194 @@ "@babel/highlight": "^7.0.0" } }, + "@babel/core": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.3.3.tgz", + "integrity": "sha512-w445QGI2qd0E0GlSnq6huRZWPMmQGCp5gd5ZWS4hagn0EiwzxD5QMFkpchyusAyVC1n27OKXzQ0/88aVU9n4xQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.3.3", + "@babel/helpers": "^7.2.0", + "@babel/parser": "^7.3.3", + "@babel/template": "^7.2.2", + "@babel/traverse": "^7.2.2", + "@babel/types": "^7.3.3", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.11", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "@babel/generator": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.3.tgz", + "integrity": "sha512-aEADYwRRZjJyMnKN7llGIlircxTCofm3dtV5pmY6ob18MSIuipHpA2yZWkPlycwu5HJcx/pADS3zssd8eY7/6A==", + "dev": true, + "requires": { + "@babel/types": "^7.3.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", + "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", + "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", + "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-transforms": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz", + "integrity": "sha512-YRD7I6Wsv+IHuTPkAmAS4HhY0dkPobgLftHp0cRGZSdrRvmZY8rFvae/GVu3bD00qscuvK3WPHB3YdNpBXUqrA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/template": "^7.2.2", + "@babel/types": "^7.2.2", + "lodash": "^4.17.10" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.0.0.tgz", + "integrity": "sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", + "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-wrap-function": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-simple-access": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", + "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "dev": true, + "requires": { + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", + "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-wrap-function": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", + "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.2.0" + } + }, + "@babel/helpers": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.3.1.tgz", + "integrity": "sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==", + "dev": true, + "requires": { + "@babel/template": "^7.1.2", + "@babel/traverse": "^7.1.5", + "@babel/types": "^7.3.0" + } + }, "@babel/highlight": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", @@ -24,15 +254,654 @@ "js-tokens": "^4.0.0" } }, + "@babel/parser": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.3.tgz", + "integrity": "sha512-xsH1CJoln2r74hR+y7cg2B5JCPaTh+Hd+EbBRk9nWGSNspuo6krjhX0Om6RnRQuIvFq8wVXCLKH3kwKDYhanSg==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", + "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0", + "@babel/plugin-syntax-async-generators": "^7.2.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz", + "integrity": "sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", + "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", + "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz", + "integrity": "sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz", + "integrity": "sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0", + "regexpu-core": "^4.1.3" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", + "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz", + "integrity": "sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0" + } + }, + "@babel/template": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", + "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.2.2", + "@babel/types": "^7.2.2" + } + }, + "@babel/traverse": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", + "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.2.2", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/parser": "^7.2.3", + "@babel/types": "^7.2.2", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.10" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "@babel/types": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.3.tgz", + "integrity": "sha512-2tACZ80Wg09UnPg5uGAOUvvInaqLk3l/IAhQzlxLQOIXacr6bMsra5SH6AWw/hIDRCSbCdHP2KzSOD+cT7TzMQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + }, + "@concordance/react": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@concordance/react/-/react-2.0.0.tgz", + "integrity": "sha512-huLSkUuM2/P+U0uy2WwlKuixMsTODD8p4JVQBI4VKeopkiN0C7M3N9XYVawb4M+4spN5RrO/eLhk7KoQX6nsfA==", + "dev": true, + "requires": { + "arrify": "^1.0.1" + } + }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bson": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.0.tgz", + "integrity": "sha512-pq/rqJwJWkbS10crsG5bgnrisL8pML79KlMKQMoQwLUjlPAkrUHMvHJ3oGwE7WHR61Lv/nadMwXVAD2b+fpD8Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/cookies": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.1.tgz", + "integrity": "sha512-ku6IvbucEyuC6i4zAVK/KnuzWNXdbFd1HkXlNLg/zhWDGTtQT5VhumiPruB/BHW34PWVFwyfwGftDQHfWNxu3Q==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/express": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", + "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.1.tgz", + "integrity": "sha512-QgbIMRU1EVRry5cIu1ORCQP4flSYqLM1lS5LYyGWfKnFT3E58f0gKto7BR13clBFVrVZ0G0rbLZ1hUpSkgQQOA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/http-assert": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.4.0.tgz", + "integrity": "sha512-TZDqvFW4nQwL9DVSNJIJu4lPLttKgzRF58COa7Vs42Ki/MrhIqUbeIw0MWn4kGLiZLXB7oCBibm7nkSjPkzfKQ==", + "dev": true + }, + "@types/jsonwebtoken": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz", + "integrity": "sha512-YKnUTR4VxwljbPORPrRon9E3uel1aD8nUdvzqArCCdMTWPvo0gnI2UZkwIHN2QATdj6HYXV/Iq3/KcecAO42Ww==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/keygrip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", + "integrity": "sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg=", + "dev": true + }, + "@types/koa": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.0.48.tgz", + "integrity": "sha512-CiIUYhHlOFJhSCTmsFoFkV2t9ij1JwW26nt0W9XZoWTvmAw6zTE0+k3IAoGICtjzIfhZpZcO323NHmI1LGmdDw==", + "dev": true, + "requires": { + "@types/accepts": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.3.tgz", + "integrity": "sha512-kXvR0DPyZ3gaFxZs4WycA8lpzlPGtFmwdbgce+NWd+TG3PycPO3o5FkePH60HoBPd8BBaSiw3vhtgM42O2kQcg==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/koa-compress": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/koa-compress/-/koa-compress-2.0.8.tgz", + "integrity": "sha512-ewDLlqKmn+rS1uwQntBeKY8okG7220pZgl66gORdDE8Sq465/dRequ/ZzbNYWRrRmYDaElZZ0Gfk/dliHUoCkg==", + "dev": true, + "requires": { + "@types/koa": "*", + "@types/node": "*" + } + }, + "@types/koa-router": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/@types/koa-router/-/koa-router-7.0.39.tgz", + "integrity": "sha512-B8b2qyl7ijLFz7ocqkWva4IJ1n4uY5dK7jtGhQ+YHIGTpzcZr4ldDSjZi/xIM+ko3xSu8oqlGbzWxeVXK3WnNg==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/koa-send": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/koa-send/-/koa-send-4.1.1.tgz", + "integrity": "sha512-ODeofnQxlkAl5PvJXhgOF/hjX9cH47oDFDqH9nE0G6zcp/Rr0vjswe7tuA+b0ZhlBrkfmN9X8VhRqQCBeOtZrw==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/koa-static": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/koa-static/-/koa-static-4.0.1.tgz", + "integrity": "sha512-SSpct5fEcAeRkBHa3RiwCIRfDHcD1cZRhwRF///ZfvRt8KhoqRrhK6wpDlYPk/vWHVFE9hPGqh68bhzsHkir4w==", + "dev": true, + "requires": { + "@types/koa": "*", + "@types/koa-send": "*" + } + }, + "@types/libxmljs": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/@types/libxmljs/-/libxmljs-0.18.3.tgz", + "integrity": "sha512-EvYww5XELtacMKQkvvEj2DrFSm9F4cusPoegt4C2S9OTw2MCJ2RZW5FMUP4MiU5307yIMZDCP25C/oBuJros1w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true + }, + "@types/mithril": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-1.1.16.tgz", + "integrity": "sha512-+HlRvSpKwXP8RXh1hrIcgSHQ/Fh+5O9nYfG7fgNN7PGr2K03G+hLoiA+HXzmUJCsQfPEuNiBQ+TwRl6n7kerSw==", + "dev": true + }, + "@types/mongodb": { + "version": "3.1.19", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.19.tgz", + "integrity": "sha512-H54hQEovAhyLrIZOhPNfGyCCDoTqKsjb8GQBy8nptJqfxrYCp5WVcPJf9v0kfTPR72xOhaz9+WcYxOXWwEg1Vg==", + "dev": true, + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "@types/node": { + "version": "11.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.4.tgz", + "integrity": "sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==", + "dev": true + }, + "@types/parsimmon": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@types/parsimmon/-/parsimmon-1.10.0.tgz", + "integrity": "sha512-bsTIJFVQv7jnvNiC42ld2pQW2KRI+pAG243L+iATvqzy3X6+NH1obz2itRKDZZ8VVhN3wjwYax/VBGCcXzgTqQ==", + "dev": true + }, + "@types/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz", + "integrity": "sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/seedrandom": { + "version": "2.4.27", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz", + "integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE=", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.4.0.tgz", + "integrity": "sha512-LXLxWdq7nwGrJhMB78EKV+PJw4uWLyQLePRbJjbz6klItPaTa+5zxAr3+GGPaKlhTHKdoCsUaIGfCLjopUA65w==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "1.4.0", + "@typescript-eslint/typescript-estree": "1.4.0", + "requireindex": "^1.2.0", + "tsutils": "^3.7.0" + } + }, + "@typescript-eslint/parser": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.4.0.tgz", + "integrity": "sha512-OvXhpgFCKLxuvoGgtFHQP5gvJvqKGpGjarSPg27E07+OPIcZJ3EpZDdELZuY+0hCA4wqnQ9JzHoTBhdEOMo9Pg==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "1.4.0", + "eslint-scope": "^4.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.4.0.tgz", + "integrity": "sha512-s0Y1/nMSRwIRK+8umAkvUoKsP12Ze8QMAdjVSYLZbrWy8NlsrTLLhpTR5rPEEW7uCnmJtv40kr51PrYAyaJVOw==", + "dev": true, + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, + "@webassemblyjs/ast": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.3.tgz", + "integrity": "sha512-xy3m06+Iu4D32+6soz6zLnwznigXJRuFNTovBX2M4GqVqLb0dnyWLbPnpcXvUSdEN+9DVyDeaq2jyH1eIL2LZQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.8.3", + "@webassemblyjs/helper-wasm-bytecode": "1.8.3", + "@webassemblyjs/wast-parser": "1.8.3" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.3.tgz", + "integrity": "sha512-vq1TISG4sts4f0lDwMUM0f3kpe0on+G3YyV5P0IySHFeaLKRYZ++n2fCFfG4TcCMYkqFeTUYFxm75L3ddlk2xA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.3.tgz", + "integrity": "sha512-BmWEynI4FnZbjk8CaYZXwcv9a6gIiu+rllRRouQUo73hglanXD3AGFJE7Q4JZCoVE0p5/jeX6kf5eKa3D4JxwQ==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.3.tgz", + "integrity": "sha512-iVIMhWnNHoFB94+/2l7LpswfCsXeMRnWfExKtqsZ/E2NxZyUx9nTeKK/MEMKTQNEpyfznIUX06OchBHQ+VKi/Q==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.3.tgz", + "integrity": "sha512-K1UxoJML7GKr1QXR+BG7eXqQkvu+eEeTjlSl5wUFQ6W6vaOc5OwSxTcb3oE9x/3+w4NHhrIKD4JXXCZmLdL2cg==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.8.3" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.3.tgz", + "integrity": "sha512-387zipfrGyO77/qm7/SDUiZBjQ5KGk4qkrVIyuoubmRNIiqn3g+6ijY8BhnlGqsCCQX5bYKOnttJobT5xoyviA==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.3.tgz", + "integrity": "sha512-lPLFdQfaRssfnGEJit5Sk785kbBPPPK4ZS6rR5W/8hlUO/5v3F+rN8XuUcMj/Ny9iZiyKhhuinWGTUuYL4VKeQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.3", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.3.tgz", + "integrity": "sha512-R1nJW7bjyJLjsJQR5t3K/9LJ0QWuZezl8fGa49DZq4IVaejgvkbNlKEQxLYTC579zgT4IIIVHb5JA59uBPHXyw==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.3.tgz", + "integrity": "sha512-P6F7D61SJY73Yz+fs49Q3+OzlYAZP86OfSpaSY448KzUy65NdfzDmo2NPVte+Rw4562MxEAacvq/mnDuvRWOcg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.3", + "@webassemblyjs/helper-buffer": "1.8.3", + "@webassemblyjs/helper-wasm-bytecode": "1.8.3", + "@webassemblyjs/wasm-gen": "1.8.3" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.3.tgz", + "integrity": "sha512-UD4HuLU99hjIvWz1pD68b52qsepWQlYCxDYVFJQfHh3BHyeAyAlBJ+QzLR1nnS5J6hAzjki3I3AoJeobNNSZlg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.3.tgz", + "integrity": "sha512-XXd3s1BmkC1gpGABuCRLqCGOD6D2L+Ma2BpwpjrQEHeQATKWAQtxAyU9Z14/z8Ryx6IG+L4/NDkIGHrccEhRUg==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.3.tgz", + "integrity": "sha512-Wv/WH9Zo5h5ZMyfCNpUrjFsLZ3X1amdfEuwdb7MLdG3cPAjRS6yc6ElULlpjLiiBTuzvmLhr3ENsuGyJ3wyCgg==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.3.tgz", + "integrity": "sha512-nB19eUx3Yhi1Vvv3yev5r+bqQixZprMtaoCs1brg9Efyl8Hto3tGaUoZ0Yb4Umn/gQCyoEGFfUxPLp1/8+Jvnw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.3", + "@webassemblyjs/helper-buffer": "1.8.3", + "@webassemblyjs/helper-wasm-bytecode": "1.8.3", + "@webassemblyjs/helper-wasm-section": "1.8.3", + "@webassemblyjs/wasm-gen": "1.8.3", + "@webassemblyjs/wasm-opt": "1.8.3", + "@webassemblyjs/wasm-parser": "1.8.3", + "@webassemblyjs/wast-printer": "1.8.3" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.3.tgz", + "integrity": "sha512-sDNmu2nLBJZ/huSzlJvd9IK8B1EjCsOl7VeMV9VJPmxKYgTJ47lbkSP+KAXMgZWGcArxmcrznqm7FrAPQ7vVGg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.3", + "@webassemblyjs/helper-wasm-bytecode": "1.8.3", + "@webassemblyjs/ieee754": "1.8.3", + "@webassemblyjs/leb128": "1.8.3", + "@webassemblyjs/utf8": "1.8.3" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.3.tgz", + "integrity": "sha512-j8lmQVFR+FR4/645VNgV4R/Jz8i50eaPAj93GZyd3EIJondVshE/D9pivpSDIXyaZt+IkCodlzOoZUE4LnQbeA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.3", + "@webassemblyjs/helper-buffer": "1.8.3", + "@webassemblyjs/wasm-gen": "1.8.3", + "@webassemblyjs/wasm-parser": "1.8.3" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.3.tgz", + "integrity": "sha512-NBI3SNNtRoy4T/KBsRZCAWUzE9lI94RH2nneLwa1KKIrt/2zzcTavWg6oY05ArCbb/PZDk3OUi63CD1RYtN65w==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.3", + "@webassemblyjs/helper-api-error": "1.8.3", + "@webassemblyjs/helper-wasm-bytecode": "1.8.3", + "@webassemblyjs/ieee754": "1.8.3", + "@webassemblyjs/leb128": "1.8.3", + "@webassemblyjs/utf8": "1.8.3" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.3.tgz", + "integrity": "sha512-gZPst4CNcmGtKC1eYQmgCx6gwQvxk4h/nPjfPBbRoD+Raw3Hs+BS3yhrfgyRKtlYP+BJ8LcY9iFODEQofl2qbg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.3", + "@webassemblyjs/floating-point-hex-parser": "1.8.3", + "@webassemblyjs/helper-api-error": "1.8.3", + "@webassemblyjs/helper-code-frame": "1.8.3", + "@webassemblyjs/helper-fsm": "1.8.3", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.3.tgz", + "integrity": "sha512-DTA6kpXuHK4PHu16yAD9QVuT1WZQRT7079oIFFmFSjqjLWGXS909I/7kiLTn931mcj7wGsaUNungjwNQ2lGQ3Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.3", + "@webassemblyjs/wast-parser": "1.8.3", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, "acorn": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", - "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.0.tgz", + "integrity": "sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", "dev": true }, "acorn-jsx": { @@ -41,10 +910,19 @@ "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", "dev": true }, + "aggregate-error": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-1.0.0.tgz", + "integrity": "sha1-iINE2tAiCnLjr1CQYRf0h3GSX6w=", + "requires": { + "clean-stack": "^1.0.0", + "indent-string": "^3.0.0" + } + }, "ajv": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", - "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", @@ -53,10 +931,70 @@ "uri-js": "^4.2.2" } }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", + "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "ansi-escapes": { - "version": "3.1.0", - "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", "dev": true }, "ansi-regex": { @@ -73,6 +1011,32 @@ "color-convert": "^1.9.0" } }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -87,6 +1051,12 @@ "readable-stream": "^2.0.6" } }, + "arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -96,22 +1066,479 @@ "sprintf-js": "~1.0.2" } }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-2.0.3.tgz", + "integrity": "sha1-AZW7AMzM8nEQbv7kpHhkiLcYBxI=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + }, + "dependencies": { + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + } + } + }, + "array-uniq": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-2.0.0.tgz", + "integrity": "sha512-O3QZEr+3wDj7otzF7PjNGs6CA3qmYMLvt5xGkjY/V0VxS+ovvqVo/5wKM/OVOAyuX4DTh9H31zE/yKtO66hTkg==", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.6.tgz", + "integrity": "sha512-Iq8TRIB+/9eQ8rbGhcP7ct5cYb/3qjNYAR2SnzLCEcwF6rvVOax8+9+fccgXk4bEhQGjOZd5TLhsksmAdsbGqQ==", + "dev": true, + "requires": { + "browserslist": "^2.11.3", + "caniuse-lite": "^1.0.30000805", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^6.0.17", + "postcss-value-parser": "^3.2.3" + }, + "dependencies": { + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000792", + "electron-to-chromium": "^1.3.30" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "ava": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ava/-/ava-1.2.1.tgz", + "integrity": "sha512-EHqbPGdd8aNvlvRNL7liD1J9Auf9kByHj5Zi7zF7Z5ukn2ZStZgVBf7LSqirKIOWScB3XZzFQbO59SnTvzD5kA==", + "dev": true, + "requires": { + "@ava/babel-preset-stage-4": "^2.0.0", + "@ava/babel-preset-transform-test-files": "^4.0.1", + "@ava/write-file-atomic": "^2.2.0", + "@babel/core": "^7.2.2", + "@babel/generator": "^7.3.0", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", + "@concordance/react": "^2.0.0", + "ansi-escapes": "^3.1.0", + "ansi-styles": "^3.2.1", + "arr-flatten": "^1.1.0", + "array-union": "^1.0.1", + "array-uniq": "^2.0.0", + "arrify": "^1.0.0", + "bluebird": "^3.5.3", + "chalk": "^2.4.2", + "chokidar": "^2.0.4", + "chunkd": "^1.0.0", + "ci-parallel-vars": "^1.0.0", + "clean-stack": "^2.0.0", + "clean-yaml-object": "^0.1.0", + "cli-cursor": "^2.1.0", + "cli-truncate": "^1.1.0", + "code-excerpt": "^2.1.1", + "common-path-prefix": "^1.0.0", + "concordance": "^4.0.0", + "convert-source-map": "^1.6.0", + "currently-unhandled": "^0.4.1", + "debug": "^4.1.1", + "del": "^3.0.0", + "dot-prop": "^4.2.0", + "emittery": "^0.4.1", + "empower-core": "^1.2.0", + "equal-length": "^1.0.0", + "escape-string-regexp": "^1.0.5", + "esm": "^3.1.3", + "figures": "^2.0.0", + "find-up": "^3.0.0", + "get-port": "^4.1.0", + "globby": "^7.1.1", + "ignore-by-default": "^1.0.0", + "import-local": "^2.0.0", + "indent-string": "^3.2.0", + "is-ci": "^2.0.0", + "is-error": "^2.2.1", + "is-observable": "^1.1.0", + "is-plain-object": "^2.0.4", + "is-promise": "^2.1.0", + "lodash.clone": "^4.5.0", + "lodash.clonedeep": "^4.5.0", + "lodash.clonedeepwith": "^4.5.0", + "lodash.debounce": "^4.0.3", + "lodash.difference": "^4.3.0", + "lodash.flatten": "^4.2.0", + "loud-rejection": "^1.2.0", + "make-dir": "^1.3.0", + "matcher": "^1.1.1", + "md5-hex": "^2.0.0", + "meow": "^5.0.0", + "ms": "^2.1.1", + "multimatch": "^3.0.0", + "observable-to-promise": "^0.5.0", + "ora": "^3.0.0", + "package-hash": "^3.0.0", + "pkg-conf": "^2.1.0", + "plur": "^3.0.1", + "pretty-ms": "^4.0.0", + "require-precompiled": "^0.1.0", + "resolve-cwd": "^2.0.0", + "slash": "^2.0.0", + "source-map-support": "^0.5.10", + "stack-utils": "^1.0.2", + "strip-ansi": "^5.0.0", + "strip-bom-buf": "^1.0.0", + "supertap": "^1.0.0", + "supports-color": "^6.1.0", + "trim-off-newlines": "^1.0.1", + "trim-right": "^1.0.1", + "unique-temp-dir": "^1.0.0", + "update-notifier": "^2.5.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", + "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", + "dev": true + }, + "clean-stack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.0.0.tgz", + "integrity": "sha512-VEoL9Qh7I8s8iHnV53DaeWSt8NJ0g3khMfK6NiCPB7H657juhro+cSw2O88uo3bo0c0X5usamtXk0/Of0wXa5A==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "strip-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", + "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", + "dev": true, + "requires": { + "ansi-regex": "^4.0.0" + } + } + } + }, + "babel-plugin-espower": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-espower/-/babel-plugin-espower-3.0.1.tgz", + "integrity": "sha512-Ms49U7VIAtQ/TtcqRbD6UBmJBUCSxiC3+zPc+eGqxKUIFO1lTshyEDRUjhoAbd2rWfwYf3cZ62oXozrd8W6J0A==", + "dev": true, + "requires": { + "@babel/generator": "^7.0.0", + "@babel/parser": "^7.0.0", + "call-matcher": "^1.0.0", + "core-js": "^2.0.0", + "espower-location-detector": "^1.0.0", + "espurify": "^1.6.0", + "estraverse": "^4.1.1" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz", + "integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==", + "dev": true + }, "bindings": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==" }, + "bluebird": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dev": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -121,35 +1548,337 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.1.tgz", + "integrity": "sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000929", + "electron-to-chromium": "^1.3.103", + "node-releases": "^1.1.3" + } + }, "bson": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { - "callsites": "^0.2.0" + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cacache": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz", + "integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==", + "dev": true, + "requires": { + "bluebird": "^3.5.3", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, + "call-matcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/call-matcher/-/call-matcher-1.1.0.tgz", + "integrity": "sha512-IoQLeNwwf9KTNbtSA7aEBb1yfDbdnzwjCetjkC8io5oGeOmK2CBNdg0xr+tadRYKO0p7uQyZzvon0kXlZbvGrw==", + "dev": true, + "requires": { + "core-js": "^2.0.0", + "deep-equal": "^1.0.0", + "espurify": "^1.6.0", + "estraverse": "^4.0.0" + } + }, + "call-signature": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/call-signature/-/call-signature-0.0.2.tgz", + "integrity": "sha1-qEq8glpV70yysCi9dOIFpluaSZY=", + "dev": true + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" } }, "callsites": { - "version": "0.2.0", - "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30000938", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000938.tgz", + "integrity": "sha512-ekW8NQ3/FvokviDxhdKLZZAx7PptXNwxKgXtnR5y+PR3hckwuP3yJ1Ir+4/c97dsHNqtAyfKUGdw8P4EYzBNgw==", + "dev": true + }, + "capture-stack-trace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", + "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", "dev": true }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" + }, + "dependencies": { + "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" + } + } } }, "chardet": { @@ -158,15 +1887,106 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chokidar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz", + "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.0" + } + }, "chownr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "chrome-trace-event": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz", + "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "chunkd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-1.0.0.tgz", + "integrity": "sha512-xx3Pb5VF9QaqCotolyZ1ywFBgyuJmu6+9dLiqBxgelEse9Xsr3yUlpoX3O4Oh11M00GT2kYMsRByTKIMJW2Lkg==", + "dev": true + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "ci-parallel-vars": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ci-parallel-vars/-/ci-parallel-vars-1.0.0.tgz", + "integrity": "sha512-u6dx20FBXm+apMi+5x7UVm6EH7BL1gc4XrcnQewjcB7HWRcor/V5qWc3RG2HwpgDJ26gIi2DSEu3B7sXynAw/g==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-stack": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", + "integrity": "sha1-noIVAa6XmYbEax1m0tQy2y/UrjE=" + }, + "clean-yaml-object": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clean-yaml-object/-/clean-yaml-object-0.1.0.tgz", + "integrity": "sha1-Y/sRDcLOGoTcIfbZM0h20BCui2g=", + "dev": true + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", "dev": true }, "cli-cursor": { @@ -178,17 +1998,134 @@ "restore-cursor": "^2.0.0" } }, + "cli-spinners": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", + "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==", + "dev": true + }, + "cli-truncate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-1.1.0.tgz", + "integrity": "sha512-bAtZo0u82gCfaAGfSNxUdTI9mNyza7D8w4CVCcaOsy7sgwDzvx6ekr6cuWJqY3UGzgnQ1+4wgENup5eIhgxEYA==", + "dev": true, + "requires": { + "slice-ansi": "^1.0.0", + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-body": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.0.0.tgz", + "integrity": "sha512-9ZIcixguuuKIptnY8yemEOuhb71L/lLf+Rl5JfJEUiDNJk0e02MBt7BPxR2GEh5mw8dPthQYR4jPI/BnS1MQgw==", + "requires": { + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + } + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "code-excerpt": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-2.1.1.tgz", + "integrity": "sha512-tJLhH3EpFm/1x7heIW0hemXJTUU5EWl2V0EIX558jp05Mt1U6DVryCgkp3l37cxqs+DNbNgxG43SkwJXpQ14Jw==", + "dev": true, + "requires": { + "convert-to-spaces": "^1.0.1" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "codemirror": { + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.43.0.tgz", + "integrity": "sha512-mljwQWUaWIf85I7QwTBryF2ASaIvmYAL4s5UCanCJFfKeXOKhrqdHWdHiZWAMNT+hjLTCnVx2S/SYTORIgxsgA==", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.0.tgz", + "integrity": "sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -204,42 +2141,578 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + }, + "common-path-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-1.0.0.tgz", + "integrity": "sha1-zVL28HEuC6q5fW+XModPIvR3UsA=", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "compressible": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.16.tgz", + "integrity": "sha512-JQfEOdnI7dASwCuSPWIeVYwc/zMsu/+tRhoUvEfXz2gxOA2DNjmG5vhtFdBlhWPPGo+RdT9S3tgc/uH5qgDiiA==", + "requires": { + "mime-db": ">= 1.38.0 < 2" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "concordance": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/concordance/-/concordance-4.0.0.tgz", + "integrity": "sha512-l0RFuB8RLfCS0Pt2Id39/oCPykE01pyxgAFypWTlaGRgvLkZrtczZ8atEHpTeEIW+zYWXTBuA9cCSeEOScxReQ==", + "dev": true, + "requires": { + "date-time": "^2.1.0", + "esutils": "^2.0.2", + "fast-diff": "^1.1.2", + "js-string-escape": "^1.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.flattendeep": "^4.4.0", + "lodash.islength": "^4.0.1", + "lodash.merge": "^4.6.1", + "md5-hex": "^2.0.0", + "semver": "^5.5.1", + "well-known-symbols": "^2.0.0" + } + }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "dev": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "convert-to-spaces": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz", + "integrity": "sha1-fj5Iu+bZl7FBfdyihoIEtNPYVxU=", + "dev": true + }, + "cookies": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.3.tgz", + "integrity": "sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==", + "requires": { + "depd": "~1.1.2", + "keygrip": "~1.0.3" + } + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" + }, + "core-js": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "cosmiconfig": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.1.0.tgz", + "integrity": "sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "lodash.get": "^4.4.2", + "parse-json": "^4.0.0" + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, + "css-color-function": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/css-color-function/-/css-color-function-1.3.3.tgz", + "integrity": "sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4=", + "dev": true, + "requires": { + "balanced-match": "0.1.0", + "color": "^0.11.0", + "debug": "^3.1.0", + "rgb": "~0.1.0" + }, + "dependencies": { + "balanced-match": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.1.0.tgz", + "integrity": "sha1-tQS9BYabOSWd0MXvw12EMXbczEo=", + "dev": true + }, + "color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "dev": true, + "requires": { + "clone": "^1.0.2", + "color-convert": "^1.3.0", + "color-string": "^0.3.0" + } + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "dev": true, + "requires": { + "color-name": "^1.0.0" + } + } + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-select": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", + "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^2.1.2", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-tree": { + "version": "1.0.0-alpha.28", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz", + "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==", + "dev": true, + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + }, + "css-unit-converter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", + "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", + "dev": true + }, + "css-url-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz", + "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=", + "dev": true + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", + "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", + "dev": true, + "requires": { + "css-tree": "1.0.0-alpha.29" + }, + "dependencies": { + "css-tree": { + "version": "1.0.0-alpha.29", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", + "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", + "dev": true, + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "date-time": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-2.1.0.tgz", + "integrity": "sha512-/9+C44X7lot0IeiyfgJmETtRMhBidBYM2QFFIkGa0U1k+hSyY87Nw7PY3eDqpvCBm7I3WCSfPeZskW/YYq6m4g==", + "dev": true, + "requires": { + "time-zone": "^1.0.0" + } + }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + } + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -251,30 +2724,378 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "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==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "^6.1.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "p-map": "^1.1.1", + "pify": "^3.0.0", + "rimraf": "^2.2.8" + }, + "dependencies": { + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + } + } + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "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" } }, - "double-ended-queue": { - "version": "2.1.0-0", - "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", - "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=", - "optional": true + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", + "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "electron-to-chromium": { + "version": "1.3.113", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz", + "integrity": "sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==", + "dev": true + }, + "elliptic": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emittery": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.4.1.tgz", + "integrity": "sha512-r4eRSeStEGf6M5SKdrQhhLK5bOwOBxQhIE3YSTnZE3GpKiLfnnhE+tPtrJE79+eDJgm39BM6LSoI8SCx4HbwlQ==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "empower-core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/empower-core/-/empower-core-1.2.0.tgz", + "integrity": "sha512-g6+K6Geyc1o6FdXs9HwrXleCFan7d66G5xSCfSF7x1mJDCes6t0om9lFQG3zOrzh3Bkb/45N0cZ5Gqsf7YrzGQ==", + "dev": true, + "requires": { + "call-signature": "0.0.2", + "core-js": "^2.0.0" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "equal-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/equal-length/-/equal-length-1.0.1.tgz", + "integrity": "sha1-IcoRLUirJLTh5//A5TOdMf38J0w=", + "dev": true + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", @@ -283,47 +3104,46 @@ "dev": true }, "eslint": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.10.0.tgz", - "integrity": "sha512-HpqzC+BHULKlnPwWae9MaVZ5AXJKpkxCVXQHrFaRw3hbDj26V/9ArYM4Rr/SQ8pi6qUPLXSSXC4RBJlyq2Z2OQ==", + "version": "5.14.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.14.1.tgz", + "integrity": "sha512-CyUMbmsjxedx8B0mr79mNOqetvkbij/zrXnFeK2zc3pGRn3/tibjiNAv/3UxFEyfMDjh+ZqTrJrEGBFiGfD5Og==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "ajv": "^6.5.3", + "ajv": "^6.9.1", "chalk": "^2.1.0", "cross-spawn": "^6.0.5", "debug": "^4.0.1", - "doctrine": "^2.1.0", + "doctrine": "^3.0.0", "eslint-scope": "^4.0.0", "eslint-utils": "^1.3.1", "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.0", + "espree": "^5.0.1", "esquery": "^1.0.1", "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", + "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", "glob": "^7.1.2", "globals": "^11.7.0", "ignore": "^4.0.6", + "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.1.0", + "inquirer": "^6.2.2", "js-yaml": "^3.12.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", - "lodash": "^4.17.5", + "lodash": "^4.17.11", "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "optionator": "^0.8.2", "path-is-inside": "^1.0.2", - "pluralize": "^7.0.0", "progress": "^2.0.0", "regexpp": "^2.0.1", - "require-uncached": "^1.0.3", "semver": "^5.5.1", "strip-ansi": "^4.0.0", "strip-json-comments": "^2.0.1", - "table": "^5.0.2", + "table": "^5.2.3", "text-table": "^0.2.0" }, "dependencies": { @@ -333,19 +3153,48 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, "debug": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", - "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "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 + }, + "import-fresh": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz", + "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, "strip-ansi": { @@ -359,6 +3208,15 @@ } } }, + "eslint-config-prettier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.0.0.tgz", + "integrity": "sha512-kWuiJxzV5NwOwZcpyozTzDT5KJhBw292bbYro9Is7BWnbNMg15Gmpluc1CTetiCatF8DRkNvgPAOaSyg+bYr3g==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, "eslint-scope": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", @@ -381,13 +3239,31 @@ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", "dev": true }, - "espree": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.0.tgz", - "integrity": "sha512-1MpUfwsdS9MMoN7ZXqAr9e9UKdVHDcvrJpyx7mm1WuQlx/ygErEQBzgi5Nh5qBHIoYweprhtMkTCb9GhcAIcsA==", + "esm": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.5.tgz", + "integrity": "sha512-rukU6Nd3agbHQCJWV4rrlZxqpbO3ix8qhUxK1BhKALGS2E465O0BFwgCOqJjNnYfO/I2MwpUBmPsW8DXoe8tcA==", + "dev": true + }, + "espower-location-detector": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/espower-location-detector/-/espower-location-detector-1.0.0.tgz", + "integrity": "sha1-oXt+zFnTDheeK+9z+0E3cEyzMbU=", "dev": true, "requires": { - "acorn": "^6.0.2", + "is-url": "^1.2.1", + "path-is-absolute": "^1.0.0", + "source-map": "^0.5.0", + "xtend": "^4.0.0" + } + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", "acorn-jsx": "^5.0.0", "eslint-visitor-keys": "^1.0.0" } @@ -398,6 +3274,15 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, + "espurify": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/espurify/-/espurify-1.8.1.tgz", + "integrity": "sha512-ZDko6eY/o+D/gHCWyHTU85mKDgYcS4FJj7S+YD6WIInm7GQ6AnOjmcL4+buFV/JOztVLELi/7MmuGU5NHta0Mg==", + "dev": true, + "requires": { + "core-js": "^2.0.0" + } + }, "esquery": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", @@ -422,12 +3307,120 @@ "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", "dev": true }, + "estree-walker": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.0.tgz", + "integrity": "sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw==", + "dev": true + }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, "external-editor": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", @@ -437,6 +3430,82 @@ "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } } }, "fast-deep-equal": { @@ -445,6 +3514,12 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -457,6 +3532,12 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -467,25 +3548,118 @@ } }, "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", "dev": true, "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" + "flat-cache": "^2.0.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-cache-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.0.0.tgz", + "integrity": "sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" } }, "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", "dev": true, "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", + "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", + "dev": true + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" } }, "fs-minipass": { @@ -496,11 +3670,558 @@ "minipass": "^2.2.1" } }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "fsevents": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": 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" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, "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", @@ -522,6 +4243,30 @@ "wide-align": "^1.1.0" } }, + "get-port": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.1.0.tgz", + "integrity": "sha512-4/fqAYrzrzOiqDrdeZRKXGdTGgbkfTEumGlNQPeP6Jy8w0PzN9mzeNQ3XgHaTNie8pQ3hOUkrwlZt2Fzk5H9mA==", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, "glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", @@ -535,41 +4280,274 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, "globals": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", - "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", + "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", "dev": true }, + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "dependencies": { + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + } + } + }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + } + }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "dev": true }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "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.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "http-assert": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.0.tgz", + "integrity": "sha512-tPVv62a6l3BbQoM/N5qo969l0OFxqpnQzNUPeYfTP6Spo4zkgWeDBD1D5thI7sDLg7jCCihXTLB0X8UtdyAy8A==", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.1" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "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==", + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", "dev": true }, "ignore-walk": { @@ -580,12 +4558,68 @@ "minimatch": "^3.0.4" } }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -606,21 +4640,21 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", - "integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz", + "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==", "dev": true, "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", - "external-editor": "^3.0.0", + "external-editor": "^3.0.3", "figures": "^2.0.0", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "mute-stream": "0.0.7", "run-async": "^2.2.0", - "rxjs": "^6.1.0", + "rxjs": "^6.4.0", "string-width": "^2.1.0", "strip-ansi": "^5.0.0", "through": "^2.3.6" @@ -678,6 +4712,157 @@ } } }, + "irregular-plurals": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz", + "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-error": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-error/-/is-error-2.2.1.tgz", + "integrity": "sha1-aEqW2EB2V3yY9M20DG0mpRI78Zw=", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "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": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -686,16 +4871,189 @@ "number-is-nan": "^1.0.0" } }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-observable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", + "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "dev": true, + "requires": { + "symbol-observable": "^1.1.0" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, - "isarray": { + "is-redirect": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "isexe": { "version": "2.0.0", @@ -703,6 +5061,34 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "isnumeric": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/isnumeric/-/isnumeric-0.2.0.tgz", + "integrity": "sha1-ojR7o2DeGeM9D/1ZD933dVy/LmQ=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "jest-worker": { + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.0.0.tgz", + "integrity": "sha512-s64/OThpfQvoCeHG963MiEZOAAxu8kHsaL/rCMF7lpdzo7vgF0CtPml9hfguOMgykgH/eOm4jFP4ibfHLruytg==", + "dev": true, + "requires": { + "merge-stream": "^1.0.1", + "supports-color": "^6.1.0" + } + }, + "js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -710,15 +5096,27 @@ "dev": true }, "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", + "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" } }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -731,11 +5129,239 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "jsonwebtoken": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.4.0.tgz", + "integrity": "sha512-coyXjRTCy0pw5WYBpMvWOMN+Kjaik2MwTUIq9cna/W7NpO9E+iYbumZONAz3hcr+tXFJECoQVrtmIoC3Oz0gvg==", + "requires": { + "jws": "^3.1.5", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1" + } + }, + "jwa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.2.0.tgz", + "integrity": "sha512-Grku9ZST5NNQ3hqNUodSkDfEBqAmGA1R8yiyPHOnLzEKI0GaCQC/XhFmsheXYuXzFQJdILbh+lYBiliqG5R/Vg==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.10", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.1.tgz", + "integrity": "sha512-bGA2omSrFUkd72dhh05bIAN832znP4wOU3lfuXtRBuGTbsmNmDXMQg28f0Vsxaxgk4myF5YkKQpz6qeRpMgX9g==", + "requires": { + "jwa": "^1.2.0", + "safe-buffer": "^5.0.1" + } + }, + "keygrip": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz", + "integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "koa": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.7.0.tgz", + "integrity": "sha512-7ojD05s2Q+hFudF8tDLZ1CpCdVZw8JQELWSkcfG9bdtoTDzMmkRF6BQBU7JzIzCCOY3xd3tftiy/loHBUYaY2Q==", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.7.1", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "error-inject": "^1.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "koa-is-json": "^1.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + } + }, + "koa-bodyparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.2.1.tgz", + "integrity": "sha512-UIjPAlMZfNYDDe+4zBaOAUKYqkwAGcIU6r2ARf1UOXPAlfennQys5IiShaVeNf7KkVBlf88f2LeLvBFvKylttw==", + "requires": { + "co-body": "^6.0.0", + "copy-to": "^2.0.1" + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "koa-compress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/koa-compress/-/koa-compress-3.0.0.tgz", + "integrity": "sha512-xol+LkNB1mozKJkB5Kj6nYXbJXhkLkZlXl9BsGBPjujVfZ8MsIXwU4GHRTT7TlSfUcl2DU3JtC+j6wOWcovfuQ==", + "requires": { + "bytes": "^3.0.0", + "compressible": "^2.0.0", + "koa-is-json": "^1.0.0", + "statuses": "^1.0.0" + } + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, + "koa-jwt": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/koa-jwt/-/koa-jwt-3.5.1.tgz", + "integrity": "sha512-Wy1TZzyPdMw2X72HPhy9GPUy0BMxafS8Wq0JzvXCPi4pEBWQDtz1lzxYWpE2ghMuXWJ0vKKRrR5e+CyYGS8LBA==", + "requires": { + "jsonwebtoken": "8.3.0", + "koa-unless": "1.0.7", + "p-any": "1.1.0" + }, + "dependencies": { + "jsonwebtoken": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz", + "integrity": "sha512-oge/hvlmeJCH+iIz1DwcO7vKPkNGJHhgkspk8OH3VKlw+mbi42WtD4ig1+VXRln765vxptAv+xT26Fd3cteqag==", + "requires": { + "jws": "^3.1.5", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1" + } + } + } + }, + "koa-router": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.4.0.tgz", + "integrity": "sha512-IWhaDXeAnfDBEpWS6hkGdZ1ablgr6Q6pGdXCyK38RbzuH4LkUOpPqPw+3f8l8aTDrQmBQ7xJc0bs2yV4dzcO+g==", + "requires": { + "debug": "^3.1.0", + "http-errors": "^1.3.1", + "koa-compose": "^3.0.0", + "methods": "^1.0.1", + "path-to-regexp": "^1.1.1", + "urijs": "^1.19.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-send": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.0.tgz", + "integrity": "sha512-90ZotV7t0p3uN9sRwW2D484rAaKIsD8tAVtypw/aBU+ryfV+fR2xrcAwhI8Wl6WRkojLUs/cB9SBSCuIb+IanQ==", + "requires": { + "debug": "^3.1.0", + "http-errors": "^1.6.3", + "mz": "^2.7.0", + "resolve-path": "^1.4.0" + } + }, + "koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "requires": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + } + }, + "koa-unless": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/koa-unless/-/koa-unless-1.0.7.tgz", + "integrity": "sha1-ud83XitNowQ5GNSGIlIMLAt58DI=" + }, "later": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/later/-/later-1.2.0.tgz", "integrity": "sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8=" }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "^4.0.0" + } + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -756,24 +5382,461 @@ "node-pre-gyp": "~0.11.0" } }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=", + "dev": true + }, + "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.clonedeepwith": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeepwith/-/lodash.clonedeepwith-4.5.0.tgz", + "integrity": "sha1-buMFc6A6GmDWcKYu8zwQzxr9vdQ=", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.islength": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.islength/-/lodash.islength-4.0.1.tgz", + "integrity": "sha1-Tpho1FJXXXUK/9NYyXlUPcIO1Xc=", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", + "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "lodash.template": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", + "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~3.0.0" + } + }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + }, + "dependencies": { + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, + "magic-string": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.2.tgz", + "integrity": "sha512-iLs9mPjh9IuTtRsqqhNGYcZXGei0Nh/A4xirrsqW7c+QhKVFL2vm7U09ru6cHRD22azaP/wMDgI+HCqbETMTtg==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "matcher": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-1.1.1.tgz", + "integrity": "sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.4" + } + }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", + "dev": true + }, + "md5-hex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-2.0.0.tgz", + "integrity": "sha1-0FiOnxx0lUSS7NJKwKxs6ZfZLjM=", + "dev": true, + "requires": { + "md5-o-matic": "^0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/md5-o-matic/-/md5-o-matic-0.1.1.tgz", + "integrity": "sha1-givM1l4RfFFPqxdrJZRdVBAKA8M=", + "dev": true + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", + "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, "memory-pager": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.3.1.tgz", - "integrity": "sha512-pUf/sGkym2WqFZYTVmdASnSbNfpGc9rwxEHOePx4lT/fD+NHGL1U16Uy4o6PMiVcDv4mp6MI/vaF0c/Kd1QEUQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", "optional": true }, + "meow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", + "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0", + "yargs-parser": "^10.0.0" + } + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -784,9 +5847,19 @@ }, "minimist": { "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, "minipass": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", @@ -804,27 +5877,72 @@ "minipass": "^2.2.1" } }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mithril": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/mithril/-/mithril-1.1.6.tgz", + "integrity": "sha512-fWcUrQTCqu8M916rj1MFGlHaPh65rznPu6U/N2U9g81H89klDCIptSK5bnkNkC+jyi3sJIXjyGhSQjUnR8jzZA==", + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" } }, "mongodb": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.10.tgz", - "integrity": "sha512-Uml42GeFxhTGQVml1XQ4cD0o/rp7J2ROy0fdYUcVitoE7vFqEhKH4TYVqRDpQr/bXtCJVxJdNQC1ntRxNREkPQ==", + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.13.tgz", + "integrity": "sha512-sz2dhvBZQWf3LRNDhbd30KHVzdjZx9IKC0L+kSZ/gzYquCF5zPOgGqRz6sSCqYZtKP2ekB4nfLxhGtzGHnIKxA==", "requires": { - "mongodb-core": "3.1.9", + "mongodb-core": "3.1.11", "safe-buffer": "^5.1.2" } }, "mongodb-core": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.9.tgz", - "integrity": "sha512-MJpciDABXMchrZphh3vMcqu8hkNf/Mi+Gk6btOimVg1XMxLXh87j6FAvRm+KmwD1A9fpu3qRQYcbQe4egj23og==", + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.11.tgz", + "integrity": "sha512-rD2US2s5qk/ckbiiGFHeu+yKYDXdJ1G87F6CG3YdaZpzdOm5zpoAZd/EKbPmFO6cQZ+XVXBXBJ660sSI0gc6qg==", "requires": { "bson": "^1.1.0", "require_optional": "^1.0.1", @@ -832,10 +5950,36 @@ "saslprep": "^1.0.0" } }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "multimatch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-3.0.0.tgz", + "integrity": "sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA==", + "dev": true, + "requires": { + "array-differ": "^2.0.3", + "array-union": "^1.0.2", + "arrify": "^1.0.1", + "minimatch": "^3.0.4" + } }, "mute-stream": { "version": "0.0.7", @@ -843,11 +5987,40 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "nan": { "version": "2.10.0", - "resolved": "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -862,14 +6035,79 @@ "debug": "^2.1.2", "iconv-lite": "^0.4.4", "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } } }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "neo-async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", + "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", + "dev": true + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-libs-browser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", + "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.0", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, "node-pre-gyp": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", @@ -887,6 +6125,15 @@ "tar": "^4" } }, + "node-releases": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.8.tgz", + "integrity": "sha512-gQm+K9mGCiT/NXHy+V/ZZS1N/LOaGGqRAAJJs3X9Ah1g+CIbRcBgNyoNYQ+SEtcyAtB9KqDruu+fF7nWjsqRaA==", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, "nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", @@ -896,20 +6143,59 @@ "osenv": "^0.1.4" } }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true + }, "npm-bundled": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" }, "npm-packlist": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", - "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.0.tgz", + "integrity": "sha512-zCAmKshsFx2MhOsXdYmZd1DO2d8ts80kVASSWX6lv8654i0edCnNCoEqwVsMygl1BSroCPW6Zh5Dcw+ann775g==", "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" } }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -921,6 +6207,21 @@ "set-blocking": "~2.0.0" } }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -931,6 +6232,120 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", + "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "observable-to-promise": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/observable-to-promise/-/observable-to-promise-0.5.0.tgz", + "integrity": "sha1-yCjw8NxH6fhq+KSXfF1VB2znqR8=", + "dev": true, + "requires": { + "is-observable": "^0.2.0", + "symbol-observable": "^1.0.4" + }, + "dependencies": { + "is-observable": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-0.2.0.tgz", + "integrity": "sha1-s2ExHYPG5dcmyr9eJQsCNxBvWuI=", + "dev": true, + "requires": { + "symbol-observable": "^0.2.2" + }, + "dependencies": { + "symbol-observable": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-0.2.4.tgz", + "integrity": "sha1-lag9smGG1q9+ehjb2XYKL4bQj0A=", + "dev": true + } + } + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -939,6 +6354,12 @@ "wrappy": "1" } }, + "onecolor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/onecolor/-/onecolor-3.1.0.tgz", + "integrity": "sha512-YZSypViXzu3ul5LMu/m6XjJ9ol8qAy9S2VjHl5E6UlhUH1KGKWabyEJifn0Jjpw23bYDzC2ucKMPGiH5kfwSGQ==", + "dev": true + }, "onetime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", @@ -948,6 +6369,11 @@ "mimic-fn": "^1.0.0" } }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -962,14 +6388,51 @@ "wordwrap": "~1.0.0" } }, + "ora": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.1.0.tgz", + "integrity": "sha512-vRBPaNCclUi8pUxRF/G8+5qEQkc6EgzKK1G2ZNJUIGu088Un5qIxFXeDgymvPRM9nmrcUOGzQgS1Vmtz+NtlMw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^1.3.1", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", + "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", + "dev": true + }, + "strip-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", + "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", + "dev": true, + "requires": { + "ansi-regex": "^4.0.0" + } + } + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, "os-homedir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { @@ -981,9 +6444,183 @@ "os-tmpdir": "^1.0.0" } }, + "p-any": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-any/-/p-any-1.1.0.tgz", + "integrity": "sha512-Ef0tVa4CZ5pTAmKn+Cg3w8ABBXh+hHO1aV8281dKOoUHfX+3tjG2EaFcC+aZyagg9b4EYGsHEjz21DnEE8Og2g==", + "requires": { + "p-some": "^2.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "p-some": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-some/-/p-some-2.0.1.tgz", + "integrity": "sha1-Zdh8ixVO289SIdFnd4ttLhUPbwY=", + "requires": { + "aggregate-error": "^1.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "dev": true + }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, + "pako": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", + "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==", + "dev": true + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parent-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.0.tgz", + "integrity": "sha512-8Mf5juOMmiE4FcmzYc4IaiS9L3+9paz2KOiXzkRviCP6aDmN49Hz6EMWz0lGNp9pX80GvvAuLADtyGfW/Em3TA==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + }, + "dependencies": { + "callsites": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz", + "integrity": "sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw==", + "dev": true + } + } + }, + "parse-asn1": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse-ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.0.0.tgz", + "integrity": "sha512-AddiXFSLLCqj+tCRJ9MrUtHZB4DWojO3tk0NVZ+g5MaMQHF2+p2ktqxuoXyPFLljz/aUK0Nfhd/uGWnhXVXEyA==", + "dev": true + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "parsimmon": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.12.0.tgz", + "integrity": "sha512-uC/BjuSfb4jfaWajKCp1mVncXXq+V1twbcYChbTxN3GM7fn+8XoHwUdvUz+PTaFtDSCRQxU8+Rnh+iMhAkVwdw==" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { @@ -998,22 +6635,1788 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pixrem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pixrem/-/pixrem-4.0.1.tgz", + "integrity": "sha1-LaSh3m7EQjxfw3lOkwuB1EkOxoY=", + "dev": true, + "requires": { + "browserslist": "^2.0.0", + "postcss": "^6.0.0", + "reduce-css-calc": "^1.2.7" + }, + "dependencies": { + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000792", + "electron-to-chromium": "^1.3.30" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "pkg-conf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", + "integrity": "sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "load-json-file": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "pleeease-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pleeease-filters/-/pleeease-filters-4.0.0.tgz", + "integrity": "sha1-ZjKy+wVkjSdY2GU4T7zteeHMrsc=", + "dev": true, + "requires": { + "onecolor": "^3.0.4", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "plur": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/plur/-/plur-3.0.1.tgz", + "integrity": "sha512-lJl0ojUynAM1BZn58Pas2WT/TXeC1+bS+UqShl0x9+49AtOn7DixRXVzaC8qrDOIxNDmepKnLuMTH7NQmkX0PA==", + "dev": true, + "requires": { + "irregular-plurals": "^2.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-apply": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/postcss-apply/-/postcss-apply-0.8.0.tgz", + "integrity": "sha1-FOVEu7XLbxweBIhXll15rgZrE0M=", + "dev": true, + "requires": { + "babel-runtime": "^6.23.0", + "balanced-match": "^0.4.2", + "postcss": "^6.0.0" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-attribute-case-insensitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-2.0.0.tgz", + "integrity": "sha1-lNxCLI+QmX8WvTOjZUu77AhJY7Q=", + "dev": true, + "requires": { + "postcss": "^6.0.0", + "postcss-selector-parser": "^2.2.3" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-calc": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", + "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss": "^7.0.5", + "postcss-selector-parser": "^5.0.0-rc.4", + "postcss-value-parser": "^3.3.1" + } + }, + "postcss-color-function": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-function/-/postcss-color-function-4.0.1.tgz", + "integrity": "sha1-QCs/LOvD9pR+YY+2vjZU++zvZEQ=", + "dev": true, + "requires": { + "css-color-function": "~1.3.3", + "postcss": "^6.0.1", + "postcss-message-helpers": "^2.0.0", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-color-gray": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-4.1.0.tgz", + "integrity": "sha512-L4iLKQLdqChz6ZOgGb6dRxkBNw78JFYcJmBz1orHpZoeLtuhDDGegRtX9gSyfoCIM7rWZ3VNOyiqqvk83BEN+w==", + "dev": true, + "requires": { + "color": "^2.0.1", + "postcss": "^6.0.14", + "postcss-message-helpers": "^2.0.0", + "reduce-function-call": "^1.0.2" + }, + "dependencies": { + "color": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz", + "integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-color-hex-alpha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-3.0.0.tgz", + "integrity": "sha1-HlPmyKyyN5Vej9CLfs2xuLgwn5U=", + "dev": true, + "requires": { + "color": "^1.0.3", + "postcss": "^6.0.1", + "postcss-message-helpers": "^2.0.0" + }, + "dependencies": { + "color": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-1.0.3.tgz", + "integrity": "sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0=", + "dev": true, + "requires": { + "color-convert": "^1.8.2", + "color-string": "^1.4.0" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-color-hsl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hsl/-/postcss-color-hsl-2.0.0.tgz", + "integrity": "sha1-EnA2ZvoxBDDj8wpFTawThjF9WEQ=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-value-parser": "^3.3.0", + "units-css": "^0.4.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-color-hwb": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hwb/-/postcss-color-hwb-3.0.0.tgz", + "integrity": "sha1-NAKxnvTYSXVAwftQcr6YY8qVVx4=", + "dev": true, + "requires": { + "color": "^1.0.3", + "postcss": "^6.0.1", + "postcss-message-helpers": "^2.0.0", + "reduce-function-call": "^1.0.2" + }, + "dependencies": { + "color": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-1.0.3.tgz", + "integrity": "sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0=", + "dev": true, + "requires": { + "color-convert": "^1.8.2", + "color-string": "^1.4.0" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-color-rebeccapurple": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-3.1.0.tgz", + "integrity": "sha512-212hJUk9uSsbwO5ECqVjmh/iLsmiVL1xy9ce9TVf+X3cK/ZlUIlaMdoxje/YpsL9cmUH3I7io+/G2LyWx5rg1g==", + "dev": true, + "requires": { + "postcss": "^6.0.22", + "postcss-values-parser": "^1.5.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-color-rgb": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rgb/-/postcss-color-rgb-2.0.0.tgz", + "integrity": "sha1-FFOcinExSUtILg3RzCZf9lFLUmM=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-color-rgba-fallback": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rgba-fallback/-/postcss-color-rgba-fallback-3.0.0.tgz", + "integrity": "sha1-N9XJNToHoJJwkSqCYGu0Kg1wLAQ=", + "dev": true, + "requires": { + "postcss": "^6.0.6", + "postcss-value-parser": "^3.3.0", + "rgb-hex": "^2.1.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-cssnext": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-cssnext/-/postcss-cssnext-3.1.0.tgz", + "integrity": "sha512-awPDhI4OKetcHCr560iVCoDuP6e/vn0r6EAqdWPpAavJMvkBSZ6kDpSN4b3mB3Ti57hQMunHHM8Wvx9PeuYXtA==", + "dev": true, + "requires": { + "autoprefixer": "^7.1.1", + "caniuse-api": "^2.0.0", + "chalk": "^2.0.1", + "pixrem": "^4.0.0", + "pleeease-filters": "^4.0.0", + "postcss": "^6.0.5", + "postcss-apply": "^0.8.0", + "postcss-attribute-case-insensitive": "^2.0.0", + "postcss-calc": "^6.0.0", + "postcss-color-function": "^4.0.0", + "postcss-color-gray": "^4.0.0", + "postcss-color-hex-alpha": "^3.0.0", + "postcss-color-hsl": "^2.0.0", + "postcss-color-hwb": "^3.0.0", + "postcss-color-rebeccapurple": "^3.0.0", + "postcss-color-rgb": "^2.0.0", + "postcss-color-rgba-fallback": "^3.0.0", + "postcss-custom-media": "^6.0.0", + "postcss-custom-properties": "^6.1.0", + "postcss-custom-selectors": "^4.0.1", + "postcss-font-family-system-ui": "^3.0.0", + "postcss-font-variant": "^3.0.0", + "postcss-image-set-polyfill": "^0.3.5", + "postcss-initial": "^2.0.0", + "postcss-media-minmax": "^3.0.0", + "postcss-nesting": "^4.0.1", + "postcss-pseudo-class-any-link": "^4.0.0", + "postcss-pseudoelements": "^5.0.0", + "postcss-replace-overflow-wrap": "^2.0.0", + "postcss-selector-matches": "^3.0.1", + "postcss-selector-not": "^3.0.1" + }, + "dependencies": { + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000792", + "electron-to-chromium": "^1.3.30" + } + }, + "caniuse-api": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-2.0.0.tgz", + "integrity": "sha1-sd21pZZrFvSNxJmERNS7xsfZ2DQ=", + "dev": true, + "requires": { + "browserslist": "^2.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "postcss-calc": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-6.0.2.tgz", + "integrity": "sha512-fiznXjEN5T42Qm7qqMCVJXS3roaj9r4xsSi+meaBVe7CJBl8t/QLOXu02Z2E6oWAMWIvCuF6JrvzFekmVEbOKA==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss": "^7.0.2", + "postcss-selector-parser": "^2.2.2", + "reduce-css-calc": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "reduce-css-calc": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.6.tgz", + "integrity": "sha512-+l5/qlQmdsbM9h6JerJ/y5vR5Ci0k93aszLNpCmbadC3nBcbRGmIBm0s9Nj59i22LvCjTGftWzdQRwdknayxhw==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-custom-media": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-6.0.0.tgz", + "integrity": "sha1-vlMnhBEOyylQRPtTlaGABushpzc=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-custom-properties": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-6.3.1.tgz", + "integrity": "sha512-zoiwn4sCiUFbr4KcgcNZLFkR6gVQom647L+z1p/KBVHZ1OYwT87apnS42atJtx6XlX2yI7N5fjXbFixShQO2QQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^6.0.18" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-custom-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-4.0.1.tgz", + "integrity": "sha1-eBOC+UxS5yfvXKR3bqKt9JphE4I=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-selector-matches": "^3.0.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-font-family-system-ui": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-family-system-ui/-/postcss-font-family-system-ui-3.0.0.tgz", + "integrity": "sha512-58G/hTxMSSKlIRpcPUjlyo6hV2MEzvcVO2m4L/T7Bb2fJTG4DYYfQjQeRvuimKQh1V1sOzCIz99g+H2aFNtlQw==", + "dev": true, + "requires": { + "postcss": "^6.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-font-variant": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-3.0.0.tgz", + "integrity": "sha1-CMzIj2BQuoLtjvLMdsDGprQfGD4=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-image-set-polyfill": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/postcss-image-set-polyfill/-/postcss-image-set-polyfill-0.3.5.tgz", + "integrity": "sha1-Dxk0E3AM8fgr05Bm7wFtZaShgYE=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-media-query-parser": "^0.2.3" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-import": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-12.0.1.tgz", + "integrity": "sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "postcss-value-parser": "^3.2.3", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-initial": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-2.0.0.tgz", + "integrity": "sha1-cnFfczbgu3k1HZnuZcSiU6hEG6Q=", + "dev": true, + "requires": { + "lodash.template": "^4.2.4", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-media-minmax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-3.0.0.tgz", + "integrity": "sha1-Z1JWA3pD70C8Twdgv9BtTcadSNI=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", + "dev": true + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=", + "dev": true + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-nesting": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-4.2.1.tgz", + "integrity": "sha512-IkyWXICwagCnlaviRexi7qOdwPw3+xVVjgFfGsxmztvRVaNxAlrypOIKqDE5mxY+BVxnId1rnUKBRQoNE2VDaA==", + "dev": true, + "requires": { + "postcss": "^6.0.11" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-pseudo-class-any-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-4.0.0.tgz", + "integrity": "sha1-kVKgYT00UHIFE+iJKFS65C0O5o4=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-selector-parser": "^2.2.3" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-pseudoelements": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudoelements/-/postcss-pseudoelements-5.0.0.tgz", + "integrity": "sha1-7vGU6NUkZFylIKlJ6V5RjoEkAss=", + "dev": true, + "requires": { + "postcss": "^6.0.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-replace-overflow-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-2.0.0.tgz", + "integrity": "sha1-eU22+qVPjbEAhUOSqTr0V2i04ls=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-selector-matches": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-3.0.1.tgz", + "integrity": "sha1-5WNAEeE5UIgYYbvdWMLQER/8lqs=", + "dev": true, + "requires": { + "balanced-match": "^0.4.2", + "postcss": "^6.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-selector-not": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-3.0.1.tgz", + "integrity": "sha1-Lk2y8JZTNsAefOx9tsYN/3ZzNdk=", + "dev": true, + "requires": { + "balanced-match": "^0.4.2", + "postcss": "^6.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "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" + } + } + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "postcss-values-parser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-1.5.0.tgz", + "integrity": "sha512-3M3p+2gMp0AH3da530TlX8kiO1nxdTnc3C6vr8dMxRLIlh8UYkz0/wcwptSXjhtx2Fr0TySI7a+BHDQ8NL7LaQ==", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, "prettier": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.15.3.tgz", - "integrity": "sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg==", + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz", + "integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==", + "dev": true + }, + "pretty-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-4.0.0.tgz", + "integrity": "sha512-qG66ahoLCwpLXD09ZPHSCbUWYTqdosB7SMP4OffgTgL2PBKXMuUsrk5Bwg8q4qPkjTXsKBMr+YK3Ltd/6F9s/Q==", + "dev": true, + "requires": { + "parse-ms": "^2.0.0" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", "dev": true }, "process-nextick-args": { @@ -1027,12 +8430,154 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "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 }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.6.0.tgz", + "integrity": "sha512-KIJqT9jQJDQx5h5uAVPimw6yVg2SekOKu959OCtktD3FjzbpvaPr8i4zzg07DOMz+igA4W/aNM7OV8H37pFYfA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + } + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -1046,14 +8591,97 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } + } + }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -1063,30 +8691,102 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } } }, - "redis": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", - "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", - "optional": true, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, "requires": { - "double-ended-queue": "^2.1.0-0", - "redis-commands": "^1.2.0", - "redis-parser": "^2.6.0" + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" } }, - "redis-commands": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz", - "integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw==", - "optional": true + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } }, - "redis-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", - "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=", - "optional": true + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "dev": true, + "requires": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "reduce-function-call": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", + "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", + "dev": true, + "requires": { + "balanced-match": "^0.4.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz", + "integrity": "sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } }, "regexpp": { "version": "2.0.1", @@ -1094,24 +8794,95 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, - "require-uncached": { - "version": "1.0.3", - "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "regexpu-core": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.4.0.tgz", + "integrity": "sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA==", "dev": true, "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^7.0.0", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.0.2" + } + }, + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "^1.0.1" + } + }, + "regjsgen": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", + "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", + "dev": true + }, + "regjsparser": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", + "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" }, "dependencies": { - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } } }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "require-precompiled": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/require-precompiled/-/require-precompiled-0.1.0.tgz", + "integrity": "sha1-WhtS63Dr7UPrmC6XTIWrWVceVvo=", + "dev": true + }, "require_optional": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", @@ -1121,11 +8892,76 @@ "semver": "^5.1.0" } }, + "requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, "resolve-from": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" }, + "resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=", + "requires": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "dependencies": { + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + } + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -1136,12 +8972,115 @@ "signal-exit": "^3.0.2" } }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rgb": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rgb/-/rgb-0.1.0.tgz", + "integrity": "sha1-vieykej+/+rBvZlylyG/pA/AN7U=", + "dev": true + }, + "rgb-hex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rgb-hex/-/rgb-hex-2.1.0.tgz", + "integrity": "sha1-x3PF/iJoolV42SU5qCp6XOU77aY=", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rollup": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.2.2.tgz", + "integrity": "sha512-fsn5KJcfSuejjrv8GV7kZNciElqxyzZdUq8rA3e528JsR3ccxrWwoptyUY8GGLlgMFAQMB3dZW8nWF2I1/xrZA==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "@types/node": "*", + "acorn": "^6.1.0" + } + }, + "rollup-plugin-json": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-json/-/rollup-plugin-json-3.1.0.tgz", + "integrity": "sha512-BlYk5VspvGpjz7lAwArVzBXR60JK+4EKtPkCHouAWg39obk9S61hZYJDBfMK+oitPdoe11i69TlxKlMQNFC/Uw==", + "dev": true, + "requires": { + "rollup-pluginutils": "^2.3.1" + } + }, + "rollup-plugin-replace": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.1.0.tgz", + "integrity": "sha512-SxrAIgpH/B5/W4SeULgreOemxcpEgKs2gcD42zXw50bhqGWmcnlXneVInQpAqzA/cIly4bJrOpeelmB9p4YXSQ==", + "dev": true, + "requires": { + "magic-string": "^0.25.1", + "minimatch": "^3.0.2", + "rollup-pluginutils": "^2.0.1" + } + }, + "rollup-plugin-terser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-4.0.4.tgz", + "integrity": "sha512-wPANT5XKVJJ8RDUN0+wIr7UPd0lIXBo4UdJ59VmlPCtlFsE20AM+14pe+tk7YunCsWEiuzkDBY3QIkSCjtrPXg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "jest-worker": "^24.0.0", + "serialize-javascript": "^1.6.1", + "terser": "^3.14.1" + } + }, + "rollup-plugin-typescript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript/-/rollup-plugin-typescript-1.0.0.tgz", + "integrity": "sha512-d2KDNMJXgaaB//dDGd/YmyMiopt1Pz965Iu3zmEoL08YqNcKRBz26uHqqc47rFGfrJV5kFqifC9IYlh6dpSCLg==", + "dev": true, + "requires": { + "resolve": "^1.8.1", + "rollup-pluginutils": "^2.3.1" + } + }, + "rollup-pluginutils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.4.1.tgz", + "integrity": "sha512-wesMQ9/172IJDIW/lYWm0vW0LiKe5Ekjws481R7z9WTRtmO59cqyM/2uUlxvf6yzm/fElFmHUobeQOYz46dZJw==", + "dev": true, + "requires": { + "estree-walker": "^0.6.0", + "micromatch": "^3.1.10" } }, "run-async": { @@ -1153,10 +9092,19 @@ "is-promise": "^2.1.0" } }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, "rxjs": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", - "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", + "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -1167,6 +9115,15 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1186,6 +9143,17 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, "seedrandom": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.4.tgz", @@ -1196,11 +9164,76 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "^5.0.3" + } + }, + "serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go=", + "dev": true + }, + "serialize-javascript": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.6.1.tgz", + "integrity": "sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==", + "dev": true + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -1221,14 +9254,35 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, "slice-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.0.0.tgz", - "integrity": "sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" }, "dependencies": { @@ -1240,6 +9294,189 @@ } } }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz", + "integrity": "sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==", + "dev": true + }, "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -1249,12 +9486,139 @@ "memory-pager": "^1.0.2" } }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", + "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, "sprintf-js": { "version": "1.0.3", - "resolved": "http://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "stack-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -1267,7 +9631,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -1275,35 +9639,924 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-bom-buf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz", + "integrity": "sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI=", + "dev": true, + "requires": { + "is-utf8": "^0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "supertap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supertap/-/supertap-1.0.0.tgz", + "integrity": "sha512-HZJ3geIMPgVwKk2VsmO5YHqnnJYl6bV5A9JW2uzqV43WmpgliNEYbuvukfor7URpaqpxuw3CfZ3ONdVbZjCgIA==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "indent-string": "^3.2.0", + "js-yaml": "^3.10.0", + "serialize-error": "^2.1.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, - "table": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/table/-/table-5.1.1.tgz", - "integrity": "sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw==", + "svgo": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.1.1.tgz", + "integrity": "sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g==", "dev": true, "requires": { - "ajv": "^6.6.1", + "coa": "~2.0.1", + "colors": "~1.1.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "~0.1.0", + "css-tree": "1.0.0-alpha.28", + "css-url-regex": "^1.1.0", + "csso": "^3.5.0", + "js-yaml": "^3.12.0", + "mkdirp": "~0.5.1", + "object.values": "^1.0.4", + "sax": "~1.2.4", + "stable": "~0.1.6", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "table": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/table/-/table-5.2.3.tgz", + "integrity": "sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==", + "dev": true, + "requires": { + "ajv": "^6.9.1", "lodash": "^4.17.11", - "slice-ansi": "2.0.0", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", + "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "string-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.0.0.tgz", + "integrity": "sha512-rr8CUxBbvOZDUvc5lNIJ+OC1nPVpz+Siw9VBtUjB9b6jZehZLFt0JMCZzShFHIsI8cbhm0EsNIfWJMFV3cu3Ew==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "strip-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", + "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", + "dev": true, + "requires": { + "ansi-regex": "^4.0.0" + } + } + } + }, + "tapable": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", + "integrity": "sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==", + "dev": true + }, + "tar": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "^0.7.0" + } + }, + "terser": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-3.16.1.tgz", + "integrity": "sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow==", + "dev": true, + "requires": { + "commander": "~2.17.1", + "source-map": "~0.6.1", + "source-map-support": "~0.5.9" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz", + "integrity": "sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg==", + "dev": true, + "requires": { + "cacache": "^11.0.2", + "find-cache-dir": "^2.0.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.4.0", + "source-map": "^0.6.1", + "terser": "^3.16.1", + "webpack-sources": "^1.1.0", + "worker-farm": "^1.5.2" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "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 + }, + "thenify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha1-mcW/VZWJZq9tBtg73zgA3IL67F0=", + "dev": true + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "ts-node": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.0.2.tgz", + "integrity": "sha512-MosTrinKmaAcWgO8tqMjMJB22h+sp3Rd1i4fdoWY4mhBDekOwIAKI/bzmRi7IcbCmjquccYg2gcF6NBkLgr0Tw==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "tsutils": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.8.0.tgz", + "integrity": "sha512-XQdPhgcoTbCD8baXC38PQ0vpTZ8T3YrE+vR66YIj/xvDt1//8iAhafpIT/4DmvzzC1QFapEImERu48Pa01dIUA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3.tgz", + "integrity": "sha512-Y21Xqe54TBVp+VDSNbuDYdGw0BpoR/Q6wo/+35M8PAU0vipahnyduJWirxxdxjsAkS7hue53x2zp8gz7F05u0A==", + "dev": true + }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz", + "integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz", + "integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==", + "dev": true + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz", + "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "unique-temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", + "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1", + "os-tmpdir": "^1.0.1", + "uid2": "0.0.3" + } + }, + "units-css": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/units-css/-/units-css-0.4.0.tgz", + "integrity": "sha1-1iKGU6UZg9fBb/KPi53Dsf/tOgc=", + "dev": true, + "requires": { + "isnumeric": "^0.2.0", + "viewport-dimensions": "^0.2.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "dev": true + }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vendors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz", + "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==", + "dev": true + }, + "viewport-dimensions": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz", + "integrity": "sha1-3nQHR9tTh/0XJfUXXpG6x2r982w=", + "dev": true + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webpack": { + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.29.5.tgz", + "integrity": "sha512-DuWlYUT982c7XVHodrLO9quFbNpVq5FNxLrMUfYUTlgKW0+yPimynYf1kttSQpEneAL1FH3P3OLNgkyImx8qIQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.3", + "@webassemblyjs/helper-module-context": "1.8.3", + "@webassemblyjs/wasm-edit": "1.8.3", + "@webassemblyjs/wasm-parser": "1.8.3", + "acorn": "^6.0.5", + "acorn-dynamic-import": "^4.0.0", + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", + "chrome-trace-event": "^1.0.0", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.0", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.3.0", + "loader-utils": "^1.1.0", + "memory-fs": "~0.4.1", + "micromatch": "^3.1.8", + "mkdirp": "~0.5.0", + "neo-async": "^2.5.0", + "node-libs-browser": "^2.0.0", + "schema-utils": "^1.0.0", + "tapable": "^1.1.0", + "terser-webpack-plugin": "^1.1.0", + "watchpack": "^1.5.0", + "webpack-sources": "^1.3.0" + } + }, + "webpack-sources": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "well-known-symbols": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", + "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "dev": true, + "requires": { "string-width": "^2.1.1" }, "dependencies": { @@ -1340,111 +10593,94 @@ } } }, - "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "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 - }, - "through": { - "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, + "worker-farm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", + "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", "dev": true, "requires": { "mkdirp": "^0.5.1" } }, + "write-file-atomic": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", + "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + }, + "yaml": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.3.2.tgz", + "integrity": "sha512-ZZZIdcApMRcAez37EVrtCim+8JUESX0zRcsv+HMfatIX79cX22CAnVkxDrZhAmzsnka2nb/mvaTybzDYcnrIew==", + "dev": true + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" + }, + "yn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.0.0.tgz", + "integrity": "sha512-+Wo/p5VRfxUgBUGy2j/6KX2mj9AYJWOHuhMjMcbBFc3y54o9/4buK1ksBvuiK01C3kby8DH9lSmJdSxw+4G/2Q==", + "dev": true } } } diff --git a/package.json b/package.json index eac82cc..62e23e1 100644 --- a/package.json +++ b/package.json @@ -16,20 +16,54 @@ "license": "AGPL-3.0", "preferGlobal": true, "directories": { - "bin": "bin" + "bin": "bin", + "tools": "tools" }, "dependencies": { + "jsonwebtoken": "^8.4.0", + "koa": "^2.7.0", + "koa-bodyparser": "^4.2.1", + "koa-compress": "^3.0.0", + "koa-jwt": "^3.5.1", + "koa-router": "^7.4.0", + "koa-static": "^5.0.0", "later": "^1.2.0", "libxmljs": "^0.19.5", - "mongodb": "^3.1.10", + "mongodb": "^3.1.13", + "parsimmon": "^1.12.0", "seedrandom": "^2.4.4" }, - "optionalDependencies": { - "redis": "^2.6.5" - }, "devDependencies": { - "eslint": "^5.10.0", - "prettier": "^1.15.3" + "@types/jsonwebtoken": "^8.3.0", + "@types/koa": "^2.0.48", + "@types/koa-compress": "^2.0.8", + "@types/koa-router": "^7.0.39", + "@types/koa-static": "^4.0.1", + "@types/libxmljs": "^0.18.3", + "@types/mithril": "^1.1.16", + "@types/mongodb": "^3.1.19", + "@types/parsimmon": "^1.10.0", + "@types/seedrandom": "^2.4.27", + "@typescript-eslint/eslint-plugin": "^1.3.0", + "ava": "^1.2.1", + "codemirror": "^5.43.0", + "cssnano": "^4.1.10", + "eslint": "^5.14.1", + "eslint-config-prettier": "^4.0.0", + "esm": "^3.2.5", + "mithril": "^1.1.6", + "postcss-cssnext": "^3.1.0", + "postcss-import": "^12.0.1", + "prettier": "^1.16.4", + "rollup": "^1.2.1", + "rollup-plugin-json": "^3.1.0", + "rollup-plugin-replace": "^2.1.0", + "rollup-plugin-terser": "^4.0.4", + "rollup-plugin-typescript": "^1.0.0", + "ts-node": "^8.0.2", + "typescript": "^3.3.3", + "webpack": "^4.29.5", + "yaml": "^1.3.2" }, "engines": { "node": ">=8.14.0" @@ -38,7 +72,12 @@ "compile": "echo 'Compile step no longer required, skipping.'", "configure": "if [ ! -f ./config/config.json ]; then cp ./config/config-sample.json ./config/config.json; fi", "install": "npm run configure", - "test": "node test", - "lint": "eslint --fix lib/*.js; prettier --write lib/*.js" + "test": "ava --verbose --color", + "lint-ln": "find bin -type f -exec sh -c 'ln -fsr \"$1\" \"bin/.${1#bin/}.ts\"' _ {} \\;; find tools -type f -exec sh -c 'ln -fsr \"$1\" \"tools/.${1#tools/}.ts\"' _ {} \\;", + "lint-eslint": "eslint --fix \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"", + "lint-prettier": "prettier --parser typescript --write \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"", + "lint-tsc": "tsc --noEmit", + "lint": "npm run lint-ln && npm run lint-eslint && npm run lint-prettier && npm run lint-tsc", + "build": "node -r esm -r ts-node/register/transpile-only build/build" } } diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..1f16570 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..2352be8 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/common.ts b/test/common.ts new file mode 100644 index 0000000..c08978d --- /dev/null +++ b/test/common.ts @@ -0,0 +1,70 @@ +import ava from "ava"; +import * as common from "../lib/common"; + +ava("typeOf", t => { + const cases = [ + [undefined, common.UNDEFINED_TYPE], + [null, common.NULL_TYPE], + [true, common.BOOLEAN_TYPE], + [false, common.BOOLEAN_TYPE], + [0, common.NUMBER_TYPE], + [1, common.NUMBER_TYPE], + [-1, common.NUMBER_TYPE], + [0.0, common.NUMBER_TYPE], + [1.0, common.NUMBER_TYPE], + [1.1, common.NUMBER_TYPE], + [-1.0, common.NUMBER_TYPE], + [-1.1, common.NUMBER_TYPE], + ["", common.STRING_TYPE], + ["TEST", common.STRING_TYPE], + [[], common.ARRAY_TYPE], + [[1, 2, 3], common.ARRAY_TYPE], + [{}, common.OBJECT_TYPE], + [{ test: 1 }, common.OBJECT_TYPE], + [/[^A-Za-z0-9_]/g, common.REGEXP_TYPE], + [new RegExp("//"), common.REGEXP_TYPE], + [new Date(), common.DATE_TYPE] + ]; + t.plan(cases.length); + for (const c of cases) t.is(common.typeOf(c[0]), c[1]); +}); + +ava("generateDeviceId", t => { + const space = [" ", "%20"]; + const special = [";", "%3B"]; + const cases = [ + [ + { + ProductClass: "TestProductClass", + OUI: "TestOUI", + SerialNumber: "TestSerialNumber" + }, + "TestOUI-TestProductClass-TestSerialNumber" + ], + [ + { + OUI: "TestOUI", + SerialNumber: "TestSerialNumber" + }, + "TestOUI-TestSerialNumber" + ], + [ + { + OUI: `TestOUIWith${space[0]}_${special[0]}2912`, + SerialNumber: `TestSerialNumberWith${space[0]}_${special[0]}2912` + }, + `TestOUIWith${space[1]}_${special[1]}2912-TestSerialNumberWith${ + space[1] + }_${special[1]}2912` + ] + ]; + t.plan(cases.length); + for (const c of cases) t.is(common.generateDeviceId(c[0]), c[1]); +}); + +ava("escapeRegExp", t => { + t.is( + common.escapeRegExp("\\ ^ $ * + ? . ( ) | { } [ ]"), + "\\\\ \\^ \\$ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]" + ); +}); diff --git a/test/device.ts b/test/device.ts new file mode 100644 index 0000000..59e603c --- /dev/null +++ b/test/device.ts @@ -0,0 +1,20 @@ +import ava from "ava"; +import Path from "../lib/common/path"; +import * as device from "../lib/device"; + +ava("getAliasDeclarations", t => { + const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b"); + const decs = device.getAliasDeclarations(path, 99); + + const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"]; + + t.plan(24); + for (const [i, d] of decs.entries()) { + t.is(d.path.toString(), expected[i]); + t.is(d.pathGet, 99); + t.is(d.pathSet, null); + t.deepEqual(d.attrGet, i ? { value: 99 } : null); + t.is(d.attrSet, null); + t.is(d.defer, true); + } +}); diff --git a/test/mongodb-functions.ts b/test/mongodb-functions.ts new file mode 100644 index 0000000..5e35ec0 --- /dev/null +++ b/test/mongodb-functions.ts @@ -0,0 +1,58 @@ +import ava from "ava"; +import * as mongodbFunctions from "../lib/mongodb-functions"; +import { stringify } from "../lib/common/expression-parser"; + +ava("mongoQueryToFilter", t => { + const tests = [ + [{}, "true"], + [{ test: "test" }, 'test = "test"'], + [{ test: { $eq: "test" } }, 'test = "test"'], + [{ test: { $ne: "test" } }, 'test <> "test"'], + [{ test: { $gte: "test" } }, 'test >= "test"'], + [{ "Tags.test": true }, "Tags.test IS NOT NULL"], + [{ "Tags.test": false }, "Tags.test IS NULL"], + [{ "Tags.test": { $exists: true } }, "Tags.test IS NOT NULL"], + [{ "Tags.test": { $exists: false } }, "Tags.test IS NULL"], + [{ "Tags.test": { $ne: true } }, "Tags.test IS NULL"], + [{ "Tags.test": { $ne: false } }, "Tags.test IS NOT NULL"], + [{ "Tags.test": { $eq: true } }, "Tags.test IS NOT NULL"], + [{ "Tags.test": { $eq: false } }, "Tags.test IS NULL"], + [{ _tags: "test" }, "Tags.test IS NOT NULL"], + [{ _tags: { $ne: "test" } }, "Tags.test IS NULL"], + [{ _tags: { $eq: "test" } }, "Tags.test IS NOT NULL"], + [ + { $and: [{ test: "test" }, { test: { $ne: "test" } }] }, + 'test = "test" AND test <> "test"' + ], + [{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'], + [ + { $or: [{ test: "test" }, { test: { $ne: "test" } }] }, + 'test = "test" OR test <> "test"' + ], + [ + { test: { $gte: "test1", $ne: "test2" } }, + 'test >= "test1" AND test <> "test2"' + ] + ]; + + const shouldFailTests = [ + [{ test: { $gee: "test" } }, "Operator $gee not supported"], + [{ test: [] }, "Invalid type"], + [{ "Tags.test": { $gt: true } }, "Invalid tag query"], + [{ _tags: [] }, "Invalid type"], + [{ _tags: { $gt: "test" } }, "Invalid tag query"], + [{ $nor: [] }, "Operator $nor not supported"] + ]; + + t.plan(tests.length + 2 * shouldFailTests.length); + for (const test of tests) + t.is(stringify(mongodbFunctions.mongoQueryToFilter(test[0])), test[1]); + + for (const test of shouldFailTests) { + const func = (): void => { + mongodbFunctions.mongoQueryToFilter(test[0]); + }; + const error = t.throws(func, Error); + t.is(error.message, test[1]); + } +}); diff --git a/test/path.ts b/test/path.ts new file mode 100644 index 0000000..d4867be --- /dev/null +++ b/test/path.ts @@ -0,0 +1,25 @@ +import ava from "ava"; +import Path from "../lib/common/path"; + +ava("toString", t => { + const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123'); + const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123'); + t.is(path1.toString(), path2.toString()); +}); + +ava("slice", t => { + const path = Path.parse("a.*.b.[x:y].c"); + const sliced = path.slice(1, -1); + t.is(sliced.toString(), '*.b.[x:"y"]'); + t.is(sliced.alias, 0b100); + t.is(sliced.wildcard, 0b1); +}); + +ava("concat", t => { + const path1 = Path.parse("a"); + const path2 = Path.parse("*.[a:b]"); + const concat = path1.concat(path2); + t.is(concat.toString(), 'a.*.[a:"b"]'); + t.is(concat.alias, 0b100); + t.is(concat.wildcard, 0b10); +}); diff --git a/tools/configure-ui b/tools/configure-ui new file mode 100755 index 0000000..714dc96 --- /dev/null +++ b/tools/configure-ui @@ -0,0 +1,232 @@ +#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only +import { MongoClient } from "mongodb"; +import { get } from "../lib/config"; +import { hashPassword, generateSalt } from "../lib/auth"; + +const permissionsData = [ + { role: "admin", resource: "devices", access: 3, validate: ["pass"] }, + { role: "admin", resource: "faults", access: 3, validate: ["pass"] }, + { role: "admin", resource: "files", access: 3, validate: ["pass"] }, + { role: "admin", resource: "presets", access: 3, validate: ["pass"] }, + { role: "admin", resource: "provisions", access: 3, validate: ["pass"] }, + { role: "admin", resource: "config", access: 3, validate: ["pass"] }, + { role: "admin", resource: "permissions", access: 3, validate: ["pass"] }, + { role: "admin", resource: "users", access: 3, validate: ["pass"] }, + { + role: "admin", + resource: "virtualParameters", + access: 3, + validate: ["pass"] + }, + { role: "view", resource: "devices", access: 2, validate: [] }, + { role: "view", resource: "faults", access: 2, validate: [] }, + { role: "view", resource: "files", access: 2, validate: [] }, + { role: "view", resource: "presets", access: 2, validate: [] }, + { role: "view", resource: "provisions", access: 2, validate: [] }, + { role: "view", resource: "config", access: 2, validate: [] }, + { role: "view", resource: "users", access: 2, validate: [] }, + { role: "view", resource: "permissions", access: 2, validate: [] }, + { role: "view", resource: "virtualParameters", access: 2, validate: [] } +]; + +const usersData = [ + { username: "admin", password: "admin", roles: ["admin"] }, + { username: "view", password: "view", roles: ["view"] } +]; + +const configData = { + "ui.device.0.type": "'tags'", + "ui.device.1.type": "'ping'", + "ui.device.2.parameters.0.components.0.type": "'parameter'", + "ui.device.2.parameters.0.components.1.chart": "'online'", + "ui.device.2.parameters.0.components.1.type": "'overview-dot'", + "ui.device.2.parameters.0.components.2.parameters.0": + "InternetGatewayDevice.DeviceInfo.HardwareVersion", + "ui.device.2.parameters.0.components.2.parameters.1": + "InternetGatewayDevice.DeviceInfo.SoftwareVersion", + "ui.device.2.parameters.0.components.2.parameters.2": + "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress", + "ui.device.2.parameters.0.components.2.parameters.3": + "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress", + "ui.device.2.parameters.0.components.2.parameters.4": + "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", + "ui.device.2.parameters.0.components.2.parameters.5": + "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase", + "ui.device.2.parameters.0.components.2.parameters.6": + "InternetGatewayDevice.LANDevice.1.Hosts.Host", + "ui.device.2.parameters.0.components.2.type": "'summon-button'", + "ui.device.2.parameters.0.element": "'span.inform'", + "ui.device.2.parameters.0.label": "'Last inform'", + "ui.device.2.parameters.0.parameter": "DATE_STRING(Events.Inform)", + "ui.device.2.parameters.0.type": "'container'", + "ui.device.2.parameters.1.label": "'Serial number'", + "ui.device.2.parameters.1.parameter": + "InternetGatewayDevice.DeviceInfo.SerialNumber", + "ui.device.2.parameters.10.label": "'WLAN passphrase'", + "ui.device.2.parameters.10.parameter": + "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase", + "ui.device.2.parameters.2.label": "'Product class'", + "ui.device.2.parameters.2.parameter": + "InternetGatewayDevice.DeviceInfo.ProductClass", + "ui.device.2.parameters.3.label": "'OUI'", + "ui.device.2.parameters.3.parameter": + "InternetGatewayDevice.DeviceInfo.ManufacturerOUI", + "ui.device.2.parameters.4.label": "'Manufacturer'", + "ui.device.2.parameters.4.parameter": + "InternetGatewayDevice.DeviceInfo.Manufacturer", + "ui.device.2.parameters.5.label": "'Hardware version'", + "ui.device.2.parameters.5.parameter": + "InternetGatewayDevice.DeviceInfo.HardwareVersion", + "ui.device.2.parameters.6.label": "'Software version'", + "ui.device.2.parameters.6.parameter": + "InternetGatewayDevice.DeviceInfo.SoftwareVersion", + "ui.device.2.parameters.7.label": "'MAC'", + "ui.device.2.parameters.7.parameter": + "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress", + "ui.device.2.parameters.8.label": "'IP'", + "ui.device.2.parameters.8.parameter": + "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress", + "ui.device.2.parameters.9.label": "'WLAN SSID'", + "ui.device.2.parameters.9.parameter": + "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", + "ui.device.2.type": "'parameter-list'", + "ui.device.3.childParameters.0.label": "'Host name'", + "ui.device.3.childParameters.0.parameter": "HostName", + "ui.device.3.childParameters.1.label": "'IP address'", + "ui.device.3.childParameters.1.parameter": "IPAddress", + "ui.device.3.childParameters.2.label": "'MAC address'", + "ui.device.3.childParameters.2.parameter": "MACAddress", + "ui.device.3.label": "'WLAN hosts'", + "ui.device.3.parameter": "InternetGatewayDevice.LANDevice.1.Hosts.Host", + "ui.device.3.type": "'parameter-table'", + "ui.device.4.components.0.components.0": "'Faults'", + "ui.device.4.components.0.element": "'h3'", + "ui.device.4.components.0.type": "'container'", + "ui.device.4.components.1.type": "'device-faults'", + "ui.device.4.element": "'div'", + "ui.device.4.type": "'container'", + "ui.device.5.components.0.components.0": "'All parameters'", + "ui.device.5.components.0.element": "'h3'", + "ui.device.5.components.0.type": "'container'", + "ui.device.5.components.1.type": "'all-parameters'", + "ui.device.5.element": "'div.container-full-width'", + "ui.device.5.type": "'container'", + "ui.device.6.type": "'device-actions'", + "ui.filters.0.label": "'Serial number'", + "ui.filters.0.parameter": "DeviceID.SerialNumber", + "ui.filters.0.type": "'string'", + "ui.filters.1.label": "'Product class'", + "ui.filters.1.parameter": "DeviceID.ProductClass", + "ui.filters.1.type": "'string'", + "ui.filters.2.label": "'Tag'", + "ui.filters.2.parameter": "tag", + "ui.filters.2.type": "'string'", + "ui.index.0.components.0.type": "'parameter'", + "ui.index.0.label": "'Serial number'", + "ui.index.0.parameter": "DeviceID.SerialNumber", + "ui.index.0.type": "'device-link'", + "ui.index.1.label": "'Product class'", + "ui.index.1.parameter": "DeviceID.ProductClass", + "ui.index.2.label": "'Software version'", + "ui.index.2.parameter": "InternetGatewayDevice.DeviceInfo.SoftwareVersion", + "ui.index.3.label": "'IP'", + "ui.index.3.parameter": + "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress", + "ui.index.4.label": "'SSID'", + "ui.index.4.parameter": + "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", + "ui.index.5.components.0.type": "'parameter'", + "ui.index.5.components.1.chart": "'online'", + "ui.index.5.components.1.type": "'overview-dot'", + "ui.index.5.element": "'span.inform'", + "ui.index.5.label": "'Last inform'", + "ui.index.5.parameter": "DATE_STRING(Events.Inform)", + "ui.index.5.type": "'container'", + "ui.index.6.label": "'Tags'", + "ui.index.6.parameter": "Tags", + "ui.index.6.type": "'tags'", + "ui.index.6.unsortable": "true", + "ui.index.6.writable": "false", + "ui.overview.charts.online.label": "'Online status'", + "ui.overview.charts.online.slices.1_onlineNow.color": "'#31a354'", + "ui.overview.charts.online.slices.1_onlineNow.filter": + "Events.Inform > NOW() - 5 * 60 * 1000", + "ui.overview.charts.online.slices.1_onlineNow.label": "'Online now'", + "ui.overview.charts.online.slices.2_past24.color": "'#addd8e'", + "ui.overview.charts.online.slices.2_past24.filter": + "Events.Inform > (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000) AND Events.Inform < (NOW() - 5 * 60 * 1000)", + "ui.overview.charts.online.slices.2_past24.label": "'Past 24 hours'", + "ui.overview.charts.online.slices.3_others.color": "'#f7fcb9'", + "ui.overview.charts.online.slices.3_others.filter": + "Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)", + "ui.overview.charts.online.slices.3_others.label": "'Others'", + "ui.overview.groups.online.charts.0": "'online'", + "ui.overview.groups.online.label": "''", + "ui.pageSize": "10" +}; + +async function run(): Promise { + const client = await MongoClient.connect(get("MONGODB_CONNECTION_URL"), { + useNewUrlParser: true + }); + + const deletes = []; + for (const col of ["permissions", "users", "config"]) { + deletes.push( + client + .db() + .collection(col) + .deleteMany({}) + ); + } + + await Promise.all(deletes); + + const inserts = []; + const objs = Object.entries(configData).map(c => ({ + _id: c[0], + value: c[1] + })); + inserts.push( + client + .db() + .collection("config") + .insertMany(objs) + ); + const perms = permissionsData.map(p => { + p.validate = (p.validate || []).join(","); + return Object.assign({ _id: `${p.role}:${p.resource}:${p.access}` }, p); + }); + inserts.push( + client + .db() + .collection("permissions") + .insertMany(perms) + ); + const saltingProms = []; + for (const u of usersData) { + saltingProms.push( + new Promise(async resolve => { + u.salt = await generateSalt(64); + u.password = await hashPassword(u.password, u.salt); + u.roles = (u.roles || []).join(","); + u._id = u.username; + delete u.username; + resolve(u); + }) + ); + } + + const users = await Promise.all(saltingProms); + inserts.push( + client + .db() + .collection("users") + .insertMany(users) + ); + + await Promise.all(inserts); + client.close(); +} + +run(); diff --git a/tools/dump-data-model b/tools/dump-data-model index e7d47c1..771d005 100755 --- a/tools/dump-data-model +++ b/tools/dump-data-model @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only /** * Copyright 2013-2017 Zaid Abdulla * @@ -17,75 +17,67 @@ * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ -"use strict"; -const fs = require("fs"); -const path = require("path"); +import * as fs from "fs"; +import * as path from "path"; +import { connect, devicesCollection } from "../lib/db"; const outputFilename = path.resolve(`data_model_${process.argv[2]}.json`); process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); -const db = require("../lib/db"); - const dataModel = {}; +function recursive(object, p): void { + if (!p) p = ""; -function recursive(object, path) { - if (!path) - path = ""; + for (const key in object) { + if (key[0] === "_" || !object[key]) continue; - for (let key in object) { - if (key[0] === "_" || !object[key]) - continue; - - let isObject = object[key]["_object"]; - let isValue = "_value" in object[key]; + const isObject = object[key]["_object"]; + const isValue = "_value" in object[key]; if ((isObject && isValue) || (!isObject && !isValue)) throw new Error("Unexpected object"); if (isValue) { let value = object[key]["_value"]; - if (typeof value === "boolean") - value = +value; + if (typeof value === "boolean") value = +value; - let values = [object[key]["_writable"] || false, value.toString()]; - if (object[key]["_type"]) - values.push(object[key]["_type"]); + const values = [object[key]["_writable"] || false, value.toString()]; + if (object[key]["_type"]) values.push(object[key]["_type"]); - dataModel[`${path}${key}`] = values; + dataModel[`${p}${key}`] = values; } if (isObject) { - let values = [object[key]["_writable"] || false]; - dataModel[`${path}${key}.`] = values; - recursive(object[key], `${path}${key}.`); + const values = [object[key]["_writable"] || false]; + dataModel[`${p}${key}.`] = values; + recursive(object[key], `${p}${key}.`); } } } +connect(err => { + if (err) throw err; -db.connect(function(err) { - if (err) - throw err; + return devicesCollection.findOne( + { + _id: process.argv[2] + }, + {}, + (err, device) => { + if (err) throw err; - return db.devicesCollection.findOne({ - _id: process.argv[2] - }, {}, function(err, device) { - if (err) - throw err; + if (!device) throw new Error("Device not found"); - if (!device) - throw new Error("Device not found"); + recursive(device); - recursive(device); + fs.writeFile(outputFilename, JSON.stringify(dataModel, null, 2), () => { + if (err) throw err; - fs.writeFile(outputFilename, JSON.stringify(dataModel, null, 2), function() { - if (err) - throw err; - - console.log(`${outputFilename} saved`); - process.exit(0); - }); - }); + process.stdout.write(`${outputFilename} saved\n`); + process.exit(0); + }); + } + ); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c3f0f21 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "lib": [ + "esnext", "dom" + ], + "module": "esnext", + "moduleResolution": "node", + "target": "esnext", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "noImplicitReturns": true, + "strictFunctionTypes": true, + "allowJs": true + }, + "include": [ + "./bin/.*.ts", + "./lib/**/*", + "./ui/**/*", + "./test/**/*", + "./build/**/*", + "./toos/.*.ts" + ] +} diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json new file mode 100644 index 0000000..1147cf1 --- /dev/null +++ b/ui/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "env": { + "browser": true, + "node": false + } +} diff --git a/ui/admin-menu.ts b/ui/admin-menu.ts new file mode 100644 index 0000000..46d2424 --- /dev/null +++ b/ui/admin-menu.ts @@ -0,0 +1,85 @@ +import m from "mithril"; +import { ClosureComponent, Component } from "mithril"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const active = { [vnode.attrs["page"]]: "active" }; + const tabs = []; + + if (window.authorizer.hasAccess("presets", 1)) { + tabs.push( + m( + "li", + { class: active["presets"] }, + m("a", { href: "#!/admin/presets" }, "Presets") + ) + ); + } + + if (window.authorizer.hasAccess("provisions", 1)) { + tabs.push( + m( + "li", + { class: active["provisions"] }, + m("a", { href: "#!/admin/provisions" }, "Provisions") + ) + ); + } + + if (window.authorizer.hasAccess("virtualParameters", 1)) { + tabs.push( + m( + "li", + { class: active["virtualParameters"] }, + m("a", { href: "#!/admin/virtualParameters" }, "Virtual Parameters") + ) + ); + } + + if (window.authorizer.hasAccess("files", 1)) { + tabs.push( + m( + "li", + { class: active["files"] }, + m("a", { href: "#!/admin/files" }, "Files") + ) + ); + } + + if (window.authorizer.hasAccess("config", 1)) { + tabs.push( + m( + "li", + { class: active["config"] }, + m("a", { href: "#!/admin/config" }, "Config") + ) + ); + } + + if (window.authorizer.hasAccess("permissions", 1)) { + tabs.push( + m( + "li", + { class: active["permissions"] }, + m("a", { href: "#!/admin/permissions" }, "Permissions") + ) + ); + } + + if (window.authorizer.hasAccess("users", 1)) { + tabs.push( + m( + "li", + { class: active["users"] }, + m("a", { href: "#!/admin/users" }, "Users") + ) + ); + } + + return m("nav#side-menu", m("ul", tabs)); + } + }; +}; + +export default component; diff --git a/ui/app.ts b/ui/app.ts new file mode 100644 index 0000000..4a6b07e --- /dev/null +++ b/ui/app.ts @@ -0,0 +1,135 @@ +import m, { RouteResolver } from "mithril"; +import layout from "./layout"; +import * as store from "./store"; +import * as loginPage from "./login-page"; +import * as overviewPage from "./overview-page"; +import * as devicesPage from "./devices-page"; +import * as devicePage from "./device-page"; +import * as errorPage from "./error-page"; +import * as faultsPage from "./faults-page"; +import * as presetsPage from "./presets-page"; +import * as provisionsPage from "./provisions-page"; +import * as virtualParametersPage from "./virtual-parameters-page"; +import * as filesPage from "./files-page"; +import * as configPage from "./config-page"; +import * as permissionsPage from "./permissions-page"; +import * as usersPage from "./users-page"; +import Authorizer from "../lib//common/authorizer"; +import * as notifications from "./notifications"; +import { contextifyComponent } from "./components"; + +declare global { + interface Window { + authorizer: Authorizer; + permissionSets: any; + username: string; + clientConfig: any; + } +} + +window.authorizer = new Authorizer(window.permissionSets); + +const adminPages = [ + "presets", + "provisions", + "virtualParameters", + "files", + "config", + "users", + "permissions" +]; + +let state; + +let lastRenderTimestamp = 0; +let pageVisitTimestamp = 0; +let fulfillTimeout = null; + +function fulfill(): void { + clearTimeout(fulfillTimeout); + fulfillTimeout = setTimeout(() => { + store.fulfill(lastRenderTimestamp, pageVisitTimestamp).then(updated => { + if (updated) m.redraw(); + }); + }, 100); +} + +function pagify(pageName, page): RouteResolver { + const component: RouteResolver = { + render: () => { + lastRenderTimestamp = Date.now(); + let p; + if (state && state.error) p = m(errorPage.component, state); + else p = m(contextifyComponent(page.component), state); + fulfill(); + const attrs = {}; + attrs["page"] = pageName; + return m(layout, attrs, p); + }, + onmatch: null + }; + + component.onmatch = (args, requestedPath) => { + pageVisitTimestamp = Date.now(); + if (!page.init) { + state = null; + fulfill(); + return null; + } + + return new Promise(resolve => { + page + .init(args) + .then(st => { + if (!st) return void m.route.set("/"); + state = st; + resolve(); + fulfill(); + }) + .catch(err => { + if (!window.username && err.message.indexOf("authorized") >= 0) { + notifications.push("error", err.message); + m.route.set("/login", { continue: requestedPath }); + } + state = { error: err.message }; + resolve(); + }); + }); + }; + + return component; +} + +function redirectAdminPage(): RouteResolver { + const component: RouteResolver = { + onmatch: () => { + for (const page of adminPages) { + if (window.authorizer.hasAccess(page, 2)) { + m.route.set(`/admin/${page}`); + return null; + } + } + return null; + } + }; + return component; +} + +m.route(document.body, "/overview", { + "/login": pagify("login", loginPage), + "/overview": pagify("overview", overviewPage), + "/devices": pagify("devices", devicesPage), + "/devices/:id": pagify("devices", devicePage), + "/faults": pagify("faults", faultsPage), + "/admin": redirectAdminPage(), + "/admin/presets": pagify("presets", presetsPage), + "/admin/provisions": pagify("provisions", provisionsPage), + "/admin/virtualParameters": pagify( + "virtualParameters", + virtualParametersPage + ), + "/admin/files": pagify("files", filesPage), + "/admin/config": pagify("config", configPage), + "/admin/users": pagify("users", usersPage), + "/admin/permissions": pagify("permissions", permissionsPage) +}); diff --git a/ui/autocomplete-compnent.ts b/ui/autocomplete-compnent.ts new file mode 100644 index 0000000..8bf76d4 --- /dev/null +++ b/ui/autocomplete-compnent.ts @@ -0,0 +1,161 @@ +export default class Autocomplete { + private callback: ( + value: string, + callback: (suggestions: string[]) => void + ) => void; + private element: HTMLInputElement; + private hideTimeout: NodeJS.Timeout; + private visible: boolean; + private default: string; + private selection: number; + private container: HTMLElement; + + public constructor(className, callback) { + this.callback = callback; + this.element = null; + this.hideTimeout = null; + this.visible = false; + this.default = null; + this.selection = null; + + this.container = document.createElement("div"); + this.container.style.position = "absolute"; + this.container.style.display = "block"; + this.container.style.opacity = "0"; + this.container.className = className; + } + + public attach(el): void { + el.setAttribute("autocomplete", "off"); + + el.addEventListener("focus", () => { + this.element = el; + const domRect = el.getBoundingClientRect(); + this.container.style.left = `${domRect.left + window.pageXOffset}px`; + this.container.style.width = `${domRect.width}px`; + this.container.style.top = `${domRect.bottom + window.pageYOffset}px`; + this.update(); + }); + + el.addEventListener("blur", () => { + if (this.element !== el) return; + if (!this.visible) return; + this.hide(); + }); + + el.addEventListener("keydown", e => { + if (this.element !== el) return; + if (e.key === "Escape") { + if (this.visible) this.hide(); + } else if (e.key === "Enter") { + if (this.default != null) { + el.value = this.default; + e.preventDefault(); + this.update(); + } + } else if (e.key === "ArrowDown") { + e.preventDefault(); + if (this.selection == null) this.selection = 0; + else ++this.selection; + this.update(); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + --this.selection; + this.update(); + } + }); + + el.addEventListener("input", () => { + if (this.element !== el) return; + this.selection = null; + this.update(); + }); + } + + private hide(): void { + this.container.style.opacity = "0"; + this.visible = false; + this.default = null; + this.selection = null; + clearTimeout(this.hideTimeout); + this.hideTimeout = setTimeout(() => { + this.hideTimeout = null; + while (this.container.firstChild) + this.container.removeChild(this.container.firstChild); + document.body.removeChild(this.container); + }, 500); + } + + private update(): void { + const el = this.element; + + this.callback(el.value, suggestions => { + if (this.element !== el) return; + this.default = null; + + if (!suggestions.length) { + if (this.visible) this.hide(); + + return; + } + + while (this.container.firstChild) + this.container.removeChild(this.container.firstChild); + + if (!this.visible) { + if (!this.hideTimeout) { + document.body.appendChild(this.container); + window.getComputedStyle(this.container).opacity; + } else { + clearTimeout(this.hideTimeout); + this.hideTimeout = null; + } + this.container.style.opacity = "1"; + this.visible = true; + } + + if (this.selection != null) { + this.selection = + ((this.selection % suggestions.length) + suggestions.length) % + suggestions.length; + this.default = suggestions[this.selection]; + } else { + this.default = suggestions[0]; + } + + let selectedElement; + for (const [idx, suggestion] of suggestions.entries()) { + const e = document.createElement("div"); + e.classList.add("suggestion"); + if (idx === this.selection) { + e.classList.add("selected"); + selectedElement = e; + } + + const t = document.createTextNode(suggestion); + e.appendChild(t); + e.addEventListener("mousedown", ev => { + ev.preventDefault(); + el.value = suggestion; + if (this.element === el) this.update(); + }); + this.container.appendChild(e); + } + + // Ensure selected element is in view + if (selectedElement) { + this.container.scrollTop = Math.min( + this.container.scrollTop, + selectedElement.offsetTop + ); + + this.container.scrollTop = Math.max( + this.container.scrollTop, + selectedElement.offsetTop + + selectedElement.scrollHeight - + this.container.clientHeight + ); + } + }); + } +} diff --git a/ui/change-password-component.ts b/ui/change-password-component.ts new file mode 100644 index 0000000..d7bcd4d --- /dev/null +++ b/ui/change-password-component.ts @@ -0,0 +1,116 @@ +import { VnodeDOM, ClosureComponent, Component } from "mithril"; +import * as notifications from "./notifications"; +import { changePassword } from "./store"; +import { m } from "./components"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const onPasswordChange = vnode.attrs["onPasswordChange"]; + const enforceAuth = !vnode.attrs["noAuth"]; + const username = vnode.attrs["username"]; + + if (username) vnode.state["username"] = username; + + const form = [ + m( + "p", + m("label", { for: "username" }, "Username"), + m("input", { + name: "username", + type: "text", + value: vnode.state["username"], + disabled: !!username, + oninput: m.withAttr("value", v => (vnode.state["username"] = v)), + oncreate: _vnode => { + (_vnode.dom as HTMLSelectElement).focus(); + } + }) + ) + ]; + + let fields = { + newPassword: "New password", + confirmPassword: "Confirm password" + }; + if (enforceAuth) + fields = Object.assign({ authPassword: "Your password" }, fields); + + for (const [f, l] of Object.entries(fields)) { + form.push( + m( + "p", + m("label", { for: f }, l), + m("input", { + name: f, + type: "password", + value: vnode.state[f], + oninput: m.withAttr("value", v => (vnode.state[f] = v)) + }) + ) + ); + } + + const submit = m( + "button.primary", + { + type: "submit" + }, + "Change password" + ) as VnodeDOM; + + form.push(m(".actions-bar", submit)); + + const children = [ + m("h1", "Change password"), + m( + "form", + { + onsubmit: e => { + e.redraw = false; + e.preventDefault(); + if ( + !vnode.state["username"] || + !vnode.state["newPassword"] || + (enforceAuth && !vnode.state["authPassword"]) + ) { + notifications.push("error", "Please fill all fields"); + } else if ( + vnode.state["newPassword"] !== vnode.state["confirmPassword"] + ) { + notifications.push( + "error", + "Password confirm doesn't match new password" + ); + } else { + (submit.dom as HTMLFormElement).disabled = true; + changePassword( + vnode.state["username"], + vnode.state["newPassword"], + vnode.state["authPassword"] + ) + .then(() => { + notifications.push( + "success", + "Password updated successfully" + ); + if (onPasswordChange) onPasswordChange(); + (submit.dom as HTMLFormElement).disabled = false; + }) + .catch(err => { + notifications.push("error", err.message); + (submit.dom as HTMLFormElement).disabled = false; + }); + } + } + }, + form + ) + ]; + + return m("div.put-form", children); + } + }; +}; + +export default component; diff --git a/ui/code-editor-component.ts b/ui/code-editor-component.ts new file mode 100644 index 0000000..0eeadee --- /dev/null +++ b/ui/code-editor-component.ts @@ -0,0 +1,42 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "./components"; +import { codeMirror } from "./dynamic-loader"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + return m("textarea", { + name: vnode.attrs["id"], + value: vnode.attrs["value"], + oncreate: _vnode => { + const editor = codeMirror.fromTextArea(_vnode.dom, { + mode: vnode.attrs["mode"], + lineNumbers: true, + readOnly: vnode.attrs["readOnly"], + extraKeys: { + "Ctrl-Enter": () => { + if (vnode.attrs["onSubmit"]) + vnode.attrs["onSubmit"](_vnode.dom); + }, + "Cmd-Enter": () => { + if (vnode.attrs["onSubmit"]) + vnode.attrs["onSubmit"](_vnode.dom); + } + } + }); + + if (vnode.attrs["onChange"]) { + editor.on("change", e => { + vnode.attrs["onChange"](e.getValue()); + }); + } + + if (vnode.attrs["focus"]) editor.focus(); + if (vnode.attrs["onReady"]) vnode.attrs["onReady"](editor); + } + }); + } + }; +}; + +export default component; diff --git a/ui/components.ts b/ui/components.ts new file mode 100644 index 0000000..5e57510 --- /dev/null +++ b/ui/components.ts @@ -0,0 +1,134 @@ +import m, { + Static, + Attributes, + Children, + ComponentTypes, + Lifecycle, + ClosureComponent, + Vnode +} from "mithril"; +import parameter from "./components/parameter"; +import parameterList from "./components/parameter-list"; +import parameterTable from "./components/parameter-table"; +import overviewDot from "./components/overview-dot"; +import container from "./components/container"; +import summonButton from "./components/summon-button"; +import deviceFaults from "./components/device-faults"; +import allParameters from "./components/all-parameters"; +import deviceActions from "./components/device-actions"; +import tags from "./components/tags"; +import ping from "./components/ping"; +import deviceLink from "./components/device-link"; +import longTextComponent from "./long-text-component"; + +const comps = { + parameter, + "parameter-list": parameterList, + "parameter-table": parameterTable, + "overview-dot": overviewDot, + container, + "summon-button": summonButton, + "device-faults": deviceFaults, + "all-parameters": allParameters, + "device-actions": deviceActions, + tags, + ping, + "device-link": deviceLink, + "long-text": longTextComponent +}; + +const contextifiedComponents = new WeakMap(); +const vnodeContext = new WeakMap(); + +interface MC extends Static { + context: { + (ctx: Attributes, selector: string, ...children: Children[]): Vnode< + any, + any + >; + ( + ctx: Attributes, + selector: string, + attributes: Attributes, + ...children: Children[] + ): Vnode; + ( + ctx: Attributes, + component: ComponentTypes, + ...args: Children[] + ): Vnode; + ( + ctx: Attributes, + component: ComponentTypes, + attributes: Attrs & Lifecycle & { key?: string | number }, + ...args: Children[] + ): Vnode; + }; +} + +const M = new Proxy(m, { + apply: (target, thisArg, argumentsList) => { + const c = argumentsList[0]; + if (typeof c !== "string") argumentsList[0] = contextifyComponent(c); + else if (comps[c]) argumentsList[0] = contextifyComponent(comps[c]); + + return Reflect.apply(target, undefined, argumentsList); + }, + get: (target, prop) => { + if (prop === "context") return contextFn; + else return Reflect.get(target, prop); + } +}) as MC; + +function contextFn(context, ...argumentsList): Vnode { + const vnode = Reflect.apply(M, undefined, argumentsList); + vnodeContext.set(vnode, context); + return vnode; +} + +function applyContext(vnode, parentContext): void { + if (Array.isArray(vnode)) { + for (const c of vnode) applyContext(c, parentContext); + } else if (vnode && typeof vnode === "object" && vnode.tag) { + const vc = Object.assign({}, parentContext, vnodeContext.get(vnode)); + vnodeContext.set(vnode, vc); + vnode.attrs = Object.assign({}, vc, vnode.attrs); + if (vnode.children && vnode.children.length) + for (const c of vnode.children) applyContext(c, vc); + } +} + +export function contextifyComponent(component: ComponentTypes): ComponentTypes { + let c = contextifiedComponents.get(component); + if (!c) { + if (typeof component !== "function") { + c = Object.assign({}, component); + const view = component.view; + c.view = function(vnode) { + const context = vnodeContext.get(vnode) || {}; + const res = Reflect.apply(view, this, [vnode]); + applyContext(res, context); + return res; + }; + } else if (!component.prototype || !component.prototype.view) { + c = initialNode => { + const state = (component as ClosureComponent)(initialNode); + const view = state.view; + state.view = function(vnode) { + const context = vnodeContext.get(vnode) || {}; + const res = Reflect.apply(view, this, [vnode]); + applyContext(res, context); + return res; + }; + return state; + }; + } else { + // TODO support class components + throw new Error("Class components not supported"); + } + contextifiedComponents.set(component, c); + } + return c; +} + +export { M as m }; diff --git a/ui/components/all-parameters.ts b/ui/components/all-parameters.ts new file mode 100644 index 0000000..ce94781 --- /dev/null +++ b/ui/components/all-parameters.ts @@ -0,0 +1,136 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; +import * as taskQueue from "../task-queue"; +import { parse } from "../../lib/common/expression-parser"; +import memoize from "../../lib/common/memoize"; + +const memoizedParse = memoize(parse); + +function escapeRegExp(str): string { + return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); +} + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const device = vnode.attrs["device"]; + + const search = m("input", { + type: "text", + placeholder: "Search parameters", + oninput: e => { + vnode.state["searchString"] = e.target.value; + e.redraw = false; + clearTimeout(vnode.state["timeout"]); + vnode.state["timeout"] = setTimeout(m.redraw, 500); + } + }); + + const instanceRegex = /\.[0-9]+$/; + let re; + if (vnode.state["searchString"]) { + const keywords = vnode.state["searchString"].split(" ").filter(s => s); + if (keywords.length) + re = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i"); + } + + const rows = Object.keys(device) + .sort() + .map(k => { + const p = device[k]; + const val = []; + const attrs = {}; + if (re) { + const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k; + if (!re.test(str)) attrs["style"] = "display: none;"; + } + + if (p.object === false) { + val.push( + m( + "parameter", + Object.assign({ device: device, parameter: memoizedParse(k) }) + ) + ); + } else if (p.object && p.writable) { + if (instanceRegex.test(k)) { + val.push( + m( + "button", + { + title: "Delete this instance", + onclick: () => { + taskQueue.queueTask({ + name: "deleteObject", + device: device["DeviceID.ID"].value[0], + objectName: k + }); + } + }, + "✕" + ) + ); + } else { + val.push( + m( + "button", + { + title: "Create a new instance", + onclick: () => { + taskQueue.queueTask({ + name: "addObject", + device: device["DeviceID.ID"].value[0], + objectName: k + }); + } + }, + "🞢" + ) + ); + } + } + + val.push( + m( + "button", + { + title: "Refresh tree", + onclick: () => { + taskQueue.queueTask({ + name: "getParameterValues", + device: device["DeviceID.ID"].value[0], + parameterNames: [k] + }); + } + }, + "↺" + ) + ); + + return m( + "tr", + attrs, + m("td.left", m("long-text", { text: k })), + m("td.right", val) + ); + }); + + return m( + ".all-parameters", + m( + "a.download-csv", + { + href: `/api/devices/${device["DeviceID.ID"].value[0]}.csv`, + download: "", + style: "float: right;" + }, + "Download" + ), + search, + m(".parameter-list", m("table", m("tbody", rows))) + ); + } + }; +}; + +export default component; diff --git a/ui/components/container.ts b/ui/components/container.ts new file mode 100644 index 0000000..3ae7a59 --- /dev/null +++ b/ui/components/container.ts @@ -0,0 +1,28 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; +import * as store from "../store"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + if (vnode.attrs["filter"]) { + if ( + !store.evaluateExpression( + vnode.attrs["filter"], + vnode.attrs["device"] + ) + ) + return null; + } + + const children = Object.values(vnode.attrs["components"]).map(c => { + if (typeof c !== "object") return `${c}`; + return m(c["type"], c); + }); + if (vnode.attrs["element"]) return m(vnode.attrs["element"], children); + else return children; + } + }; +}; + +export default component; diff --git a/ui/components/device-actions.ts b/ui/components/device-actions.ts new file mode 100644 index 0000000..3dda48d --- /dev/null +++ b/ui/components/device-actions.ts @@ -0,0 +1,91 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; +import * as taskQueue from "../task-queue"; +import * as notifications from "../notifications"; +import * as store from "../store"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const device = vnode.attrs["device"]; + + const buttons = []; + + buttons.push( + m( + "button.primary", + { + title: "Reboot device", + onclick: () => { + taskQueue.queueTask({ + name: "reboot", + device: device["DeviceID.ID"].value[0] + }); + } + }, + "Reboot" + ) + ); + + buttons.push( + m( + "button.critical", + { + title: "Factory reset device", + onclick: () => { + taskQueue.queueTask({ + name: "factoryReset", + device: device["DeviceID.ID"].value[0] + }); + } + }, + "Reset" + ) + ); + + buttons.push( + m( + "button.critical", + { + title: "Push a firmware or a config file", + onclick: () => { + taskQueue.stageDownload({ + name: "download", + device: device["DeviceID.ID"].value[0] + }); + } + }, + "Push file" + ) + ); + + buttons.push( + m( + "button.primary", + { + title: "Delete device", + onclick: () => { + if (!confirm("Deleting this device. Are you sure?")) return; + const deviceId = device["DeviceID.ID"].value[0]; + + store + .deleteResource("devices", deviceId) + .then(() => { + notifications.push("success", `${deviceId}: Device deleted`); + m.route.set("/devices"); + }) + .catch(err => { + notifications.push("error", err.message); + }); + } + }, + "Delete" + ) + ); + + return m(".actions-bar", buttons); + } + }; +}; + +export default component; diff --git a/ui/components/device-faults.ts b/ui/components/device-faults.ts new file mode 100644 index 0000000..85c0179 --- /dev/null +++ b/ui/components/device-faults.ts @@ -0,0 +1,76 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; +import * as store from "../store"; +import * as notifications from "../notifications"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const device = vnode.attrs["device"]; + const deviceId = device["DeviceID.ID"].value[0]; + const faults = store.fetch("faults", [ + "AND", + [">", ["PARAM", "_id"], `${deviceId}:`], + ["<", ["PARAM", "_id"], `${deviceId}:zzzz`] + ]); + + const headers = [ + "Channel", + "Code", + "Message", + "Retries", + "Timestamp" + ].map(l => m("th", l)); + const thead = m("thead", m("tr", headers)); + + const rows = []; + for (const f of faults.value) { + rows.push([ + m("td", f["channel"]), + m("td", f["code"]), + m("td", f["message"]), + m("td", f["retries"]), + m("td", new Date(f["timestamp"]).toLocaleString()), + m( + "td", + m( + "button", + { + title: "Delete fault", + onclick: e => { + e.redraw = false; + store + .deleteResource("faults", f["_id"]) + .then(() => { + notifications.push("success", "Fault deleted"); + store.fulfill(Date.now(), Date.now()); + m.redraw(); + }) + .catch(err => { + notifications.push("error", err.message); + store.fulfill(Date.now(), Date.now()); + }); + } + }, + "✕" + ) + ) + ]); + } + + let tbody; + if (rows.length) { + tbody = m("tbody", rows.map(r => m("tr", r))); + } else { + tbody = m( + "tbody", + m("tr.empty", m("td", { colspan: headers.length }, "No faults")) + ); + } + + return m("table.table", thead, tbody); + } + }; +}; + +export default component; diff --git a/ui/components/device-link.ts b/ui/components/device-link.ts new file mode 100644 index 0000000..12508e0 --- /dev/null +++ b/ui/components/device-link.ts @@ -0,0 +1,29 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + let deviceId; + if (vnode.attrs["device"]) + deviceId = vnode.attrs["device"]["DeviceID.ID"].value[0]; + + const children = Object.values(vnode.attrs["components"]).map(c => { + if (typeof c !== "object") return `${c}`; + const attrs = Object.assign({}, vnode.attrs, c); + return m(attrs["type"], attrs); + }); + if (deviceId) { + return m( + "a", + { href: `#!/devices/${encodeURIComponent(deviceId)}` }, + children + ); + } else { + return children; + } + } + }; +}; + +export default component; diff --git a/ui/components/overview-dot.ts b/ui/components/overview-dot.ts new file mode 100644 index 0000000..0dda69a --- /dev/null +++ b/ui/components/overview-dot.ts @@ -0,0 +1,40 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; +import config from "../config"; +import * as store from "../store"; + +const CHARTS = config.ui.overview.charts; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const device = vnode.attrs["device"]; + const chart = CHARTS[vnode.attrs["chart"]]; + if (!chart) return null; + for (const slice of Object.values(chart.slices)) { + const filter = slice["filter"]; + if (store.evaluateExpression(filter, device)) { + const dot = m( + "svg", + { + width: "1em", + height: "1em", + xmlns: "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink" + }, + m("circle", { + cx: "0.5em", + cy: "0.5em", + r: "0.4em", + fill: slice["color"] + }) + ); + return m("span.overview-dot", dot, `${slice["label"]}`); + } + } + return null; + } + }; +}; + +export default component; diff --git a/ui/components/parameter-list.ts b/ui/components/parameter-list.ts new file mode 100644 index 0000000..72eb002 --- /dev/null +++ b/ui/components/parameter-list.ts @@ -0,0 +1,38 @@ +import { ClosureComponent, Component, VnodeDOM } from "mithril"; +import { m } from "../components"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const device = vnode.attrs["device"]; + + const rows = Object.values(vnode.attrs["parameters"]).map(parameter => { + const p = m.context( + { + device: device, + parameter: parameter["parameter"] + }, + parameter["type"] || "parameter", + parameter + ); + + return m( + "tr", + { + onupdate: vn => { + (vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom + ? "" + : "none"; + } + }, + m("th", parameter["label"]), + m("td", p) + ); + }); + + return m("table.parameter-list", rows); + } + }; +}; + +export default component; diff --git a/ui/components/parameter-table.ts b/ui/components/parameter-table.ts new file mode 100644 index 0000000..41e6332 --- /dev/null +++ b/ui/components/parameter-table.ts @@ -0,0 +1,136 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; +import * as taskQueue from "../task-queue"; +import * as store from "../store"; +import * as expressionParser from "../../lib/common/expression-parser"; + +const component: ClosureComponent = (): Component => { + return { + oninit: vnode => { + const obj = vnode.attrs["parameter"]; + if (!Array.isArray(obj) || obj[0] !== "PARAM") + throw new Error("Object must be a parameter path"); + vnode.state["object"] = obj[1]; + vnode.state["parameters"] = Object.values(vnode.attrs["childParameters"]); + }, + view: vnode => { + const device = vnode.attrs["device"]; + const object = store.evaluateExpression(vnode.state["object"], device); + const parameters = vnode.state["parameters"]; + + if (!device[object]) return null; + + const instances = new Set(); + const prefix = `${object}.`; + for (const p in device) { + if (p.startsWith(prefix)) { + const i = p.indexOf(".", prefix.length); + if (i === -1) instances.add(p); + else instances.add(p.slice(0, i)); + } + } + + const headers = Object.values(parameters).map(p => m("th", p["label"])); + + const thead = m("thead", m("tr", headers)); + + const rows = []; + for (const i of instances) { + let filter = + vnode.attrs["filter"] != null ? vnode.attrs["filter"] : true; + + filter = expressionParser.map(filter, e => { + if (Array.isArray(e) && e[0] === "PARAM") + return ["PARAM", ["||", i, ".", e[1]]]; + return e; + }); + + if (!store.evaluateExpression(filter, device)) continue; + + const row = parameters.map(p => { + const param = expressionParser.map(p.parameter, e => { + if (Array.isArray(e) && e[0] === "PARAM") + return ["PARAM", ["||", i, ".", e[1]]]; + return e; + }); + return m( + "td", + m.context( + { + device: device, + parameter: param + }, + p.type || "parameter", + Object.assign({}, p, { + device: device, + parameter: param + }) + ) + ); + }); + + if (device[i].writable === true) { + row.push( + m( + "td", + m( + "button", + { + title: "Delete this instance", + onclick: () => { + taskQueue.queueTask({ + name: "deleteObject", + device: device["DeviceID.ID"].value[0], + objectName: i + }); + } + }, + "✕" + ) + ) + ); + } + rows.push(m("tr", row)); + } + + if (!rows.length) { + rows.push( + m("tr.empty", m("td", { colspan: headers.length }, "No instances")) + ); + } + + if (device[object].writable === true) { + rows.push( + m( + "tr", + m("td", { colspan: headers.length }), + m( + "td", + m( + "button", + { + title: "Create a new instance", + onclick: () => { + taskQueue.queueTask({ + name: "addObject", + device: device["DeviceID.ID"].value[0], + objectName: object + }); + } + }, + "🞢" + ) + ) + ) + ); + } + + let label; + if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]); + + return [label, m("table.table", thead, m("tbody", rows))]; + } + }; +}; + +export default component; diff --git a/ui/components/parameter.ts b/ui/components/parameter.ts new file mode 100644 index 0000000..eb25121 --- /dev/null +++ b/ui/components/parameter.ts @@ -0,0 +1,93 @@ +import { ClosureComponent, Component, VnodeDOM } from "mithril"; +import { m } from "../components"; +import * as taskQueue from "../task-queue"; +import * as store from "../store"; +import * as expression from "../../lib/common/expression"; +import memoize from "../../lib/common/memoize"; +import timeAgo from "../timeago"; + +const evaluateParam = memoize((exp, obj, now) => { + let timestamp = now; + const params = new Set(); + const value = expression.evaluate(exp, obj, now, e => { + if (Array.isArray(e)) { + if (e[0] === "PARAM") { + params.add(e[1]); + if (!Array.isArray(e[1])) { + const p = obj[e[1]]; + if (p && p.valueTimestamp) + timestamp = Math.min(timestamp, p.valueTimestamp); + } + } + if (e[0] === "FUNC" && e[1] === "DATE_STRING" && !Array.isArray(e[2])) + return new Date(e[2]).toLocaleString(); + } + return e; + }); + + let parameter = params.size === 1 ? params.values().next().value : null; + if (Array.isArray(parameter)) parameter = null; + return { value, timestamp, parameter }; +}); + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const device = vnode.attrs["device"]; + + const { value, timestamp, parameter } = evaluateParam( + vnode.attrs["parameter"], + vnode.attrs["device"], + store.getTimestamp() + ); + + if (value == null) return null; + + let edit; + if (device[parameter] && device[parameter].writable) { + edit = m( + "button", + { + title: "Edit parameter value", + onclick: () => { + taskQueue.stageSpv({ + name: "setParameterValues", + device: device["DeviceID.ID"].value[0], + parameterValues: [ + [ + parameter, + device[parameter].value[0], + device[parameter].value[1] + ] + ] + }); + } + }, + "✎" + ); + } + + const el = m("long-text", { text: `${value}` }); + + return m( + "span", + { + class: "parameter-value", + onmouseover: e => { + e.redraw = false; + // Don't update any child element + if (e.target === (el as VnodeDOM).dom) { + const now = Date.now(); + const localeString = new Date(timestamp).toLocaleString(); + e.target.title = `${localeString} (${timeAgo(now - timestamp)})`; + } + } + }, + el, + edit + ); + } + }; +}; + +export default component; diff --git a/ui/components/ping.ts b/ui/components/ping.ts new file mode 100644 index 0000000..3c98e13 --- /dev/null +++ b/ui/components/ping.ts @@ -0,0 +1,57 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; +import * as store from "../store"; + +const REFRESH_INTERVAL = 3000; + +const component: ClosureComponent = (): Component => { + return { + oninit: vnode => { + vnode.state["timestamp"] = 0; + vnode.state["ping"] = null; + vnode.state["host"] = null; + vnode.state["timeout"] = null; + }, + view: vnode => { + const refresh = (): void => { + const device = vnode.attrs["device"]; + let param = + device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]; + if (!param) + param = device["Device.ManagementServer.ConnectionRequestURL"]; + if (!param || !param.value) return; + + const url = new URL(param.value[0]); + vnode.state["host"] = url.hostname; + vnode.state["timestamp"] = Date.now(); + store + .ping(vnode.state["host"]) + .then(res => { + vnode.state["ping"] = res; + }) + .catch(() => { + // Do nothing + }); + }; + + const t = vnode.state["timestamp"] + REFRESH_INTERVAL - Date.now(); + clearTimeout(vnode.state["timeout"]); + if (t <= 0) refresh(); + else vnode.state["timeout"] = setTimeout(refresh, t); + + if (vnode.state["host"]) { + let status = ""; + if (vnode.state["ping"]) { + if (vnode.state["ping"].avg != null) + status = `${Math.trunc(vnode.state["ping"].avg)} ms`; + else status = "Unreachable"; + } + + return m("div", `Pinging ${vnode.state["host"]}: ${status}`); + } + return null; + } + }; +}; + +export default component; diff --git a/ui/components/summon-button.ts b/ui/components/summon-button.ts new file mode 100644 index 0000000..5c2b3f5 --- /dev/null +++ b/ui/components/summon-button.ts @@ -0,0 +1,72 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; +import * as taskQueue from "../task-queue"; +import * as store from "../store"; +import * as notifications from "../notifications"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const device = vnode.attrs["device"]; + + return m( + "button.primary", + { + title: "Initiate session and refresh basic parameters", + onclick: e => { + e.target.disabled = true; + const params = Object.values(vnode.attrs["parameters"]) + .map(exp => { + if (!Array.isArray(exp) || exp[0] !== "PARAM") return null; + return store.evaluateExpression(exp[1], device); + }) + .filter(exp => !!exp); + + const task = { + name: "getParameterValues", + parameterNames: params, + device: device["DeviceID.ID"].value[0] + }; + + taskQueue + .commit( + [task], + (deviceId, err, connectionRequestStatus, tasks2) => { + if (err) { + notifications.push("error", `${deviceId}: ${err.message}`); + return; + } + + for (const t of tasks2) + if (t.status === "stale") taskQueue.deleteTask(t); + + if (connectionRequestStatus !== "OK") { + notifications.push( + "error", + `${deviceId}: ${connectionRequestStatus}` + ); + } else if (tasks2[0].status === "stale") { + notifications.push( + "error", + `${deviceId}: No contact from device` + ); + } else if (tasks2[0].status === "fault") { + notifications.push("error", `${deviceId}: Refresh faulted`); + } else { + notifications.push("success", `${deviceId}: Summoned`); + } + } + ) + .then(() => { + e.target.disabled = false; + store.fulfill(0, Date.now()); + }); + } + }, + "Summon" + ); + } + }; +}; + +export default component; diff --git a/ui/components/tags.ts b/ui/components/tags.ts new file mode 100644 index 0000000..39e2467 --- /dev/null +++ b/ui/components/tags.ts @@ -0,0 +1,91 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "../components"; +import * as notifications from "../notifications"; +import * as store from "../store"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const device = vnode.attrs["device"]; + let writable = true; + if (vnode.attrs["writable"] != null) writable = vnode.attrs["writable"]; + + const tags = []; + for (const p of Object.keys(device)) + if (p.startsWith("Tags.")) tags.push(p.slice(5)); + + tags.sort(); + + if (!writable) return m(".tags", tags.map(t => m("span.tag", t))); + + return m( + ".tags", + tags.map(tag => + m( + "span.tag", + tag, + m( + "button", + { + onclick: e => { + e.target.disabled = true; + const deviceId = device["DeviceID.ID"].value[0]; + store + .updateTags(deviceId, { [tag]: false }) + .then(() => { + e.target.disabled = false; + notifications.push( + "success", + `${deviceId}: Tags updated` + ); + store.fulfill(0, Date.now()); + }) + .catch(err => { + e.target.disabled = false; + notifications.push( + "error", + `${deviceId}: ${err.message}` + ); + }); + } + }, + "✕" + ) + ) + ), + m( + "span.tag.writable", + m( + "button", + { + onclick: e => { + e.target.disabled = true; + const deviceId = device["DeviceID.ID"].value[0]; + const tag = prompt(`Enter tag to assign to device:`); + if (!tag) { + e.target.disabled = false; + return; + } + + store + .updateTags(deviceId, { [tag]: true }) + .then(() => { + e.target.disabled = false; + notifications.push("success", `${deviceId}: Tags updated`); + store.fulfill(0, Date.now()); + }) + .catch(err => { + e.target.disabled = false; + notifications.push("error", `${deviceId}: ${err.message}`); + }); + } + }, + "🞢" + ) + ) + ); + } + }; +}; + +export default component; diff --git a/ui/config-functions.ts b/ui/config-functions.ts new file mode 100644 index 0000000..154fe5e --- /dev/null +++ b/ui/config-functions.ts @@ -0,0 +1,92 @@ +interface Config { + _id: string; + value: string; +} + +interface Diff { + add: Config[]; + remove: string[]; +} + +export function flattenConfig(config): any { + const flatten = {}; + const recuresive = (obj: any, root: string): void => { + for (const [k, v] of Object.entries(obj)) { + const key = root ? `${root}.${k}` : k; + if (v === undefined) continue; + if (v === null || typeof v !== "object") flatten[key] = v; + else recuresive(v, key); + } + }; + + if (config !== null && typeof config === "object") recuresive(config, ""); + return flatten; +} + +export function structureConfig(config: Config[]): any { + config.sort((a, b) => (a._id > b._id ? 1 : a._id < b._id ? -1 : 0)); + const _config = {}; + for (const c of config) { + const keys = c._id.split("."); + let ref = _config; + while (keys.length > 1) { + const k = keys.shift(); + if (typeof ref[k] !== "object") ref[k] = {}; + ref = ref[k]; + } + ref[keys[0]] = c.value; + } + + const toArray = function(object): any { + const MAX_BITS = 30; + const MAX_ARRAY_SIZE = MAX_BITS * 10; + + if (object == null || typeof object !== "object") return object; + + if (Object.keys(object).length <= MAX_ARRAY_SIZE) { + let indexes = []; + for (const key of Object.keys(object)) { + const idx = Math.floor(+key); + if (idx >= 0 && idx < MAX_ARRAY_SIZE && String(idx) === key) { + const pos = Math.floor(idx / MAX_BITS); + if (!indexes[pos]) indexes[pos] = 0; + indexes[pos] |= 1 << idx % MAX_BITS; + } else { + indexes = []; + break; + } + } + + let index = 0; + while (indexes.length && (index = indexes.shift()) === 1073741823); + + if (index && (~index & (index + 1)) === index + 1) { + // its an array + const array = []; + for (let i = 0; i < Object.keys(object).length; i++) + array[i] = object[i]; + + object = array; + } + } + + for (const [k, v] of Object.entries(object)) object[k] = toArray(v); + return object; + }; + + return toArray(_config); +} + +export function diffConfig(current, target): Diff { + const diff = { + add: [], + remove: [] + }; + + for (const [k, v] of Object.entries(target)) + if (!current[k] || current[k] !== v) diff.add.push({ _id: k, value: v }); + + for (const k of Object.keys(current)) if (!target[k]) diff.remove.push(k); + + return diff; +} diff --git a/ui/config-page.ts b/ui/config-page.ts new file mode 100644 index 0000000..a88ed18 --- /dev/null +++ b/ui/config-page.ts @@ -0,0 +1,343 @@ +import { m } from "./components"; +import * as store from "./store"; +import * as notifications from "./notifications"; +import putFormComponent from "./put-form-component"; +import uiConfigComponent from "./ui-config-component"; +import * as overlay from "./overlay"; +import * as expressionParser from "../lib/common/expression-parser"; +import { loadCodeMirror, loadYaml } from "./dynamic-loader"; +import { ClosureComponent, Component, Children } from "mithril"; + +const attributes = [ + { id: "_id", label: "Key" }, + { id: "value", label: "Value" } +]; + +interface ValidationErrors { + [prop: string]: string; +} + +function putActionHandler(action, _object, isNew?): Promise { + return new Promise((resolve, reject) => { + const object = Object.assign({}, _object); + if (action === "save") { + const id = object["_id"]; + delete object["_id"]; + + if (!id) return void resolve({ _id: "ID can not be empty" }); + try { + expressionParser.parse(object.value); + } catch (err) { + return void resolve({ + value: "Config value must be valid expression" + }); + } + + store + .resourceExists("config", id) + .then(exists => { + if (exists && isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "Config already exists" }); + } + + if (!exists && !isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "Config does not exist" }); + } + + store + .putResource("config", id, object) + .then(() => { + notifications.push( + "success", + `Config ${exists ? "updated" : "created"}` + ); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(reject); + }) + .catch(reject); + } else if (action === "delete") { + store + .deleteResource("config", object["_id"]) + .then(() => { + notifications.push("success", "Config deleted"); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(err => { + store.fulfill(0, Date.now()); + reject(err); + }); + } else { + reject(new Error("Undefined action")); + } + }); +} + +const formData = { + resource: "config", + attributes: attributes +}; + +function escapeRegExp(str): string { + return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); +} + +export function init(): Promise<{}> { + if (!window.authorizer.hasAccess("config", 2)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + return new Promise((resolve, reject) => { + Promise.all([loadCodeMirror(), loadYaml()]) + .then(() => { + resolve({}); + }) + .catch(reject); + }); +} + +function renderTable(confsResponse, searchString): Children { + const confs = confsResponse.value.sort((a, b) => { + return a._id < b._id ? -1 : 1; + }); + + let regex; + if (searchString) { + const keywords = searchString.split(" ").filter(s => s); + if (keywords.length) + regex = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i"); + } + + const rows = []; + for (const conf of confs) { + const attrs = {}; + if (regex && !regex.test(conf._id) && !regex.test(conf.value)) + attrs["style"] = "display: none;"; + + const edit = m( + "button", + { + title: "Edit config value", + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + base: conf, + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, false) + .then(errors => { + const ErrorList = errors ? Object.values(errors) : []; + if (ErrorList.length) { + for (const err of ErrorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "✎" + ); + + const del = m( + "button", + { + title: "Delete config", + onclick: () => { + if (!confirm(`Deleting ${conf._id} config. Are you sure?`)) return; + + putActionHandler("delete", conf).catch(err => { + throw err; + }); + } + }, + "✕" + ); + + rows.push( + m( + "tr", + attrs, + m("td.left", m("long-text", { text: conf._id })), + m( + "td.right", + m("span", [m("long-text", { text: `${conf.value}` }), edit, del]) + ) + ) + ); + } + + if (!rows.length) + rows.push(m("tr.empty", m("td", { colspan: 2 }, "No config"))); + + return m("table", m("tbody", rows)); +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Config - GenieACS"; + + const search = m("input", { + type: "text", + placeholder: "Search config", + oninput: e => { + vnode.state["searchString"] = e.target.value; + e.redraw = false; + clearTimeout(vnode.state["timeout"]); + vnode.state["timeout"] = setTimeout(m.redraw, 250); + } + }); + + const confs = store.fetch("config", true); + + let newConfig; + const subs = []; + + if (window.authorizer.hasAccess("config", 3)) { + newConfig = m( + "button.primary", + { + title: "Create new config", + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, true) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "New config" + ); + + const subsData = [ + { name: "overview", prefix: "ui.overview.groups.", data: [] }, + { name: "charts", prefix: "ui.overview.charts.", data: [] }, + { name: "filters", prefix: "ui.filters.", data: [] }, + { name: "index page", prefix: "ui.index.", data: [] }, + { name: "device page", prefix: "ui.device.", data: [] } + ]; + + if (confs.fulfilled) { + for (const conf of confs.value) { + for (const sub of subsData) { + if (conf["_id"].startsWith(sub["prefix"])) { + sub["data"].push(conf); + break; + } + } + } + } + + for (const sub of subsData) { + const attrs = { prefix: sub.prefix, name: sub.name, data: sub.data }; + subs.push( + m( + "button", + { + onclick: () => { + const cb = (): Children => { + return m( + uiConfigComponent, + Object.assign( + { + onUpdate: errs => { + const errors = errs ? Object.values(errs) : []; + if (errors.length) { + for (const err of errors) + notifications.push("error", err); + } else { + notifications.push( + "success", + `${sub.name.replace( + /^[a-z]/, + sub.name[0].toUpperCase() + )} config updated` + ); + overlay.close(cb); + } + store.fulfill(0, Date.now()); + }, + onError: err => { + notifications.push("error", err.message); + store.fulfill(0, Date.now()); + overlay.close(cb); + } + }, + attrs + ) + ); + }; + overlay.open(cb); + } + }, + `Edit ${sub.name}` + ) + ); + } + } + + return [ + m("h1", "Listing config"), + m( + ".all-parameters", + search, + m( + ".parameter-list", + { style: "height: 400px" }, + renderTable(confs, vnode.state["searchString"]) + ), + m(".actions-bar", [newConfig].concat(subs)) + ) + ]; + } + }; +}; diff --git a/ui/config.ts b/ui/config.ts new file mode 100644 index 0000000..7d4591f --- /dev/null +++ b/ui/config.ts @@ -0,0 +1 @@ +export default window.clientConfig; diff --git a/ui/css/app.css b/ui/css/app.css new file mode 100644 index 0000000..f5976c6 --- /dev/null +++ b/ui/css/app.css @@ -0,0 +1,685 @@ +@import "normalize.css"; +@import "codemirror/lib/codemirror.css"; + +:root { + --base-font-size: 13px; + --base-line-height: 16px; + + --color1: #ebe0bf; + --color2: #b9b098; + --color3: #f2ebd5; + --color4: #046380; + --color5: #002f2f; + + --fade: 0.2s; + + --disabled: #666; + + --status-red: hsl(0, 80%, 80%); + --status-green: hsl(125, 80%, 80%); + --status-gray: hsl(58, 3%, 80%); +} + +body, +input, +button, +select, +option, +textarea { + font-family: sans-serif; + color: var(--color5); +} + +a { + color: var(--color4); + cursor: pointer; + transition: background-color var(--fade), color var(--fade); + &:visited { + color: var(--color4); + } + &:hover { + color: white; + background-color: var(--color4); + } + /* text-decoration does not propagate to inline-block children */ + & span { + text-decoration: inherit; + } +} + +strong, +b { + font-weight: bold; +} + +i { + font-style: italic; +} + +@keyframes pending-blink { + from { + background-color: color(var(--status-green) a(90%)); + } + to { + background-color: transparent; + } +} + +#header { + background-color: #f2eddf; + border-bottom: 1px solid var(--color1); + overflow: visible; + position: relative; + + & > .drawer-wrapper { + position: fixed; + z-index: 10; + width: 720px; + margin-left: -360px; + top: 0; + left: 50%; + & > .drawer { + background-color: rgba(255, 255, 255, 0.9); + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + border-bottom: 1px solid var(--color1); + border-left: 1px solid var(--color1); + border-right: 1px solid var(--color1); + margin-top: -1px; + box-shadow: 0 0 10px #ccc; + overflow: hidden; + transition: height var(--fade), opacity var(--fade); + + & > div { + padding: 0.3em; + } + + & > .status { + display: flex; + & > span { + margin-right: 1em; + padding-left: 0.2em; + padding-right: 0.2em; + border-radius: 0.4em; + } + & > .pending.active { + &.active { + animation: 0.5s linear 0s infinite alternate pending-blink; + } + } + & > .stale.active { + background-color: color(var(--status-gray) a(90%)); + } + & > .fault.active { + background-color: color(var(--status-red) a(90%)); + } + & > .queued.active { + font-weight: bold; + } + + & > .actions { + flex-grow: 1; + text-align: right; + } + } + + & > .staging { + & .parameter { + font-style: italic; + max-width: 500px; + text-overflow: ellipsis; + display: inline-block; + overflow: hidden; + vertical-align: text-bottom; + } + + & input { + width: 100%; + padding: 0.3em; + margin-top: 0.3em; + margin-bottom: 0.3em; + } + & .actions { + text-align: right; + } + } + + & > .queue { + & > .pending { + animation: 0.5s linear 0s infinite alternate pending-blink; + border-radius: 0.4em; + } + & > .stale { + background-color: color(var(--status-gray) a(90%)); + border-radius: 0.4em; + } + & > .fault { + background-color: color(var(--status-red) a(90%)); + border-radius: 0.4em; + } + & .parameter { + font-style: italic; + max-width: 400px; + text-overflow: ellipsis; + display: inline-block; + overflow: hidden; + vertical-align: text-bottom; + } + & .value { + max-width: 200px; + text-overflow: ellipsis; + display: inline-block; + overflow: hidden; + vertical-align: text-bottom; + white-space: nowrap; + } + & > div { + display: flex; + & > .actions { + flex-grow: 1; + text-align: right; + } + } + } + } + & > .notifications-wrapper { + & > .notification { + padding: 0.3em; + left: 0; + right: 0; + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid var(--color1); + border-radius: 6px; + box-shadow: 0 0 10px #ccc; + transition: opacity var(--fade), top var(--fade); + + &.error { + background-color: color(var(--status-red) a(90%)); + border: 1px solid var(--status-red); + } + &.success { + background-color: color(var(--status-green) a(90%)); + border: 1px solid var(--status-green); + } + } + } + } + & > nav { + display: inline-block; + overflow: visible; + position: absolute; + bottom: -1px; + + & > ul { + font-weight: bold; + display: inline-block; + margin: 0 10px; + padding: 0; + + & > li { + margin: 0 2px; + background-color: var(--color1); + display: inline-block; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + border-top: 1px solid var(--color1); + border-left: 1px solid var(--color1); + border-right: 1px solid var(--color1); + transition: background-color var(--fade); + &.active, + &:hover { + background-color: white; + } + + & > a, + & > a:visited, + & > a:hover { + display: inline-block; + min-width: 140px; + color: var(--color4); + text-decoration: none; + background: none; + padding: 8px; + } + } + } + } + + & > .logo { + margin: 10px; + display: inline; + height: 56px; + } + + & > .user-menu { + float: right; + margin: 5px; + } +} + +#content-wrapper { + display: flex; +} + +#side-menu { + width: 200px; + min-width: 200px; + & > ul { + padding: 0; + list-style: none; + + & > li { + & > a { + display: block; + text-decoration: none; + margin: 5px 0; + padding: 8px; + border-bottom-right-radius: 6px; + border-top-right-radius: 6px; + background-color: var(--color3); + &:hover { + background-color: var(--color4); + color: var(--color3); + } + } + + &.active { + font-weight: bold; + } + } + } +} + +#content { + flex-grow: 1; + margin: 10px; + min-height: 250px; +} + +table.table { + border-spacing: 0; + text-align: left; + display: block; + overflow-x: auto; + + & table.table { + font-size: 90%; + } + + & th { + white-space: nowrap; + color: var(--color4); + font-weight: bold; + border-bottom: 2px solid var(--color4); + margin: 0; + padding: 0.2em 0.5em; + } + + & td { + margin: 0; + padding: 0.2em 0.5em; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 400px; + } + + & tfoot td > a { + margin-left: 1em; + } + + &.highlight > tbody > tr { + & > td { + border-bottom: 1px dotted var(--color1); + } + transition: background-color var(--fade); + &:hover { + background-color: var(--color3); + } + } + + & tbody > tr.empty { + text-align: center; + color: var(--disabled); + &:hover { + background-color: initial; + } + } +} + +h1 { + color: var(--color4); + font-size: 130%; + font-weight: bold; + margin-top: 1em; + margin-bottom: 0.5em; +} + +h2 { + color: var(--color5); + font-weight: bold; + font-size: 120%; + margin-top: 1em; + margin-bottom: 0.5em; +} + +h3 { + color: var(--color5); + font-size: 110%; + margin-top: 1em; + margin-bottom: 0.5em; +} + +p.error { + color: #a00; +} + +/* Remove button padding in FF */ +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +input[type="button"], +button { + background: transparent; + border: 0; + color: var(--color4); + border-radius: 4px; + transition: background-color var(--fade), color var(--fade); + &.primary { + border: 1px solid var(--color1); + background-color: var(--color3); + } + &.critical { + border: 1px solid var(--color1); + background-color: var(--color3); + color: #700; + } + &:hover:enabled { + background-color: var(--color4); + color: var(--color3); + } + &:disabled { + color: var(--disabled); + cursor: not-allowed; + } +} + +.CodeMirror, +textarea, +select, +input { + display: block; + border: 1px solid var(--color1); + background-color: var(--color3); + border-radius: 4px; + transition: border-color var(--fade), background-color var(--fade); + &.error { + background-color: #efcbcf; + border-color: #e6b2b7; + } + &:focus, + &:hover { + border-color: var(--color4); + } +} + +.filter { + margin-bottom: 0.5em; + & > input { + font-size: 90%; + width: 30em; + margin: 2px; + padding: 2px; + } +} + +.page-overview { + & > h1 { + text-align: center; + } +} + +.overview-chart-group { + display: flex; + justify-content: center; + + & h2 { + text-align: center; + } +} + +table.parameter-list { + border-spacing: 0; + text-align: left; + & th { + padding: 0; + padding-right: 1em; + } +} + +.overview-dot { + & > svg { + margin-bottom: -0.125em; + margin-left: 0.2em; + margin-right: 0.2em; + & > circle { + stroke: var(--color5); + stroke-width: 0.08em; + } + } +} + +span.inform { + & > * { + margin-right: 0.5em; + } +} + +.pie-chart { + margin: 20px; + text-align: center; + + & > svg { + & > path { + stroke: var(--color1); + stroke-width: 1px; + } + + & > a { + transition: opacity var(--fade); + opacity: 0; + + & > text { + font-weight: bold; + opacity: 0.4; + } + + & > path { + stroke: var(--color4); + } + + &:hover { + opacity: 1; + } + } + } + + & > .legend { + text-align: left; + margin: 15px; + + & > .legend-line { + & > .color { + height: 12px; + width: 12px; + margin-right: 5px; + border: 1px solid var(--color1); + display: inline-block; + } + } + } +} + +span.na { + color: var(--disabled); +} + +.all-parameters { + & > input { + font-family: monospace; + margin-bottom: 2px; + padding: 4px; + width: 600px; + } + + & > .parameter-list { + font-family: monospace; + overflow-y: scroll; + height: 200px; + + & > table { + width: 100%; + table-layout: fixed; + border-spacing: 0; + & > tbody { + & > tr { + & > td { + overflow: hidden; + white-space: nowrap; + border-bottom: 1px dotted var(--color1); + } + transition: background-color var(--fade); + &:hover { + background-color: var(--color3); + } + + & > td.left { + & > * { + max-width: 100%; + } + } + & > td.right { + text-align: right; + padding-right: 0.5em; + } + } + } + } + } +} + +.container-right { + float: right; + clear: right; +} + +.container-full-width { + clear: both; +} + +.actions-bar { + margin-top: 10px; + & > button { + margin-right: 10px; + } +} + +span.tag { + display: inline-block; + background-image: url('data:image/svg+xml;utf-8,'); + background-size: cover; + background-repeat: no-repeat; + border-radius: 0.2em; + padding-left: 1em; + padding-right: 0.2em; +} + +.tags { + & > .tag { + margin-right: 0.5em; + } +} + +.overlay-wrapper { + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.2); + z-index: 100; + transition: opacity var(--fade), top var(--fade); + display: flex; + align-items: center; + justify-content: center; + + & > .overlay { + max-width: 90vw; + max-height: 90vh; + background-color: white; + border: 1px solid var(--color1); + border-radius: 10px; + padding: 10px; + box-shadow: 0 0 15px #aaa; + overflow: auto; + } +} + +.overlay > .put-form { + min-width: 700px; +} + +.autocomplete { + box-shadow: 0 0 10px #ccc; + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid var(--color1); + border-radius: 4px; + max-height: 300px; + overflow-y: auto; + transition: opacity var(--fade); + & > .suggestion { + padding: 5px; + + &.selected, + &:hover { + background-color: var(--color3); + } + } +} + +span.long-text { + display: inline-block; + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 400px; + /* + "overflow: hidden" changes the baseline as explained here: + https://stackoverflow.com/questions/9273016/why-is-this-inline-block-element-pushed-downward + */ + vertical-align: top; +} + +span.long-text-overflowed { + cursor: pointer; + &:hover { + text-decoration: underline; + text-decoration-style: dotted; + } +} + +textarea.long-text { + font-family: monospace; + min-width: 10vw; + min-height: 10vh; +} + +span.parameter-value { + & > span:empty::before { + content: "blank"; + color: #aaa; + } +} diff --git a/ui/css/normalize.css b/ui/css/normalize.css new file mode 100644 index 0000000..04beed5 --- /dev/null +++ b/ui/css/normalize.css @@ -0,0 +1,450 @@ +/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers (opinionated). + */ + +body { + margin: 0; +} + +/** + * Add the correct display in IE 9-. + */ + +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ + +figcaption, +figure, +main { + /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ + +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + background-color: transparent; /* 1 */ + -webkit-text-decoration-skip: objects; /* 2 */ +} + +/** + * 1. Remove the bottom border in Chrome 57- and Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ + +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ + +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ + +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ + +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers (opinionated). + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { + /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { + /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ + +button, +html [type="button"], +/* 1 */ [type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ + +details, +/* 1 */ menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ + +template { + display: none; +} + +/* Hidden + ========================================================================== */ + +/** + * Add the correct display in IE 10-. + */ + +[hidden] { + display: none; +} diff --git a/ui/device-page.ts b/ui/device-page.ts new file mode 100644 index 0000000..5ebf571 --- /dev/null +++ b/ui/device-page.ts @@ -0,0 +1,35 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "./components"; +import config from "./config"; +import * as store from "./store"; + +export function init(args): Promise<{}> { + if (!window.authorizer.hasAccess("devices", 2)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + + return Promise.resolve({ + deviceId: args.id, + deviceFilter: ["=", ["PARAM", "DeviceID.ID"], args.id] + }); +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`; + + const dev = store.fetch("devices", vnode.attrs["deviceFilter"]).value; + if (!dev.length) return "Loading"; + const conf = config.ui.device; + const cmps = []; + + for (const c of Object.values(conf)) + cmps.push(m.context({ device: dev[0] }, c["type"], c)); + + return [m("h1", vnode.attrs["deviceId"]), cmps]; + } + }; +}; diff --git a/ui/devices-page.ts b/ui/devices-page.ts new file mode 100644 index 0000000..e9b6fac --- /dev/null +++ b/ui/devices-page.ts @@ -0,0 +1,411 @@ +import { ClosureComponent, Component, Children } from "mithril"; +import { m } from "./components"; +import config from "./config"; +import filterComponent from "./filter-component"; +import * as store from "./store"; +import * as taskQueue from "./task-queue"; +import * as notifications from "./notifications"; +import { evaluate, extractParams } from "../lib/common/expression"; +import { parse, stringify } from "../lib/common/expression-parser"; +import memoize from "../lib/common/memoize"; +import * as smartQuery from "./smart-query"; +import * as expressionParser from "../lib/common/expression-parser"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(parse); +const memoizedJsonParse = memoize(JSON.parse); +const memoizedGetSortable = memoize(p => { + const expressionParams = extractParams(p); + if (expressionParams.length === 1) { + const param = evaluate(expressionParams[0]); + if (typeof param === "string") return param; + } + return null; +}); + +const getDownloadUrl = memoize((filter, indexParameters) => { + const columns = {}; + for (const p of indexParameters) columns[p.label] = stringify(p.parameter); + return `/api/devices.csv?${m.buildQueryString({ + filter: stringify(filter), + columns: JSON.stringify(columns) + })}`; +}); + +const unpackSmartQuery = memoize(query => { + return expressionParser.map(query, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack("devices", e[2], e[3]); + return e; + }); +}); + +export function init(args): Promise<{}> { + return new Promise((resolve, reject) => { + if (!window.authorizer.hasAccess("devices", 2)) + return void reject(new Error("You are not authorized to view this page")); + + const filter = args.filter; + const sort = args.sort; + const indexParameters = Object.values(config.ui.index); + resolve({ filter, indexParameters, sort }); + }); +} + +function renderTable( + devicesResponse, + parameters, + total, + selected, + showMoreCallback, + downloadUrl, + sort, + onSortChange +): Children { + const devices = devicesResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: devices.length && selected.size === devices.length, + onchange: e => { + for (const d of devices) { + if (e.target.checked) selected.add(d["DeviceID.ID"].value[0]); + else selected.delete(d["DeviceID.ID"].value[0]); + } + }, + disabled: !total + }); + + const labels = [m("th", selectAll)]; + for (const param of parameters) { + const label = param.label; + let _param; + if (!param.unsortable && (_param = memoizedGetSortable(param.parameter))) { + let direction = 1; + + let symbol = "\u2981"; + if (sort[_param] > 0) symbol = "\u2bc6"; + else if (sort[_param] < 0) symbol = "\u2bc5"; + + const sortable = m( + "button", + { + onclick: () => { + if (sort[_param] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [_param]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } else { + labels.push(m("th", label)); + } + } + + const rows = []; + for (const device of devices) { + const checkbox = m("input", { + type: "checkbox", + checked: selected.has(device["DeviceID.ID"].value[0]), + onchange: e => { + if (e.target.checked) selected.add(device["DeviceID.ID"].value[0]); + else selected.delete(device["DeviceID.ID"].value[0]); + } + }); + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(device["DeviceID.ID"].value[0])) + selected.add(device["DeviceID.ID"].value[0]); + } + }, + m("td", checkbox), + parameters.map(p => { + const comp = m.context( + { device: device, parameter: p.parameter }, + p.type || "parameter", + p + ); + return m("td", comp); + }), + m( + "td.table-row-links", + m( + "a", + { + href: `#!/devices/${encodeURIComponent( + device["DeviceID.ID"].value[0] + )}` + }, + "Show" + ) + ) + ) + ); + } + + if (!rows.length) { + rows.push( + m("tr.empty", m("td", { colspan: parameters.length + 1 }, "No devices")) + ); + } + + const footerElements = []; + if (total != null) footerElements.push(`${devices.length}/${total}`); + else footerElements.push(`${devices.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more devices", + onclick: showMoreCallback, + disabled: devices.length >= total || !devicesResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) { + footerElements.push( + m("a.download-csv", { href: downloadUrl, download: "" }, "Download") + ); + } + + const tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + return m("table.table.highlight", [ + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ]); +} + +function renderActions(selected): Children { + const buttons = []; + + buttons.push( + m( + "button.primary", + { + title: "Reboot selected devices", + disabled: !selected.size, + onclick: () => { + for (const d of selected) { + taskQueue.queueTask({ + name: "reboot", + device: d + }); + } + } + }, + "Reboot" + ) + ); + + buttons.push( + m( + "button.critical", + { + title: "Factory reset selected devices", + disabled: !selected.size, + onclick: () => { + for (const d of selected) { + taskQueue.queueTask({ + name: "factoryReset", + device: d + }); + } + } + }, + "Reset" + ) + ); + + buttons.push( + m( + "button.primary", + { + title: "Delete selected devices", + disabled: !selected.size, + onclick: () => { + const ids = Array.from(selected); + if (!confirm(`Deleting ${ids.length} devices. Are you sure?`)) return; + + let counter = 1; + for (const id of ids) { + ++counter; + store + .deleteResource("devices", id) + .then(() => { + notifications.push("success", `${id}: Deleted`); + if (--counter === 0) store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", `${id}: ${err.message}`); + if (--counter === 0) store.fulfill(0, Date.now()); + }); + } + if (--counter === 0) store.fulfill(0, Date.now()); + } + }, + "Delete" + ) + ); + + buttons.push( + m( + "button.primary", + { + title: "Tag selected devices", + disabled: !selected.size, + onclick: () => { + const ids = Array.from(selected); + const tag = prompt(`Enter tag to assign to ${ids.length} devices:`); + if (!tag) return; + + let counter = 1; + for (const id of ids) { + ++counter; + store + .updateTags(id, { [tag]: true }) + .then(() => { + notifications.push("success", `${id}: Tags updated`); + if (--counter === 0) store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", `${id}: ${err.message}`); + if (--counter === 0) store.fulfill(0, Date.now()); + }); + } + if (--counter === 0) store.fulfill(0, Date.now()); + } + }, + "Tag" + ) + ); + + buttons.push( + m( + "button.primary", + { + title: "Untag selected devices", + disabled: !selected.size, + onclick: () => { + const ids = Array.from(selected); + const tag = prompt( + `Enter tag to unassign from ${ids.length} devices:` + ); + if (!tag) return; + + let counter = 1; + for (const id of ids) { + ++counter; + store + .updateTags(id, { [tag]: false }) + .then(() => { + notifications.push("success", `${id}: Tags updated`); + if (--counter === 0) store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", `${id}: ${err.message}`); + if (--counter === 0) store.fulfill(0, Date.now()); + }); + } + if (--counter === 0) store.fulfill(0, Date.now()); + } + }, + "Untag" + ) + ); + + return m(".actions-bar", buttons); +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Devices - GenieACS"; + + function showMore(): void { + vnode.state["showCount"] = + (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter): void { + const ops = { filter }; + if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; + m.route.set("/devices", ops); + } + + function onSortChange(sort): void { + const ops = { sort }; + if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; + m.route.set("/devices", ops); + } + + const sort = vnode.attrs["sort"] + ? memoizedJsonParse(vnode.attrs["sort"]) + : {}; + let filter = vnode.attrs["filter"] + ? memoizedParse(vnode.attrs["filter"]) + : true; + filter = unpackSmartQuery(filter); + + const devs = store.fetch("devices", filter, { + limit: vnode.state["showCount"] || PAGE_SIZE, + sort: sort + }); + const count = store.count("devices", filter); + + const selected = new Set(); + if (vnode.state["selected"]) { + for (const d of devs.value) { + if (vnode.state["selected"].has(d["DeviceID.ID"].value[0])) + selected.add(d["DeviceID.ID"].value[0]); + } + } + vnode.state["selected"] = selected; + + const downloadUrl = getDownloadUrl( + filter, + vnode.attrs["indexParameters"] + ); + + const attrs = {}; + attrs["resource"] = "devices"; + attrs["filter"] = vnode.attrs["filter"]; + attrs["onChange"] = onFilterChanged; + + return [ + m("h1", "Listing devices"), + m(filterComponent, attrs), + renderTable( + devs, + vnode.attrs["indexParameters"], + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ), + renderActions(selected) + ]; + } + }; +}; diff --git a/ui/drawer-component.ts b/ui/drawer-component.ts new file mode 100644 index 0000000..3ab3889 --- /dev/null +++ b/ui/drawer-component.ts @@ -0,0 +1,510 @@ +import m, { + Children, + Child, + ClosureComponent, + Component, + VnodeDOM +} from "mithril"; +import * as taskQueue from "./task-queue"; +import * as store from "./store"; +import * as notifications from "./notifications"; + +function mparam(param): Children { + return m("span.parameter", { title: param }, `${param}`); +} + +function mval(val): Children { + return m("span.value", { title: val }, `${val}`); +} + +function renderStagingSpv(task, queueFunc, cancelFunc): Children { + function keydown(e): void { + if (e.keyCode === 13) queueFunc(); + else if (e.keyCode === 27) cancelFunc(); + else e.redraw = false; + } + + let input; + if (task.parameterValues[0][2] === "xsd:boolean") { + input = m( + "select", + { + value: task.parameterValues[0][1].toString(), + onchange: e => { + e.redraw = false; + task.parameterValues[0][1] = input.dom.value; + }, + onkeydown: keydown, + oncreate: vnode => { + (vnode.dom as HTMLSelectElement).focus(); + } + }, + [ + m("option", { value: "true" }, "true"), + m("option", { value: "false" }, "false") + ] + ); + } else { + input = m("input", { + type: ["xsd:dateTime", "xsd:int", "xsd:unsignedInt"].includes( + task.parameterValues[0][2] + ) + ? "number" + : "text", + value: task.parameterValues[0][1], + oninput: e => { + e.redraw = false; + task.parameterValues[0][1] = input.dom.value; + }, + onkeydown: keydown, + oncreate: vnode => { + (vnode.dom as HTMLInputElement).focus(); + (vnode.dom as HTMLInputElement).select(); + // Need to prevent scrolling on focus because + // we're animating height and using overflow: hidden + (vnode.dom.parentNode.parentNode as Element).scrollTop = 0; + } + }); + } + + return [m("span", "Editing ", mparam(task.parameterValues[0][0])), input]; +} + +function renderStagingDownload(task): Children { + task.invalid = !task.fileName || !task.fileType; + const files = store.fetch("files", true); + const idParts = task.device.split("-"); + const oui = decodeURIComponent(idParts[0]); + const productClass = + idParts.length === 3 ? decodeURIComponent(idParts[1]) : ""; + + const typesList = [ + "", + "1 Firmware Upgrade Image", + "2 Web Content", + "3 Vendor Configuration File", + "4 Tone File", + "5 Ringer File" + ].map(t => + m( + "option", + { + disabled: !t, + value: t, + selected: (task.fileType || "") === t, + onclick: () => { + task.fileType = t; + } + }, + t + ) + ); + + const filesList = [""] + .concat( + files.value + .filter( + f => + (!f["metadata.oui"] || f["metadata.oui"] === oui) && + (!f["metadata.productClass"] || + f["metadata.productClass"] === productClass) + ) + .map(f => f._id) + ) + .map(f => + m( + "option", + { + disabled: !f, + value: f, + selected: (task.fileName || "") === f, + onclick: () => { + task.fileName = f; + task.fileType = ""; + for (const file of files.value) + if (file._id === f) task.fileType = file["metadata.fileType"]; + } + }, + f + ) + ); + + return [ + "Push ", + m( + "select", + { disabled: files.fulfilling, style: "width: 350px" }, + filesList + ), + " as ", + m("select", typesList) + ]; +} + +function renderStaging(staging): Child[] { + const elements: Child[] = []; + + for (const s of staging) { + const queueFunc = (): void => { + staging.delete(s); + taskQueue.queueTask(s); + }; + const cancelFunc = (): void => { + staging.delete(s); + }; + + let elms; + if (s.name === "setParameterValues") + elms = renderStagingSpv(s, queueFunc, cancelFunc); + else if (s.name === "download") elms = renderStagingDownload(s); + + const queue = m( + "button.primary", + { title: "Queue task", onclick: queueFunc, disabled: s.invalid }, + "Queue" + ); + const cancel = m( + "button", + { title: "Cancel edit", onclick: cancelFunc }, + "Cancel" + ); + + elements.push(m(".staging", elms, m("div.actions", queue, cancel))); + } + return elements; +} + +function renderQueue(queue): Child[] { + const details: Child[] = []; + const devices: { [deviceId: string]: any[] } = {}; + for (const t of queue) { + devices[t.device] = devices[t.device] || []; + devices[t.device].push(t); + } + + for (const [k, v] of Object.entries(devices)) { + details.push(m("strong", k)); + for (const t of v) { + const actions = []; + + if (t.status === "fault" || t.status === "stale") { + actions.push( + m( + "button", + { + title: "Retry this task", + onclick: () => { + taskQueue.queueTask(t); + } + }, + "↺" + ) + ); + } + + actions.push( + m( + "button", + { + title: "Remove this task", + onclick: () => { + taskQueue.deleteTask(t); + } + }, + "✕" + ) + ); + + if (t.name === "setParameterValues") { + details.push( + m( + `div.${t.status}`, + m( + "span", + "Set ", + mparam(t.parameterValues[0][0]), + " to '", + mval(t.parameterValues[0][1]), + "'" + ), + m(".actions", actions) + ) + ); + } else if (t.name === "refreshObject") { + details.push( + m( + `div.${t.status}`, + m("span", "Refresh ", mparam(t.parameterName)), + m(".actions", actions) + ) + ); + } else if (t.name === "reboot") { + details.push(m(`div.${t.status}`, "Reboot", m(".actions", actions))); + } else if (t.name === "factoryReset") { + details.push( + m(`div.${t.status}`, "Factory reset", m(".actions", actions)) + ); + } else if (t.name === "addObject") { + details.push( + m( + `div.${t.status}`, + m("span", "Add ", mparam(t.objectName)), + m(".actions", actions) + ) + ); + } else if (t.name === "deleteObject") { + details.push( + m( + `div.${t.status}`, + m("span", "Delete ", mparam(t.objectName)), + m(".actions", actions) + ) + ); + } else if (t.name === "getParameterValues") { + details.push( + m( + `div.${t.status}`, + `Refresh ${t.parameterNames.length} parameters`, + m(".actions", actions) + ) + ); + } else if (t.name === "download") { + details.push( + m( + `div.${t.status}`, + `Push file: ${t.fileName} (${t.fileType})`, + m(".actions", actions) + ) + ); + } else { + details.push(m(`div.${t.status}`, t.name, m(".actions", actions))); + } + } + } + + return details; +} + +function renderNotifications(notifs): Child[] { + const notificationElements: Child[] = []; + + for (const n of notifs) { + notificationElements.push( + m( + "div.notification", + { + class: n.type, + style: "position: absolute;opacity: 0", + oncreate: vnode => { + (vnode.dom as HTMLDivElement).style.opacity = "1"; + }, + onbeforeremove: vnode => { + (vnode.dom as HTMLDivElement).style.opacity = "0"; + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 500); + }); + }, + key: n.timestamp + }, + n.message + ) + ); + } + return notificationElements; +} + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const queue = taskQueue.getQueue(); + const staging = taskQueue.getStaging(); + const notifs = notifications.getNotifications(); + + let drawerElement, statusElement; + const notificationElements = renderNotifications(notifs); + const stagingElements = renderStaging(staging); + const queueElements = renderQueue(queue); + + function repositionNotifications(): void { + let top = 10; + for (const c of notificationElements as VnodeDOM[]) { + (c.dom as HTMLDivElement).style.top = `${top}`; + top += (c.dom as HTMLDivElement).offsetHeight + 10; + } + } + + function resizeDrawer(): void { + let height = + statusElement.dom.offsetTop + statusElement.dom.offsetHeight; + if (stagingElements.length) { + for (const s of stagingElements as VnodeDOM[]) { + height = Math.max( + height, + (s.dom as HTMLDivElement).offsetTop + + (s.dom as HTMLDivElement).offsetHeight + ); + } + } else if (vnode.state["mouseIn"]) { + for (const c of drawerElement.children) + height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight); + } + drawerElement.dom.style.height = height; + } + + if (stagingElements.length + queueElements.length) { + const statusCount = { queued: 0, pending: 0, fault: 0, stale: 0 }; + for (const t of queue) statusCount[t["status"]] += 1; + + const actions = m( + ".actions", + m( + "button.primary", + { + title: "Commit queued tasks", + disabled: !statusCount.queued, + onclick: () => { + const tasks = Array.from(taskQueue.getQueue()).filter( + t => t["status"] === "queued" + ); + taskQueue + .commit( + tasks, + (deviceId, err, connectionRequestStatus, tasks2) => { + if (err) { + notifications.push( + "error", + `${deviceId}: ${err.message}` + ); + return; + } + + if (connectionRequestStatus !== "OK") { + notifications.push( + "error", + `${deviceId}: ${connectionRequestStatus}` + ); + return; + } + + for (const t of tasks2) { + if (t.status === "stale") { + notifications.push( + "error", + `${deviceId}: No contact from device` + ); + return; + } else if (t.status === "fault") { + notifications.push( + "error", + `${deviceId}: Task(s) faulted` + ); + return; + } + } + + notifications.push( + "success", + `${deviceId}: Task(s) committed` + ); + } + ) + .then(() => { + store.fulfill(0, Date.now()); + }); + } + }, + "Commit" + ), + m( + "button", + { + title: "Clear tasks", + onclick: taskQueue.clear, + disabled: !queueElements.length + }, + "Clear" + ) + ); + + statusElement = m( + ".status", + m( + "span.queued", + { class: statusCount.queued ? "active" : "" }, + `Queued: ${statusCount.queued}` + ), + m( + "span.pending", + { class: statusCount.pending ? "active" : "" }, + `Pending: ${statusCount.pending}` + ), + m( + "span.fault", + { class: statusCount.fault ? "active" : "" }, + `Fault: ${statusCount.fault}` + ), + m( + "span.stale", + { class: statusCount.stale ? "active" : "" }, + `Stale: ${statusCount.stale}` + ), + actions + ); + + drawerElement = m( + ".drawer", + { + key: "drawer", + style: "opacity: 0;height: 0;", + oncreate: vnode2 => { + vnode.state["mouseIn"] = false; + (vnode2.dom as HTMLDivElement).style.opacity = "1"; + resizeDrawer(); + }, + onmouseover: e => { + vnode.state["mouseIn"] = true; + resizeDrawer(); + e.redraw = false; + }, + onmouseleave: e => { + vnode.state["mouseIn"] = false; + resizeDrawer(); + e.redraw = false; + }, + onupdate: resizeDrawer, + onbeforeremove: vnode2 => { + (vnode2.dom as HTMLDivElement).onmouseover = null; + (vnode2.dom as HTMLDivElement).onmouseleave = null; + (vnode2.dom as HTMLDivElement).style.opacity = "0"; + (vnode2.dom as HTMLDivElement).style.height = "0"; + return new Promise(resolve => { + setTimeout(resolve, 500); + }); + } + }, + statusElement, + stagingElements.length ? stagingElements : m(".queue", queueElements) + ); + } + + return m( + "div.drawer-wrapper", + drawerElement, + m( + "div.notifications-wrapper", + { + key: "notifications", + style: "position: relative;", + onupdate: repositionNotifications, + oncreate: repositionNotifications + }, + notificationElements + ) + ); + } + }; +}; + +export default component; diff --git a/ui/dynamic-loader.ts b/ui/dynamic-loader.ts new file mode 100644 index 0000000..ef1af42 --- /dev/null +++ b/ui/dynamic-loader.ts @@ -0,0 +1,33 @@ +export let codeMirror; +export let yaml; + +export function loadCodeMirror(): Promise { + if (codeMirror) return Promise.resolve(); + + return new Promise((resolve, reject) => { + const promises = [ + import(/* webpackChunkName: "codemirror" */ "codemirror"), + import(/* webpackChunkName: "codemirror" */ "codemirror/mode/javascript/javascript"), + import(/* webpackChunkName: "codemirror" */ "codemirror/mode/yaml/yaml") + ]; + Promise.all(promises) + .then(modules => { + codeMirror = modules[0]; + resolve(); + }) + .catch(reject); + }); +} + +export function loadYaml(): Promise { + if (yaml) return Promise.resolve(); + + return new Promise((resolve, reject) => { + import(/* webpackChunkName: "yaml" */ "yaml") + .then(module => { + yaml = module; + resolve(); + }) + .catch(reject); + }); +} diff --git a/ui/error-page.ts b/ui/error-page.ts new file mode 100644 index 0000000..0475378 --- /dev/null +++ b/ui/error-page.ts @@ -0,0 +1,11 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "./components"; + +export const component: ClosureComponent = (): Component => { + return { + view: function(vnode) { + document.title = "Error! - GenieACS"; + return m("p.error", vnode.attrs["error"]); + } + }; +}; diff --git a/ui/faults-page.ts b/ui/faults-page.ts new file mode 100644 index 0000000..3f3bb0b --- /dev/null +++ b/ui/faults-page.ts @@ -0,0 +1,297 @@ +import { ClosureComponent, Component, Children } from "mithril"; +import { m } from "./components"; +import config from "./config"; +import filterComponent from "./filter-component"; +import * as store from "./store"; +import * as notifications from "./notifications"; +import memoize from "../lib/common/memoize"; +import * as smartQuery from "./smart-query"; +import { map, parse, stringify } from "../lib/common/expression-parser"; +import { loadYaml, yaml } from "./dynamic-loader"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(parse); +const memoizedJsonParse = memoize(JSON.parse); + +const attributes = [ + { id: "device", label: "Device" }, + { id: "channel", label: "Channel" }, + { id: "code", label: "Code" }, + { id: "message", label: "Message" }, + { id: "detail", label: "Detail", unsortable: true }, + { id: "retries", label: "Retries" }, + { id: "timestamp", label: "Timestamp" } +]; + +const getDownloadUrl = memoize(filter => { + const cols = {}; + for (const attr of attributes) { + cols[attr.label] = + attr.id === "timestamp" ? `DATE_STRING(${attr.id})` : attr.id; + } + + return `/api/faults.csv?${m.buildQueryString({ + filter: stringify(filter), + columns: JSON.stringify(cols) + })}`; +}); + +const unpackSmartQuery = memoize(query => { + return map(query, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack("faults", e[2], e[3]); + return e; + }); +}); + +export function init(args): Promise<{}> { + if (!window.authorizer.hasAccess("faults", 2)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + + const sort = args.sort; + const filter = args.filter; + return new Promise((resolve, reject) => { + loadYaml() + .then(() => { + resolve({ filter, sort }); + }) + .catch(reject); + }); +} + +function renderTable( + faultsResponse, + total, + selected, + showMoreCallback, + downloadUrl, + sort, + onSortChange +): Children { + const faults = faultsResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: faults.length && selected.size === faults.length, + onchange: e => { + for (const f of faults) { + if (e.target.checked) selected.add(f["_id"]); + else selected.delete(f["_id"]); + } + }, + disabled: !total + }); + + const labels = [m("th", selectAll)]; + for (const attr of attributes) { + const label = attr.label; + + if (attr.unsortable) { + labels.push(m("th", label)); + continue; + } + + // Offset direction by 1 since sort["_id"] = -1 + let direction = 2; + + let symbol = "\u2981"; + if (sort[attr.id] > 0) symbol = "\u2bc6"; + else if (sort[attr.id] < 0) symbol = "\u2bc5"; + + const sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } + + const rows = []; + for (const f of faults) { + const checkbox = m("input", { + type: "checkbox", + checked: selected.has(f["_id"]), + onchange: e => { + if (e.target.checked) selected.add(f["_id"]); + else selected.delete(f["_id"]); + }, + onclick: e => { + e.stopPropagation(); + e.redraw = false; + } + }); + + const deviceHref = `#!/devices/${encodeURIComponent(f["device"])}`; + + const tds = [m("td", checkbox)]; + for (const attr of attributes) { + if (attr.id === "device") + tds.push(m("td", m("a", { href: deviceHref }, f[attr.id]))); + else if (attr.id === "timestamp") + tds.push(m("td", new Date(f[attr.id]).toLocaleString())); + else if (attr.id === "detail") + tds.push(m("td", m("long-text", { text: yaml.stringify(f[attr.id]) }))); + else tds.push(m("td", f[attr.id])); + } + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(f["_id"])) selected.add(f["_id"]); + } + }, + tds + ) + ); + } + + if (!rows.length) + rows.push(m("tr.empty", m("td", { colspan: 7 }, "No faults"))); + + const footerElements = []; + if (total != null) footerElements.push(`${faults.length}/${total}`); + else footerElements.push(`${faults.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more faults", + onclick: showMoreCallback, + disabled: faults.length >= total || !faultsResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) { + footerElements.push( + m("a.download-csv", { href: downloadUrl, download: "" }, "Download") + ); + } + + const tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + const buttons = [ + m( + "button.primary", + { + title: "Delete selected faults", + disabled: !selected.size, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + Promise.all( + Array.from(selected).map(id => store.deleteResource("faults", id)) + ) + .then(res => { + notifications.push("success", `${res.length} faults deleted`); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + store.fulfill(0, Date.now()); + }); + } + }, + "Delete" + ) + ]; + + return [ + m( + "table.table.highlight", + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ), + buttons + ]; +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Faults - GenieACS"; + + function showMore(): void { + vnode.state["showCount"] = + (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter): void { + const ops = { filter }; + if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; + m.route.set("/faults", ops); + } + + function onSortChange(sort): void { + const ops = { sort }; + if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; + m.route.set("/faults", ops); + } + + const sort = vnode.attrs["sort"] + ? memoizedJsonParse(vnode.attrs["sort"]) + : {}; + let filter = vnode.attrs["filter"] + ? memoizedParse(vnode.attrs["filter"]) + : true; + filter = unpackSmartQuery(filter); + + const faults = store.fetch("faults", filter, { + limit: vnode.state["showCount"] || PAGE_SIZE, + sort: sort + }); + const count = store.count("faults", filter); + + const selected = new Set(); + if (vnode.state["selected"]) { + for (const f of faults.value) + if (vnode.state["selected"].has(f["_id"])) selected.add(f["_id"]); + } + vnode.state["selected"] = selected; + + const downloadUrl = getDownloadUrl(filter); + + const attrs = {}; + attrs["resource"] = "faults"; + attrs["filter"] = vnode.attrs["filter"]; + attrs["onChange"] = onFilterChanged; + + return [ + m("h1", "Listing faults"), + m(filterComponent, attrs), + renderTable( + faults, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; + } + }; +}; diff --git a/ui/files-page.ts b/ui/files-page.ts new file mode 100644 index 0000000..8d19025 --- /dev/null +++ b/ui/files-page.ts @@ -0,0 +1,400 @@ +import { ClosureComponent, Component, Children } from "mithril"; +import { m } from "./components"; +import config from "./config"; +import filterComponent from "./filter-component"; +import * as store from "./store"; +import * as notifications from "./notifications"; +import memoize from "../lib/common/memoize"; +import putFormComponent from "./put-form-component"; +import * as overlay from "./overlay"; +import * as smartQuery from "./smart-query"; +import { map, parse, stringify } from "../lib/common/expression-parser"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(parse); +const memoizedJsonParse = memoize(JSON.parse); + +const attributes: { + id: string; + label: string; + type?: string; + options?: any; +}[] = [ + { id: "_id", label: "Name" }, + { + id: "metadata.fileType", + label: "Type", + type: "combo", + options: [ + "1 Firmware Upgrade Image", + "2 Web Content", + "3 Vendor Configuration File", + "4 Tone File", + "5 Ringer File" + ] + }, + { id: "metadata.oui", label: "OUI" }, + { id: "metadata.productClass", label: "Product Class" }, + { id: "metadata.version", label: "Version" } +]; + +const formData = { + resource: "files", + attributes: attributes + .slice(1) // remove _id from new object form + .concat([{ id: "file", label: "File", type: "file" }]) +}; + +const unpackSmartQuery = memoize(query => { + return map(query, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack("files", e[2], e[3]); + return e; + }); +}); + +interface ValidationErrors { + [prop: string]: string; +} + +function putActionHandler(action, _object): Promise { + return new Promise((resolve, reject) => { + const object = Object.assign({}, _object); + if (action === "save") { + const file = object["file"] ? object["file"][0] : null; + delete object["file"]; + + if (!file) return void resolve({ file: "File not selected" }); + + const id = file.name; + + store + .resourceExists("files", id) + .then(exists => { + if (exists) { + store.fulfill(0, Date.now()); + return void resolve({ file: "File already exists" }); + } + const headers = Object.assign( + { + "Content-Type": "application/octet-stream", + Accept: "application/octet-stream" + }, + object + ); + + m.request({ + method: "PUT", + headers: headers, + url: `/api/files/${encodeURIComponent(id)}`, + serialize: body => body, // Identity function to prevent JSON.parse on blob data + data: file + }) + .then(() => { + notifications.push( + "success", + `File ${exists ? "updated" : "created"}` + ); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(reject); + }) + .catch(reject); + } else { + reject(new Error("Undefined action")); + } + }); +} + +const getDownloadUrl = memoize(filter => { + const cols = {}; + for (const attr of attributes) cols[attr.label] = attr.id; + return `/api/files.csv?${m.buildQueryString({ + filter: stringify(filter), + columns: JSON.stringify(cols) + })}`; +}); + +export function init(args): Promise<{}> { + if (!window.authorizer.hasAccess("files", 2)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + + const sort = args.sort; + const filter = args.filter; + return Promise.resolve({ filter, sort }); +} + +function renderTable( + filesResponse, + total, + selected, + showMoreCallback, + downloadUrl, + sort, + onSortChange +): Children { + const files = filesResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: files.length && selected.size === files.length, + onchange: e => { + for (const file of files) { + if (e.target.checked) selected.add(file["_id"]); + else selected.delete(file["_id"]); + } + }, + disabled: !total + }); + + const labels = [m("th", selectAll)]; + for (const attr of attributes) { + const label = attr.label; + + let direction = 1; + + let symbol = "\u2981"; + if (sort[attr.id] > 0) symbol = "\u2bc6"; + else if (sort[attr.id] < 0) symbol = "\u2bc5"; + + const sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } + + const rows = []; + for (const file of files) { + const checkbox = m("input", { + type: "checkbox", + checked: selected.has(file["_id"]), + onchange: e => { + if (e.target.checked) selected.add(file["_id"]); + else selected.delete(file["_id"]); + }, + onclick: e => { + e.stopPropagation(); + e.redraw = false; + } + }); + + const tds = [m("td", checkbox)]; + for (const attr of attributes) { + if (attr.id === "script") + tds.push(m("td", { title: file[attr.id] }, file[attr.id])); + else tds.push(m("td", file[attr.id])); + } + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(file["_id"])) selected.add(file["_id"]); + } + }, + tds + ) + ); + } + + if (!rows.length) + rows.push(m("tr.empty", m("td", { colspan: labels.length }, "No files"))); + + const footerElements = []; + if (total != null) footerElements.push(`${files.length}/${total}`); + else footerElements.push(`${files.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more files", + onclick: showMoreCallback, + disabled: files.length >= total || !filesResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) { + footerElements.push( + m("a.download-csv", { href: downloadUrl, download: "" }, "Download") + ); + } + + const tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + const buttons = [ + m( + "button.primary", + { + title: "Delete selected files", + disabled: !selected.size, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + Promise.all( + Array.from(selected).map(id => store.deleteResource("files", id)) + ) + .then(res => { + notifications.push("success", `${res.length} files deleted`); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + store.fulfill(0, Date.now()); + }); + } + }, + "Delete" + ) + ]; + + if (window.authorizer.hasAccess("files", 3)) { + buttons.push( + m( + "button.primary", + { + title: "Create new file", + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "New" + ) + ); + } + + return [ + m( + "table.table.highlight", + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ), + m("div.actions-bar", buttons) + ]; +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Files - GenieACS"; + + function showMore(): void { + vnode.state["showCount"] = + (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter): void { + const ops = { filter }; + if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort): void { + const ops = { sort }; + if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs["sort"] + ? memoizedJsonParse(vnode.attrs["sort"]) + : {}; + let filter = vnode.attrs["filter"] + ? memoizedParse(vnode.attrs["filter"]) + : true; + filter = unpackSmartQuery(filter); + + const files = store.fetch("files", filter, { + limit: vnode.state["showCount"] || PAGE_SIZE, + sort: sort + }); + + const count = store.count("files", filter); + + const selected = new Set(); + if (vnode.state["selected"]) { + for (const file of files.value) { + if (vnode.state["selected"].has(file["_id"])) + selected.add(file["_id"]); + } + } + vnode.state["selected"] = selected; + + const downloadUrl = getDownloadUrl(filter); + + const attrs = {}; + attrs["resource"] = "files"; + attrs["filter"] = vnode.attrs["filter"]; + attrs["onChange"] = onFilterChanged; + + return [ + m("h1", "Listing files"), + m(filterComponent, attrs), + renderTable( + files, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; + } + }; +}; diff --git a/ui/filter-component.ts b/ui/filter-component.ts new file mode 100644 index 0000000..34b66b7 --- /dev/null +++ b/ui/filter-component.ts @@ -0,0 +1,121 @@ +import m, { ClosureComponent, Component } from "mithril"; +import { parse, stringify, map } from "../lib/common/expression-parser"; +import memoize from "../lib/common/memoize"; +import Autocomplete from "./autocomplete-compnent"; +import * as smartQuery from "./smart-query"; +import { filterToMongoQuery } from "../lib/mongodb-functions"; +import { Expression } from "../lib/types"; + +const getAutocomplete = memoize(resource => { + const labels = smartQuery.getLabels(resource); + const autocomplete = new Autocomplete("autocomplete", (txt, cb) => { + txt = txt.toLowerCase(); + cb(labels.filter(s => s.toLowerCase().includes(txt)).map(s => `${s}: `)); + }); + return autocomplete; +}); + +function parseFilter(resource, f): Expression { + let exp; + if (/^[\s0-9a-zA-Z]+:/.test(f)) { + const k = f.split(":", 1)[0]; + const v = f.slice(k.length + 1).trim(); + exp = ["FUNC", "Q", k.trim(), v]; + } else { + exp = parse(f); + } + + // Throw exception if invalid Mongo query + filterToMongoQuery( + map(exp, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack(resource, e[2], e[3]); + return e; + }) + ); + + return exp; +} + +function stringifyFilter(f: Expression): string { + if (Array.isArray(f) && f[0] === "FUNC" && f[1] === "Q") + return `${f[2]}: ${f[3]}`; + return stringify(f); +} + +const splitFilter = memoize(filter => { + if (!filter) return [""]; + const list = []; + const f = parse(filter); + if (Array.isArray(f) && f[0] === "AND") + for (const ff of f.slice(1)) list.push(stringifyFilter(ff)); + else list.push(stringifyFilter(f)); + + list.push(""); + return list; +}); + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + if ( + !vnode.state["filterList"] || + vnode.attrs["filter"] !== vnode.state["filter"] + ) { + vnode.state["filterInvalid"] = 0; + vnode.state["filter"] = vnode.attrs["filter"]; + vnode.state["filterList"] = splitFilter(vnode.attrs["filter"]); + } + + function onChange(): void { + vnode.state["filterInvalid"] = 0; + vnode.state["filterList"] = vnode.state["filterList"].filter(f => f); + let filter = vnode.state["filterList"].map((f, idx) => { + try { + return parseFilter(vnode.attrs["resource"], f); + } catch (err) { + vnode.state["filterInvalid"] |= 1 << idx; + } + return null; + }); + vnode.state["filterList"].push(""); + + if (!vnode.state["filterInvalid"]) { + delete vnode.state["filter"]; + if (filter.length === 0) { + vnode.attrs["onChange"](""); + } else { + if (filter.length > 1) filter = ["AND"].concat(filter); + else filter = filter[0]; + vnode.attrs["onChange"](stringify(filter)); + } + } + } + + return m( + "div.filter", + [m("b", "Filter")].concat( + vnode.state["filterList"].map((fltr, idx) => { + return m("input", { + type: "text", + class: `${ + (vnode.state["filterInvalid"] >> idx) & 1 ? "error" : "" + }`, + value: fltr, + onchange: e => { + vnode.state["filterList"] = vnode.state["filterList"].slice(); + vnode.state["filterList"][idx] = e.target.value.trim(); + onChange(); + }, + oncreate: vn => { + getAutocomplete(vnode.attrs["resource"]).attach(vn.dom); + } + }); + }) + ) + ); + } + }; +}; + +export default component; diff --git a/ui/layout.ts b/ui/layout.ts new file mode 100644 index 0000000..01a9a26 --- /dev/null +++ b/ui/layout.ts @@ -0,0 +1,53 @@ +import m, { ClosureComponent, Component } from "mithril"; +import menu from "./menu"; +import drawerComponent from "./drawer-component"; +import userMenu from "./user-menu"; +import adminMenu from "./admin-menu"; +import * as overlay from "./overlay"; + +const adminPages = [ + "presets", + "provisions", + "virtualParameters", + "files", + "config", + "users", + "permissions" +]; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + let sideMenu, group; + + if (adminPages.includes(vnode.attrs["page"])) { + group = "admin"; + const attrs = {}; + attrs["page"] = vnode.attrs["page"]; + sideMenu = m(adminMenu, attrs); + } + + const attrs = {}; + attrs["page"] = group || vnode.attrs["page"]; + + return [ + m("#header", [ + m("img.logo", { src: "logo.svg" }), + m(userMenu), + m(menu, attrs), + m(drawerComponent) + ]), + m( + "#content-wrapper", + sideMenu, + m("#content", { class: `page-${vnode.attrs["page"]}` }, [ + vnode.children + ]) + ), + overlay.render() + ]; + } + }; +}; + +export default component; diff --git a/ui/login-page.ts b/ui/login-page.ts new file mode 100644 index 0000000..81ac1eb --- /dev/null +++ b/ui/login-page.ts @@ -0,0 +1,90 @@ +import { ClosureComponent, Component, Children } from "mithril"; +import { m } from "./components"; +import * as store from "./store"; +import * as notifications from "./notifications"; +import * as overlay from "./overlay"; +import changePasswordComponent from "./change-password-component"; + +export function init(args): Promise<{}> { + return Promise.resolve(args); +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + if (window.username) m.route.set(vnode.attrs["continue"] || "/"); + + document.title = "Login - GenieACS"; + return [ + m("h1", "Log in"), + m( + "form", + m( + "p", + m("label", { for: "username" }, "Username"), + m("input", { + name: "username", + type: "text", + value: vnode.state["username"], + oncreate: vnode2 => { + (vnode2.dom as HTMLInputElement).focus(); + }, + oninput: m.withAttr("value", v => (vnode.state["username"] = v)) + }) + ), + m( + "p", + m("label", { for: "password" }, "Password"), + m("input", { + name: "password", + type: "password", + value: vnode.state["password"], + oninput: m.withAttr("value", v => (vnode.state["password"] = v)) + }) + ), + m( + "p", + m( + "button.primary", + { + type: "submit", + onclick: e => { + e.target.disabled = true; + store + .logIn(vnode.state["username"], vnode.state["password"]) + .then(() => { + location.reload(); + }) + .catch(err => { + notifications.push("error", err.message); + e.target.disabled = false; + }); + return false; + } + }, + "Login" + ) + ) + ), + m( + "a", + { + onclick: () => { + const cb = (): Children => { + const attrs = { + onPasswordChange: () => { + overlay.close(cb); + m.redraw(); + } + }; + return m(changePasswordComponent, attrs); + }; + overlay.open(cb); + } + }, + "Change password" + ) + ]; + } + }; +}; diff --git a/ui/long-text-component.ts b/ui/long-text-component.ts new file mode 100644 index 0000000..8d6f0b3 --- /dev/null +++ b/ui/long-text-component.ts @@ -0,0 +1,58 @@ +import m, { ClosureComponent, Component } from "mithril"; +import * as overlay from "./overlay"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const text = vnode.attrs["text"]; + const element = vnode.attrs["element"] || "span"; + + function overflowed(_vnode): void { + if (_vnode.dom.classList.contains("long-text-overflowed")) return; + _vnode.dom.classList.add("long-text-overflowed"); + _vnode.dom.onclick = e => { + overlay.open(() => { + return m("textarea.long-text", { + value: text, + cols: 80, + rows: 24, + readonly: "", + oncreate: vnode2 => { + (vnode2.dom as HTMLTextAreaElement).focus(); + (vnode2.dom as HTMLTextAreaElement).select(); + } + }); + }); + // prevent index page selection + e.stopPropagation(); + m.redraw(); + }; + } + + return m( + element, + { + oncreate: vnode2 => { + if (vnode2.dom.clientWidth !== vnode2.dom.scrollWidth) + overflowed(vnode2); + }, + onupdate: vnode2 => { + if (vnode2.dom.clientWidth === vnode2.dom.scrollWidth) { + (vnode2.dom as HTMLElement).classList.remove( + "long-text-overflowed" + ); + (vnode2.dom as HTMLElement).onclick = null; + } else { + overflowed(vnode2); + } + }, + class: "long-text", + title: text + }, + text + ); + } + }; +}; + +export default component; diff --git a/ui/menu.ts b/ui/menu.ts new file mode 100644 index 0000000..a5a94b0 --- /dev/null +++ b/ui/menu.ts @@ -0,0 +1,63 @@ +import m, { ClosureComponent, Component } from "mithril"; + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const active = { [vnode.attrs["page"]]: "active" }; + + const tabs = []; + if (window.authorizer.hasAccess("devices", 1)) { + tabs.push( + m( + "li", + { class: active["overview"] }, + m("a", { href: "#!/overview" }, "Overview") + ) + ); + } + + if (window.authorizer.hasAccess("devices", 2)) { + tabs.push( + m( + "li", + { class: active["devices"] }, + m("a", { href: "#!/devices" }, "Devices") + ) + ); + } + + if (window.authorizer.hasAccess("faults", 2)) { + tabs.push( + m( + "li", + { class: active["faults"] }, + m("a", { href: "#!/faults" }, "Faults") + ) + ); + } + + const adminPages = [ + "presets", + "provisions", + "virtualParameters", + "files" + ]; + for (const page of adminPages) { + if (window.authorizer.hasAccess(page, 2)) { + tabs.push( + m( + "li", + { class: active["admin"] }, + m("a", { href: "#!/admin" }, "Admin") + ) + ); + break; + } + } + + return m("nav", m("ul", tabs)); + } + }; +}; + +export default component; diff --git a/ui/notifications.ts b/ui/notifications.ts new file mode 100644 index 0000000..06dc2d5 --- /dev/null +++ b/ui/notifications.ts @@ -0,0 +1,23 @@ +import m from "mithril"; + +interface Notification { + type: string; + message: string; + timestamp: number; +} + +const notifications = new Set(); + +export function push(type, message): void { + const n = { type: type, message: message, timestamp: Date.now() }; + notifications.add(n); + m.redraw(); + setTimeout(() => { + notifications.delete(n); + m.redraw(); + }, 4000); +} + +export function getNotifications(): Set { + return notifications; +} diff --git a/ui/overlay.ts b/ui/overlay.ts new file mode 100644 index 0000000..eab65ba --- /dev/null +++ b/ui/overlay.ts @@ -0,0 +1,64 @@ +import m, { Children } from "mithril"; + +type OverlayCallback = () => Children; + +let overlayCallback: OverlayCallback = null; + +export function open(callback: OverlayCallback): void { + overlayCallback = callback; +} + +export function close(callback: OverlayCallback): boolean { + if (callback === overlayCallback) { + overlayCallback = null; + return true; + } + + return false; +} + +export function render(): Children { + if (overlayCallback) { + return m( + ".overlay-wrapper", + { + tabindex: 0, + onclick: () => { + close(overlayCallback); + }, + style: "opacity: 0", + oncreate: vnode => { + (vnode.dom as HTMLDivElement).focus(); + (vnode.dom as HTMLDivElement).style.opacity = "1"; + }, + onbeforeremove: vnode => { + (vnode.dom as HTMLDivElement).style.opacity = "0"; + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 500); + }); + } + }, + m( + ".overlay", + { + onclick: e => { + e.stopPropagation(); + } + }, + overlayCallback() + ) + ); + } + + return null; +} + +document.addEventListener("keydown", e => { + if (overlayCallback && e.keyCode === 27 && close(overlayCallback)) m.redraw(); +}); + +window.addEventListener("popstate", () => { + if (close(overlayCallback)) m.redraw(); +}); diff --git a/ui/overview-page.ts b/ui/overview-page.ts new file mode 100644 index 0000000..7cfa6fb --- /dev/null +++ b/ui/overview-page.ts @@ -0,0 +1,65 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "./components"; +import config from "./config"; +import * as store from "./store"; +import pieChartComponent from "./pie-chart-component"; + +const GROUPS = config.ui.overview.groups; +const CHARTS = {}; +for (const group of Object.values(GROUPS)) { + for (const chartName of Object.values(group["charts"]) as string[]) + CHARTS[chartName] = config.ui.overview.charts[chartName]; +} + +function queryCharts(charts): {} { + charts = Object.assign({}, charts); + for (let [chartName, chart] of Object.entries(charts)) { + charts[chartName] = chart = Object.assign({}, chart); + chart["slices"] = Object.assign({}, chart["slices"]); + for (let [sliceName, slice] of Object.entries(chart["slices"])) { + const filter = slice["filter"]; + chart["slices"][sliceName] = slice = Object.assign({}, slice); + slice["count"] = store.count("devices", filter); + } + } + return charts; +} + +export function init(): Promise<{}> { + if (!window.authorizer.hasAccess("devices", 1)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + + return Promise.resolve({ charts: queryCharts(CHARTS) }); +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Overview - GenieACS"; + const children = []; + for (const group of Object.values(GROUPS)) { + if (group["label"]) children.push(m("h1", group["label"])); + + const groupChildren = []; + for (const chartName of Object.values(group["charts"]) as string[]) { + const chart = vnode.attrs["charts"][chartName]; + const chartChildren = []; + if (chart.label) chartChildren.push(m("h2", chart.label)); + + const attrs = {}; + attrs["chart"] = chart; + chartChildren.push(m(pieChartComponent, attrs)); + + groupChildren.push(m(".overview-chart", chartChildren)); + } + + children.push(m(".overview-chart-group", groupChildren)); + } + + return children; + } + }; +}; diff --git a/ui/permissions-page.ts b/ui/permissions-page.ts new file mode 100644 index 0000000..9ca59c1 --- /dev/null +++ b/ui/permissions-page.ts @@ -0,0 +1,465 @@ +"use strict"; + +import { m } from "./components"; + +import config from "./config"; +import * as store from "./store"; +import * as notifications from "./notifications"; +import memoize from "../lib/common/memoize"; +import putFormComponent from "./put-form-component"; +import * as overlay from "./overlay"; +import * as smartQuery from "./smart-query"; +import { validators } from "../lib/common/authorizer"; +import { map, parse, stringify } from "../lib/common/expression-parser"; +import filterComponent from "./filter-component"; +import { Children, ClosureComponent, Component } from "mithril"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(parse); +const memoizedJsonParse = memoize(JSON.parse); + +const attributes = [ + { id: "role", label: "Role" }, + { + id: "resource", + label: "Resource", + type: "combo", + options: [ + "config", + "devices", + "faults", + "files", + "permissions", + "presets", + "provisions", + "virtualParameters" + ] + }, + { id: "filter", label: "Filter", unsortable: true }, + { + id: "access", + label: "Access", + type: "combo", + options: ["1: count", "2: read", "3: write"] + }, + { + id: "validate", + label: "Validators", + type: "multi", + options: Object.keys(validators), + unsortable: true + } +]; + +const unpackSmartQuery = memoize(query => { + return map(query, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack("permissions", e[2], e[3]); + return e; + }); +}); + +interface ValidationErrors { + [prop: string]: string; +} + +function putActionHandler(action, _object): Promise { + return new Promise((resolve, reject) => { + const object = Object.assign({}, _object); + if (action === "save") { + if (!object.role) return void resolve({ role: "Role can not be empty" }); + if (!object.resource) + return void resolve({ resource: "Resource can not be empty" }); + if (!object.access) + return void resolve({ access: "Access can not be empty" }); + + if (object.access === "3: write") object.access = 3; + else if (object.access === "2: read") object.access = 2; + else if (object.access === "1: count") object.access = 1; + else return void resolve({ access: "Invalid access level" }); + + object.filter = object.filter || ""; + try { + memoizedParse(object.filter); + } catch (err) { + return void resolve({ + filter: "Filter must be valid expression" + }); + } + + object.validate = (object.validate || []).join(","); + + const id = `${object.role}:${object.resource}:${object.access}`; + + store + .resourceExists("permissions", id) + .then(exists => { + if (exists) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "Permission already exists" }); + } + + store + .putResource("permissions", id, object) + .then(() => { + notifications.push("success", "Permission created"); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(reject); + }) + .catch(reject); + } else if (action === "delete") { + store + .deleteResource("permissions", object["_id"]) + .then(() => { + notifications.push("success", "Permission deleted"); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(err => { + store.fulfill(0, Date.now()); + reject(err); + }); + } else { + reject(new Error("Undefined action")); + } + }); +} + +const formData = { + resource: "permissions", + attributes: attributes +}; + +const getDownloadUrl = memoize(filter => { + const cols = {}; + for (const attr of attributes) cols[attr.label] = attr.id; + return `/api/permissions.csv?${m.buildQueryString({ + filter: stringify(filter), + columns: JSON.stringify(cols) + })}`; +}); + +export function init(args): Promise<{}> { + if (!window.authorizer.hasAccess("permissions", 2)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + const sort = args.sort; + const filter = args.filter; + return Promise.resolve({ filter, sort }); +} + +function renderTable( + permissionsResponse, + total, + selected, + showMoreCallback, + downloadUrl, + sort, + onSortChange +): Children { + const permissions = permissionsResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: permissions.length && selected.size === permissions.length, + onchange: e => { + for (const permission of permissions) { + if (e.target.checked) selected.add(permission["_id"]); + else selected.delete(permission["_id"]); + } + }, + disabled: !total + }); + + const labels = [m("th", selectAll)]; + for (const attr of attributes) { + const label = attr.label; + + if (attr.unsortable) { + labels.push(m("th", label)); + continue; + } + + let direction = 1; + + let symbol = "\u2981"; + if (sort[attr.id] > 0) symbol = "\u2bc6"; + else if (sort[attr.id] < 0) symbol = "\u2bc5"; + + const sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } + + const rows = []; + for (const permission of permissions) { + const checkbox = m("input", { + type: "checkbox", + checked: selected.has(permission["_id"]), + onchange: e => { + if (e.target.checked) selected.add(permission["_id"]); + else selected.delete(permission["_id"]); + }, + onclick: e => { + e.stopPropagation(); + e.redraw = false; + } + }); + + const tds = [m("td", checkbox)]; + for (const attr of attributes) { + let val = permission[attr.id]; + if (attr.id === "access") { + if (val === 1) val = "1: count"; + else if (val === 2) val = "2: read"; + else if (val === 3) val = "3: write"; + } + tds.push(m("td", val)); + } + + tds.push( + m( + "td.table-row-links", + m( + "button", + { + title: "Delete permission", + onclick: () => { + if ( + !confirm(`Deleting ${permission._id} permission. Are you sure?`) + ) + return; + + putActionHandler("delete", permission).catch(err => { + throw err; + }); + } + }, + "✕" + ) + ) + ); + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(permission["_id"])) + selected.add(permission["_id"]); + } + }, + tds + ) + ); + } + + if (!rows.length) { + rows.push( + m("tr.empty", m("td", { colspan: labels.length }, "No permissions")) + ); + } + + const footerElements = []; + if (total != null) footerElements.push(`${permissions.length}/${total}`); + else footerElements.push(`${permissions.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more permissions", + onclick: showMoreCallback, + disabled: permissions.length >= total || !permissionsResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) { + footerElements.push( + m("a.download-csv", { href: downloadUrl, download: "" }, "Download") + ); + } + + const tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + const buttons = [ + m( + "button.primary", + { + title: "Delete selected permissions", + disabled: !selected.size, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + Promise.all( + Array.from(selected).map(id => + store.deleteResource("permissions", id) + ) + ) + .then(res => { + notifications.push( + "success", + `${res.length} permissions deleted` + ); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + store.fulfill(0, Date.now()); + }); + } + }, + "Delete" + ) + ]; + + if (window.authorizer.hasAccess("permissions", 3)) { + buttons.push( + m( + "button.primary", + { + title: "Create new permission", + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "New" + ) + ); + } + + return [ + m( + "table.table.highlight", + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ), + m("div.actions-bar", buttons) + ]; +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Permissions - GenieACS"; + + function showMore(): void { + vnode.state["showCount"] = + (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter): void { + const ops = { filter }; + if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort): void { + const ops = { sort }; + if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs["sort"] + ? memoizedJsonParse(vnode.attrs["sort"]) + : {}; + let filter = vnode.attrs["filter"] + ? memoizedParse(vnode.attrs["filter"]) + : true; + filter = unpackSmartQuery(filter); + + const permissions = store.fetch("permissions", filter, { + limit: vnode.state["showCount"] || PAGE_SIZE, + sort: sort + }); + + const count = store.count("permissions", filter); + + const selected = new Set(); + if (vnode.state["selected"]) { + for (const permission of permissions.value) { + if (vnode.state["selected"].has(permission["_id"])) + selected.add(permission["_id"]); + } + } + vnode.state["selected"] = selected; + + const downloadUrl = getDownloadUrl(filter); + + const attrs = {}; + attrs["resource"] = "permissions"; + attrs["filter"] = vnode.attrs["filter"]; + attrs["onChange"] = onFilterChanged; + + return [ + m("h1", "Listing permissions"), + m(filterComponent, attrs), + renderTable( + permissions, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; + } + }; +}; diff --git a/ui/pie-chart-component.ts b/ui/pie-chart-component.ts new file mode 100644 index 0000000..7fc91a9 --- /dev/null +++ b/ui/pie-chart-component.ts @@ -0,0 +1,125 @@ +import { ClosureComponent, Component, Children } from "mithril"; +import { m } from "./components"; +import { stringify } from "../lib/common/expression-parser"; +import memoize from "../lib/common/memoize"; + +const memoizedStringify = memoize(stringify); + +function drawChart(chartData): Children { + const slices = chartData.slices; + const total: number = Array.from(Object.values(chartData.slices)).reduce( + (a: number, s) => a + (s["count"]["value"] || 0), + 0 + ); + const legend = []; + const paths = []; + const links = []; + let currentProgressPercentage = 0; + let startX = Math.cos(2 * Math.PI * currentProgressPercentage) * 100; + let startY = Math.sin(2 * Math.PI * currentProgressPercentage) * 100; + let endX, endY; + + for (const slice of Object.values(slices)) { + const percent = total > 0 ? (slice["count"]["value"] || 0) / total : 0; + legend.push( + m(".legend-line", [ + m("span.color", { + style: `background-color: ${slice["color"]} !important;` + }), + `${slice["label"]}: `, + m( + "a", + { + href: `/#!/devices/?${m.buildQueryString({ + filter: memoizedStringify(slice["filter"]) + })}` + }, + slice["count"]["value"] || 0 + ), + ` (${(percent * 100).toFixed(2)}%)` + ]) + ); + + if (percent > 0) { + currentProgressPercentage += percent; + endX = Math.cos(2 * Math.PI * currentProgressPercentage) * 100; + endY = Math.sin(2 * Math.PI * currentProgressPercentage) * 100; + const isBigArc = percent > 0.5 ? 1 : 0; + + const sketch = + `M ${startX} ${startY} ` + // Move to the starting point + `A 100 100 0 ${isBigArc} 1 ${endX} ${endY} ` + // Draw an Arc from starting point to ending point + `L 0 0 z`; // complete the shape by drawing a line to the center of circle + + startX = endX; + startY = endY; + + paths.push( + m("path", { + d: sketch, + fill: slice["color"] + }) + ); + + const percentageX = + Math.cos(2 * Math.PI * (currentProgressPercentage - percent / 2)) * 50; + const percentageY = + Math.sin(2 * Math.PI * (currentProgressPercentage - percent / 2)) * 50; + + links.push( + m( + "a", + { + "xlink:href": `/#!/devices/?${m.buildQueryString({ + filter: memoizedStringify(slice["filter"]) + })}` + }, + [ + m("path", { + d: sketch, + "fill-opacity": 0 + }), + m( + "text", + { + x: percentageX, + y: percentageY, + "dominant-baseline": "middle", + "text-anchor": "middle" + }, + `${(percent * 100).toFixed(2)}%` + ) + ] + ) + ); + } + } + + legend.push(m("span.legend-total", `Total: ${total}`)); + + return m("div", { class: "pie-chart" }, [ + m( + "svg", + { + // Adding 2 as padding; strokes must not be more than 2 + viewBox: "-102 -102 204 204", + width: "204px", + height: "204px", + xmlns: "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink" + }, + paths.concat(links) + ), + m(".legend", legend) + ]); +} + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + return drawChart(vnode.attrs["chart"]); + } + }; +}; + +export default component; diff --git a/ui/presets-page.ts b/ui/presets-page.ts new file mode 100644 index 0000000..d075616 --- /dev/null +++ b/ui/presets-page.ts @@ -0,0 +1,481 @@ +import { ClosureComponent, Component, Children } from "mithril"; +import { m } from "./components"; +import config from "./config"; +import filterComponent from "./filter-component"; +import * as overlay from "./overlay"; +import * as store from "./store"; +import * as notifications from "./notifications"; +import putFormComponent from "./put-form-component"; +import memoize from "../lib/common/memoize"; +import * as smartQuery from "./smart-query"; +import { map, parse, stringify } from "../lib/common/expression-parser"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(parse); +const memoizedJsonParse = memoize(JSON.parse); + +const attributes = [ + { id: "_id", label: "Name" }, + { id: "channel", label: "Channel" }, + { id: "weight", label: "Weight" }, + { id: "schedule", label: "Schedule" }, + { id: "events", label: "Events", unsortable: true }, + { id: "precondition", label: "Precondition", unsortable: true }, + { id: "provision", label: "Provision", type: "combo", unsortable: true }, + { id: "provisionArgs", label: "Arguments", unsortable: true } +]; + +const unpackSmartQuery = memoize(query => { + return map(query, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack("presets", e[2], e[3]); + return e; + }); +}); + +interface ValidationErrors { + [prop: string]: string; +} + +function putActionHandler(action, _object, isNew): Promise { + return new Promise((resolve, reject) => { + const object = Object.assign({}, _object); + if (action === "save") { + const id = object["_id"]; + delete object["_id"]; + + const errors = {}; + + if (!id) errors["_id"] = "ID can not be empty"; + if (!object.provision) errors["provision"] = "Provision not selected"; + + if (Object.keys(errors).length) return void resolve(errors); + + store + .resourceExists("presets", id) + .then(exists => { + if (exists && isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "Preset already exists" }); + } + + if (!exists && !isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "Preset does not exist" }); + } + + store + .putResource("presets", id, object) + .then(() => { + notifications.push( + "success", + `Preset ${exists ? "updated" : "created"}` + ); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(reject); + }) + .catch(reject); + } else if (action === "delete") { + store + .deleteResource("presets", object["_id"]) + .then(() => { + notifications.push("success", "Preset deleted"); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(err => { + reject(err); + store.fulfill(0, Date.now()); + }); + } else { + reject(new Error("Undefined action")); + } + }); +} + +const formData = { + resource: "presets", + attributes: attributes +}; + +const getDownloadUrl = memoize(filter => { + const cols = {}; + for (const attr of attributes) cols[attr.label] = attr.id; + return `/api/presets.csv?${m.buildQueryString({ + filter: stringify(filter), + columns: JSON.stringify(cols) + })}`; +}); + +export function init(args): Promise<{}> { + if (!window.authorizer.hasAccess("presets", 2)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + + const sort = args.sort; + const filter = args.filter; + return Promise.resolve({ filter, sort }); +} + +function renderTable( + presetsResponse, + total, + selected, + showMoreCallback, + downloadUrl, + sort, + onSortChange +): Children { + const presets = presetsResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: presets.length && selected.size === presets.length, + onchange: e => { + for (const preset of presets) { + if (e.target.checked) selected.add(preset["_id"]); + else selected.delete(preset["_id"]); + } + }, + disabled: !total + }); + + const labels = [m("th", selectAll)]; + + for (const attr of attributes) { + const label = attr.label; + + if (attr.unsortable) { + labels.push(m("th", label)); + continue; + } + + let direction = 1; + + let symbol = "\u2981"; + if (sort[attr.id] > 0) symbol = "\u2bc6"; + else if (sort[attr.id] < 0) symbol = "\u2bc5"; + + const sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } + + const rows = []; + for (const preset of presets) { + const checkbox = m("input", { + type: "checkbox", + checked: selected.has(preset["_id"]), + onchange: e => { + if (e.target.checked) selected.add(preset["_id"]); + else selected.delete(preset["_id"]); + }, + onclick: e => { + e.stopPropagation(); + e.redraw = false; + } + }); + + let devicesUrl = "/#!/devices"; + if (preset["precondition"].length) { + devicesUrl += `?${m.buildQueryString({ + filter: preset["precondition"] + })}`; + } + + const tds = [m("td", checkbox)]; + for (const attr of attributes) { + if (attr.id === "precondition") { + tds.push( + m( + "td", + { title: preset[attr.id] }, + m("a", { href: devicesUrl }, preset[attr.id]) + ) + ); + } else { + tds.push(m("td", preset[attr.id])); + } + } + + tds.push( + m( + "td.table-row-links", + m( + "a", + { + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + base: preset, + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, false) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "Show" + ) + ) + ); + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(preset["_id"])) selected.add(preset["_id"]); + } + }, + tds + ) + ); + } + + if (!rows.length) + rows.push(m("tr.empty", m("td", { colspan: labels.length }, "No presets"))); + + const footerElements = []; + if (total != null) footerElements.push(`${presets.length}/${total}`); + else footerElements.push(`${presets.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more presets", + onclick: showMoreCallback, + disabled: presets.length >= total || !presetsResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) { + footerElements.push( + m("a.download-csv", { href: downloadUrl, download: "" }, "Download") + ); + } + + const tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + const buttons = [ + m( + "button.primary", + { + title: "Delete selected presets", + disabled: !selected.size, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + Promise.all( + Array.from(selected).map(id => store.deleteResource("presets", id)) + ) + .then(res => { + notifications.push("success", `${res.length} presets deleted`); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + store.fulfill(0, Date.now()); + }); + } + }, + "Delete" + ) + ]; + + if (window.authorizer.hasAccess("presets", 3)) { + buttons.push( + m( + "button.primary", + { + title: "Create new preset", + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, true) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "New" + ) + ); + } + + return [ + m( + "table.table.highlight", + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ), + m("div.actions-bar", buttons) + ]; +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Presets - GenieACS"; + + function showMore(): void { + vnode.state["showCount"] = + (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter): void { + const ops = { filter }; + if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort): void { + const ops = { sort }; + if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs["sort"] + ? memoizedJsonParse(vnode.attrs["sort"]) + : {}; + let filter = vnode.attrs["filter"] + ? memoizedParse(vnode.attrs["filter"]) + : true; + filter = unpackSmartQuery(filter); + + const presets = store.fetch("presets", filter, { + limit: vnode.state["showCount"] || PAGE_SIZE, + sort: sort + }); + const count = store.count("presets", filter); + + const provisions = store.fetch("provisions", true); + if (provisions.fulfilled) { + const provisionAttr = attributes.find(attr => { + return attr.id === "provision"; + }); + + const provisionIds = new Set([ + "refresh", + "value", + "tag", + "reboot", + "reset", + "download", + "instances" + ]); + + for (const p of provisions.value) provisionIds.add(p["_id"]); + + provisionAttr["options"] = Array.from(provisionIds); + } + + const selected = new Set(); + if (vnode.state["selected"]) { + for (const preset of presets.value) { + if (vnode.state["selected"].has(preset["_id"])) + selected.add(preset["_id"]); + } + } + vnode.state["selected"] = selected; + + const downloadUrl = getDownloadUrl(filter); + + const attrs = {}; + attrs["resource"] = "presets"; + attrs["filter"] = vnode.attrs["filter"]; + attrs["onChange"] = onFilterChanged; + + return [ + m("h1", "Listing presets"), + m(filterComponent, attrs), + renderTable( + presets, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; + } + }; +}; diff --git a/ui/provisions-page.ts b/ui/provisions-page.ts new file mode 100644 index 0000000..b585591 --- /dev/null +++ b/ui/provisions-page.ts @@ -0,0 +1,449 @@ +import { ClosureComponent, Component, Children } from "mithril"; +import { m } from "./components"; +import config from "./config"; +import filterComponent from "./filter-component"; +import * as store from "./store"; +import * as notifications from "./notifications"; +import memoize from "../lib/common/memoize"; +import putFormComponent from "./put-form-component"; +import * as overlay from "./overlay"; +import * as smartQuery from "./smart-query"; +import { map, parse, stringify } from "../lib/common/expression-parser"; +import { loadCodeMirror } from "./dynamic-loader"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(parse); +const memoizedJsonParse = memoize(JSON.parse); + +const attributes = [ + { id: "_id", label: "Name" }, + { id: "script", label: "Script", type: "code" } +]; + +const unpackSmartQuery = memoize(query => { + return map(query, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack("provisions", e[2], e[3]); + return e; + }); +}); + +interface ValidationErrors { + [prop: string]: string; +} + +function putActionHandler(action, _object, isNew): Promise { + return new Promise((resolve, reject) => { + const object = Object.assign({}, _object); + if (action === "save") { + const id = object["_id"]; + delete object["_id"]; + + if (!id) return void resolve({ _id: "ID can not be empty" }); + + store + .resourceExists("provisions", id) + .then(exists => { + if (exists && isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "Provision already exists" }); + } + + if (!exists && !isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "Provision does not exist" }); + } + + store + .putResource("provisions", id, object) + .then(() => { + notifications.push( + "success", + `Provision ${exists ? "updated" : "created"}` + ); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(reject); + }) + .catch(reject); + } else if (action === "delete") { + store + .deleteResource("provisions", object["_id"]) + .then(() => { + notifications.push("success", "Provision deleted"); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(err => { + store.fulfill(0, Date.now()); + reject(err); + }); + } else { + reject(new Error("Undefined action")); + } + }); +} + +const formData = { + resource: "provisions", + attributes: attributes +}; + +const getDownloadUrl = memoize(filter => { + const cols = {}; + for (const attr of attributes) cols[attr.label] = attr.id; + return `/api/provisions.csv?${m.buildQueryString({ + filter: stringify(filter), + columns: JSON.stringify(cols) + })}`; +}); + +export function init(args): Promise<{}> { + if (!window.authorizer.hasAccess("provisions", 2)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + + const sort = args.sort; + const filter = args.filter; + + return new Promise((resolve, reject) => { + loadCodeMirror() + .then(() => { + resolve({ filter, sort }); + }) + .catch(reject); + }); +} + +function renderTable( + provisionsResponse, + total, + selected, + showMoreCallback, + downloadUrl, + sort, + onSortChange +): Children { + const provisions = provisionsResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: provisions.length && selected.size === provisions.length, + onchange: e => { + for (const provision of provisions) { + if (e.target.checked) selected.add(provision["_id"]); + else selected.delete(provision["_id"]); + } + }, + disabled: !total + }); + + const labels = [m("th", selectAll)]; + for (const attr of attributes) { + const label = attr.label; + + let direction = 1; + + let symbol = "\u2981"; + if (sort[attr.id] > 0) symbol = "\u2bc6"; + else if (sort[attr.id] < 0) symbol = "\u2bc5"; + + const sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } + + const rows = []; + for (const provision of provisions) { + const checkbox = m("input", { + type: "checkbox", + checked: selected.has(provision["_id"]), + onchange: e => { + if (e.target.checked) selected.add(provision["_id"]); + else selected.delete(provision["_id"]); + }, + onclick: e => { + e.stopPropagation(); + e.redraw = false; + } + }); + + const tds = [m("td", checkbox)]; + for (const attr of attributes) { + if (attr.id === "script") { + const firstLines = provision[attr.id].split("\n", 11); + if (firstLines.length > 10) firstLines[10] = ["\ufe19"]; + tds.push( + m("td", { title: firstLines.join("\n") }, firstLines[0] || "") + ); + } else { + tds.push(m("td", provision[attr.id])); + } + } + + tds.push( + m( + "td.table-row-links", + m( + "a", + { + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + base: provision, + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, false) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "Show" + ) + ) + ); + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(provision["_id"])) + selected.add(provision["_id"]); + } + }, + tds + ) + ); + } + + if (!rows.length) { + rows.push( + m("tr.empty", m("td", { colspan: labels.length }, "No provisions")) + ); + } + + const footerElements = []; + if (total != null) footerElements.push(`${provisions.length}/${total}`); + else footerElements.push(`${provisions.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more provisions", + onclick: showMoreCallback, + disabled: provisions.length >= total || !provisionsResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) { + footerElements.push( + m("a.download-csv", { href: downloadUrl, download: "" }, "Download") + ); + } + + const tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + const buttons = [ + m( + "button.primary", + { + title: "Delete selected provisions", + disabled: !selected.size, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + Promise.all( + Array.from(selected).map(id => + store.deleteResource("provisions", id) + ) + ) + .then(res => { + notifications.push("success", `${res.length} provisions deleted`); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + store.fulfill(0, Date.now()); + }); + } + }, + "Delete" + ) + ]; + + if (window.authorizer.hasAccess("provisions", 3)) { + buttons.push( + m( + "button.primary", + { + title: "Create new provision", + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, true) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "New" + ) + ); + } + + return [ + m( + "table.table.highlight", + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ), + m("div.actions-bar", buttons) + ]; +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Provisions - GenieACS"; + + function showMore(): void { + vnode.state["showCount"] = + (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter): void { + const ops = { filter }; + if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort): void { + const ops = { sort }; + if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs["sort"] + ? memoizedJsonParse(vnode.attrs["sort"]) + : {}; + let filter = vnode.attrs["filter"] + ? memoizedParse(vnode.attrs["filter"]) + : true; + filter = unpackSmartQuery(filter); + + const provisions = store.fetch("provisions", filter, { + limit: vnode.state["showCount"] || PAGE_SIZE, + sort: sort + }); + + const count = store.count("provisions", filter); + + const selected = new Set(); + if (vnode.state["selected"]) { + for (const provision of provisions.value) { + if (vnode.state["selected"].has(provision["_id"])) + selected.add(provision["_id"]); + } + } + vnode.state["selected"] = selected; + + const downloadUrl = getDownloadUrl(filter); + + const attrs = {}; + attrs["resource"] = "provisions"; + attrs["filter"] = vnode.attrs["filter"]; + attrs["onChange"] = onFilterChanged; + + return [ + m("h1", "Listing provisions"), + m(filterComponent, attrs), + renderTable( + provisions, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; + } + }; +}; diff --git a/ui/put-form-component.ts b/ui/put-form-component.ts new file mode 100644 index 0000000..bc8975e --- /dev/null +++ b/ui/put-form-component.ts @@ -0,0 +1,200 @@ +import { VnodeDOM, ClosureComponent, Component, Children } from "mithril"; +import { m } from "./components"; +import codeEditorComponent from "./code-editor-component"; + +const singular = { + presets: "preset", + provisions: "provision", + virtualParameters: "virtual parameter", + files: "file", + users: "user" +}; + +function createField(current, attr, focus): Children { + if (attr.type === "combo") { + const options = [m("option", { value: "" }, "--Please choose--")]; + for (const op of attr.options) options.push(m("option", { value: op }, op)); + + let selected = ""; + if (attr.options.includes(current.object[attr.id])) + selected = current.object[attr.id]; + return m( + "select", + { + name: attr.id, + value: selected, + oncreate: focus + ? _vnode => { + (_vnode.dom as HTMLSelectElement).focus(); + } + : null, + onchange: e => { + current.object[attr.id] = e.target.value; + e.redraw = false; + } + }, + options + ); + } else if (attr.type === "multi") { + const currentSelected = new Set(current.object[attr.id]); + const options = attr.options.map(op => { + const id = `${attr.id}-${op}`; + const opts = { + type: "checkbox", + id: id, + value: op, + oncreate: _vnode => { + if (focus && !options.length) _vnode.dom.focus(); + if (currentSelected.has(op)) _vnode.dom.checked = true; + }, + onchange: e => { + if (e.target.checked) currentSelected.add(op); + else currentSelected.delete(op); + current.object[attr.id] = Array.from(currentSelected); + e.redraw = false; + } + }; + + return m("tr", [m("td", m("input", opts)), m("td", op)]); + }); + + return m("table", options); + } else if (attr.type === "code") { + const attrs = { + id: attr.id, + value: current.object[attr.id], + mode: "javascript", + onSubmit: dom => { + dom.form.querySelector("button[type=submit]").click(); + }, + onChange: value => { + current.object[attr.id] = value; + } + }; + return m(codeEditorComponent, attrs); + } else if (attr.type === "file") { + return m("input", { + type: "file", + name: attr.id, + oncreate: focus + ? _vnode => { + (_vnode.dom as HTMLInputElement).focus(); + } + : null, + onchange: e => { + current.object[attr.id] = e.target.files; + e.redraw = false; + } + }); + } + + return m("input", { + type: attr.type === "password" ? "password" : "text", + name: attr.id, + disabled: attr.id === "_id" && !current.isNew, + value: current.object[attr.id], + oncreate: focus + ? _vnode => { + (_vnode.dom as HTMLInputElement).focus(); + } + : null, + oninput: e => { + current.object[attr.id] = e.target.value; + e.redraw = false; + } + }); +} + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const actionHandler = vnode.attrs["actionHandler"]; + const attributes = vnode.attrs["attributes"]; + const resource = vnode.attrs["resource"]; + const base = vnode.attrs["base"] || {}; + if (!vnode.state["current"]) { + vnode.state["current"] = { + isNew: !base["_id"], + object: Object.assign({}, base) + }; + } + + const current = vnode.state["current"]; + + const form = []; + let focused = false; + for (const attr of attributes) { + let focus = false; + if (!focused && (current.isNew || attr.id !== "_id")) + focus = focused = true; + + form.push( + m( + "p", + m("label", { for: attr.id }, attr.label || attr.id), + m("br"), + createField(current, attr, focus) + ) + ); + } + + const submit = m( + "button.primary", + { type: "submit" }, + "Save" + ) as VnodeDOM; + const buttons = [submit]; + + if (!current.isNew) { + buttons.push(m( + "button.primary", + { + type: "button", + title: `Delete ${singular[resource] || resource}`, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + actionHandler("delete", current.object).then(() => { + e.target.disabled = false; + }); + } + }, + "Delete" + ) as VnodeDOM); + } + + form.push(m(".actions-bar", buttons)); + + const children = [ + m( + "h1", + `${current.isNew ? "New" : "Editing"} ${singular[resource] || + resource}` + ), + m( + "form", + { + onsubmit: e => { + e.redraw = false; + // const onsubmit = e.target.onsubmit; + e.preventDefault(); + // e.target.onsubmit = null; + (submit.dom as HTMLFormElement).disabled = true; + // submit.dom.textContent = "Loading ..."; + actionHandler("save", current.object).then(() => { + // submit.dom.textContent = "Save"; + // e.target.onsubmit = onsubmit; + (submit.dom as HTMLFormElement).disabled = false; + }); + } + }, + form + ) + ]; + + return m("div.put-form", children); + } + }; +}; + +export default component; diff --git a/ui/smart-query.ts b/ui/smart-query.ts new file mode 100644 index 0000000..a3eb448 --- /dev/null +++ b/ui/smart-query.ts @@ -0,0 +1,145 @@ +import config from "./config"; +import { Expression } from "../lib/types"; + +const resources = { + devices: {}, + faults: { + Device: { parameter: ["PARAM", "device"], type: "string" }, + Channel: { parameter: ["PARAM", "channel"], type: "string" }, + Code: { parameter: ["PARAM", "code"], type: "string" }, + Retries: { parameter: ["PARAM", "retries"], type: "number" }, + Timestamp: { parameter: ["PARAM", "timestamp"], type: "timestamp" } + }, + presets: { + ID: { parameter: ["PARAM", "_id"], type: "string" }, + Channel: { parameter: ["PARAM", "channel"], type: "string" }, + Weight: { parameter: ["PARAM", "weight"], type: "number" } + }, + provisions: { + ID: { parameter: ["PARAM", "_id"], type: "string" } + }, + virtualParameters: { + ID: { parameter: ["PARAM", "_id"], type: "string" } + }, + files: { + ID: { parameter: ["PARAM", "_id"], type: "string" }, + Type: { parameter: ["PARAM", "metadata.fileType"], type: "string" }, + OUI: { parameter: ["PARAM", "metadata.oui"], type: "string" }, + "Product class": { + parameter: ["PARAM", "metadata.productClass"], + type: "string" + }, + Version: { parameter: ["PARAM", "metadata.version"], type: "string" } + }, + permissions: { + Role: { parameter: ["PARAM", "role"], type: "string" }, + Resource: { parameter: ["PARAM", "resource"], type: "string" }, + Access: { parameter: ["PARAM", "access"], type: "number" } + }, + users: { Username: { parameter: ["PARAM", "_id"], type: "string" } } +}; + +for (const v of Object.values(config.ui.filters as { + label: string; + parameter: string; + type: string; +}[])) { + resources.devices[v.label] = { + parameter: v.parameter, + type: (v.type || "").split(",").map(s => s.trim()) + }; +} + +export function getLabels(resource): string[] { + if (!resources[resource]) return []; + return Object.keys(resources[resource]); +} + +function queryNumber(param, value): Expression { + let op = "="; + for (const o of ["<>", "=", "<=", "<", ">=", ">"]) { + if (value.startsWith(o)) { + op = o; + value = value.slice(o.length).trim(); + break; + } + } + + const v = parseInt(value); + if (v !== +value) return null; + + return [op, param, v]; +} + +function queryTimestamp(param, value): Expression { + let op = "="; + for (const o of ["<>", "=", "<=", "<", ">=", ">"]) { + if (value.startsWith(o)) { + op = o; + value = value.slice(o.length).trim(); + break; + } + } + + let v = parseInt(value); + if (v !== +value) v = Date.parse(value); + if (isNaN(v)) return null; + return [op, param, v]; +} + +function queryString(param, value): Expression { + return ["LIKE", ["FUNC", "LOWER", param], value.toLowerCase()]; +} + +function queryMac(param, value): Expression { + value = value.replace(/[^a-f0-9]/gi, "").toLowerCase(); + if (!value) return null; + if (value.length === 12) { + return [ + "LIKE", + ["FUNC", "LOWER", param], + value.replace(/(..)(?!$)/g, "$1:") + ]; + } + + return [ + "OR", + [ + "LIKE", + ["FUNC", "LOWER", param], + `%${value.replace(/(..)(?!$)/g, "$1:")}%` + ], + ["LIKE", ["FUNC", "LOWER", param], `%${value.replace(/(.)(.)/g, "$1:$2")}%`] + ]; +} + +export function unpack(resource, label, value): Expression { + if (!resources[resource]) return null; + const type = resources[resource][label].type; + value = value.trim(); + const res: Expression = ["OR"]; + + if (type.length === 0 || type.includes("number")) { + const q = queryNumber(resources[resource][label].parameter, value); + if (q) res.push(q); + } + + if (type.length === 0 || type.includes("string")) { + const q = queryString(resources[resource][label].parameter, value); + if (q) res.push(q); + } + + if (type.length === 0 || type.includes("timestamp")) { + const q = queryTimestamp(resources[resource][label].parameter, value); + if (q) res.push(q); + } + + if (type.includes("mac")) { + const q = queryMac(resources[resource][label].parameter, value); + if (q) res.push(q); + } + + if (res.length <= 1) return null; + else if (res.length === 2) return res[1]; + else return res; +} diff --git a/ui/store.ts b/ui/store.ts new file mode 100644 index 0000000..a3ad1ae --- /dev/null +++ b/ui/store.ts @@ -0,0 +1,587 @@ +import m from "mithril"; +import { map, stringify } from "../lib/common/expression-parser"; +import { or, and, not, evaluate, subset } from "../lib/common/expression"; +import memoize from "../lib/common/memoize"; +import { QueryOptions, Expression } from "../lib/types"; + +const memoizedStringify = memoize(stringify); +const memoizedEvaluate = memoize(evaluate); + +let fulfillTimestamp = 0; + +const queries = { + filter: new WeakMap(), + bookmark: new WeakMap(), + limit: new WeakMap(), + sort: new WeakMap(), + fulfilled: new WeakMap(), + fulfilling: new WeakSet(), + accessed: new WeakMap(), + value: new WeakMap() +}; + +interface Resources { + [resource: string]: { + objects: Map; + count: Map; + fetch: Map; + combinedFilter: Expression; + }; +} + +const resources: Resources = {}; +for (const r of [ + "devices", + "faults", + "files", + "presets", + "provisions", + "virtualParameters", + "files", + "config", + "users", + "permissions" +]) { + resources[r] = { + objects: new Map(), + count: new Map(), + fetch: new Map(), + combinedFilter: null + }; +} + +class QueryResponse { + public get fulfilled(): boolean { + queries.accessed.set(this, Date.now()); + return !!queries.fulfilled.get(this); + } + + public get fulfilling(): boolean { + queries.accessed.set(this, Date.now()); + return queries.fulfilling.has(this); + } + + public get value(): any { + queries.accessed.set(this, Date.now()); + return queries.value.get(this); + } +} + +export function unpackExpression(exp): Expression { + if (!Array.isArray(exp)) return exp; + const e = memoizedEvaluate(exp, null, fulfillTimestamp); + return e; +} + +export function count(resourceType, filter): QueryResponse { + const filterStr = memoizedStringify(filter); + let queryResponse = resources[resourceType].count.get(filterStr); + if (queryResponse) return queryResponse; + + queryResponse = new QueryResponse(); + + resources[resourceType].count.set(filterStr, queryResponse); + queries.filter.set(queryResponse, filter); + return queryResponse; +} + +function limitFilter(filter, sort, bookmark): Expression { + const sortSort = (a, b): number => Math.abs(b[1]) - Math.abs(a[1]); + const arr = Object.entries(sort) + .sort(sortSort) + .reverse(); + return and( + filter, + arr.reduce( + (cur, kv) => { + const [param, asc] = kv; + if (asc <= 0) { + if (bookmark[param] == null) { + return or( + ["IS NOT NULL", ["PARAM", param]], + and(["IS NULL", ["PARAM", param]], cur) + ); + } + + let f = null; + + if (typeof bookmark[param] !== "string") + f = or(f, [">=", ["PARAM", param], ""]); + + f = or(f, [">", ["PARAM", param], bookmark[param]]); + return or(f, and(["=", ["PARAM", param], bookmark[param]], cur)); + } else { + let f: Expression = ["IS NULL", ["PARAM", param]]; + if (bookmark[param] == null) return and(f, cur); + + if (typeof bookmark[param] !== "number") { + f = or(f, [">=", ["PARAM", param], 0]); + f = or(f, ["<", ["PARAM", param], 0]); + } + + f = or(f, ["<", ["PARAM", param], bookmark[param]]); + + return or(f, and(["=", ["PARAM", param], bookmark[param]], cur)); + } + }, + true as Expression + ) + ); +} + +function compareFunction(sort: { + [param: string]: number; +}): (a: any, b: any) => number { + const sortEntries = Object.entries(sort).sort( + (a, b) => Math.abs(b[1]) - Math.abs(a[1]) + ); + + return (a, b) => { + for (const [param, asc] of sortEntries) { + let v1 = a[param]; + let v2 = b[param]; + if (v1 != null && typeof v1 === "object") { + if (v1.value) v1 = v1.value[0]; + else v1 = null; + } + + if (v2 != null && typeof v2 === "object") { + if (v2.value) v2 = v2.value[0]; + else v2 = null; + } + + if (v1 > v2) { + return asc; + } else if (v1 < v2) { + return asc * -1; + } else if (v1 !== v2) { + const w = { + null: 1, + number: 2, + string: 3 + }; + const w1 = w[v1 == null ? "null" : typeof v1] || 4; + const w2 = w[v2 == null ? "null" : typeof v2] || 4; + return Math.max(-1, Math.min(1, w1 - w2)) * asc; + } + } + return 0; + }; +} + +function findMatches(resourceType, filter, sort, limit): any[] { + // Handle "tag =" and "tag <>" special cases + if (resourceType === "devices") { + filter = map(filter, e => { + if ( + Array.isArray(e) && + Array.isArray(e[1]) && + e[1][0] === "PARAM" && + e[1][1] === "tag" + ) { + if (e[0] === "=") return ["IS NOT NULL", ["PARAM", `Tags.${e[2]}`]]; + else if (e[0] === "<>") return ["IS NULL", ["PARAM", `Tags.${e[2]}`]]; + } + return e; + }); + } + + let value = []; + for (const obj of resources[resourceType].objects.values()) + if (evaluate(filter, obj, fulfillTimestamp)) value.push(obj); + + value = value.sort(compareFunction(sort)); + if (limit) value = value.slice(0, limit); + + return value; +} + +function inferQuery(resourceType, queryResponse): void { + const limit = queries.limit.get(queryResponse); + let filter = queries.filter.get(queryResponse); + filter = unpackExpression(filter); + const bookmark = queries.bookmark.get(queryResponse); + const sort = queries.sort.get(queryResponse); + if (bookmark || !limit) { + if (bookmark) filter = limitFilter(filter, sort, bookmark); + if ( + resources[resourceType].combinedFilter && + subset(filter, resources[resourceType].combinedFilter) + ) + queries.fulfilled.set(queryResponse, fulfillTimestamp); + } + + queries.value.set( + queryResponse, + findMatches(resourceType, filter, sort, limit) + ); +} + +export function fetch( + resourceType, + filter, + options: QueryOptions = {} +): QueryResponse { + const filterStr = memoizedStringify(filter); + const sort = Object.assign({}, options.sort); + for (const [k, v] of Object.entries(sort)) sort[k] += Math.sign(v); + + const limit = options.limit || 0; + if (resourceType === "devices") + sort["DeviceID.ID"] = sort["DeviceID.ID"] || 1; + else sort["_id"] = sort["_id"] || 1; + + const key = `${filterStr}:${limit}:${JSON.stringify(sort)}`; + let queryResponse = resources[resourceType].fetch.get(key); + if (queryResponse) return queryResponse; + + queryResponse = new QueryResponse(); + resources[resourceType].fetch.set(key, queryResponse); + queries.filter.set(queryResponse, filter); + queries.limit.set(queryResponse, limit); + queries.sort.set(queryResponse, sort); + inferQuery(resourceType, queryResponse); + return queryResponse; +} + +export function fulfill(accessTimestamp, _fulfillTimestamp): Promise { + let updated = false; + + if (_fulfillTimestamp > fulfillTimestamp) { + for (const resource of Object.values(resources)) + resource.combinedFilter = null; + fulfillTimestamp = _fulfillTimestamp; + } + + const allPromises = []; + + for (const [resourceType, resource] of Object.entries(resources)) { + for (const [queryResponseKey, queryResponse] of resource.count) { + if (!(queries.accessed.get(queryResponse) >= accessTimestamp)) { + resource.count.delete(queryResponseKey); + continue; + } + + if (queries.fulfilling.has(queryResponse)) continue; + + if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) { + queries.fulfilling.add(queryResponse); + allPromises.push( + new Promise((resolve, reject) => { + updated = true; + let filter = queries.filter.get(queryResponse); + filter = unpackExpression(filter); + m.request({ + method: "HEAD", + url: + `/api/${resourceType}/?` + + m.buildQueryString({ + filter: memoizedStringify(filter) + }), + extract: xhr => +xhr.getResponseHeader("x-total-count"), + background: true + }) + .then(c => { + queries.value.set(queryResponse, c); + queries.fulfilled.set(queryResponse, fulfillTimestamp); + queries.fulfilling.delete(queryResponse); + resolve(); + }) + .catch(err => reject(err)); + }) + ); + } + } + } + + const toFetchAll: { [resourceType: string]: QueryResponse[] } = {}; + + for (const [resourceType, resource] of Object.entries(resources)) { + for (const [queryResponseKey, queryResponse] of resource.fetch) { + if (!(queries.accessed.get(queryResponse) >= accessTimestamp)) { + resource.fetch.delete(queryResponseKey); + continue; + } + + if (queries.fulfilling.has(queryResponse)) continue; + + if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) { + queries.fulfilling.add(queryResponse); + toFetchAll[resourceType] = toFetchAll[resourceType] || []; + toFetchAll[resourceType].push(queryResponse); + const limit = queries.limit.get(queryResponse); + const sort = queries.sort.get(queryResponse); + if (limit) { + allPromises.push( + new Promise((resolve, reject) => { + updated = true; + let filter = queries.filter.get(queryResponse); + filter = unpackExpression(filter); + m.request({ + method: "GET", + url: + `/api/${resourceType}/?` + + m.buildQueryString({ + filter: memoizedStringify(filter), + limit: 1, + skip: limit - 1, + sort: JSON.stringify(sort), + projection: Object.keys(sort).join(",") + }) + }) + .then(res => { + if ((res as {}[]).length) { + // Generate bookmark object + const bm = Object.keys(sort).reduce((b, k) => { + if (res[0][k] != null) { + if (typeof res[0][k] === "object") { + if (res[0][k].value != null) + b[k] = res[0][k].value[0]; + } else { + b[k] = res[0][k]; + } + } + + return b; + }, {}); + queries.bookmark.set(queryResponse, bm); + } else { + queries.bookmark.delete(queryResponse); + } + resolve(); + }) + .catch(reject); + }) + ); + } + } + } + } + + return new Promise((resolve, reject) => { + Promise.all(allPromises) + .then(() => { + const allPromises2 = []; + for (let [resourceType, toFetch] of Object.entries(toFetchAll)) { + let combinedFilter = null; + + toFetch = toFetch.filter(queryResponse => { + let filter = queries.filter.get(queryResponse); + filter = unpackExpression(filter); + const limit = queries.limit.get(queryResponse); + const bookmark = queries.bookmark.get(queryResponse); + const sort = queries.sort.get(queryResponse); + if (bookmark) filter = limitFilter(filter, sort, bookmark); + + if ( + resources[resourceType].combinedFilter && + subset(filter, resources[resourceType].combinedFilter) + ) { + queries.value.set( + queryResponse, + findMatches(resourceType, filter, sort, limit) + ); + queries.fulfilled.set(queryResponse, fulfillTimestamp); + queries.fulfilling.delete(queryResponse); + return false; + } + + combinedFilter = or(combinedFilter, filter); + return true; + }); + + if (combinedFilter == null) continue; + + updated = true; + let deleted = new Set(); + if (!resources[resourceType].combinedFilter) + deleted = new Set(resources[resourceType].objects.keys()); + let combinedFilterDiff = combinedFilter; + if (resources[resourceType].combinedFilter) { + combinedFilterDiff = and( + combinedFilterDiff, + not(resources[resourceType].combinedFilter) + ); + } + resources[resourceType].combinedFilter = or( + combinedFilter, + resources[resourceType].combinedFilter + ); + + allPromises2.push( + new Promise((resolve2, reject2) => { + m.request({ + method: "GET", + url: + `/api/${resourceType}/?` + + m.buildQueryString({ + filter: memoizedStringify(combinedFilterDiff) + }) + }) + .then(res => { + for (const r of res as {}[]) { + const id = + resourceType === "devices" + ? r["DeviceID.ID"].value[0] + : r["_id"]; + resources[resourceType].objects.set(id, r); + deleted.delete(id); + } + + for (const d of deleted) { + const obj = resources[resourceType].objects.get(d); + if (evaluate(combinedFilterDiff, obj, fulfillTimestamp)) + resources[resourceType].objects.delete(d); + } + + for (const queryResponse of toFetch) { + let filter = queries.filter.get(queryResponse); + filter = unpackExpression(filter); + const limit = queries.limit.get(queryResponse); + const bookmark = queries.bookmark.get(queryResponse); + const sort = queries.sort.get(queryResponse); + if (bookmark) filter = limitFilter(filter, sort, bookmark); + + queries.value.set( + queryResponse, + findMatches(resourceType, filter, sort, limit) + ); + queries.fulfilled.set(queryResponse, fulfillTimestamp); + queries.fulfilling.delete(queryResponse); + } + resolve2(); + }) + .catch(reject2); + }) + ); + } + Promise.all(allPromises2) + .then(() => resolve(updated)) + .catch(reject); + }) + .catch(reject); + }); +} + +export function getTimestamp(): number { + return fulfillTimestamp; +} + +export function postTasks(deviceId, tasks): Promise { + for (const t of tasks) { + t.status = "pending"; + t.device = deviceId; + } + + return new Promise((resolve, reject) => { + m.request({ + method: "POST", + url: `/api/devices/${encodeURIComponent(deviceId)}/tasks`, + data: tasks, + extract: xhr => { + if (xhr.status !== 200) throw new Error(xhr.response); + const connectionRequestStatus = xhr.getResponseHeader( + "Connection-Request" + ); + const st = JSON.parse(xhr.response); + for (const [i, t] of st.entries()) { + tasks[i]._id = t._id; + tasks[i].status = t.status; + tasks[i].fault = t.fault; + } + resolve(connectionRequestStatus); + } + }).catch(reject); + }); +} + +export function updateTags(deviceId, tags): Promise { + return m.request({ + method: "POST", + url: `/api/devices/${encodeURIComponent(deviceId)}/tags`, + data: tags + }); +} + +export function deleteResource(resourceType, id): Promise { + return m.request({ + method: "DELETE", + url: `/api/${resourceType}/${encodeURIComponent(id)}` + }); +} + +export function putResource(resourceType, id, object): Promise { + for (const k in object) if (object[k] === undefined) object[k] = null; + + return m.request({ + method: "PUT", + url: `/api/${resourceType}/${encodeURIComponent(id)}`, + data: object + }); +} + +export function queryConfig(pattern = "%"): Promise { + const filter = stringify(["LIKE", ["PARAM", "_id"], pattern]); + return m.request({ + method: "GET", + url: `api/config/?${m.buildQueryString({ filter: filter })}`, + background: true + }); +} + +export function resourceExists(resource, id): Promise { + const param = resource === "devices" ? "DeviceID.ID" : "_id"; + const filter = ["=", ["PARAM", param], id]; + return m.request({ + method: "HEAD", + url: + `/api/${resource}/?` + + m.buildQueryString({ + filter: memoizedStringify(filter) + }), + extract: xhr => +xhr.getResponseHeader("x-total-count"), + background: true + }); +} + +export function evaluateExpression(exp, obj): Expression { + if (!Array.isArray(exp)) return exp; + return memoizedEvaluate(exp, obj, fulfillTimestamp); +} + +export function changePassword( + username, + newPassword, + authPassword? +): Promise { + const data = { newPassword }; + if (authPassword) data["authPassword"] = authPassword; + return m.request({ + method: "PUT", + url: `/api/users/${username}/password`, + background: true, + data + }); +} + +export function logIn(username, password): Promise { + return m.request({ + method: "POST", + url: "/login", + background: true, + data: { username, password } + }); +} + +export function logOut(): Promise { + return m.request({ + method: "POST", + url: "/logout" + }); +} + +export function ping(host): Promise<{}> { + return m.request({ + url: `/api/ping/${encodeURIComponent(host)}` + }); +} diff --git a/ui/task-queue.ts b/ui/task-queue.ts new file mode 100644 index 0000000..25d6aae --- /dev/null +++ b/ui/task-queue.ts @@ -0,0 +1,72 @@ +import * as store from "./store"; + +const queue = new Set(); +const staging = new Set(); + +export function queueTask(task): void { + staging.delete(task); + task.status = "queued"; + queue.add(task); +} + +export function deleteTask(task): void { + staging.delete(task); + queue.delete(task); +} + +export function getQueue(): Set<{}> { + return queue; +} + +export function clear(): void { + queue.clear(); +} + +export function getStaging(): Set<{}> { + return staging; +} + +export function clearStaging(): void { + staging.clear(); +} + +export function stageSpv(task): void { + staging.add(task); +} + +export function stageDownload(task): void { + staging.add(task); +} + +export function commit(tasks, callback): Promise { + const devices: { [deviceId: string]: any[] } = {}; + for (const t of tasks) { + devices[t.device] = devices[t.device] || []; + devices[t.device].push(t); + queueTask(t); + } + + return new Promise(resolve => { + let counter = 1; + for (const [deviceId, tasks2] of Object.entries(devices)) { + ++counter; + store + .postTasks(deviceId, tasks) + .then(connectionRequestStatus => { + for (const t of tasks2) { + if (t.status === "pending") t.status = "stale"; + else if (t.status === "done") queue.delete(t); + } + callback(deviceId, null, connectionRequestStatus, tasks2); + if (--counter === 0) resolve(); + }) + .catch(err => { + for (const t of tasks2) t.status = "stale"; + callback(deviceId, err, null, tasks2); + if (--counter === 0) resolve(); + }); + } + + if (--counter === 0) resolve(); + }); +} diff --git a/ui/timeago.ts b/ui/timeago.ts new file mode 100644 index 0000000..e03d23d --- /dev/null +++ b/ui/timeago.ts @@ -0,0 +1,30 @@ +const UNITS = { + year: 12 * 30 * 24 * 60 * 60 * 1000, + month: 30 * 24 * 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + hour: 60 * 60 * 1000, + minute: 60 * 1000, + second: 1000 +}; + +export default function timeAgo(dtime): string { + let res = ""; + let level = 2; + + for (const [u, t] of Object.entries(UNITS)) { + if (dtime >= t) { + let n; + if (level > 1) { + n = Math.floor(dtime / t); + dtime -= n * t; + } else { + n = Math.round(dtime / t); + } + if (n > 1) res += `${n} ${u}s `; + else res += `${n} ${u} `; + if (!--level) break; + } + } + + return res + "ago"; +} diff --git a/ui/ui-config-component.ts b/ui/ui-config-component.ts new file mode 100644 index 0000000..063af6d --- /dev/null +++ b/ui/ui-config-component.ts @@ -0,0 +1,126 @@ +import { ClosureComponent, Component } from "mithril"; +import { m } from "./components"; +import * as store from "./store"; +import { yaml } from "./dynamic-loader"; +import * as configFunctions from "./config-functions"; +import codeEditorComponent from "./code-editor-component"; +import { parse } from "../lib/common/expression-parser"; + +function putActionHandler(prefix: string[], dataYaml: string): Promise { + return new Promise((resolve, reject) => { + try { + let updated = yaml.parse(dataYaml, { schema: "failsafe" }); + if (updated) { + const config = {}; + let ref = config; + prefix.forEach((seg, index) => { + if (index < prefix.length - 1) { + ref[seg] = {}; + ref = ref[seg]; + } else { + ref[seg] = updated; + } + }); + updated = configFunctions.flattenConfig(config); + } else { + updated = {}; + } + + // Try parse to ensure valid expressions + for (const v of Object.values(updated)) parse(v); + + store + .queryConfig(`${prefix.join(".")}.%`) + .then(res => { + const current = {}; + for (const f of res) current[f._id] = f.value; + + const diff = configFunctions.diffConfig(current, updated); + if (!diff.add.length && !diff.remove.length) return void resolve(); + + const promises = []; + + for (const obj of diff.add) + promises.push(store.putResource("config", obj._id, obj)); + + for (const id of diff.remove) + promises.push(store.deleteResource("config", id)); + + Promise.all(promises) + .then(() => { + resolve(); + }) + .catch(reject); + }) + .catch(reject); + } catch (error) { + resolve({ config: error.message }); + } + }); +} + +const component: ClosureComponent = (): Component => { + return { + view: vnode => { + const prefix = vnode.attrs["prefix"].split("."); + const name = vnode.attrs["name"]; + const data = vnode.attrs["data"]; + + if (prefix[prefix.length - 1] === "") prefix.pop(); + + let config; + if (data.length) { + config = configFunctions.structureConfig(data); + for (const seg of prefix) config = config[seg]; + } + + const yamlString = + config && Object.values(config).length + ? yaml.stringify(config, { + schema: "failsafe", + tags: args => { + return args; + } + }) + : ""; + + const attrs = { + id: `${name}-ui-config`, + value: yamlString, + mode: "yaml", + focus: true, + onSubmit: dom => { + dom.form.querySelector("button[type=submit]").click(); + }, + onChange: value => { + vnode.state["updatedYaml"] = value; + } + }; + + const code = m(codeEditorComponent, attrs); + const submit = m("button.primary", { type: "submit" }, "Save"); + + return m("div.put-form", [ + m("h1", `Editing ${name}`), + m( + "form", + { + onsubmit: e => { + e.redraw = false; + e.preventDefault(); + if (vnode.state["updatedYaml"] == null) + vnode.state["updatedYaml"] = yamlString; + + putActionHandler(prefix, vnode.state["updatedYaml"]) + .then(vnode.attrs["onUpdate"]) + .catch(vnode.attrs["onError"]); + } + }, + [code, m(".actions-bar", [submit])] + ) + ]); + } + }; +}; + +export default component; diff --git a/ui/user-menu.ts b/ui/user-menu.ts new file mode 100644 index 0000000..3e4da8a --- /dev/null +++ b/ui/user-menu.ts @@ -0,0 +1,50 @@ +import m, { ClosureComponent, Component } from "mithril"; +import * as store from "./store"; +import * as notifications from "./notifications"; + +const component: ClosureComponent = (): Component => { + return { + view: () => { + if (window.username) { + return m( + "div.user-menu", + window.username, + m( + "button", + { + onclick: e => { + e.target.disabled = true; + store + .logOut() + .then(() => { + location.hash = ""; + location.reload(); + }) + .catch(err => { + e.target.disabled = false; + notifications.push("error", err.message); + }); + return false; + } + }, + "Log out" + ) + ); + } else { + return m( + "div.user-menu", + m( + "a", + { + href: + "#!/login?" + m.buildQueryString({ continue: m.route.get() }) + }, + "Log in" + ) + ); + } + } + }; +}; + +export default component; diff --git a/ui/users-page.ts b/ui/users-page.ts new file mode 100644 index 0000000..b1a619d --- /dev/null +++ b/ui/users-page.ts @@ -0,0 +1,506 @@ +"use strict"; + +import { m } from "./components"; + +import config from "./config"; +import * as store from "./store"; +import * as notifications from "./notifications"; +import memoize from "../lib/common/memoize"; +import putFormComponent from "./put-form-component"; +import * as overlay from "./overlay"; +import * as smartQuery from "./smart-query"; +import { map, parse, stringify } from "../lib/common/expression-parser"; +import filterComponent from "./filter-component"; +import changePasswordComponent from "./change-password-component"; +import { Children, ClosureComponent, Component } from "mithril"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(parse); +const memoizedJsonParse = memoize(JSON.parse); + +const attributes = [ + { id: "_id", label: "Username" }, + { + id: "roles", + label: "Roles", + type: "multi", + options: [], + unsortable: true + } +]; + +const unpackSmartQuery = memoize(query => { + return map(query, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack("users", e[2], e[3]); + return e; + }); +}); + +interface ValidationErrors { + [prop: string]: string; +} + +function putActionHandler(action, _object, isNew): Promise { + return new Promise((resolve, reject) => { + const object = Object.assign({}, _object); + if (action === "save") { + const id = object["_id"]; + const password = object["password"]; + const confirm = object["confirm"]; + delete object["_id"]; + delete object["password"]; + delete object["confirm"]; + + if (!id) return void resolve({ _id: "ID can not be empty" }); + + if (isNew) { + if (!password) { + return void resolve({ password: "Password can not be empty" }); + } else if (password !== confirm) { + return void resolve({ + confirm: "Confirm password doesn't match password" + }); + } + } + + if (!Array.isArray(object.roles) || !object.roles.length) + return void resolve({ roles: "Role(s) must be selected" }); + + object.roles = object.roles.join(","); + + store + .resourceExists("users", id) + .then(exists => { + if (exists && isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "User already exists" }); + } + + if (!exists && !isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "User does not exist" }); + } + + store + .putResource("users", id, object) + .then(() => { + if (isNew) { + store + .changePassword(id, password) + .then(() => { + notifications.push("success", "User created"); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(reject); + } else { + notifications.push("success", "User updated"); + store.fulfill(0, Date.now()); + resolve(); + } + }) + .catch(reject); + }) + .catch(reject); + } else if (action === "delete") { + store + .deleteResource("users", object["_id"]) + .then(() => { + notifications.push("success", "User deleted"); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(err => { + store.fulfill(0, Date.now()); + reject(err); + }); + } else { + reject(new Error("Undefined action")); + } + }); +} + +const getDownloadUrl = memoize(filter => { + const cols = {}; + for (const attr of attributes) cols[attr.label] = attr.id; + + return `/api/users.csv?${m.buildQueryString({ + filter: stringify(filter), + columns: JSON.stringify(cols) + })}`; +}); + +export function init(args): Promise<{}> { + if (!window.authorizer.hasAccess("users", 2)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + + const sort = args.sort; + const filter = args.filter; + return Promise.resolve({ filter, sort }); +} + +function renderTable( + usersResponse, + total, + selected, + showMoreCallback, + downloadUrl, + sort, + onSortChange +): Children { + const users = usersResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: users.length && selected.size === users.length, + onchange: e => { + for (const user of users) { + if (e.target.checked) selected.add(user["_id"]); + else selected.delete(user["_id"]); + } + }, + disabled: !total + }); + + const labels = [m("th", selectAll)]; + for (const attr of attributes) { + const label = attr.label; + + if (attr.unsortable) { + labels.push(m("th", label)); + continue; + } + + let direction = 1; + + let symbol = "\u2981"; + if (sort[attr.id] > 0) symbol = "\u2bc6"; + else if (sort[attr.id] < 0) symbol = "\u2bc5"; + + const sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } + + const canChangePasswords = window.authorizer.hasAccess("users", 3); + + const rows = []; + for (const user of users) { + const checkbox = m("input", { + type: "checkbox", + checked: selected.has(user["_id"]), + onchange: e => { + if (e.target.checked) selected.add(user["_id"]); + else selected.delete(user["_id"]); + }, + onclick: e => { + e.stopPropagation(); + e.redraw = false; + } + }); + + const tds = [m("td", checkbox)]; + for (const attr of attributes) tds.push(m("td", user[attr.id])); + + tds.push( + m( + "td.table-row-links", + m( + "a", + { + onclick: () => { + const cb = (): Children => { + const children = [ + m( + putFormComponent, + Object.assign( + { + base: { + _id: user._id, + roles: user.roles.split(",") + }, + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, false) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + { + resource: "users", + attributes: attributes + } + ) + ) + ]; + + if (canChangePasswords) { + children.push(m("hr")); + const attrs = { + noAuth: true, + username: user._id, + onPasswordChange: () => { + overlay.close(cb); + m.redraw(); + } + }; + children.push(m(changePasswordComponent, attrs)); + } + + return children; + }; + overlay.open(cb); + } + }, + "Show" + ) + ) + ); + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(user["_id"])) selected.add(user["_id"]); + } + }, + tds + ) + ); + } + + if (!rows.length) + rows.push(m("tr.empty", m("td", { colspan: labels.length }, "No users"))); + + const footerElements = []; + if (total != null) footerElements.push(`${users.length}/${total}`); + else footerElements.push(`${users.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more users", + onclick: showMoreCallback, + disabled: users.length >= total || !usersResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) { + footerElements.push( + m("a.download-csv", { href: downloadUrl, download: "" }, "Download") + ); + } + + const tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + const buttons = [ + m( + "button.primary", + { + title: "Delete selected users", + disabled: !selected.size, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + Promise.all( + Array.from(selected).map(id => store.deleteResource("users", id)) + ) + .then(res => { + notifications.push("success", `${res.length} users deleted`); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + store.fulfill(0, Date.now()); + }); + } + }, + "Delete" + ) + ]; + + if (window.authorizer.hasAccess("users", 3)) { + const formData = { + resource: "users", + attributes: [ + attributes[0], + { id: "password", label: "Password", type: "password" }, + { id: "confirm", label: "Confirm password", type: "password" }, + attributes[1] + ] + }; + buttons.push( + m( + "button.primary", + { + title: "Create new user", + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, true) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "New" + ) + ); + } + + return [ + m( + "table.table.highlight", + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ), + m("div.actions-bar", buttons) + ]; +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Users - GenieACS"; + + function showMore(): void { + vnode.state["showCount"] = + (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter): void { + const ops = { filter }; + if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort): void { + const ops = { sort }; + if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs["sort"] + ? memoizedJsonParse(vnode.attrs["sort"]) + : {}; + let filter = vnode.attrs["filter"] + ? memoizedParse(vnode.attrs["filter"]) + : true; + filter = unpackSmartQuery(filter); + + const users = store.fetch("users", filter, { + limit: vnode.state["showCount"] || PAGE_SIZE, + sort: sort + }); + + const count = store.count("users", filter); + + // Getting the roles + const permissions = store.fetch("permissions", true); + if (permissions.fulfilled) { + for (const attr of attributes) { + if (attr.id === "roles") + attr.options = [...new Set(permissions.value.map(p => p.role))]; + } + } + + const selected = new Set(); + if (vnode.state["selected"]) { + for (const user of users.value) { + if (vnode.state["selected"].has(user["_id"])) + selected.add(user["_id"]); + } + } + vnode.state["selected"] = selected; + + const downloadUrl = getDownloadUrl(filter); + + const attrs = {}; + attrs["resource"] = "users"; + attrs["filter"] = vnode.attrs["filter"]; + attrs["onChange"] = onFilterChanged; + + return [ + m("h1", "Listing users"), + m(filterComponent, attrs), + renderTable( + users, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; + } + }; +}; diff --git a/ui/virtual-parameters-page.ts b/ui/virtual-parameters-page.ts new file mode 100644 index 0000000..41dd3dd --- /dev/null +++ b/ui/virtual-parameters-page.ts @@ -0,0 +1,459 @@ +import { ClosureComponent, Component, Children } from "mithril"; +import { m } from "./components"; +import config from "./config"; +import filterComponent from "./filter-component"; +import * as store from "./store"; +import * as notifications from "./notifications"; +import memoize from "../lib/common/memoize"; +import putFormComponent from "./put-form-component"; +import * as overlay from "./overlay"; +import * as smartQuery from "./smart-query"; +import { map, parse, stringify } from "../lib/common/expression-parser"; +import { loadCodeMirror } from "./dynamic-loader"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(parse); +const memoizedJsonParse = memoize(JSON.parse); + +const attributes = [ + { id: "_id", label: "Name" }, + { id: "script", label: "Script", type: "code" } +]; + +const unpackSmartQuery = memoize(query => { + return map(query, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack("virtualParameters", e[2], e[3]); + return e; + }); +}); + +interface ValidationErrors { + [prop: string]: string; +} + +function putActionHandler(action, _object, isNew): Promise { + return new Promise((resolve, reject) => { + const object = Object.assign({}, _object); + if (action === "save") { + const id = object["_id"]; + delete object["_id"]; + + if (!id) return void resolve({ _id: "ID can not be empty" }); + + store + .resourceExists("virtualParameters", id) + .then(exists => { + if (exists && isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "Virtual parameter already exists" }); + } + + if (!exists && !isNew) { + store.fulfill(0, Date.now()); + return void resolve({ _id: "Virtual parameter does not exist" }); + } + + store + .putResource("virtualParameters", id, object) + .then(() => { + notifications.push( + "success", + `Virtual parameter ${exists ? "updated" : "created"}` + ); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(reject); + }) + .catch(reject); + } else if (action === "delete") { + store + .deleteResource("virtualParameters", object["_id"]) + .then(() => { + notifications.push("success", "Virtual parameter deleted"); + store.fulfill(0, Date.now()); + resolve(); + }) + .catch(err => { + store.fulfill(0, Date.now()); + reject(err); + }); + } else { + reject(new Error("Undefined action")); + } + }); +} + +const formData = { + resource: "virtualParameters", + attributes: attributes +}; + +const getDownloadUrl = memoize(filter => { + const cols = {}; + for (const attr of attributes) cols[attr.label] = attr.id; + return `/api/virtualParameters.csv?${m.buildQueryString({ + filter: stringify(filter), + columns: JSON.stringify(cols) + })}`; +}); + +export function init(args): Promise<{}> { + if (!window.authorizer.hasAccess("virtualParameters", 2)) { + return Promise.reject( + new Error("You are not authorized to view this page") + ); + } + + const sort = args.sort; + const filter = args.filter; + + return new Promise((resolve, reject) => { + loadCodeMirror() + .then(() => { + resolve({ filter, sort }); + }) + .catch(reject); + }); +} + +function renderTable( + virtualParametersResponse, + total, + selected, + showMoreCallback, + downloadUrl, + sort, + onSortChange +): Children { + const virtualParameters = virtualParametersResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: + virtualParameters.length && selected.size === virtualParameters.length, + onchange: e => { + for (const virtualParameter of virtualParameters) { + if (e.target.checked) selected.add(virtualParameter["_id"]); + else selected.delete(virtualParameter["_id"]); + } + }, + disabled: !total + }); + + const labels = [m("th", selectAll)]; + for (const attr of attributes) { + const label = attr.label; + + let direction = 1; + + let symbol = "\u2981"; + if (sort[attr.id] > 0) symbol = "\u2bc6"; + else if (sort[attr.id] < 0) symbol = "\u2bc5"; + + const sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } + + const rows = []; + for (const virtualParameter of virtualParameters) { + const checkbox = m("input", { + type: "checkbox", + checked: selected.has(virtualParameter["_id"]), + onchange: e => { + if (e.target.checked) selected.add(virtualParameter["_id"]); + else selected.delete(virtualParameter["_id"]); + }, + onclick: e => { + e.stopPropagation(); + e.redraw = false; + } + }); + + const tds = [m("td", checkbox)]; + for (const attr of attributes) { + if (attr.id === "script") { + const firstLines = virtualParameter[attr.id].split("\n", 11); + if (firstLines.length > 10) firstLines[10] = ["\ufe19"]; + tds.push( + m("td", { title: firstLines.join("\n") }, firstLines[0] || "") + ); + } else { + tds.push(m("td", virtualParameter[attr.id])); + } + } + + tds.push( + m( + "td.table-row-links", + m( + "a", + { + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + base: virtualParameter, + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, false) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "Show" + ) + ) + ); + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(virtualParameter["_id"])) + selected.add(virtualParameter["_id"]); + } + }, + tds + ) + ); + } + + if (!rows.length) { + rows.push( + m( + "tr.empty", + m("td", { colspan: labels.length }, "No virtual parameters") + ) + ); + } + + const footerElements = []; + if (total != null) + footerElements.push(`${virtualParameters.length}/${total}`); + else footerElements.push(`${virtualParameters.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more virtual parameters", + onclick: showMoreCallback, + disabled: + virtualParameters.length >= total || + !virtualParametersResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) { + footerElements.push( + m("a.download-csv", { href: downloadUrl, download: "" }, "Download") + ); + } + + const tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + const buttons = [ + m( + "button.primary", + { + title: "Delete selected virtual parameters", + disabled: !selected.size, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + Promise.all( + Array.from(selected).map(id => + store.deleteResource("virtualParameters", id) + ) + ) + .then(res => { + notifications.push( + "success", + `${res.length} virtual parameters deleted` + ); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + store.fulfill(0, Date.now()); + }); + } + }, + "Delete" + ) + ]; + + if (window.authorizer.hasAccess("virtualParameters", 3)) { + buttons.push( + m( + "button.primary", + { + title: "Create new virtual parameter", + onclick: () => { + const cb = (): Children => { + return m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, true) + .then(errors => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "New" + ) + ); + } + + return [ + m( + "table.table.highlight", + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ), + m("div.actions-bar", buttons) + ]; +} + +export const component: ClosureComponent = (): Component => { + return { + view: vnode => { + document.title = "Virtual Parameters - GenieACS"; + + function showMore(): void { + vnode.state["showCount"] = + (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter): void { + const ops = { filter }; + if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort): void { + const ops = { sort }; + if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs["sort"] + ? memoizedJsonParse(vnode.attrs["sort"]) + : {}; + let filter = vnode.attrs["filter"] + ? memoizedParse(vnode.attrs["filter"]) + : true; + filter = unpackSmartQuery(filter); + + const virtualParameters = store.fetch("virtualParameters", filter, { + limit: vnode.state["showCount"] || PAGE_SIZE, + sort: sort + }); + + const count = store.count("virtualParameters", filter); + + const selected = new Set(); + if (vnode.state["selected"]) { + for (const virtualParameter of virtualParameters.value) { + if (vnode.state["selected"].has(virtualParameter["_id"])) + selected.add(virtualParameter["_id"]); + } + } + vnode.state["selected"] = selected; + + const downloadUrl = getDownloadUrl(filter); + + const attrs = {}; + attrs["resource"] = "virtualParameters"; + attrs["filter"] = vnode.attrs["filter"]; + attrs["onChange"] = onFilterChanged; + + return [ + m("h1", "Listing virtual parameters"), + m(filterComponent, attrs), + renderTable( + virtualParameters, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; + } + }; +};