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
+ )
+ ];
+ }
+ };
+};