Просмотр исходного кода

Merge branch 'master' into bg-support-other-runtimes

port coffee changes to js
Henry Oswald 6 лет назад
Родитель
Сommit
e1c90ec234
100 измененных файлов с 13872 добавлено и 7193 удалено
  1. 3 2
      services/clsi/.dockerignore
  2. 64 0
      services/clsi/.eslintrc
  3. 0 6
      services/clsi/.gitignore
  4. 1 1
      services/clsi/.nvmrc
  5. 7 0
      services/clsi/.prettierrc
  6. 0 15
      services/clsi/.travis.yml
  7. 14 7
      services/clsi/Dockerfile
  8. 8 0
      services/clsi/Jenkinsfile
  9. 49 12
      services/clsi/Makefile
  10. 0 244
      services/clsi/app.coffee
  11. 371 0
      services/clsi/app.js
  12. 0 11
      services/clsi/app/coffee/CommandRunner.coffee
  13. 0 119
      services/clsi/app/coffee/CompileController.coffee
  14. 0 345
      services/clsi/app/coffee/CompileManager.coffee
  15. 0 24
      services/clsi/app/coffee/ContentTypeMapper.coffee
  16. 0 13
      services/clsi/app/coffee/DbQueue.coffee
  17. 0 56
      services/clsi/app/coffee/DockerLockManager.coffee
  18. 0 360
      services/clsi/app/coffee/DockerRunner.coffee
  19. 0 24
      services/clsi/app/coffee/DraftModeManager.coffee
  20. 0 25
      services/clsi/app/coffee/Errors.coffee
  21. 0 95
      services/clsi/app/coffee/LatexRunner.coffee
  22. 0 48
      services/clsi/app/coffee/LocalCommandRunner.coffee
  23. 0 31
      services/clsi/app/coffee/LockManager.coffee
  24. 0 2
      services/clsi/app/coffee/Metrics.coffee
  25. 0 199
      services/clsi/app/coffee/OutputCacheManager.coffee
  26. 0 50
      services/clsi/app/coffee/OutputFileFinder.coffee
  27. 0 55
      services/clsi/app/coffee/OutputFileOptimiser.coffee
  28. 0 84
      services/clsi/app/coffee/ProjectPersistenceManager.coffee
  29. 0 128
      services/clsi/app/coffee/RequestParser.coffee
  30. 0 72
      services/clsi/app/coffee/ResourceStateManager.coffee
  31. 0 142
      services/clsi/app/coffee/ResourceWriter.coffee
  32. 0 25
      services/clsi/app/coffee/SafeReader.coffee
  33. 0 41
      services/clsi/app/coffee/StaticServerForbidSymlinks.coffee
  34. 0 37
      services/clsi/app/coffee/TikzManager.coffee
  35. 0 134
      services/clsi/app/coffee/UrlCache.coffee
  36. 0 70
      services/clsi/app/coffee/UrlFetcher.coffee
  37. 0 55
      services/clsi/app/coffee/db.coffee
  38. 20 0
      services/clsi/app/js/CommandRunner.js
  39. 238 0
      services/clsi/app/js/CompileController.js
  40. 705 0
      services/clsi/app/js/CompileManager.js
  41. 38 0
      services/clsi/app/js/ContentTypeMapper.js
  42. 18 0
      services/clsi/app/js/DbQueue.js
  43. 113 0
      services/clsi/app/js/DockerLockManager.js
  44. 695 0
      services/clsi/app/js/DockerRunner.js
  45. 57 0
      services/clsi/app/js/DraftModeManager.js
  46. 36 0
      services/clsi/app/js/Errors.js
  47. 204 0
      services/clsi/app/js/LatexRunner.js
  48. 91 0
      services/clsi/app/js/LocalCommandRunner.js
  49. 72 0
      services/clsi/app/js/LockManager.js
  50. 3 0
      services/clsi/app/js/Metrics.js
  51. 399 0
      services/clsi/app/js/OutputCacheManager.js
  52. 115 0
      services/clsi/app/js/OutputFileFinder.js
  53. 104 0
      services/clsi/app/js/OutputFileOptimiser.js
  54. 163 0
      services/clsi/app/js/ProjectPersistenceManager.js
  55. 217 0
      services/clsi/app/js/RequestParser.js
  56. 154 0
      services/clsi/app/js/ResourceStateManager.js
  57. 352 0
      services/clsi/app/js/ResourceWriter.js
  58. 60 0
      services/clsi/app/js/SafeReader.js
  59. 94 0
      services/clsi/app/js/StaticServerForbidSymlinks.js
  60. 94 0
      services/clsi/app/js/TikzManager.js
  61. 278 0
      services/clsi/app/js/UrlCache.js
  62. 120 0
      services/clsi/app/js/UrlFetcher.js
  63. 67 0
      services/clsi/app/js/db.js
  64. 0 21
      services/clsi/bin/install_texlive_gce.sh
  65. 6 5
      services/clsi/buildscript.txt
  66. 0 72
      services/clsi/config/settings.defaults.coffee
  67. 102 0
      services/clsi/config/settings.defaults.js
  68. 2 0
      services/clsi/db/.gitignore
  69. 1 1
      services/clsi/docker-compose-config.yml
  70. 1 12
      services/clsi/docker-compose.ci.yml
  71. 7 24
      services/clsi/docker-compose.yml
  72. 8 15
      services/clsi/entrypoint.sh
  73. 3 4
      services/clsi/nodemon.json
  74. 0 3619
      services/clsi/npm-shrinkwrap.json
  75. 7179 0
      services/clsi/package-lock.json
  76. 49 35
      services/clsi/package.json
  77. 0 48
      services/clsi/test/acceptance/coffee/BrokenLatexFileTests.coffee
  78. 0 36
      services/clsi/test/acceptance/coffee/DeleteOldFilesTest.coffee
  79. 0 129
      services/clsi/test/acceptance/coffee/ExampleDocumentTests.coffee
  80. 0 41
      services/clsi/test/acceptance/coffee/SimpleLatexFileTests.coffee
  81. 0 41
      services/clsi/test/acceptance/coffee/SynctexTests.coffee
  82. 0 34
      services/clsi/test/acceptance/coffee/TimeoutTests.coffee
  83. 0 222
      services/clsi/test/acceptance/coffee/UrlCachingTests.coffee
  84. 0 38
      services/clsi/test/acceptance/coffee/WordcountTests.coffee
  85. 0 105
      services/clsi/test/acceptance/coffee/helpers/Client.coffee
  86. 0 24
      services/clsi/test/acceptance/coffee/helpers/ClsiApp.coffee
  87. 88 0
      services/clsi/test/acceptance/js/BrokenLatexFileTests.js
  88. 73 0
      services/clsi/test/acceptance/js/DeleteOldFilesTest.js
  89. 285 0
      services/clsi/test/acceptance/js/ExampleDocumentTests.js
  90. 71 0
      services/clsi/test/acceptance/js/SimpleLatexFileTests.js
  91. 91 0
      services/clsi/test/acceptance/js/SynctexTests.js
  92. 62 0
      services/clsi/test/acceptance/js/TimeoutTests.js
  93. 373 0
      services/clsi/test/acceptance/js/UrlCachingTests.js
  94. 72 0
      services/clsi/test/acceptance/js/WordcountTests.js
  95. 208 0
      services/clsi/test/acceptance/js/helpers/Client.js
  96. 64 0
      services/clsi/test/acceptance/js/helpers/ClsiApp.js
  97. 0 71
      services/clsi/test/load/coffee/loadTest.coffee
  98. 0 0
      services/clsi/test/load/js/bulk.tex
  99. 103 0
      services/clsi/test/load/js/loadTest.js
  100. 0 64
      services/clsi/test/smoke/coffee/SmokeTests.coffee

+ 3 - 2
services/clsi/.dockerignore

@@ -5,5 +5,6 @@ gitrev
 .npm
 .nvmrc
 nodemon.json
-app.js
-**/js/*
+cache/
+compiles/
+db/

+ 64 - 0
services/clsi/.eslintrc

@@ -0,0 +1,64 @@
+// this file was auto-generated, do not edit it directly.
+// instead run bin/update_build_scripts from
+// https://github.com/sharelatex/sharelatex-dev-environment
+{
+  "extends": [
+    "standard",
+    "prettier",
+    "prettier/standard"
+  ],
+  "parserOptions": {
+    "ecmaVersion": 2017
+  },
+  "plugins": [
+    "mocha",
+    "chai-expect",
+    "chai-friendly"
+  ],
+  "env": {
+    "node": true,
+    "mocha": true
+  },
+  "rules": {
+    // Swap the no-unused-expressions rule with a more chai-friendly one
+    "no-unused-expressions": 0,
+    "chai-friendly/no-unused-expressions": "error"
+  },
+  "overrides": [
+    {
+      // Test specific rules
+      "files": ["test/**/*.js"],
+      "globals": {
+        "expect": true
+      },
+      "rules": {
+        // mocha-specific rules
+        "mocha/handle-done-callback": "error",
+        "mocha/no-exclusive-tests": "error",
+        "mocha/no-global-tests": "error",
+        "mocha/no-identical-title": "error",
+        "mocha/no-nested-tests": "error",
+        "mocha/no-pending-tests": "error",
+        "mocha/no-skipped-tests": "error",
+        "mocha/no-mocha-arrows": "error",
+
+        // chai-specific rules
+        "chai-expect/missing-assertion": "error",
+        "chai-expect/terminating-properties": "error",
+
+        // prefer-arrow-callback applies to all callbacks, not just ones in mocha tests.
+        // we don't enforce this at the top-level - just in tests to manage `this` scope
+        // based on mocha's context mechanism
+        "mocha/prefer-arrow-callback": "error"
+      }
+    },
+    {
+      // Backend specific rules
+      "files": ["app/**/*.js", "app.js", "index.js"],
+      "rules": {
+        // don't allow console.log in backend code
+        "no-console": "error"
+      }
+    }
+  ]
+}

+ 0 - 6
services/clsi/.gitignore

@@ -1,13 +1,7 @@
 **.swp
 node_modules
-app/js
-test/unit/js
-test/smoke/js
-test/acceptance/js
 test/acceptance/fixtures/tmp
 compiles
-app.js
-**/*.map
 .DS_Store
 *~
 cache

+ 1 - 1
services/clsi/.nvmrc

@@ -1 +1 @@
-10.15.0
+10.19.0

+ 7 - 0
services/clsi/.prettierrc

@@ -0,0 +1,7 @@
+# This file was auto-generated, do not edit it directly.
+# Instead run bin/update_build_scripts from
+# https://github.com/sharelatex/sharelatex-dev-environment
+{
+  "semi": false,
+  "singleQuote": true
+}

+ 0 - 15
services/clsi/.travis.yml

@@ -1,15 +0,0 @@
-language: node_js
-
-before_install:
-    - npm install -g grunt-cli
-
-install:
-    - npm install
-    - grunt install
-
-script:
-    - grunt test:unit
-
-services:
-    - redis-server
-    - mongodb

+ 14 - 7
services/clsi/Dockerfile

@@ -1,6 +1,16 @@
-FROM gcr.io/overleaf-ops/node:10.19.0 as app
+# This file was auto-generated, do not edit it directly.
+# Instead run bin/update_build_scripts from
+# https://github.com/sharelatex/sharelatex-dev-environment
+
+FROM node:10.19.0 as base
 
 WORKDIR /app
+COPY install_deps.sh /app
+RUN chmod 0755 ./install_deps.sh && ./install_deps.sh
+ENTRYPOINT ["/bin/sh", "entrypoint.sh"]
+COPY entrypoint.sh /app
+
+FROM base as app
 
 #wildcard as some files may not be in all repos
 COPY package*.json npm-shrink*.json /app/
@@ -10,14 +20,11 @@ RUN npm install --quiet
 COPY . /app
 
 
-RUN npm run compile:all
 
-FROM gcr.io/overleaf-ops/node:10.19.0
+FROM base
 
 COPY --from=app /app /app
-
-WORKDIR /app
-RUN chmod 0755 ./install_deps.sh && ./install_deps.sh
-ENTRYPOINT ["/bin/sh", "entrypoint.sh"]
+RUN mkdir -p cache compiles db \
+&&  chown node:node cache compiles db
 
 CMD ["node", "--expose-gc", "app.js"]

+ 8 - 0
services/clsi/Jenkinsfile

@@ -16,6 +16,7 @@ pipeline {
   }
 
   stages {
+
     stage('Install') {
       steps {
         withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) {
@@ -36,6 +37,13 @@ pipeline {
       }
     }
 
+    stage('Linting') {
+      steps {
+        sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make format'
+        sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make lint'
+      }
+    }
+
     stage('Unit Tests') {
       steps {
         sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit'

+ 49 - 12
services/clsi/Makefile

@@ -1,11 +1,12 @@
 # This file was auto-generated, do not edit it directly.
 # Instead run bin/update_build_scripts from
 # https://github.com/sharelatex/sharelatex-dev-environment
-# Version: 1.1.24
 
 BUILD_NUMBER ?= local
 BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
 PROJECT_NAME = clsi
+BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]')
+
 DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
 DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \
 	BRANCH_NAME=$(BRANCH_NAME) \
@@ -13,29 +14,64 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \
 	MOCHA_GREP=${MOCHA_GREP} \
 	docker-compose ${DOCKER_COMPOSE_FLAGS}
 
+DOCKER_COMPOSE_TEST_ACCEPTANCE = \
+	COMPOSE_PROJECT_NAME=test_acceptance_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE)
+
+DOCKER_COMPOSE_TEST_UNIT = \
+	COMPOSE_PROJECT_NAME=test_unit_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE)
+
 clean:
 	docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
 	docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
-	rm -f app.js
-	rm -rf app/js
-	rm -rf test/unit/js
-	rm -rf test/acceptance/js
 
-test: test_unit test_acceptance
+format:
+	$(DOCKER_COMPOSE) run --rm test_unit npm run format
+
+format_fix:
+	$(DOCKER_COMPOSE) run --rm test_unit npm run format:fix
+
+lint:
+	$(DOCKER_COMPOSE) run --rm test_unit npm run lint
+
+test: format lint test_unit test_acceptance
 
 test_unit:
-	@[ ! -d test/unit ] && echo "clsi has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit
+ifneq (,$(wildcard test/unit))
+	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
+	$(MAKE) test_unit_clean
+endif
 
-test_acceptance: test_clean test_acceptance_pre_run test_acceptance_run
+test_clean: test_unit_clean
+test_unit_clean:
+ifneq (,$(wildcard test/unit))
+	$(DOCKER_COMPOSE_TEST_UNIT) down -v -t 0
+endif
+
+test_acceptance: test_acceptance_clean test_acceptance_pre_run test_acceptance_run
+	$(MAKE) test_acceptance_clean
+
+test_acceptance_debug: test_acceptance_clean test_acceptance_pre_run test_acceptance_run_debug
+	$(MAKE) test_acceptance_clean
 
 test_acceptance_run:
-	@[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance
+ifneq (,$(wildcard test/acceptance))
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance
+endif
+
+test_acceptance_run_debug:
+ifneq (,$(wildcard test/acceptance))
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
+endif
 
-test_clean:
-	$(DOCKER_COMPOSE) down -v -t 0
+test_clean: test_acceptance_clean
+test_acceptance_clean:
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) down -v -t 0
 
 test_acceptance_pre_run:
-	@[ ! -f test/acceptance/js/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/js/scripts/pre-run
+ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance test/acceptance/js/scripts/pre-run
+endif
+
 build:
 	docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \
 		--tag gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \
@@ -48,4 +84,5 @@ publish:
 
 	docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
 
+
 .PHONY: clean test test_unit test_acceptance test_clean build publish

+ 0 - 244
services/clsi/app.coffee

@@ -1,244 +0,0 @@
-Metrics = require "metrics-sharelatex"
-Metrics.initialize("clsi")
-
-CompileController = require "./app/js/CompileController"
-Settings = require "settings-sharelatex"
-logger = require "logger-sharelatex"
-logger.initialize("clsi")
-if Settings.sentry?.dsn?
-	logger.initializeErrorReporting(Settings.sentry.dsn)
-
-smokeTest = require "smoke-test-sharelatex"
-ContentTypeMapper = require "./app/js/ContentTypeMapper"
-Errors = require './app/js/Errors'
-
-Path = require "path"
-fs = require "fs"
-
-
-Metrics.open_sockets.monitor(logger)
-Metrics.memory.monitor(logger)
-
-ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager"
-OutputCacheManager = require "./app/js/OutputCacheManager"
-
-require("./app/js/db").sync()
-
-express = require "express"
-bodyParser = require "body-parser"
-app = express()
-
-Metrics.injectMetricsRoute(app)
-app.use Metrics.http.monitor(logger)
-
-# Compile requests can take longer than the default two
-# minutes (including file download time), so bump up the 
-# timeout a bit.
-TIMEOUT = 10 * 60 * 1000
-app.use (req, res, next) ->
-	req.setTimeout TIMEOUT
-	res.setTimeout TIMEOUT
-	res.removeHeader("X-Powered-By")
-	next()
-
-app.param 'project_id', (req, res, next, project_id) ->
-	if project_id?.match /^[a-zA-Z0-9_-]+$/
-		next()
-	else
-		next new Error("invalid project id")
-
-app.param 'user_id', (req, res, next, user_id) ->
-	if user_id?.match /^[0-9a-f]{24}$/
-		next()
-	else
-		next new Error("invalid user id")
-
-app.param 'build_id', (req, res, next, build_id) ->
-	if build_id?.match OutputCacheManager.BUILD_REGEX
-		next()
-	else
-		next new Error("invalid build id #{build_id}")
-
-
-app.post   "/project/:project_id/compile", bodyParser.json(limit: Settings.compileSizeLimit), CompileController.compile
-app.post   "/project/:project_id/compile/stop", CompileController.stopCompile
-app.delete "/project/:project_id", CompileController.clearCache
-
-app.get  "/project/:project_id/sync/code", CompileController.syncFromCode
-app.get  "/project/:project_id/sync/pdf", CompileController.syncFromPdf
-app.get  "/project/:project_id/wordcount", CompileController.wordcount
-app.get  "/project/:project_id/status", CompileController.status
-
-# Per-user containers
-app.post   "/project/:project_id/user/:user_id/compile", bodyParser.json(limit: Settings.compileSizeLimit), CompileController.compile
-app.post   "/project/:project_id/user/:user_id/compile/stop", CompileController.stopCompile
-app.delete "/project/:project_id/user/:user_id", CompileController.clearCache
-
-app.get  "/project/:project_id/user/:user_id/sync/code", CompileController.syncFromCode
-app.get  "/project/:project_id/user/:user_id/sync/pdf", CompileController.syncFromPdf
-app.get  "/project/:project_id/user/:user_id/wordcount", CompileController.wordcount
-
-ForbidSymlinks = require "./app/js/StaticServerForbidSymlinks"
-
-# create a static server which does not allow access to any symlinks
-# avoids possible mismatch of root directory between middleware check
-# and serving the files
-staticServer = ForbidSymlinks express.static, Settings.path.compilesDir, setHeaders: (res, path, stat) ->
-	if Path.basename(path) == "output.pdf"
-		# Calculate an etag in the same way as nginx
-		# https://github.com/tj/send/issues/65
-		etag = (path, stat) ->
-			'"' + Math.ceil(+stat.mtime / 1000).toString(16) +
-			'-' + Number(stat.size).toString(16) + '"'
-		res.set("Etag", etag(path, stat))
-	res.set("Content-Type", ContentTypeMapper.map(path))
-
-app.get "/project/:project_id/user/:user_id/build/:build_id/output/*", (req, res, next) ->
-	# for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId)
-	req.url = "/#{req.params.project_id}-#{req.params.user_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}")
-	staticServer(req, res, next)
-
-app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) ->
-	# for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId)
-	req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}")
-	staticServer(req, res, next)
-
-app.get "/project/:project_id/user/:user_id/output/*", (req, res, next) ->
-	# for specific user get the path to the top level file
-	req.url = "/#{req.params.project_id}-#{req.params.user_id}/#{req.params[0]}"
-	staticServer(req, res, next)
-
-app.get "/project/:project_id/output/*", (req, res, next) ->
-	if req.query?.build? && req.query.build.match(OutputCacheManager.BUILD_REGEX)
-		# for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId)
-		req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.query.build, "/#{req.params[0]}")
-	else
-		req.url = "/#{req.params.project_id}/#{req.params[0]}"
-	staticServer(req, res, next)
-
-app.get "/oops", (req, res, next) ->
-	logger.error {err: "hello"}, "test error"
-	res.send "error\n"
-
-
-app.get "/status", (req, res, next) ->
-	res.send "CLSI is alive\n"
-
-resCacher =
-	contentType:(@setContentType)->
-	send:(@code, @body)->
-
-	#default the server to be down
-	code:500
-	body:{}
-	setContentType:"application/json"
-
-if Settings.smokeTest
-	do runSmokeTest = ->
-		logger.log("running smoke tests")
-		smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))({}, resCacher)
-		setTimeout(runSmokeTest, 30 * 1000)
-
-app.get "/health_check", (req, res)->
-	res.contentType(resCacher?.setContentType)
-	res.status(resCacher?.code).send(resCacher?.body)
-
-app.get "/smoke_test_force", (req, res)->
-	smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))(req, res)
-
-profiler = require "v8-profiler-node8"
-app.get "/profile", (req, res) ->
-	time = parseInt(req.query.time || "1000")
-	profiler.startProfiling("test")
-	setTimeout () ->
-		profile = profiler.stopProfiling("test")
-		res.json(profile)
-	, time
-
-app.get "/heapdump", (req, res)->
-	require('heapdump').writeSnapshot '/tmp/' + Date.now() + '.clsi.heapsnapshot', (err, filename)->
-		res.send filename
-
-app.use (error, req, res, next) ->
-	if error instanceof Errors.NotFoundError
-		logger.warn {err: error, url: req.url}, "not found error"
-		return res.sendStatus(404)
-	else
-		logger.error {err: error, url: req.url}, "server error"
-		res.sendStatus(error?.statusCode || 500)
-
-net = require "net"
-os = require "os"
-
-STATE = "up"
-
-
-loadTcpServer = net.createServer (socket) ->
-	socket.on "error", (err)->
-		if err.code == "ECONNRESET"
-			# this always comes up, we don't know why
-			return
-		logger.err err:err, "error with socket on load check"
-		socket.destroy()
-	
-	if STATE == "up" and Settings.internal.load_balancer_agent.report_load
-		currentLoad = os.loadavg()[0]
-
-		# staging clis's have 1 cpu core only
-		if os.cpus().length == 1
-			availableWorkingCpus = 1
-		else
-			availableWorkingCpus = os.cpus().length - 1
-
-		freeLoad = availableWorkingCpus - currentLoad
-		freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100)
-		if freeLoadPercentage <= 0
-			freeLoadPercentage = 1 # when its 0 the server is set to drain and will move projects to different servers
-		socket.write("up, #{freeLoadPercentage}%\n", "ASCII")
-		socket.end()
-	else
-		socket.write("#{STATE}\n", "ASCII")
-		socket.end()
-
-loadHttpServer = express()
-
-loadHttpServer.post "/state/up", (req, res, next) ->
-	STATE = "up"
-	logger.info "getting message to set server to down"
-	res.sendStatus 204
-
-loadHttpServer.post "/state/down", (req, res, next) ->
-	STATE = "down"
-	logger.info "getting message to set server to down"
-	res.sendStatus 204
-
-loadHttpServer.post "/state/maint", (req, res, next) ->
-	STATE = "maint"
-	logger.info "getting message to set server to maint"
-	res.sendStatus 204
-	
-
-port = (Settings.internal?.clsi?.port or 3013)
-host = (Settings.internal?.clsi?.host or "localhost")
-
-load_tcp_port = Settings.internal.load_balancer_agent.load_port
-load_http_port = Settings.internal.load_balancer_agent.local_port
-
-if !module.parent # Called directly
-	app.listen port, host, (error) ->
-		logger.info "CLSI starting up, listening on #{host}:#{port}"
-
-	loadTcpServer.listen load_tcp_port, host, (error) ->
-		throw error if error?
-		logger.info "Load tcp agent listening on load port #{load_tcp_port}"
-
-	loadHttpServer.listen load_http_port, host, (error) ->
-		throw error if error?
-		logger.info "Load http agent listening on load port #{load_http_port}"
-
-module.exports = app
-
-setInterval () ->
-	ProjectPersistenceManager.clearExpiredProjects()
-, tenMinutes = 10 * 60 * 1000
-

+ 371 - 0
services/clsi/app.js

@@ -0,0 +1,371 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let tenMinutes
+const Metrics = require('metrics-sharelatex')
+Metrics.initialize('clsi')
+
+const CompileController = require('./app/js/CompileController')
+const Settings = require('settings-sharelatex')
+const logger = require('logger-sharelatex')
+logger.initialize('clsi')
+if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) {
+  logger.initializeErrorReporting(Settings.sentry.dsn)
+}
+
+const smokeTest = require('smoke-test-sharelatex')
+const ContentTypeMapper = require('./app/js/ContentTypeMapper')
+const Errors = require('./app/js/Errors')
+
+const Path = require('path')
+
+Metrics.open_sockets.monitor(logger)
+Metrics.memory.monitor(logger)
+
+const ProjectPersistenceManager = require('./app/js/ProjectPersistenceManager')
+const OutputCacheManager = require('./app/js/OutputCacheManager')
+
+require('./app/js/db').sync()
+
+const express = require('express')
+const bodyParser = require('body-parser')
+const app = express()
+
+Metrics.injectMetricsRoute(app)
+app.use(Metrics.http.monitor(logger))
+
+// Compile requests can take longer than the default two
+// minutes (including file download time), so bump up the
+// timeout a bit.
+const TIMEOUT = 10 * 60 * 1000
+app.use(function(req, res, next) {
+  req.setTimeout(TIMEOUT)
+  res.setTimeout(TIMEOUT)
+  res.removeHeader('X-Powered-By')
+  return next()
+})
+
+app.param('project_id', function(req, res, next, project_id) {
+  if (project_id != null ? project_id.match(/^[a-zA-Z0-9_-]+$/) : undefined) {
+    return next()
+  } else {
+    return next(new Error('invalid project id'))
+  }
+})
+
+app.param('user_id', function(req, res, next, user_id) {
+  if (user_id != null ? user_id.match(/^[0-9a-f]{24}$/) : undefined) {
+    return next()
+  } else {
+    return next(new Error('invalid user id'))
+  }
+})
+
+app.param('build_id', function(req, res, next, build_id) {
+  if (
+    build_id != null
+      ? build_id.match(OutputCacheManager.BUILD_REGEX)
+      : undefined
+  ) {
+    return next()
+  } else {
+    return next(new Error(`invalid build id ${build_id}`))
+  }
+})
+
+app.post(
+  '/project/:project_id/compile',
+  bodyParser.json({ limit: Settings.compileSizeLimit }),
+  CompileController.compile
+)
+app.post('/project/:project_id/compile/stop', CompileController.stopCompile)
+app.delete('/project/:project_id', CompileController.clearCache)
+
+app.get('/project/:project_id/sync/code', CompileController.syncFromCode)
+app.get('/project/:project_id/sync/pdf', CompileController.syncFromPdf)
+app.get('/project/:project_id/wordcount', CompileController.wordcount)
+app.get('/project/:project_id/status', CompileController.status)
+
+// Per-user containers
+app.post(
+  '/project/:project_id/user/:user_id/compile',
+  bodyParser.json({ limit: Settings.compileSizeLimit }),
+  CompileController.compile
+)
+app.post(
+  '/project/:project_id/user/:user_id/compile/stop',
+  CompileController.stopCompile
+)
+app.delete('/project/:project_id/user/:user_id', CompileController.clearCache)
+
+app.get(
+  '/project/:project_id/user/:user_id/sync/code',
+  CompileController.syncFromCode
+)
+app.get(
+  '/project/:project_id/user/:user_id/sync/pdf',
+  CompileController.syncFromPdf
+)
+app.get(
+  '/project/:project_id/user/:user_id/wordcount',
+  CompileController.wordcount
+)
+
+const ForbidSymlinks = require('./app/js/StaticServerForbidSymlinks')
+
+// create a static server which does not allow access to any symlinks
+// avoids possible mismatch of root directory between middleware check
+// and serving the files
+const staticServer = ForbidSymlinks(express.static, Settings.path.compilesDir, {
+  setHeaders(res, path, stat) {
+    if (Path.basename(path) === 'output.pdf') {
+      // Calculate an etag in the same way as nginx
+      // https://github.com/tj/send/issues/65
+      const etag = (path, stat) =>
+        `"${Math.ceil(+stat.mtime / 1000).toString(16)}` +
+        '-' +
+        Number(stat.size).toString(16) +
+        '"'
+      res.set('Etag', etag(path, stat))
+    }
+    return res.set('Content-Type', ContentTypeMapper.map(path))
+  }
+})
+
+app.get('/project/:project_id/user/:user_id/build/:build_id/output/*', function(
+  req,
+  res,
+  next
+) {
+  // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId)
+  req.url =
+    `/${req.params.project_id}-${req.params.user_id}/` +
+    OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`)
+  return staticServer(req, res, next)
+})
+
+app.get('/project/:project_id/build/:build_id/output/*', function(
+  req,
+  res,
+  next
+) {
+  // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId)
+  req.url =
+    `/${req.params.project_id}/` +
+    OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`)
+  return staticServer(req, res, next)
+})
+
+app.get('/project/:project_id/user/:user_id/output/*', function(
+  req,
+  res,
+  next
+) {
+  // for specific user get the path to the top level file
+  req.url = `/${req.params.project_id}-${req.params.user_id}/${req.params[0]}`
+  return staticServer(req, res, next)
+})
+
+app.get('/project/:project_id/output/*', function(req, res, next) {
+  if (
+    (req.query != null ? req.query.build : undefined) != null &&
+    req.query.build.match(OutputCacheManager.BUILD_REGEX)
+  ) {
+    // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId)
+    req.url =
+      `/${req.params.project_id}/` +
+      OutputCacheManager.path(req.query.build, `/${req.params[0]}`)
+  } else {
+    req.url = `/${req.params.project_id}/${req.params[0]}`
+  }
+  return staticServer(req, res, next)
+})
+
+app.get('/oops', function(req, res, next) {
+  logger.error({ err: 'hello' }, 'test error')
+  return res.send('error\n')
+})
+
+app.get('/status', (req, res, next) => res.send('CLSI is alive\n'))
+
+const resCacher = {
+  contentType(setContentType) {
+    this.setContentType = setContentType
+  },
+  send(code, body) {
+    this.code = code
+    this.body = body
+  },
+
+  // default the server to be down
+  code: 500,
+  body: {},
+  setContentType: 'application/json'
+}
+
+let shutdownTime
+if (Settings.processLifespanLimitMs) {
+  Settings.processLifespanLimitMs +=
+    Settings.processLifespanLimitMs * (Math.random() / 10)
+  shutdownTime = Date.now() + Settings.processLifespanLimitMs
+  logger.info('Lifespan limited to ', shutdownTime)
+}
+
+const checkIfProcessIsTooOld = function(cont) {
+  if (shutdownTime && shutdownTime < Date.now()) {
+    logger.log('shutting down, process is too old')
+    resCacher.send = function() {}
+    resCacher.code = 500
+    resCacher.body = { processToOld: true }
+  } else {
+    cont()
+  }
+}
+
+if (Settings.smokeTest) {
+  const runSmokeTest = function() {
+    checkIfProcessIsTooOld(function() {
+      logger.log('running smoke tests')
+      smokeTest.run(
+        require.resolve(__dirname + '/test/smoke/js/SmokeTests.js')
+      )({}, resCacher)
+      return setTimeout(runSmokeTest, 30 * 1000)
+    })
+  }
+  runSmokeTest()
+}
+
+app.get('/health_check', function(req, res) {
+  res.contentType(resCacher.setContentType)
+  return res.status(resCacher.code).send(resCacher.body)
+})
+
+app.get('/smoke_test_force', (req, res) =>
+  smokeTest.run(require.resolve(__dirname + '/test/smoke/js/SmokeTests.js'))(
+    req,
+    res
+  )
+)
+
+app.use(function(error, req, res, next) {
+  if (error instanceof Errors.NotFoundError) {
+    logger.warn({ err: error, url: req.url }, 'not found error')
+    return res.sendStatus(404)
+  } else {
+    logger.error({ err: error, url: req.url }, 'server error')
+    return res.sendStatus((error != null ? error.statusCode : undefined) || 500)
+  }
+})
+
+const net = require('net')
+const os = require('os')
+
+let STATE = 'up'
+
+const loadTcpServer = net.createServer(function(socket) {
+  socket.on('error', function(err) {
+    if (err.code === 'ECONNRESET') {
+      // this always comes up, we don't know why
+      return
+    }
+    logger.err({ err }, 'error with socket on load check')
+    return socket.destroy()
+  })
+
+  if (STATE === 'up' && Settings.internal.load_balancer_agent.report_load) {
+    let availableWorkingCpus
+    const currentLoad = os.loadavg()[0]
+
+    // staging clis's have 1 cpu core only
+    if (os.cpus().length === 1) {
+      availableWorkingCpus = 1
+    } else {
+      availableWorkingCpus = os.cpus().length - 1
+    }
+
+    const freeLoad = availableWorkingCpus - currentLoad
+    let freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100)
+    if (freeLoadPercentage <= 0) {
+      freeLoadPercentage = 1 // when its 0 the server is set to drain and will move projects to different servers
+    }
+    socket.write(`up, ${freeLoadPercentage}%\n`, 'ASCII')
+    return socket.end()
+  } else {
+    socket.write(`${STATE}\n`, 'ASCII')
+    return socket.end()
+  }
+})
+
+const loadHttpServer = express()
+
+loadHttpServer.post('/state/up', function(req, res, next) {
+  STATE = 'up'
+  logger.info('getting message to set server to down')
+  return res.sendStatus(204)
+})
+
+loadHttpServer.post('/state/down', function(req, res, next) {
+  STATE = 'down'
+  logger.info('getting message to set server to down')
+  return res.sendStatus(204)
+})
+
+loadHttpServer.post('/state/maint', function(req, res, next) {
+  STATE = 'maint'
+  logger.info('getting message to set server to maint')
+  return res.sendStatus(204)
+})
+
+const port =
+  __guard__(
+    Settings.internal != null ? Settings.internal.clsi : undefined,
+    x => x.port
+  ) || 3013
+const host =
+  __guard__(
+    Settings.internal != null ? Settings.internal.clsi : undefined,
+    x1 => x1.host
+  ) || 'localhost'
+
+const load_tcp_port = Settings.internal.load_balancer_agent.load_port
+const load_http_port = Settings.internal.load_balancer_agent.local_port
+
+if (!module.parent) {
+  // Called directly
+  app.listen(port, host, error =>
+    logger.info(`CLSI starting up, listening on ${host}:${port}`)
+  )
+
+  loadTcpServer.listen(load_tcp_port, host, function(error) {
+    if (error != null) {
+      throw error
+    }
+    return logger.info(`Load tcp agent listening on load port ${load_tcp_port}`)
+  })
+
+  loadHttpServer.listen(load_http_port, host, function(error) {
+    if (error != null) {
+      throw error
+    }
+    return logger.info(
+      `Load http agent listening on load port ${load_http_port}`
+    )
+  })
+}
+
+module.exports = app
+
+setInterval(
+  () => ProjectPersistenceManager.clearExpiredProjects(),
+  (tenMinutes = 10 * 60 * 1000)
+)
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 0 - 11
services/clsi/app/coffee/CommandRunner.coffee

@@ -1,11 +0,0 @@
-Settings = require "settings-sharelatex"
-logger = require "logger-sharelatex"
-
-if Settings.clsi?.dockerRunner == true
-	commandRunnerPath = "./DockerRunner"
-else 
-	commandRunnerPath = "./LocalCommandRunner"
-logger.info commandRunnerPath:commandRunnerPath, "selecting command runner for clsi"
-CommandRunner = require(commandRunnerPath)
-
-module.exports = CommandRunner

+ 0 - 119
services/clsi/app/coffee/CompileController.coffee

@@ -1,119 +0,0 @@
-RequestParser = require "./RequestParser"
-CompileManager = require "./CompileManager"
-Settings = require "settings-sharelatex"
-Metrics = require "./Metrics"
-ProjectPersistenceManager = require "./ProjectPersistenceManager"
-logger = require "logger-sharelatex"
-Errors = require "./Errors"
-
-module.exports = CompileController =
-	compile: (req, res, next = (error) ->) ->
-		timer = new Metrics.Timer("compile-request")
-		RequestParser.parse req.body, (error, request) ->
-			return next(error) if error?
-			request.project_id = req.params.project_id
-			request.user_id = req.params.user_id if req.params.user_id?
-			ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) ->
-				return next(error) if error?
-				CompileManager.doCompileWithLock request, (error, outputFiles = []) ->
-					if error instanceof Errors.AlreadyCompilingError
-						code = 423 # Http 423 Locked
-						status = "compile-in-progress"
-					else if error instanceof Errors.FilesOutOfSyncError
-						code = 409 # Http 409 Conflict
-						status = "retry"
-					else if error?.terminated
-						status = "terminated"
-					else if error?.validate
-						status = "validation-#{error.validate}"
-					else if error?.timedout
-						status = "timedout"
-						logger.log err: error, project_id: request.project_id, "timeout running compile"
-					else if error?
-						status = "error"
-						code = 500
-						logger.warn err: error, project_id: request.project_id, "error running compile"
-					else
-						status = "failure"
-						for file in outputFiles
-							if file.path?.match(/output\.pdf$/)
-								status = "success"
-
-						if status == "failure"
-							logger.warn project_id: request.project_id, outputFiles:outputFiles, "project failed to compile successfully, no output.pdf generated"
-
-						# log an error if any core files are found
-						for file in outputFiles
-							if file.path is "core"
-								logger.error project_id:request.project_id, req:req, outputFiles:outputFiles, "core file found in output"
-
-					if error?
-						outputFiles = error.outputFiles || []
-
-					timer.done()
-					res.status(code or 200).send {
-						compile:
-							status: status
-							error:  error?.message or error
-							outputFiles: outputFiles.map (file) ->
-								url:
-									"#{Settings.apis.clsi.url}/project/#{request.project_id}" +
-									(if request.user_id? then "/user/#{request.user_id}" else "") +
-									(if file.build? then "/build/#{file.build}" else "") +
-									"/output/#{file.path}"
-								path: file.path
-								type: file.type
-								build: file.build
-					}
-
-	stopCompile: (req, res, next) ->
-		{project_id, user_id} = req.params
-		CompileManager.stopCompile project_id, user_id, (error) ->
-			return next(error) if error?
-			res.sendStatus(204)
-
-	clearCache: (req, res, next = (error) ->) ->
-		ProjectPersistenceManager.clearProject req.params.project_id, req.params.user_id, (error) ->
-			return next(error) if error?
-			res.sendStatus(204) # No content
-
-	syncFromCode: (req, res, next = (error) ->) ->
-		file   = req.query.file
-		line   = parseInt(req.query.line, 10)
-		column = parseInt(req.query.column, 10)
-		project_id = req.params.project_id
-		user_id = req.params.user_id
-		CompileManager.syncFromCode project_id, user_id, file, line, column, (error, pdfPositions) ->
-			return next(error) if error?
-			res.json {
-				pdf: pdfPositions
-			}
-
-	syncFromPdf: (req, res, next = (error) ->) ->
-		page   = parseInt(req.query.page, 10)
-		h      = parseFloat(req.query.h)
-		v      = parseFloat(req.query.v)
-		project_id = req.params.project_id
-		user_id = req.params.user_id
-		CompileManager.syncFromPdf project_id, user_id, page, h, v, (error, codePositions) ->
-			return next(error) if error?
-			res.json {
-				code: codePositions
-			}
-
-	wordcount: (req, res, next = (error) ->) ->
-		file   = req.query.file || "main.tex"
-		project_id = req.params.project_id
-		user_id = req.params.user_id
-		image = req.query.image
-		logger.log {image, file, project_id}, "word count request"
-
-		CompileManager.wordcount project_id, user_id, file, image, (error, result) ->
-			return next(error) if error?
-			res.json {
-				texcount: result
-			}
-
-	status: (req, res, next = (error)-> )->
-		res.send("OK")
-

+ 0 - 345
services/clsi/app/coffee/CompileManager.coffee

@@ -1,345 +0,0 @@
-ResourceWriter = require "./ResourceWriter"
-LatexRunner = require "./LatexRunner"
-OutputFileFinder = require "./OutputFileFinder"
-OutputCacheManager = require "./OutputCacheManager"
-Settings = require("settings-sharelatex")
-Path = require "path"
-logger = require "logger-sharelatex"
-Metrics = require "./Metrics"
-child_process = require "child_process"
-DraftModeManager = require "./DraftModeManager"
-TikzManager = require "./TikzManager"
-LockManager = require "./LockManager"
-fs = require("fs")
-fse = require "fs-extra"
-os = require("os")
-async = require "async"
-Errors = require './Errors'
-CommandRunner = require "./CommandRunner"
-
-getCompileName = (project_id, user_id) ->
-	if user_id? then "#{project_id}-#{user_id}" else project_id
-
-getCompileDir = (project_id, user_id) ->
-	Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id))
-
-module.exports = CompileManager =
-
-	doCompileWithLock: (request, callback = (error, outputFiles) ->) ->
-		compileDir = getCompileDir(request.project_id, request.user_id)
-		lockFile = Path.join(compileDir, ".project-lock")
-		# use a .project-lock file in the compile directory to prevent
-		# simultaneous compiles
-		fse.ensureDir compileDir, (error) ->
-			return callback(error) if error?
-			LockManager.runWithLock lockFile, (releaseLock) ->
-				CompileManager.doCompile(request, releaseLock)
-			, callback
-
-	doCompile: (request, callback = (error, outputFiles) ->) ->
-		compileDir = getCompileDir(request.project_id, request.user_id)
-		timer = new Metrics.Timer("write-to-disk")
-		logger.log project_id: request.project_id, user_id: request.user_id, "syncing resources to disk"
-		ResourceWriter.syncResourcesToDisk request, compileDir, (error, resourceList) ->
-			# NOTE: resourceList is insecure, it should only be used to exclude files from the output list
-			if error? and error instanceof Errors.FilesOutOfSyncError
-				logger.warn project_id: request.project_id, user_id: request.user_id, "files out of sync, please retry"
-				return callback(error)
-			else if error?
-				logger.err err:error, project_id: request.project_id, user_id: request.user_id, "error writing resources to disk"
-				return callback(error)
-			logger.log project_id: request.project_id, user_id: request.user_id, time_taken: Date.now() - timer.start, "written files to disk"
-			timer.done()
-
-			injectDraftModeIfRequired = (callback) ->
-				if request.draft
-					DraftModeManager.injectDraftMode Path.join(compileDir, request.rootResourcePath), callback
-				else
-					callback()
-
-			createTikzFileIfRequired = (callback) ->
-				TikzManager.checkMainFile compileDir, request.rootResourcePath, resourceList, (error, needsMainFile) ->
-					return callback(error) if error?
-					if needsMainFile
-						TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback
-					else
-						callback()
-
-			# set up environment variables for chktex
-			env = {}
-			# only run chktex on LaTeX files (not knitr .Rtex files or any others)
-			isLaTeXFile = request.rootResourcePath?.match(/\.tex$/i)
-			if request.check? and isLaTeXFile
-				env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16'
-				env['CHKTEX_ULIMIT_OPTIONS'] = '-t 5 -v 64000'
-				if request.check is 'error'
-					env['CHKTEX_EXIT_ON_ERROR'] =  1
-				if request.check is 'validate'
-					env['CHKTEX_VALIDATE'] =  1
-
-			# apply a series of file modifications/creations for draft mode and tikz
-			async.series [injectDraftModeIfRequired, createTikzFileIfRequired], (error) ->
-				return callback(error) if error?
-				timer = new Metrics.Timer("run-compile")
-				# find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite)
-				tag = request.imageName?.match(/:(.*)/)?[1]?.replace(/\./g,'-') or "default"
-				tag = "other" if not request.project_id.match(/^[0-9a-f]{24}$/) # exclude smoke test
-				Metrics.inc("compiles")
-				Metrics.inc("compiles-with-image.#{tag}")
-				compileName = getCompileName(request.project_id, request.user_id)
-				LatexRunner.runLatex compileName, {
-					directory: compileDir
-					mainFile:  request.rootResourcePath
-					compiler:  request.compiler
-					timeout:   request.timeout
-					image:     request.imageName
-					flags:     request.flags
-					environment: env
-				}, (error, output, stats, timings) ->
-					# request was for validation only
-					if request.check is "validate"
-						result = if error?.code then "fail" else "pass"
-						error = new Error("validation")
-						error.validate = result
-					# request was for compile, and failed on validation
-					if request.check is "error" and error?.message is 'exited'
-						error = new Error("compilation")
-						error.validate = "fail"
-					# compile was killed by user, was a validation, or a compile which failed validation
-					if error?.terminated or error?.validate or error?.timedout
-						OutputFileFinder.findOutputFiles resourceList, compileDir, (err, outputFiles) ->
-							return callback(err) if err?
-							error.outputFiles = outputFiles  # return output files so user can check logs
-							callback(error)
-						return
-					# compile completed normally
-					return callback(error) if error?
-					Metrics.inc("compiles-succeeded")
-					for metric_key, metric_value of stats or {}
-						Metrics.count(metric_key, metric_value)
-					for metric_key, metric_value of timings or {}
-						Metrics.timing(metric_key, metric_value)
-					loadavg = os.loadavg?()
-					Metrics.gauge("load-avg", loadavg[0]) if loadavg?
-					ts = timer.done()
-					logger.log {project_id: request.project_id, user_id: request.user_id, time_taken: ts, stats:stats, timings:timings, loadavg:loadavg}, "done compile"
-					if stats?["latex-runs"] > 0
-						Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"])
-					if stats?["latex-runs"] > 0 and timings?["cpu-time"] > 0
-						Metrics.timing("run-compile-cpu-time-per-pass", timings["cpu-time"] / stats["latex-runs"])
-
-					OutputFileFinder.findOutputFiles resourceList, compileDir, (error, outputFiles) ->
-						return callback(error) if error?
-						OutputCacheManager.saveOutputFiles outputFiles, compileDir,  (error, newOutputFiles) ->
-							callback null, newOutputFiles
-
-	stopCompile: (project_id, user_id, callback = (error) ->) ->
-		compileName = getCompileName(project_id, user_id)
-		LatexRunner.killLatex compileName, callback
-
-	clearProject: (project_id, user_id, _callback = (error) ->) ->
-		callback = (error) ->
-			_callback(error)
-			_callback = () ->
-
-		compileDir = getCompileDir(project_id, user_id)
-
-		CompileManager._checkDirectory compileDir, (err, exists) ->
-			return callback(err) if err?
-			return callback() if not exists # skip removal if no directory present
-
-			proc = child_process.spawn "rm", ["-r", compileDir]
-
-			proc.on "error", callback
-
-			stderr = ""
-			proc.stderr.on "data", (chunk) -> stderr += chunk.toString()
-
-			proc.on "close", (code) ->
-				if code == 0
-					return callback(null)
-				else
-					return callback(new Error("rm -r #{compileDir} failed: #{stderr}"))
-
-	_findAllDirs: (callback = (error, allDirs) ->) ->
-		root = Settings.path.compilesDir
-		fs.readdir root, (err, files) ->
-			return callback(err) if err?
-			allDirs = (Path.join(root, file) for file in files)
-			callback(null, allDirs)
-
-	clearExpiredProjects: (max_cache_age_ms, callback = (error) ->) ->
-		now = Date.now()
-		# action for each directory
-		expireIfNeeded = (checkDir, cb) ->
-			fs.stat checkDir, (err, stats) ->
-				return cb() if err?  # ignore errors checking directory
-				age = now - stats.mtime
-				hasExpired = (age > max_cache_age_ms)
-				if hasExpired then fse.remove(checkDir, cb) else cb()
-		# iterate over all project directories
-		CompileManager._findAllDirs (error, allDirs) ->
-			return callback() if error?
-			async.eachSeries allDirs, expireIfNeeded,	callback
-
-	_checkDirectory: (compileDir, callback = (error, exists) ->) ->
-		fs.lstat compileDir, (err, stats) ->
-			if err?.code is 'ENOENT'
-				return callback(null, false) #  directory does not exist
-			else if err?
-				logger.err {dir: compileDir, err:err}, "error on stat of project directory for removal"
-				return callback(err)
-			else if not stats?.isDirectory()
-				logger.err {dir: compileDir, stats:stats}, "bad project directory for removal"
-				return callback new Error("project directory is not directory")
-			else
-				callback(null, true) # directory exists
-
-	syncFromCode: (project_id, user_id, file_name, line, column, callback = (error, pdfPositions) ->) ->
-		# If LaTeX was run in a virtual environment, the file path that synctex expects
-		# might not match the file path on the host. The .synctex.gz file however, will be accessed
-		# wherever it is on the host.
-		compileName = getCompileName(project_id, user_id)
-		base_dir = Settings.path.synctexBaseDir(compileName)
-		file_path = base_dir + "/" + file_name
-		compileDir = getCompileDir(project_id, user_id)
-		synctex_path =  "#{base_dir}/output.pdf"
-		command = ["code", synctex_path, file_path, line, column]
-		fse.ensureDir compileDir, (error) ->
-			if error?
-				logger.err {error, project_id, user_id, file_name}, "error ensuring dir for sync from code"
-				return callback(error)
-			CompileManager._runSynctex project_id, user_id, command, (error, stdout) ->
-				return callback(error) if error?
-				logger.log project_id: project_id, user_id:user_id, file_name: file_name, line: line, column: column, command:command, stdout: stdout, "synctex code output"
-				callback null, CompileManager._parseSynctexFromCodeOutput(stdout)
-
-	syncFromPdf: (project_id, user_id, page, h, v, callback = (error, filePositions) ->) ->
-		compileName = getCompileName(project_id, user_id)
-		compileDir = getCompileDir(project_id, user_id)
-		base_dir = Settings.path.synctexBaseDir(compileName)
-		synctex_path =  "#{base_dir}/output.pdf"
-		command = ["pdf", synctex_path, page, h, v]
-		fse.ensureDir compileDir, (error) ->
-			if error?
-				logger.err {error, project_id, user_id, file_name}, "error ensuring dir for sync to code"
-				return callback(error)
-			CompileManager._runSynctex  project_id, user_id, command, (error, stdout) ->
-				return callback(error) if error?
-				logger.log project_id: project_id, user_id:user_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output"
-				callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)
-
-	_checkFileExists: (path, callback = (error) ->) ->
-		synctexDir = Path.dirname(path)
-		synctexFile = Path.join(synctexDir, "output.synctex.gz")
-		fs.stat synctexDir, (error, stats) ->
-			if error?.code is 'ENOENT'
-				return callback(new Errors.NotFoundError("called synctex with no output directory"))
-			return callback(error) if error?
-			fs.stat synctexFile, (error, stats) ->
-				if error?.code is 'ENOENT'
-					return callback(new Errors.NotFoundError("called synctex with no output file"))
-				return callback(error) if error?
-				return callback(new Error("not a file")) if not stats?.isFile()
-				callback()
-
-	_runSynctex: (project_id, user_id, command, callback = (error, stdout) ->) ->
-		seconds = 1000
-
-		command.unshift("/opt/synctex")
-
-		directory = getCompileDir(project_id, user_id)
-		timeout = 60 * 1000 # increased to allow for large projects
-		compileName = getCompileName(project_id, user_id)
-		CommandRunner.run compileName, command, directory, Settings.clsi?.docker.image, timeout, {}, (error, output) ->
-			if error?
-				logger.err err:error, command:command, project_id:project_id, user_id:user_id, "error running synctex"
-				return callback(error)
-			callback(null, output.stdout)
-
-	_parseSynctexFromCodeOutput: (output) ->
-		results = []
-		for line in output.split("\n")
-			[node, page, h, v, width, height] = line.split("\t")
-			if node == "NODE"
-				results.push {
-					page:   parseInt(page, 10)
-					h:      parseFloat(h)
-					v:      parseFloat(v)
-					height: parseFloat(height)
-					width:  parseFloat(width)
-				}
-		return results
-
-	_parseSynctexFromPdfOutput: (output, base_dir) ->
-		results = []
-		for line in output.split("\n")
-			[node, file_path, line, column] = line.split("\t")
-			if node == "NODE"
-				file = file_path.slice(base_dir.length + 1)
-				results.push {
-					file: file
-					line: parseInt(line, 10)
-					column: parseInt(column, 10)
-				}
-		return results
-
-
-	wordcount: (project_id, user_id, file_name, image, callback = (error, pdfPositions) ->) ->
-		logger.log project_id:project_id, user_id:user_id, file_name:file_name, image:image, "running wordcount"
-		file_path = "$COMPILE_DIR/" + file_name
-		command = [ "texcount", '-nocol', '-inc', file_path, "-out=" + file_path + ".wc"]
-		compileDir = getCompileDir(project_id, user_id)
-		timeout = 60 * 1000
-		compileName = getCompileName(project_id, user_id)
-		fse.ensureDir compileDir, (error) ->
-			if error?
-				logger.err {error, project_id, user_id, file_name}, "error ensuring dir for sync from code"
-				return callback(error)
-			CommandRunner.run compileName, command, compileDir, image, timeout, {}, (error) ->
-				return callback(error) if error?
-				fs.readFile compileDir + "/" + file_name + ".wc", "utf-8", (err, stdout) ->
-					if err?
-						#call it node_err so sentry doesn't use random path error as unique id so it can't be ignored
-						logger.err node_err:err, command:command, compileDir:compileDir, project_id:project_id, user_id:user_id, "error reading word count output"
-						return callback(err)
-					results = CompileManager._parseWordcountFromOutput(stdout)
-					logger.log project_id:project_id, user_id:user_id, wordcount: results, "word count results"
-					callback null, results
-
-	_parseWordcountFromOutput: (output) ->
-		results = {
-			encode: ""
-			textWords: 0
-			headWords: 0
-			outside: 0
-			headers: 0
-			elements: 0
-			mathInline: 0
-			mathDisplay: 0
-			errors: 0
-			messages: ""
-		}
-		for line in output.split("\n")
-			[data, info] = line.split(":")
-			if data.indexOf("Encoding") > -1
-				results['encode'] = info.trim()
-			if data.indexOf("in text") > -1
-				results['textWords'] = parseInt(info, 10)
-			if data.indexOf("in head") > -1
-				results['headWords'] = parseInt(info, 10)
-			if data.indexOf("outside") > -1
-				results['outside'] = parseInt(info, 10)
-			if data.indexOf("of head") > -1
-				results['headers'] = parseInt(info, 10)
-			if data.indexOf("Number of floats/tables/figures") > -1
-				results['elements'] = parseInt(info, 10)
-			if data.indexOf("Number of math inlines") > -1
-				results['mathInline'] = parseInt(info, 10)
-			if data.indexOf("Number of math displayed") > -1
-				results['mathDisplay'] = parseInt(info, 10)
-			if data is "(errors"  # errors reported as (errors:123)
-				results['errors'] = parseInt(info, 10)
-			if line.indexOf("!!! ") > -1  # errors logged as !!! message !!!
-				results['messages'] += line + "\n"
-		return results

+ 0 - 24
services/clsi/app/coffee/ContentTypeMapper.coffee

@@ -1,24 +0,0 @@
-Path = require 'path'
-
-# here we coerce html, css and js to text/plain,
-# otherwise choose correct mime type based on file extension,
-# falling back to octet-stream
-module.exports = ContentTypeMapper =
-	map: (path) ->
-		switch Path.extname(path)
-			when '.txt', '.html', '.js', '.css', '.svg'
-				return 'text/plain'
-			when '.csv'
-				return 'text/csv'
-			when '.pdf'
-				return 'application/pdf'
-			when '.png'
-				return 'image/png'
-			when '.jpg', '.jpeg'
-				return 'image/jpeg'
-			when '.tiff'
-				return 'image/tiff'
-			when '.gif'
-				return 'image/gif'
-			else
-				return 'application/octet-stream'

+ 0 - 13
services/clsi/app/coffee/DbQueue.coffee

@@ -1,13 +0,0 @@
-async = require "async"
-Settings = require "settings-sharelatex"
-logger = require("logger-sharelatex")
-queue = async.queue((task, cb)->
-		task(cb)
-	, Settings.parallelSqlQueryLimit)
-
-queue.drain = ()->
-	logger.debug('all items have been processed')
-
-module.exports = 
-	queue: queue
-

+ 0 - 56
services/clsi/app/coffee/DockerLockManager.coffee

@@ -1,56 +0,0 @@
-logger = require "logger-sharelatex"
-
-LockState = {} # locks for docker container operations, by container name
-
-module.exports = LockManager =
-
-	MAX_LOCK_HOLD_TIME: 15000  # how long we can keep a lock
-	MAX_LOCK_WAIT_TIME: 10000  # how long we wait for a lock
-	LOCK_TEST_INTERVAL: 1000   # retry time
-
-	tryLock: (key, callback = (err, gotLock) ->) ->
-		existingLock = LockState[key]
-		if existingLock? # the lock is already taken, check how old it is
-			lockAge = Date.now() - existingLock.created
-			if lockAge < LockManager.MAX_LOCK_HOLD_TIME
-				return callback(null, false) # we didn't get the lock, bail out
-			else
-				logger.error {key: key, lock: existingLock, age:lockAge}, "taking old lock by force"
-		# take the lock
-		LockState[key] = lockValue = {created: Date.now()}
-		callback(null, true, lockValue)
-
-	getLock: (key, callback = (error, lockValue) ->) ->
-		startTime = Date.now()
-		do attempt = () ->
-			LockManager.tryLock key, (error, gotLock, lockValue) ->
-				return callback(error) if error?
-				if gotLock
-					callback(null, lockValue)
-				else if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME
-					e = new Error("Lock timeout")
-					e.key = key
-					return callback(e)
-				else
-					setTimeout attempt, LockManager.LOCK_TEST_INTERVAL
-
-	releaseLock: (key, lockValue, callback = (error) ->) ->
-		existingLock = LockState[key]
-		if existingLock is lockValue # lockValue is an object, so we can test by reference
-			delete LockState[key] # our lock, so we can free it
-			callback()
-		else if existingLock? # lock exists but doesn't match ours
-			logger.error {key:key, lock: existingLock}, "tried to release lock taken by force"
-			callback()
-		else
-			logger.error {key:key, lock: existingLock}, "tried to release lock that has gone"
-			callback()
-
-	runWithLock: (key, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) ->
-		LockManager.getLock key, (error, lockValue) ->
-			return callback(error) if error?
-			runner (error1, args...) ->
-				LockManager.releaseLock key, lockValue, (error2) ->
-					error = error1 or error2
-					return callback(error) if error?
-					callback(null, args...)

+ 0 - 360
services/clsi/app/coffee/DockerRunner.coffee

@@ -1,360 +0,0 @@
-Settings = require "settings-sharelatex"
-logger = require "logger-sharelatex"
-Docker = require("dockerode")
-dockerode = new Docker()
-crypto = require "crypto"
-async = require "async"
-LockManager = require "./DockerLockManager"
-fs = require "fs"
-Path = require 'path'
-_ = require "underscore"
-
-logger.info "using docker runner"
-
-usingSiblingContainers = () ->
-	Settings?.path?.sandboxedCompilesHostDir?
-
-module.exports = DockerRunner =
-	ERR_NOT_DIRECTORY: new Error("not a directory")
-	ERR_TERMINATED: new Error("terminated")
-	ERR_EXITED: new Error("exited")
-	ERR_TIMED_OUT: new Error("container timed out")
-
-	run: (project_id, command, directory, image, timeout, environment, callback = (error, output) ->) ->
-
-		if usingSiblingContainers()
-			_newPath = Settings.path.sandboxedCompilesHostDir
-			logger.log {path: _newPath}, "altering bind path for sibling containers"
-			# Server Pro, example:
-			#   '/var/lib/sharelatex/data/compiles/<project-id>'
-			#   ... becomes ...
-			#   '/opt/sharelatex_data/data/compiles/<project-id>'
-			directory = Path.join(Settings.path.sandboxedCompilesHostDir, Path.basename(directory))
-
-		volumes = {}
-		volumes[directory] = "/compile"
-
-		command = (arg.toString().replace?('$COMPILE_DIR', "/compile") for arg in command)
-		if !image?
-			image = Settings.clsi.docker.image
-
-		if Settings.texliveImageNameOveride?
-			img = image.split("/")
-			image = "#{Settings.texliveImageNameOveride}/#{img[2]}"
-
-		options      = DockerRunner._getContainerOptions(command, image, volumes, timeout, environment)
-		fingerprint  = DockerRunner._fingerprintContainer(options)
-		options.name = name = "project-#{project_id}-#{fingerprint}"
-
-		# logOptions = _.clone(options)
-		# logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging"
-		logger.log project_id: project_id, "running docker container"
-		DockerRunner._runAndWaitForContainer options, volumes, timeout, (error, output) ->
-			if error?.message?.match("HTTP code is 500")
-				logger.log err: error, project_id: project_id, "error running container so destroying and retrying"
-				DockerRunner.destroyContainer name, null, true, (error) ->
-					return callback(error) if error?
-					DockerRunner._runAndWaitForContainer options, volumes, timeout, callback
-			else
-				callback(error, output)
-
-		return name # pass back the container name to allow it to be killed
-
-	kill: (container_id, callback = (error) ->) ->
-		logger.log container_id: container_id, "sending kill signal to container"
-		container = dockerode.getContainer(container_id)
-		container.kill (error) ->
-			if error? and error?.message?.match?(/Cannot kill container .* is not running/)
-				logger.warn err: error, container_id: container_id, "container not running, continuing"
-				error = null
-			if error?
-				logger.error err: error, container_id: container_id, "error killing container"
-				return callback(error)
-			else
-				callback()
-
-	_runAndWaitForContainer: (options, volumes, timeout, _callback = (error, output) ->) ->
-		callback = (args...) ->
-			_callback(args...)
-			# Only call the callback once
-			_callback = () ->
-
-		name = options.name
-
-		streamEnded = false
-		containerReturned = false
-		output = {}
-
-		callbackIfFinished = () ->
-			if streamEnded and containerReturned
-				callback(null, output)
-
-		attachStreamHandler = (error, _output) ->
-			return callback(error) if error?
-			output = _output
-			streamEnded = true
-			callbackIfFinished()
-
-		DockerRunner.startContainer options, volumes, attachStreamHandler, (error, containerId) ->
-			return callback(error) if error?
-			
-			DockerRunner.waitForContainer name, timeout, (error, exitCode) ->
-				return callback(error) if error?
-				if exitCode is 137  # exit status from kill -9
-					err = DockerRunner.ERR_TERMINATED
-					err.terminated = true
-					return callback(err)
-				if exitCode is 1 # exit status from chktex
-					err = DockerRunner.ERR_EXITED
-					err.code = exitCode
-					return callback(err)
-				containerReturned = true
-				options?.HostConfig?.SecurityOpt = null #small log line
-				logger.log err:err, exitCode:exitCode, options:options, "docker container has exited"
-				callbackIfFinished()
-
-	_getContainerOptions: (command, image, volumes, timeout, environment) ->
-		timeoutInSeconds = timeout / 1000
-
-		dockerVolumes = {}
-		for hostVol, dockerVol of volumes
-			dockerVolumes[dockerVol] = {}
-
-			if volumes[hostVol].slice(-3).indexOf(":r") == -1
-				volumes[hostVol] = "#{dockerVol}:rw"
-
-		# merge settings and environment parameter
-		env = {}
-		for src in [Settings.clsi.docker.env, environment or {}]
-			env[key] = value for key, value of src
-		# set the path based on the image year
-		if m = image.match /:([0-9]+)\.[0-9]+/
-			year = m[1]
-		else
-			year = "2014"
-		env['PATH'] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/#{year}/bin/x86_64-linux/"
-		options =
-			"Cmd"             : command,
-			"Image"           : image
-			"Volumes"         : dockerVolumes
-			"WorkingDir"      : "/compile"
-			"NetworkDisabled" : true
-			"Memory"          : 1024 * 1024 * 1024 * 1024 # 1 Gb
-			"User"            : Settings.clsi.docker.user
-			"Env"             : ("#{key}=#{value}" for key, value of env)	# convert the environment hash to an array
-			"HostConfig"      :
-				"Binds": ("#{hostVol}:#{dockerVol}" for hostVol, dockerVol of volumes)
-				"LogConfig": {"Type": "none", "Config": {}}
-				"Ulimits": [{'Name': 'cpu', 'Soft': timeoutInSeconds+5, 'Hard': timeoutInSeconds+10}]
-				"CapDrop": "ALL"
-				"SecurityOpt": ["no-new-privileges"]
-
-		if Settings.path?.synctexBinHostPath?
-			options["HostConfig"]["Binds"].push("#{Settings.path.synctexBinHostPath}:/opt/synctex:ro")
-
-		if Settings.clsi.docker.runtime?
-			options["HostConfig"]["Runtime"] = Settings.clsi.docker.runtime
-
-		if Settings.clsi.docker.seccomp_profile?
-			options.HostConfig.SecurityOpt.push "seccomp=#{Settings.clsi.docker.seccomp_profile}"
-
-		return options
-
-	_fingerprintContainer: (containerOptions) ->
-		# Yay, Hashing!
-		json = JSON.stringify(containerOptions)
-		return crypto.createHash("md5").update(json).digest("hex")
-
-	startContainer: (options, volumes, attachStreamHandler, callback) ->
-		LockManager.runWithLock options.name, (releaseLock) ->
-			# Check that volumes exist before starting the container.
-			# When a container is started with volume pointing to a
-			# non-existent directory then docker creates the directory but
-			# with root ownership.
-			DockerRunner._checkVolumes options, volumes, (err) ->
-				return releaseLock(err) if err?
-				DockerRunner._startContainer options, volumes, attachStreamHandler, releaseLock
-		, callback
-
-	# Check that volumes exist and are directories
-	_checkVolumes: (options, volumes, callback = (error, containerName) ->) ->
-		if usingSiblingContainers()
-			# Server Pro, with sibling-containers active, skip checks
-			return callback(null)
-
-		checkVolume = (path, cb) ->
-			fs.stat path, (err, stats) ->
-				return cb(err) if err?
-				return cb(DockerRunner.ERR_NOT_DIRECTORY) if not stats?.isDirectory()
-				cb()
-		jobs = []
-		for vol of volumes
-			do (vol) ->
-				jobs.push (cb) -> checkVolume(vol, cb)
-		async.series jobs, callback
-
-	_startContainer: (options, volumes, attachStreamHandler, callback = ((error, output) ->)) ->
-		callback = _.once(callback)
-		name = options.name
-
-		logger.log {container_name: name}, "starting container"
-		container = dockerode.getContainer(name)
-
-		createAndStartContainer = ->
-			dockerode.createContainer options, (error, container) ->
-				return callback(error) if error?
-				startExistingContainer()
-
-		startExistingContainer = ->
-			DockerRunner.attachToContainer options.name, attachStreamHandler, (error)->
-				return callback(error) if error?
-				container.start (error) ->
-					if error? and error?.statusCode != 304 #already running
-						return callback(error)
-					else
-						callback()
-
-		container.inspect (error, stats)->
-			if error?.statusCode == 404
-				createAndStartContainer()
-			else if error?
-				logger.err {container_name: name, error:error}, "unable to inspect container to start"
-				return callback(error)
-			else
-				startExistingContainer()
-
-
-	attachToContainer: (containerId, attachStreamHandler, attachStartCallback) ->
-		container = dockerode.getContainer(containerId)
-		container.attach {stdout: 1, stderr: 1, stream: 1}, (error, stream) ->
-			if error?
-				logger.error err: error, container_id: containerId, "error attaching to container"
-				return attachStartCallback(error)
-			else
-				attachStartCallback()
-
-
-			logger.log container_id: containerId, "attached to container"
-
-			MAX_OUTPUT = 1024 * 1024 # limit output to 1MB
-			createStringOutputStream = (name) ->
-				return {
-					data: ""
-					overflowed: false
-					write: (data) ->
-						return if @overflowed
-						if @data.length < MAX_OUTPUT
-							@data += data
-						else
-							logger.error container_id: containerId, length: @data.length, maxLen: MAX_OUTPUT, "#{name} exceeds max size"
-							@data += "(...truncated at #{MAX_OUTPUT} chars...)"
-							@overflowed = true
-							# kill container if too much output
-							# docker.containers.kill(containerId, () ->)
-				}
-
-			stdout = createStringOutputStream "stdout"
-			stderr = createStringOutputStream "stderr"
-
-			container.modem.demuxStream(stream, stdout, stderr)
-
-			stream.on "error", (err) ->
-				logger.error err: err, container_id: containerId, "error reading from container stream"
-
-			stream.on "end", () ->
-				attachStreamHandler null, {stdout: stdout.data, stderr: stderr.data}
-
-	waitForContainer: (containerId, timeout, _callback = (error, exitCode) ->) ->
-		callback = (args...) ->
-			_callback(args...)
-			# Only call the callback once
-			_callback = () ->
-
-		container = dockerode.getContainer(containerId)
-
-		timedOut = false
-		timeoutId = setTimeout () ->
-			timedOut = true
-			logger.log container_id: containerId, "timeout reached, killing container"
-			container.kill(() ->)
-		, timeout
-
-		logger.log container_id: containerId, "waiting for docker container"
-		container.wait (error, res) ->
-			if error?
-				clearTimeout timeoutId
-				logger.error err: error, container_id: containerId, "error waiting for container"
-				return callback(error)
-			if timedOut
-				logger.log containerId: containerId, "docker container timed out"
-				error = DockerRunner.ERR_TIMED_OUT
-				error.timedout = true
-				callback error
-			else
-				clearTimeout timeoutId
-				logger.log container_id: containerId, exitCode: res.StatusCode, "docker container returned"
-				callback null, res.StatusCode
-
-	destroyContainer: (containerName, containerId, shouldForce, callback = (error) ->) ->
-		# We want the containerName for the lock and, ideally, the
-		# containerId to delete.  There is a bug in the docker.io module
-		# where if you delete by name and there is an error, it throws an
-		# async exception, but if you delete by id it just does a normal
-		# error callback. We fall back to deleting by name if no id is
-		# supplied.
-		LockManager.runWithLock containerName, (releaseLock) ->
-			DockerRunner._destroyContainer containerId or containerName, shouldForce, releaseLock
-		, callback
-
-	_destroyContainer: (containerId, shouldForce, callback = (error) ->) ->
-		logger.log container_id: containerId, "destroying docker container"
-		container = dockerode.getContainer(containerId)
-		container.remove {force: shouldForce == true}, (error) ->
-			if error? and error?.statusCode == 404
-				logger.warn err: error, container_id: containerId, "container not found, continuing"
-				error = null
-			if error?
-				logger.error err: error, container_id: containerId, "error destroying container"
-			else
-				logger.log container_id: containerId, "destroyed container"
-			callback(error)
-
-	# handle expiry of docker containers
-
-	MAX_CONTAINER_AGE: Settings.clsi.docker.maxContainerAge or oneHour = 60 * 60 * 1000
-
-	examineOldContainer: (container, callback = (error, name, id, ttl)->) ->
-		name = container.Name or container.Names?[0]
-		created = container.Created * 1000 # creation time is returned in seconds
-		now = Date.now()
-		age = now - created
-		maxAge = DockerRunner.MAX_CONTAINER_AGE
-		ttl = maxAge - age
-		logger.log {containerName: name, created: created, now: now, age: age, maxAge: maxAge, ttl: ttl}, "checking whether to destroy container"
-		callback(null, name, container.Id, ttl)
-
-	destroyOldContainers: (callback = (error) ->) ->
-		dockerode.listContainers all: true, (error, containers) ->
-			return callback(error) if error?
-			jobs = []
-			for container in containers or []
-				do (container) ->
-					DockerRunner.examineOldContainer container, (err, name, id, ttl) ->
-						if name.slice(0, 9) == '/project-' && ttl <= 0
-							jobs.push	(cb) ->
-								DockerRunner.destroyContainer name, id, false, () -> cb()
-								# Ignore errors because some containers get stuck but
-								# will be destroyed next time
-			async.series jobs, callback
-
-	startContainerMonitor: () ->
-		logger.log {maxAge: DockerRunner.MAX_CONTAINER_AGE}, "starting container expiry"
-		# randomise the start time
-		randomDelay = Math.floor(Math.random() * 5 * 60 * 1000)
-		setTimeout () ->
-			setInterval () ->
-				DockerRunner.destroyOldContainers()
-			, oneHour = 60 * 60 * 1000
-		, randomDelay
-
-DockerRunner.startContainerMonitor()

+ 0 - 24
services/clsi/app/coffee/DraftModeManager.coffee

@@ -1,24 +0,0 @@
-fs = require "fs"
-logger = require "logger-sharelatex"
-
-module.exports = DraftModeManager =
-	injectDraftMode: (filename, callback = (error) ->) ->
-		fs.readFile filename, "utf8", (error, content) ->
-			return callback(error) if error?
-			# avoid adding draft mode more than once
-			if content?.indexOf("\\documentclass\[draft") >= 0
-				return callback()
-			modified_content = DraftModeManager._injectDraftOption content
-			logger.log {
-				content: content.slice(0,1024), # \documentclass is normally v near the top
-				modified_content: modified_content.slice(0,1024),
-				filename
-			}, "injected draft class"
-			fs.writeFile filename, modified_content, callback
-	
-	_injectDraftOption: (content) ->
-		content
-			# With existing options (must be first, otherwise both are applied)
-			.replace(/\\documentclass\[/g, "\\documentclass[draft,")
-			# Without existing options
-			.replace(/\\documentclass\{/g, "\\documentclass[draft]{")

+ 0 - 25
services/clsi/app/coffee/Errors.coffee

@@ -1,25 +0,0 @@
-NotFoundError = (message) ->
-	error = new Error(message)
-	error.name = "NotFoundError"
-	error.__proto__ = NotFoundError.prototype
-	return error
-NotFoundError.prototype.__proto__ = Error.prototype
-
-FilesOutOfSyncError = (message) ->
-	error = new Error(message)
-	error.name = "FilesOutOfSyncError"
-	error.__proto__ = FilesOutOfSyncError.prototype
-	return error
-FilesOutOfSyncError.prototype.__proto__ = Error.prototype
-
-AlreadyCompilingError = (message) ->
-	error = new Error(message)
-	error.name = "AlreadyCompilingError"
-	error.__proto__ = AlreadyCompilingError.prototype
-	return error
-AlreadyCompilingError.prototype.__proto__ = Error.prototype
-
-module.exports = Errors =
-	NotFoundError: NotFoundError
-	FilesOutOfSyncError: FilesOutOfSyncError
-	AlreadyCompilingError: AlreadyCompilingError

+ 0 - 95
services/clsi/app/coffee/LatexRunner.coffee

@@ -1,95 +0,0 @@
-Path = require "path"
-Settings = require "settings-sharelatex"
-logger = require "logger-sharelatex"
-Metrics = require "./Metrics"
-CommandRunner = require "./CommandRunner"
-
-ProcessTable = {}  # table of currently running jobs (pids or docker container names)
-
-module.exports = LatexRunner =
-	runLatex: (project_id, options, callback = (error) ->) ->
-		{directory, mainFile, compiler, timeout, image, environment, flags} = options
-		compiler ||= "pdflatex"
-		timeout  ||= 60000 # milliseconds
-
-		logger.log directory: directory, compiler: compiler, timeout: timeout, mainFile: mainFile, environment: environment, flags:flags, "starting compile"
-
-		# We want to run latexmk on the tex file which we will automatically
-		# generate from the Rtex/Rmd/md file.
-		mainFile = mainFile.replace(/\.(Rtex|md|Rmd)$/, ".tex")
-
-		if compiler == "pdflatex"
-			command = LatexRunner._pdflatexCommand mainFile, flags
-		else if compiler == "latex"
-			command = LatexRunner._latexCommand mainFile, flags
-		else if compiler == "xelatex"
-			command = LatexRunner._xelatexCommand mainFile, flags
-		else if compiler == "lualatex"
-			command = LatexRunner._lualatexCommand mainFile, flags
-		else
-			return callback new Error("unknown compiler: #{compiler}")
-
-		if Settings.clsi?.strace
-			command = ["strace", "-o", "strace", "-ff"].concat(command)
-
-		id = "#{project_id}" # record running project under this id
-
-		ProcessTable[id] = CommandRunner.run project_id, command, directory, image, timeout, environment, (error, output) ->
-			delete ProcessTable[id]
-			return callback(error) if error?
-			runs = output?.stderr?.match(/^Run number \d+ of .*latex/mg)?.length or 0
-			failed = if output?.stdout?.match(/^Latexmk: Errors/m)? then 1 else 0
-			# counters from latexmk output
-			stats = {}
-			stats["latexmk-errors"] = failed
-			stats["latex-runs"] = runs
-			stats["latex-runs-with-errors"] = if failed then runs else 0
-			stats["latex-runs-#{runs}"] = 1
-			stats["latex-runs-with-errors-#{runs}"] = if failed then 1 else 0
-			# timing information from /usr/bin/time
-			timings = {}
-			stderr = output?.stderr
-			timings["cpu-percent"] = stderr?.match(/Percent of CPU this job got: (\d+)/m)?[1] or 0
-			timings["cpu-time"] = stderr?.match(/User time.*: (\d+.\d+)/m)?[1] or 0
-			timings["sys-time"] = stderr?.match(/System time.*: (\d+.\d+)/m)?[1] or 0
-			callback error, output, stats, timings
-
-	killLatex: (project_id, callback = (error) ->) ->
-		id = "#{project_id}"
-		logger.log {id:id}, "killing running compile"
-		if not ProcessTable[id]?
-			logger.warn {id}, "no such project to kill"
-			return callback(null)
-		else
-			CommandRunner.kill ProcessTable[id], callback
-
-	_latexmkBaseCommand: (flags) ->
-		args = ["latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR", "-synctex=1","-interaction=batchmode"]
-		if flags
-			args = args.concat(flags)
-		(Settings?.clsi?.latexmkCommandPrefix || []).concat(args)
-
-	_pdflatexCommand: (mainFile, flags) ->
-		LatexRunner._latexmkBaseCommand(flags).concat [
-			"-pdf",
-			Path.join("$COMPILE_DIR", mainFile)
-		]
-
-	_latexCommand: (mainFile, flags) ->
-		LatexRunner._latexmkBaseCommand(flags).concat [
-			"-pdfdvi",
-			Path.join("$COMPILE_DIR", mainFile)
-		]
-
-	_xelatexCommand: (mainFile, flags) ->
-		LatexRunner._latexmkBaseCommand(flags).concat [
-			"-xelatex",
-			Path.join("$COMPILE_DIR", mainFile)
-		]
-
-	_lualatexCommand: (mainFile, flags) ->
-		LatexRunner._latexmkBaseCommand(flags).concat [
-			"-lualatex",
-			Path.join("$COMPILE_DIR", mainFile)
-		]
-

+ 0 - 48
services/clsi/app/coffee/LocalCommandRunner.coffee

@@ -1,48 +0,0 @@
-spawn = require("child_process").spawn
-logger = require "logger-sharelatex"
-
-logger.info "using standard command runner"
-
-module.exports = CommandRunner =
-	run: (project_id, command, directory, image, timeout, environment, callback = (error) ->) ->
-		command = (arg.toString().replace('$COMPILE_DIR', directory) for arg in command)
-		logger.log project_id: project_id, command: command, directory: directory, "running command"
-		logger.warn "timeouts and sandboxing are not enabled with CommandRunner"
-
-		# merge environment settings
-		env = {}
-		env[key] = value for key, value of process.env
-		env[key] = value for key, value of environment
-
-		# run command as detached process so it has its own process group (which can be killed if needed)
-		proc = spawn command[0], command.slice(1), cwd: directory, env: env
-
-		stdout = ""
-		proc.stdout.on "data", (data)->
-			stdout += data
-
-		proc.on "error", (err)->
-			logger.err err:err, project_id:project_id, command: command, directory: directory, "error running command"
-			callback(err)
-
-		proc.on "close", (code, signal) ->
-			logger.info code:code, signal:signal, project_id:project_id, "command exited"
-			if signal is 'SIGTERM' # signal from kill method below
-				err = new Error("terminated")
-				err.terminated = true
-				return callback(err)
-			else if code is 1 # exit status from chktex
-				err = new Error("exited")
-				err.code = code
-				return callback(err)
-			else
-				callback(null, {"stdout": stdout})
-
-		return proc.pid # return process id to allow job to be killed if necessary
-
-	kill: (pid, callback = (error) ->) ->
-		try
-			process.kill -pid # kill all processes in group
-		catch err
-			return callback(err)
-		callback()

+ 0 - 31
services/clsi/app/coffee/LockManager.coffee

@@ -1,31 +0,0 @@
-Settings = require('settings-sharelatex')
-logger = require "logger-sharelatex"
-Lockfile = require('lockfile') # from https://github.com/npm/lockfile
-Errors = require "./Errors"
-fs = require("fs")
-Path = require("path")
-module.exports = LockManager =
-	LOCK_TEST_INTERVAL: 1000 # 50ms between each test of the lock
-	MAX_LOCK_WAIT_TIME: 15000 # 10s maximum time to spend trying to get the lock
-	LOCK_STALE: 5*60*1000 # 5 mins time until lock auto expires
-
-	runWithLock: (path, runner = ((releaseLock = (error) ->) ->), callback = ((error) ->)) ->
-		lockOpts =
-			wait: @MAX_LOCK_WAIT_TIME
-			pollPeriod: @LOCK_TEST_INTERVAL
-			stale: @LOCK_STALE
-		Lockfile.lock path, lockOpts, (error) ->
-			if error?.code is 'EEXIST'
-				return callback new Errors.AlreadyCompilingError("compile in progress")
-			else if error?
-				fs.lstat path, (statLockErr, statLock)->
-					fs.lstat Path.dirname(path), (statDirErr, statDir)->
-						fs.readdir Path.dirname(path), (readdirErr, readdirDir)->
-							logger.err error:error, path:path, statLock:statLock, statLockErr:statLockErr, statDir:statDir, statDirErr: statDirErr, readdirErr:readdirErr, readdirDir:readdirDir, "unable to get lock"
-							return callback(error)
-			else
-				runner (error1, args...) ->
-					Lockfile.unlock path, (error2) ->
-						error = error1 or error2
-						return callback(error) if error?
-						callback(null, args...)

+ 0 - 2
services/clsi/app/coffee/Metrics.coffee

@@ -1,2 +0,0 @@
-module.exports = require "metrics-sharelatex"
-

+ 0 - 199
services/clsi/app/coffee/OutputCacheManager.coffee

@@ -1,199 +0,0 @@
-async = require "async"
-fs = require "fs"
-fse = require "fs-extra"
-Path = require "path"
-logger = require "logger-sharelatex"
-_ = require "underscore"
-Settings = require "settings-sharelatex"
-crypto = require "crypto"
-
-OutputFileOptimiser = require "./OutputFileOptimiser"
-
-module.exports = OutputCacheManager =
-	CACHE_SUBDIR: '.cache/clsi'
-	ARCHIVE_SUBDIR: '.archive/clsi'
-	# build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes
-	# for backwards compatibility, make the randombytes part optional
-	BUILD_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/
-	CACHE_LIMIT: 2  # maximum number of cache directories
-	CACHE_AGE: 60*60*1000 # up to one hour old
-
-	path: (buildId, file) ->
-		# used by static server, given build id return '.cache/clsi/buildId'
-		if buildId.match OutputCacheManager.BUILD_REGEX
-			return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId, file)
-		else
-			# for invalid build id, return top level
-			return file
-
-	generateBuildId: (callback = (error, buildId) ->) ->
-		# generate a secure build id from Date.now() and 8 random bytes in hex
-		crypto.randomBytes 8, (err, buf) ->
-			return callback(err) if err?
-			random = buf.toString('hex')
-			date = Date.now().toString(16)
-			callback err, "#{date}-#{random}"
-
-	saveOutputFiles: (outputFiles, compileDir, callback = (error) ->) ->
-		OutputCacheManager.generateBuildId (err, buildId) ->
-			return callback(err) if err?
-			OutputCacheManager.saveOutputFilesInBuildDir outputFiles, compileDir, buildId, callback
-
-	saveOutputFilesInBuildDir: (outputFiles, compileDir, buildId, callback = (error) ->) ->
-		# make a compileDir/CACHE_SUBDIR/build_id directory and
-		# copy all the output files into it
-		cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR)
-		# Put the files into a new cache subdirectory
-		cacheDir = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR, buildId)
-		# Is it a per-user compile? check if compile directory is PROJECTID-USERID
-		perUser = Path.basename(compileDir).match(/^[0-9a-f]{24}-[0-9a-f]{24}$/)
-
-		# Archive logs in background
-		if Settings.clsi?.archive_logs or Settings.clsi?.strace
-			OutputCacheManager.archiveLogs outputFiles, compileDir, buildId, (err) ->
-				if err?
-					logger.warn err:err, "erroring archiving log files"
-
-		# make the new cache directory
-		fse.ensureDir cacheDir, (err) ->
-			if err?
-				logger.error err: err, directory: cacheDir, "error creating cache directory"
-				callback(err, outputFiles)
-			else
-				# copy all the output files into the new cache directory
-				results = []
-				async.mapSeries outputFiles, (file, cb) ->
-					# don't send dot files as output, express doesn't serve them
-					if OutputCacheManager._fileIsHidden(file.path)
-						logger.debug compileDir: compileDir, path: file.path, "ignoring dotfile in output"
-						return cb()
-					# copy other files into cache directory if valid
-					newFile = _.clone(file)
-					[src, dst] = [Path.join(compileDir, file.path), Path.join(cacheDir, file.path)]
-					OutputCacheManager._checkFileIsSafe src, (err, isSafe) ->
-						return cb(err) if err?
-						if !isSafe
-							return cb()
-						OutputCacheManager._checkIfShouldCopy src, (err, shouldCopy) ->
-							return cb(err) if err?
-							if !shouldCopy
-								return cb()
-							OutputCacheManager._copyFile src, dst, (err) ->
-								return cb(err) if err?
-								newFile.build = buildId  # attach a build id if we cached the file
-								results.push newFile
-								cb()
-				, (err) ->
-					if err?
-						# pass back the original files if we encountered *any* error
-						callback(err, outputFiles)
-						# clean up the directory we just created
-						fse.remove cacheDir, (err) ->
-							if err?
-								logger.error err: err, dir: cacheDir, "error removing cache dir after failure"
-					else
-						# pass back the list of new files in the cache
-						callback(err, results)
-						# let file expiry run in the background, expire all previous files if per-user
-						OutputCacheManager.expireOutputFiles cacheRoot, {keep: buildId, limit: if perUser then 1 else null}
-
-	archiveLogs: (outputFiles, compileDir, buildId, callback = (error) ->) ->
-		archiveDir = Path.join(compileDir, OutputCacheManager.ARCHIVE_SUBDIR, buildId)
-		logger.log {dir: archiveDir}, "archiving log files for project"
-		fse.ensureDir archiveDir, (err) ->
-			return callback(err) if err?
-			async.mapSeries outputFiles, (file, cb) ->
-				[src, dst] = [Path.join(compileDir, file.path), Path.join(archiveDir, file.path)]
-				OutputCacheManager._checkFileIsSafe src, (err, isSafe) ->
-					return cb(err) if err?
-					return cb() if !isSafe
-					OutputCacheManager._checkIfShouldArchive src, (err, shouldArchive) ->
-						return cb(err) if err?
-						return cb() if !shouldArchive
-						OutputCacheManager._copyFile src, dst, cb
-			, callback
-
-	expireOutputFiles: (cacheRoot, options, callback = (error) ->) ->
-		# look in compileDir for build dirs and delete if > N or age of mod time > T
-		fs.readdir cacheRoot, (err, results) ->
-			if err?
-				return callback(null) if err.code == 'ENOENT'	# cache directory is empty
-				logger.error err: err, project_id: cacheRoot, "error clearing cache"
-				return callback(err)
-
-			dirs =  results.sort().reverse()
-			currentTime = Date.now()
-
-			isExpired = (dir, index) ->
-				return false if options?.keep == dir
-				# remove any directories over the requested (non-null) limit
-				return true if options?.limit? and index > options.limit
-				# remove any directories over the hard limit
-				return true if index > OutputCacheManager.CACHE_LIMIT
-				# we can get the build time from the first part of the directory name DDDD-RRRR
-				# DDDD is date and RRRR is random bytes
-				dirTime = parseInt(dir.split('-')?[0], 16)
-				age = currentTime - dirTime
-				return age > OutputCacheManager.CACHE_AGE
-
-			toRemove = _.filter(dirs, isExpired)
-
-			removeDir = (dir, cb) ->
-				fse.remove Path.join(cacheRoot, dir), (err, result) ->
-					logger.log cache: cacheRoot, dir: dir, "removed expired cache dir"
-					if err?
-						logger.error err: err, dir: dir, "cache remove error"
-					cb(err, result)
-
-			async.eachSeries toRemove, (dir, cb) ->
-				removeDir dir, cb
-			, callback
-
-	_fileIsHidden: (path) ->
-		return path?.match(/^\.|\/\./)?
-
-	_checkFileIsSafe: (src, callback = (error, isSafe) ->) ->
-		# check if we have a valid file to copy into the cache
-		fs.stat src, (err, stats) ->
-			if err?.code is 'ENOENT'
-				logger.warn err: err, file: src, "file has disappeared before copying to build cache"
-				callback(err, false)
-			else if err?
-				# some other problem reading the file
-				logger.error err: err, file: src, "stat error for file in cache"
-				callback(err, false)
-			else if not stats.isFile()
-				# other filetype - reject it
-				logger.warn src: src, stat: stats, "nonfile output - refusing to copy to cache"
-				callback(null, false)
-			else
-				# it's a plain file, ok to copy
-				callback(null, true)
-
-	_copyFile: (src, dst, callback) ->
-		# copy output file into the cache
-		fse.copy src, dst, (err) ->
-			if err?.code is 'ENOENT'
-				logger.warn err: err, file: src, "file has disappeared when copying to build cache"
-				callback(err, false)
-			else if err?
-				logger.error err: err, src: src, dst: dst, "copy error for file in cache"
-				callback(err)
-			else
-				if Settings.clsi?.optimiseInDocker
-					# don't run any optimisations on the pdf when they are done
-					# in the docker container
-					callback()
-				else
-					# call the optimiser for the file too
-					OutputFileOptimiser.optimiseFile src, dst, callback
-
-	_checkIfShouldCopy: (src, callback = (err, shouldCopy) ->) ->
-		return callback(null, !Path.basename(src).match(/^strace/))
-
-	_checkIfShouldArchive: (src, callback = (err, shouldCopy) ->) ->
-		if Path.basename(src).match(/^strace/)
-			return callback(null, true)
-		if Settings.clsi?.archive_logs and Path.basename(src) in ["output.log", "output.blg"]
-			return callback(null, true)
-		return callback(null, false)

+ 0 - 50
services/clsi/app/coffee/OutputFileFinder.coffee

@@ -1,50 +0,0 @@
-async = require "async"
-fs = require "fs"
-Path = require "path"
-spawn = require("child_process").spawn
-logger = require "logger-sharelatex"
-
-module.exports = OutputFileFinder =
-	findOutputFiles: (resources, directory, callback = (error, outputFiles, allFiles) ->) ->
-		incomingResources = {}
-		for resource in resources
-			incomingResources[resource.path] = true
-			
-		OutputFileFinder._getAllFiles directory, (error, allFiles = []) ->
-			if error?
-				logger.err err:error, "error finding all output files"
-				return callback(error)
-			outputFiles = []
-			for file in allFiles
-				if !incomingResources[file]
-					outputFiles.push {
-						path: file
-						type: file.match(/\.([^\.]+)$/)?[1]
-					}
-			callback null, outputFiles, allFiles
-
-	_getAllFiles: (directory, _callback = (error, fileList) ->) ->
-		callback = (error, fileList) ->
-			_callback(error, fileList)
-			_callback = () ->
-
-		# don't include clsi-specific files/directories in the output list
-		EXCLUDE_DIRS = ["-name", ".cache", "-o", "-name", ".archive","-o", "-name", ".project-*"]
-		args = [directory, "(", EXCLUDE_DIRS..., ")", "-prune", "-o", "-type", "f", "-print"]
-		logger.log args: args, "running find command"
-
-		proc = spawn("find", args)
-		stdout = ""
-		proc.stdout.on "data", (chunk) ->
-			stdout += chunk.toString()	
-		proc.on "error", callback	
-		proc.on "close", (code) ->
-			if code != 0
-				logger.warn {directory, code}, "find returned error, directory likely doesn't exist"
-				return callback null, []
-			fileList = stdout.trim().split("\n")
-			fileList = fileList.map (file) ->
-				# Strip leading directory
-				path = Path.relative(directory, file)
-			return callback null, fileList
-

+ 0 - 55
services/clsi/app/coffee/OutputFileOptimiser.coffee

@@ -1,55 +0,0 @@
-fs = require "fs"
-Path = require "path"
-spawn = require("child_process").spawn
-logger = require "logger-sharelatex"
-Metrics = require "./Metrics"
-_ = require "underscore"
-
-module.exports = OutputFileOptimiser =
-
-	optimiseFile: (src, dst, callback = (error) ->) ->
-		# check output file (src) and see if we can optimise it, storing
-		# the result in the build directory (dst)
-		if src.match(/\/output\.pdf$/)
-			OutputFileOptimiser.checkIfPDFIsOptimised src, (err, isOptimised) ->
-				return callback(null) if err? or isOptimised
-				OutputFileOptimiser.optimisePDF src, dst, callback
-		else
-			callback (null)
-
-	checkIfPDFIsOptimised: (file, callback) ->
-		SIZE = 16*1024 # check the header of the pdf
-		result = new Buffer(SIZE)
-		result.fill(0) # prevent leakage of uninitialised buffer
-		fs.open file, "r", (err, fd) ->
-			return callback(err) if err?
-			fs.read fd, result, 0, SIZE, 0, (errRead, bytesRead, buffer) ->
-				fs.close fd, (errClose) ->
-					return callback(errRead) if errRead?
-					return callback(errClose) if errReadClose?
-					isOptimised = buffer.toString('ascii').indexOf("/Linearized 1") >= 0
-					callback(null, isOptimised)
-
-	optimisePDF: (src, dst, callback = (error) ->) ->
-		tmpOutput = dst + '.opt'
-		args = ["--linearize", src, tmpOutput]
-		logger.log args: args, "running qpdf command"
-
-		timer = new Metrics.Timer("qpdf")
-		proc = spawn("qpdf", args)
-		stdout = ""
-		proc.stdout.on "data", (chunk) ->
-			stdout += chunk.toString()
-		callback = _.once(callback) # avoid double call back for error and close event
-		proc.on "error", (err) ->
-			logger.warn {err, args}, "qpdf failed"
-			callback(null) # ignore the error
-		proc.on "close", (code) ->
-			timer.done()
-			if code != 0
-				logger.warn {code, args}, "qpdf returned error"
-				return callback(null) # ignore the error
-			fs.rename tmpOutput, dst, (err) ->
-				if err?
-					logger.warn {tmpOutput, dst}, "failed to rename output of qpdf command"
-				callback(null) # ignore the error

+ 0 - 84
services/clsi/app/coffee/ProjectPersistenceManager.coffee

@@ -1,84 +0,0 @@
-UrlCache = require "./UrlCache"
-CompileManager = require "./CompileManager"
-db = require "./db"
-dbQueue = require "./DbQueue"
-async = require "async"
-logger = require "logger-sharelatex"
-oneDay = 24 * 60 * 60 * 1000
-Settings = require "settings-sharelatex"
-
-module.exports = ProjectPersistenceManager =
-
-	EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5
-
-	markProjectAsJustAccessed: (project_id, callback = (error) ->) ->
-		job = (cb)->
-			db.Project.findOrCreate(where: {project_id: project_id})
-				.spread(
-					(project, created) ->
-						project.updateAttributes(lastAccessed: new Date())
-							.then(() -> cb())
-							.error cb
-				)
-				.error cb
-		dbQueue.queue.push(job, callback)
-
-
-	clearExpiredProjects: (callback = (error) ->) ->
-		ProjectPersistenceManager._findExpiredProjectIds (error, project_ids) ->
-			return callback(error) if error?
-			logger.log project_ids: project_ids, "clearing expired projects"
-			jobs = for project_id in (project_ids or [])
-				do (project_id) ->
-					(callback) ->
-						ProjectPersistenceManager.clearProjectFromCache project_id, (err) ->
-							if err?
-								logger.error err: err, project_id: project_id, "error clearing project"
-							callback()
-			async.series jobs, (error) ->
-				return callback(error) if error?
-				CompileManager.clearExpiredProjects ProjectPersistenceManager.EXPIRY_TIMEOUT, (error) ->
-					callback() # ignore any errors from deleting directories
-
-	clearProject: (project_id, user_id, callback = (error) ->) ->
-		logger.log project_id: project_id, user_id:user_id, "clearing project for user"
-		CompileManager.clearProject project_id, user_id, (error) ->
-			return callback(error) if error?
-			ProjectPersistenceManager.clearProjectFromCache project_id, (error) ->
-				return callback(error) if error?
-				callback()
-
-	clearProjectFromCache: (project_id, callback = (error) ->) ->
-		logger.log project_id: project_id, "clearing project from cache"
-		UrlCache.clearProject project_id, (error) ->
-			if error?
-				logger.err error:error, project_id: project_id, "error clearing project from cache"
-				return callback(error) 
-			ProjectPersistenceManager._clearProjectFromDatabase project_id, (error) ->
-				if error?
-					logger.err error:error, project_id:project_id, "error clearing project from database"
-				callback(error)
-
-	_clearProjectFromDatabase: (project_id, callback = (error) ->) ->
-		logger.log project_id:project_id, "clearing project from database"
-		job = (cb)->
-			db.Project.destroy(where: {project_id: project_id})
-				.then(() -> cb())
-				.error cb
-		dbQueue.queue.push(job, callback)
-
-
-	_findExpiredProjectIds: (callback = (error, project_ids) ->) ->
-		job = (cb)->
-			keepProjectsFrom = new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT)
-			q = {}
-			q[db.op.lt] = keepProjectsFrom
-			db.Project.findAll(where:{lastAccessed:q})
-				.then((projects) ->
-					cb null, projects.map((project) -> project.project_id)
-				).error cb
-
-		dbQueue.queue.push(job, callback)
-
-
-logger.log {EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT}, "project assets kept timeout"

+ 0 - 128
services/clsi/app/coffee/RequestParser.coffee

@@ -1,128 +0,0 @@
-settings = require("settings-sharelatex")
-
-module.exports = RequestParser =
-	VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
-	MAX_TIMEOUT: 600
-
-	parse: (body, callback = (error, data) ->) ->
-		response = {}
-
-		if !body.compile?
-			return callback "top level object should have a compile attribute"
-
-		compile = body.compile
-		compile.options ||= {}
-
-		try
-			response.compiler = @_parseAttribute "compiler",
-				compile.options.compiler,
-				validValues: @VALID_COMPILERS
-				default: "pdflatex"
-				type: "string"
-			response.timeout = @_parseAttribute "timeout",
-				compile.options.timeout
-				default: RequestParser.MAX_TIMEOUT
-				type: "number"
-			response.imageName = @_parseAttribute "imageName",
-				compile.options.imageName,
-				type: "string"
-			response.draft = @_parseAttribute "draft",
-				compile.options.draft,
-				default: false,
-				type: "boolean"
-			response.check = @_parseAttribute "check",
-				compile.options.check,
-				type: "string"
-			response.flags = @_parseAttribute "flags",
-				compile.options.flags,
-				default: [],
-				type: "object"
-
-			# The syncType specifies whether the request contains all
-			# resources (full) or only those resources to be updated
-			# in-place (incremental).
-			response.syncType = @_parseAttribute "syncType",
-				compile.options.syncType,
-				validValues: ["full", "incremental"]
-				type: "string"
-
-			# The syncState is an identifier passed in with the request
-			# which has the property that it changes when any resource is
-			# added, deleted, moved or renamed.
-			#
-			# on syncType full the syncState identifier is passed in and
-			# stored
-			#
-			# on syncType incremental the syncState identifier must match
-			# the stored value
-			response.syncState = @_parseAttribute "syncState",
-				compile.options.syncState,
-				type: "string"
-
-			if response.timeout > RequestParser.MAX_TIMEOUT
-				response.timeout = RequestParser.MAX_TIMEOUT
-			response.timeout = response.timeout * 1000 # milliseconds
-
-			response.resources = (@_parseResource(resource) for resource in (compile.resources or []))
-
-			rootResourcePath = @_parseAttribute "rootResourcePath",
-				compile.rootResourcePath
-				default: "main.tex"
-				type: "string"
-			originalRootResourcePath = rootResourcePath
-			sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath)
-			response.rootResourcePath = RequestParser._checkPath(sanitizedRootResourcePath)
-
-			for resource in response.resources
-				if resource.path == originalRootResourcePath
-					resource.path = sanitizedRootResourcePath
-		catch error
-			return callback error
-
-		callback null, response
-
-	_parseResource: (resource) ->
-		if !resource.path? or typeof resource.path != "string"
-			throw "all resources should have a path attribute"
-
-		if resource.modified?
-			modified = new Date(resource.modified)
-			if isNaN(modified.getTime())
-				throw "resource modified date could not be understood: #{resource.modified}"
-
-		if !resource.url? and !resource.content?
-			throw "all resources should have either a url or content attribute"
-		if resource.content? and typeof resource.content != "string"
-			throw "content attribute should be a string"
-		if resource.url? and typeof resource.url != "string"
-			throw "url attribute should be a string"
-
-		return {
-			path: resource.path
-			modified: modified
-			url: resource.url
-			content: resource.content
-		}
-
-	_parseAttribute: (name, attribute, options) ->
-		if attribute?
-			if options.validValues?
-				if options.validValues.indexOf(attribute) == -1
-					throw "#{name} attribute should be one of: #{options.validValues.join(", ")}"
-			if options.type?
-				if typeof attribute != options.type
-					throw "#{name} attribute should be a #{options.type}"
-		else
-			return options.default if options.default?
-		return attribute
-
-	_sanitizePath: (path) ->
-		# See http://php.net/manual/en/function.escapeshellcmd.php
-		path.replace(/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g, "")
-
-	_checkPath: (path) ->
-		# check that the request does not use a relative path
-		for dir in path.split('/')
-			if dir == '..'
-				throw "relative path in root resource"
-		return path

+ 0 - 72
services/clsi/app/coffee/ResourceStateManager.coffee

@@ -1,72 +0,0 @@
-Path = require "path"
-fs = require "fs"
-logger = require "logger-sharelatex"
-settings = require("settings-sharelatex")
-Errors = require "./Errors"
-SafeReader = require "./SafeReader"
-
-module.exports = ResourceStateManager =
-
-	# The sync state is an identifier which must match for an
-	# incremental update to be allowed.
-	#
-	# The initial value is passed in and stored on a full
-	# compile, along with the list of resources..
-	#
-	# Subsequent incremental compiles must come with the same value - if
-	# not they will be rejected with a 409 Conflict response. The
-	# previous list of resources is returned.
-	#
-	# An incremental compile can only update existing files with new
-	# content.  The sync state identifier must change if any docs or
-	# files are moved, added, deleted or renamed.
-
-	SYNC_STATE_FILE: ".project-sync-state"
-	SYNC_STATE_MAX_SIZE: 128*1024
-
-	saveProjectState: (state, resources, basePath, callback = (error) ->) ->
-		stateFile = Path.join(basePath, @SYNC_STATE_FILE)
-		if not state? # remove the file if no state passed in
-			logger.log state:state, basePath:basePath, "clearing sync state"
-			fs.unlink stateFile, (err) ->
-				if err? and err.code isnt 'ENOENT'
-					return callback(err)
-				else
-					return callback()
-		else
-			logger.log state:state, basePath:basePath, "writing sync state"
-			resourceList = (resource.path for resource in resources)
-			fs.writeFile stateFile, [resourceList..., "stateHash:#{state}"].join("\n"), callback
-
-	checkProjectStateMatches: (state, basePath, callback = (error, resources) ->) ->
-		stateFile = Path.join(basePath, @SYNC_STATE_FILE)
-		size = @SYNC_STATE_MAX_SIZE
-		SafeReader.readFile stateFile, size, 'utf8', (err, result, bytesRead) ->
-			return callback(err) if err?
-			if bytesRead is size
-				logger.error file:stateFile, size:size, bytesRead:bytesRead, "project state file truncated"
-			[resourceList..., oldState] = result?.toString()?.split("\n") or []
-			newState = "stateHash:#{state}"
-			logger.log state:state, oldState: oldState, basePath:basePath, stateMatches: (newState is oldState), "checking sync state"
-			if newState isnt oldState
-				return callback new Errors.FilesOutOfSyncError("invalid state for incremental update")
-			else
-				resources = ({path: path} for path in resourceList)
-				callback(null, resources)
-
-	checkResourceFiles: (resources, allFiles, basePath, callback = (error) ->) ->
-		# check the paths are all relative to current directory
-		for file in resources or []
-			for dir in file?.path?.split('/')
-				if dir == '..'
-					return callback new Error("relative path in resource file list")
-		# check if any of the input files are not present in list of files
-		seenFile = {}
-		for file in allFiles
-			seenFile[file] = true
-		missingFiles = (resource.path for resource in resources when not seenFile[resource.path])
-		if missingFiles?.length > 0
-			logger.err missingFiles:missingFiles, basePath:basePath, allFiles:allFiles, resources:resources, "missing input files for project"
-			return callback new Errors.FilesOutOfSyncError("resource files missing in incremental update")
-		else
-			callback()

+ 0 - 142
services/clsi/app/coffee/ResourceWriter.coffee

@@ -1,142 +0,0 @@
-UrlCache = require "./UrlCache"
-Path = require "path"
-fs = require "fs"
-async = require "async"
-mkdirp = require "mkdirp"
-OutputFileFinder = require "./OutputFileFinder"
-ResourceStateManager = require "./ResourceStateManager"
-Metrics = require "./Metrics"
-logger = require "logger-sharelatex"
-settings = require("settings-sharelatex")
-
-parallelFileDownloads = settings.parallelFileDownloads or 1
-
-module.exports = ResourceWriter =
-
-	syncResourcesToDisk: (request, basePath, callback = (error, resourceList) ->) ->
-		if request.syncType is "incremental"
-			logger.log project_id: request.project_id, user_id: request.user_id, "incremental sync"
-			ResourceStateManager.checkProjectStateMatches request.syncState, basePath, (error, resourceList) ->
-				return callback(error) if error?
-				ResourceWriter._removeExtraneousFiles resourceList, basePath, (error, outputFiles, allFiles) ->
-					return callback(error) if error?
-					ResourceStateManager.checkResourceFiles resourceList, allFiles, basePath, (error) ->
-						return callback(error) if error?
-						ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) ->
-							return callback(error) if error?
-							callback(null, resourceList)
-		else
-			logger.log project_id: request.project_id, user_id: request.user_id, "full sync"
-			@saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) ->
-				return callback(error) if error?
-				ResourceStateManager.saveProjectState request.syncState, request.resources, basePath, (error) ->
-					return callback(error) if error?
-					callback(null, request.resources)
-
-	saveIncrementalResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) ->
-		@_createDirectory basePath, (error) =>
-			return callback(error) if error?
-			jobs = for resource in resources
-				do (resource) =>
-					(callback) => @_writeResourceToDisk(project_id, resource, basePath, callback)
-			async.parallelLimit jobs, parallelFileDownloads, callback
-
-	saveAllResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) ->
-		@_createDirectory basePath, (error) =>
-			return callback(error) if error?
-			@_removeExtraneousFiles resources, basePath, (error) =>
-				return callback(error) if error?
-				jobs = for resource in resources
-					do (resource) =>
-						(callback) => @_writeResourceToDisk(project_id, resource, basePath, callback)
-				async.parallelLimit jobs, parallelFileDownloads, callback
-
-	_createDirectory: (basePath, callback = (error) ->) ->
-		fs.mkdir basePath, (err) ->
-			if err?
-				if err.code is 'EEXIST'
-					return callback()
-				else
-					logger.log {err: err, dir:basePath}, "error creating directory"
-					return callback(err)
-			else
-				return callback()
-
-	_removeExtraneousFiles: (resources, basePath, _callback = (error, outputFiles, allFiles) ->) ->
-		timer = new Metrics.Timer("unlink-output-files")
-		callback = (error, result...) ->
-			timer.done()
-			_callback(error, result...)
-
-		OutputFileFinder.findOutputFiles resources, basePath, (error, outputFiles, allFiles) ->
-			return callback(error) if error?
-
-			jobs = []
-			for file in outputFiles or []
-				do (file) ->
-					path = file.path
-					should_delete = true
-					if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache
-						should_delete = false
-					if path.match(/^output-.*/) # Tikz cached figures (default case)
-						should_delete = false
-					if path.match(/\.(pdf|dpth|md5)$/) # Tikz cached figures (by extension)
-						should_delete = false
-					if path.match(/\.(pygtex|pygstyle)$/) or path.match(/(^|\/)_minted-[^\/]+\//) # minted files/directory
-						should_delete = false
-					if path.match(/\.md\.tex$/) or path.match(/(^|\/)_markdown_[^\/]+\//) # markdown files/directory
-						should_delete = false
-					if path.match(/-eps-converted-to\.pdf$/) # Epstopdf generated files
-						should_delete = false 
-					if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv"
-						should_delete = true
-					if path == "output.tex" # created by TikzManager if present in output files
-						should_delete = true
-					if should_delete
-						jobs.push (callback) -> ResourceWriter._deleteFileIfNotDirectory Path.join(basePath, path), callback
-
-			async.series jobs, (error) ->
-				return callback(error) if error?
-				callback(null, outputFiles, allFiles)
-
-	_deleteFileIfNotDirectory: (path, callback = (error) ->) ->
-		fs.stat path, (error, stat) ->
-			if error? and error.code is 'ENOENT'
-				return callback()
-			else if error?
-				logger.err {err: error, path: path}, "error stating file in deleteFileIfNotDirectory"
-				return callback(error)
-			else if stat.isFile()
-				fs.unlink path, (error) ->
-					if error?
-						logger.err {err: error, path: path}, "error removing file in deleteFileIfNotDirectory"
-						callback(error)
-					else
-						callback()
-			else
-				callback()
-
-	_writeResourceToDisk: (project_id, resource, basePath, callback = (error) ->) ->
-		ResourceWriter.checkPath basePath, resource.path, (error, path) ->
-			return callback(error) if error?
-			mkdirp Path.dirname(path), (error) ->
-				return callback(error) if error?
-				# TODO: Don't overwrite file if it hasn't been modified
-				if resource.url?
-					UrlCache.downloadUrlToFile project_id, resource.url, path, resource.modified, (err)->
-						if err?
-							logger.err err:err, project_id:project_id, path:path, resource_url:resource.url, modified:resource.modified, "error downloading file for resources"
-						callback() #try and continue compiling even if http resource can not be downloaded at this time
-				else
-					process = require("process")
-					fs.writeFile path, resource.content, callback
-					try 
-						result = fs.lstatSync(path)
-					catch e
-
-	checkPath: (basePath, resourcePath, callback) ->
-		path = Path.normalize(Path.join(basePath, resourcePath))
-		if (path.slice(0, basePath.length + 1) != basePath + "/")
-			return callback new Error("resource path is outside root directory")
-		else
-			return callback(null, path)

+ 0 - 25
services/clsi/app/coffee/SafeReader.coffee

@@ -1,25 +0,0 @@
-fs = require "fs"
-logger = require "logger-sharelatex"
-
-module.exports = SafeReader =
-
-	# safely read up to size bytes from a file and return result as a
-	# string
-
-	readFile: (file, size, encoding, callback = (error, result) ->) ->
-		fs.open file, 'r', (err, fd) ->
-			return callback() if err? and err.code is 'ENOENT'
-			return callback(err) if err?
-
-			# safely return always closing the file
-			callbackWithClose = (err, result...) ->
-				fs.close fd, (err1) ->
-					return callback(err) if err?
-					return callback(err1) if err1?
-					callback(null, result...)
-
-			buff = new Buffer(size, 0) # fill with zeros
-			fs.read fd, buff, 0, buff.length, 0, (err, bytesRead, buffer) ->
-				return callbackWithClose(err) if err?
-				result = buffer.toString(encoding, 0, bytesRead)
-				callbackWithClose(null, result, bytesRead)

+ 0 - 41
services/clsi/app/coffee/StaticServerForbidSymlinks.coffee

@@ -1,41 +0,0 @@
-Path = require("path")
-fs = require("fs")
-Settings = require("settings-sharelatex")
-logger = require("logger-sharelatex")
-url = require "url"
-
-module.exports = ForbidSymlinks = (staticFn, root, options) ->
-	expressStatic = staticFn root, options
-	basePath = Path.resolve(root)
-	return (req, res, next) ->
-		path = url.parse(req.url)?.pathname
-		# check that the path is of the form /project_id_or_name/path/to/file.log
-		if result = path.match(/^\/?([a-zA-Z0-9_-]+)\/(.*)/)
-			project_id = result[1]
-			file = result[2]
-		else
-			logger.warn path: path, "unrecognized file request"
-			return res.sendStatus(404)
-		# check that the file does not use a relative path
-		for dir in file.split('/')
-			if dir == '..'
-				logger.warn path: path, "attempt to use a relative path"
-				return res.sendStatus(404)
-		# check that the requested path is normalized
-		requestedFsPath = "#{basePath}/#{project_id}/#{file}"
-		if requestedFsPath != Path.normalize(requestedFsPath)
-				logger.error path: requestedFsPath, "requestedFsPath is not normalized"
-				return res.sendStatus(404)
-		# check that the requested path is not a symlink
-		fs.realpath requestedFsPath, (err, realFsPath)->
-			if err?
-				if err.code == 'ENOENT'
-					return res.sendStatus(404)
-				else
-					logger.error err:err, requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "error checking file access"
-					return res.sendStatus(500)
-			else if requestedFsPath != realFsPath
-				logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting"
-				return res.sendStatus(404)
-			else
-				expressStatic(req, res, next)

+ 0 - 37
services/clsi/app/coffee/TikzManager.coffee

@@ -1,37 +0,0 @@
-fs = require "fs"
-Path = require "path"
-ResourceWriter = require "./ResourceWriter"
-SafeReader = require "./SafeReader"
-logger = require "logger-sharelatex"
-
-# for \tikzexternalize or pstool to work the main file needs to match the
-# jobname.  Since we set the -jobname to output, we have to create a
-# copy of the main file as 'output.tex'.
-
-module.exports = TikzManager =
-
-	checkMainFile: (compileDir, mainFile, resources, callback = (error, needsMainFile) ->) ->
-		# if there's already an output.tex file, we don't want to touch it
-		for resource in resources
-			if resource.path is "output.tex"
-				logger.log compileDir: compileDir, mainFile: mainFile, "output.tex already in resources"
-				return callback(null, false)
-		# if there's no output.tex, see if we are using tikz/pgf or pstool in the main file
-		ResourceWriter.checkPath compileDir, mainFile, (error, path) ->
-			return callback(error) if error?
-			SafeReader.readFile path, 65536, "utf8", (error, content) ->
-				return callback(error) if error?
-				usesTikzExternalize = content?.indexOf("\\tikzexternalize") >= 0
-				usesPsTool = content?.indexOf("{pstool}") >= 0
-				logger.log compileDir: compileDir, mainFile: mainFile, usesTikzExternalize:usesTikzExternalize, usesPsTool: usesPsTool, "checked for packages needing main file as output.tex"
-				needsMainFile = (usesTikzExternalize || usesPsTool)
-				callback null, needsMainFile
-
-	injectOutputFile: (compileDir, mainFile, callback = (error) ->) ->
-		ResourceWriter.checkPath compileDir, mainFile, (error, path) ->
-			return callback(error) if error?
-			fs.readFile path, "utf8", (error, content) ->
-				return callback(error) if error?
-				logger.log compileDir: compileDir, mainFile: mainFile, "copied file to output.tex as project uses packages which require it"
-				# use wx flag to ensure that output file does not already exist
-				fs.writeFile Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback

+ 0 - 134
services/clsi/app/coffee/UrlCache.coffee

@@ -1,134 +0,0 @@
-db = require("./db")
-dbQueue = require "./DbQueue"
-UrlFetcher = require("./UrlFetcher")
-Settings = require("settings-sharelatex")
-crypto = require("crypto")
-fs = require("fs")
-logger = require "logger-sharelatex"
-async = require "async"
-
-module.exports = UrlCache =
-	downloadUrlToFile: (project_id, url, destPath, lastModified, callback = (error) ->) ->
-		UrlCache._ensureUrlIsInCache project_id, url, lastModified, (error, pathToCachedUrl) =>
-			return callback(error) if error?
-			UrlCache._copyFile pathToCachedUrl, destPath, (error) ->
-				if error?
-					UrlCache._clearUrlDetails project_id, url, () ->
-						callback(error)
-				else
-					callback(error)
-
-	clearProject: (project_id, callback = (error) ->) ->
-		UrlCache._findAllUrlsInProject project_id, (error, urls) ->
-			logger.log project_id: project_id, url_count: urls.length, "clearing project URLs"
-			return callback(error) if error?
-			jobs = for url in (urls or [])
-				do (url) ->
-					(callback) ->
-						UrlCache._clearUrlFromCache project_id, url, (error) ->
-							if error?
-								logger.error err: error, project_id: project_id, url: url, "error clearing project URL"
-							callback()
-			async.series jobs, callback
-
-	_ensureUrlIsInCache: (project_id, url, lastModified, callback = (error, pathOnDisk) ->) ->
-		if lastModified?
-			# MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds.
-			# So round down to seconds
-			lastModified = new Date(Math.floor(lastModified.getTime() / 1000) * 1000)
-		UrlCache._doesUrlNeedDownloading project_id, url, lastModified, (error, needsDownloading) =>
-			return callback(error) if error?
-			if needsDownloading
-				logger.log url: url, lastModified: lastModified, "downloading URL"
-				UrlFetcher.pipeUrlToFile url, UrlCache._cacheFilePathForUrl(project_id, url), (error) =>
-					return callback(error) if error?
-					UrlCache._updateOrCreateUrlDetails project_id, url, lastModified, (error) =>
-						return callback(error) if error?
-						callback null, UrlCache._cacheFilePathForUrl(project_id, url)
-			else
-				logger.log url: url, lastModified: lastModified, "URL is up to date in cache"
-				callback null, UrlCache._cacheFilePathForUrl(project_id, url)
-	
-	_doesUrlNeedDownloading: (project_id, url, lastModified, callback = (error, needsDownloading) ->) ->
-		if !lastModified?
-			return callback null, true
-		UrlCache._findUrlDetails project_id, url, (error, urlDetails) ->
-			return callback(error) if error?
-			if !urlDetails? or !urlDetails.lastModified? or urlDetails.lastModified.getTime() < lastModified.getTime()
-				return callback null, true
-			else
-				return callback null, false
-
-	_cacheFileNameForUrl: (project_id, url) ->
-		project_id + ":" + crypto.createHash("md5").update(url).digest("hex")
-
-	_cacheFilePathForUrl: (project_id, url) ->
-		"#{Settings.path.clsiCacheDir}/#{UrlCache._cacheFileNameForUrl(project_id, url)}"
-
-	_copyFile: (from, to, _callback = (error) ->) ->
-		callbackOnce = (error) ->
-			if error?
-				logger.error err: error, from:from, to:to, "error copying file from cache"
-			_callback(error)
-			_callback = () ->
-		writeStream = fs.createWriteStream(to)
-		readStream = fs.createReadStream(from)
-		writeStream.on "error", callbackOnce
-		readStream.on "error", callbackOnce
-		writeStream.on "close", callbackOnce
-		writeStream.on "open", () ->
-			readStream.pipe(writeStream)
-
-	_clearUrlFromCache: (project_id, url, callback = (error) ->) ->
-		UrlCache._clearUrlDetails project_id, url, (error) ->
-			return callback(error) if error?
-			UrlCache._deleteUrlCacheFromDisk project_id, url, (error) ->
-				return callback(error) if error?
-				callback null
-
-	_deleteUrlCacheFromDisk: (project_id, url, callback = (error) ->) ->
-		fs.unlink UrlCache._cacheFilePathForUrl(project_id, url), (error) ->
-			if error? and error.code != 'ENOENT' # no error if the file isn't present
-				return callback(error)
-			else
-				return callback()
-
-	_findUrlDetails: (project_id, url, callback = (error, urlDetails) ->) ->
-		job = (cb)->
-			db.UrlCache.find(where: { url: url, project_id: project_id })
-				.then((urlDetails) -> cb null, urlDetails)
-				.error cb
-		dbQueue.queue.push job, callback
-
-	_updateOrCreateUrlDetails: (project_id, url, lastModified, callback = (error) ->) ->
-		job = (cb)->
-			db.UrlCache.findOrCreate(where: {url: url, project_id: project_id})
-				.spread(
-					(urlDetails, created) ->
-						urlDetails.updateAttributes(lastModified: lastModified)
-							.then(() -> cb())
-							.error(cb)
-				)
-				.error cb
-		dbQueue.queue.push(job, callback)
-
-	_clearUrlDetails: (project_id, url, callback = (error) ->) ->
-		job = (cb)->
-			db.UrlCache.destroy(where: {url: url, project_id: project_id})
-				.then(() -> cb null)
-				.error cb
-		dbQueue.queue.push(job, callback)
-
-
-	_findAllUrlsInProject: (project_id, callback = (error, urls) ->) ->
-		job = (cb)->
-			db.UrlCache.findAll(where: { project_id: project_id })
-				.then(
-					(urlEntries) ->
-						cb null, urlEntries.map((entry) -> entry.url)
-				)
-				.error cb
-		dbQueue.queue.push(job, callback)
-
-		
-		

+ 0 - 70
services/clsi/app/coffee/UrlFetcher.coffee

@@ -1,70 +0,0 @@
-request = require("request").defaults(jar: false)
-fs = require("fs")
-logger = require "logger-sharelatex"
-settings = require("settings-sharelatex")
-URL = require('url');
-
-oneMinute = 60 * 1000
-
-module.exports = UrlFetcher =
-	pipeUrlToFile: (url, filePath, _callback = (error) ->) ->
-		callbackOnce = (error) ->
-			clearTimeout timeoutHandler if timeoutHandler?
-			_callback(error)
-			_callback = () ->
-
-		if settings.filestoreDomainOveride?
-			p = URL.parse(url).path
-			url = "#{settings.filestoreDomainOveride}#{p}"
-		timeoutHandler = setTimeout () ->
-			timeoutHandler = null
-			logger.error url:url, filePath: filePath, "Timed out downloading file to cache"
-			callbackOnce(new Error("Timed out downloading file to cache #{url}"))
-			# FIXME: maybe need to close fileStream here
-		, 3 * oneMinute
-
-		logger.log url:url, filePath: filePath, "started downloading url to cache"
-		urlStream = request.get({url: url, timeout: oneMinute})
-		urlStream.pause() # stop data flowing until we are ready
-
-		# attach handlers before setting up pipes
-		urlStream.on "error", (error) ->
-			logger.error err: error, url:url, filePath: filePath, "error downloading url"
-			callbackOnce(error or new Error("Something went wrong downloading the URL #{url}"))
-
-		urlStream.on "end", () ->
-			logger.log url:url, filePath: filePath, "finished downloading file into cache"
-
-		urlStream.on "response", (res) ->
-			if res.statusCode >= 200 and res.statusCode < 300
-				fileStream = fs.createWriteStream(filePath)
-
-				# attach handlers before setting up pipes
-				fileStream.on 'error', (error) ->
-					logger.error err: error, url:url, filePath: filePath, "error writing file into cache"
-					fs.unlink filePath, (err) ->
-						if err?
-							logger.err err: err, filePath: filePath, "error deleting file from cache"
-						callbackOnce(error)
-
-				fileStream.on 'finish', () ->
-					logger.log url:url, filePath: filePath, "finished writing file into cache"
-					callbackOnce()
-
-				fileStream.on 'pipe', () ->
-					logger.log url:url, filePath: filePath, "piping into filestream"
-
-				urlStream.pipe(fileStream)
-				urlStream.resume() # now we are ready to handle the data
-			else
-				logger.error statusCode: res.statusCode, url:url, filePath: filePath, "unexpected status code downloading url to cache"
-				# https://nodejs.org/api/http.html#http_class_http_clientrequest
-				# If you add a 'response' event handler, then you must consume
-				# the data from the response object, either by calling
-				# response.read() whenever there is a 'readable' event, or by
-				# adding a 'data' handler, or by calling the .resume()
-				# method. Until the data is consumed, the 'end' event will not
-				# fire. Also, until the data is read it will consume memory
-				# that can eventually lead to a 'process out of memory' error.
-				urlStream.resume() # discard the data
-				callbackOnce(new Error("URL returned non-success status code: #{res.statusCode} #{url}"))

+ 0 - 55
services/clsi/app/coffee/db.coffee

@@ -1,55 +0,0 @@
-Sequelize = require("sequelize")
-Settings = require("settings-sharelatex")
-_ = require("underscore")
-logger = require "logger-sharelatex"
-
-options = _.extend {logging:false}, Settings.mysql.clsi
-
-logger.log dbPath:Settings.mysql.clsi.storage, "connecting to db"
-
-sequelize = new Sequelize(
-	Settings.mysql.clsi.database,
-	Settings.mysql.clsi.username,
-	Settings.mysql.clsi.password,
-	options
-)
-
-if Settings.mysql.clsi.dialect == "sqlite"
-	logger.log "running PRAGMA journal_mode=WAL;"
-	sequelize.query("PRAGMA journal_mode=WAL;")
-	sequelize.query("PRAGMA synchronous=OFF;")
-	sequelize.query("PRAGMA read_uncommitted = true;")
-
-module.exports =
-	UrlCache: sequelize.define("UrlCache", {
-		url: Sequelize.STRING
-		project_id: Sequelize.STRING
-		lastModified: Sequelize.DATE
-	}, {
-		indexes: [
-			{fields: ['url', 'project_id']},
-			{fields: ['project_id']}
-		]
-	})
-
-	Project: sequelize.define("Project", {
-		project_id: {type: Sequelize.STRING, primaryKey: true}
-		lastAccessed: Sequelize.DATE
-	}, {
-		indexes: [
-			{fields: ['lastAccessed']}
-		]
-	})
-
-	op: Sequelize.Op
-	
-	sync: () -> 
-		logger.log dbPath:Settings.mysql.clsi.storage, "syncing db schema"
-		sequelize.sync()
-			.then(-> 
-				logger.log "db sync complete"
-			).catch((err)->
-				console.log err, "error syncing"
-			)
-
-	

+ 20 - 0
services/clsi/app/js/CommandRunner.js

@@ -0,0 +1,20 @@
+// TODO: This file was created by bulk-decaffeinate.
+// Sanity-check the conversion and remove this comment.
+/*
+ * decaffeinate suggestions:
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let commandRunnerPath
+const Settings = require('settings-sharelatex')
+const logger = require('logger-sharelatex')
+
+if ((Settings.clsi != null ? Settings.clsi.dockerRunner : undefined) === true) {
+  commandRunnerPath = './DockerRunner'
+} else {
+  commandRunnerPath = './LocalCommandRunner'
+}
+logger.info({ commandRunnerPath }, 'selecting command runner for clsi')
+const CommandRunner = require(commandRunnerPath)
+
+module.exports = CommandRunner

+ 238 - 0
services/clsi/app/js/CompileController.js

@@ -0,0 +1,238 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let CompileController
+const RequestParser = require('./RequestParser')
+const CompileManager = require('./CompileManager')
+const Settings = require('settings-sharelatex')
+const Metrics = require('./Metrics')
+const ProjectPersistenceManager = require('./ProjectPersistenceManager')
+const logger = require('logger-sharelatex')
+const Errors = require('./Errors')
+
+module.exports = CompileController = {
+  compile(req, res, next) {
+    if (next == null) {
+      next = function(error) {}
+    }
+    const timer = new Metrics.Timer('compile-request')
+    return RequestParser.parse(req.body, function(error, request) {
+      if (error != null) {
+        return next(error)
+      }
+      request.project_id = req.params.project_id
+      if (req.params.user_id != null) {
+        request.user_id = req.params.user_id
+      }
+      return ProjectPersistenceManager.markProjectAsJustAccessed(
+        request.project_id,
+        function(error) {
+          if (error != null) {
+            return next(error)
+          }
+          return CompileManager.doCompileWithLock(request, function(
+            error,
+            outputFiles
+          ) {
+            let code, status
+            if (outputFiles == null) {
+              outputFiles = []
+            }
+            if (error instanceof Errors.AlreadyCompilingError) {
+              code = 423 // Http 423 Locked
+              status = 'compile-in-progress'
+            } else if (error instanceof Errors.FilesOutOfSyncError) {
+              code = 409 // Http 409 Conflict
+              status = 'retry'
+            } else if (error != null ? error.terminated : undefined) {
+              status = 'terminated'
+            } else if (error != null ? error.validate : undefined) {
+              status = `validation-${error.validate}`
+            } else if (error != null ? error.timedout : undefined) {
+              status = 'timedout'
+              logger.log(
+                { err: error, project_id: request.project_id },
+                'timeout running compile'
+              )
+            } else if (error != null) {
+              status = 'error'
+              code = 500
+              logger.warn(
+                { err: error, project_id: request.project_id },
+                'error running compile'
+              )
+            } else {
+              let file
+              status = 'failure'
+              for (file of Array.from(outputFiles)) {
+                if (
+                  file.path != null
+                    ? file.path.match(/output\.pdf$/)
+                    : undefined
+                ) {
+                  status = 'success'
+                }
+              }
+
+              if (status === 'failure') {
+                logger.warn(
+                  { project_id: request.project_id, outputFiles },
+                  'project failed to compile successfully, no output.pdf generated'
+                )
+              }
+
+              // log an error if any core files are found
+              for (file of Array.from(outputFiles)) {
+                if (file.path === 'core') {
+                  logger.error(
+                    { project_id: request.project_id, req, outputFiles },
+                    'core file found in output'
+                  )
+                }
+              }
+            }
+
+            if (error != null) {
+              outputFiles = error.outputFiles || []
+            }
+
+            timer.done()
+            return res.status(code || 200).send({
+              compile: {
+                status,
+                error: (error != null ? error.message : undefined) || error,
+                outputFiles: outputFiles.map(file => ({
+                  url:
+                    `${Settings.apis.clsi.url}/project/${request.project_id}` +
+                    (request.user_id != null
+                      ? `/user/${request.user_id}`
+                      : '') +
+                    (file.build != null ? `/build/${file.build}` : '') +
+                    `/output/${file.path}`,
+                  path: file.path,
+                  type: file.type,
+                  build: file.build
+                }))
+              }
+            })
+          })
+        }
+      )
+    })
+  },
+
+  stopCompile(req, res, next) {
+    const { project_id, user_id } = req.params
+    return CompileManager.stopCompile(project_id, user_id, function(error) {
+      if (error != null) {
+        return next(error)
+      }
+      return res.sendStatus(204)
+    })
+  },
+
+  clearCache(req, res, next) {
+    if (next == null) {
+      next = function(error) {}
+    }
+    return ProjectPersistenceManager.clearProject(
+      req.params.project_id,
+      req.params.user_id,
+      function(error) {
+        if (error != null) {
+          return next(error)
+        }
+        return res.sendStatus(204)
+      }
+    )
+  }, // No content
+
+  syncFromCode(req, res, next) {
+    if (next == null) {
+      next = function(error) {}
+    }
+    const { file } = req.query
+    const line = parseInt(req.query.line, 10)
+    const column = parseInt(req.query.column, 10)
+    const { project_id } = req.params
+    const { user_id } = req.params
+    return CompileManager.syncFromCode(
+      project_id,
+      user_id,
+      file,
+      line,
+      column,
+      function(error, pdfPositions) {
+        if (error != null) {
+          return next(error)
+        }
+        return res.json({
+          pdf: pdfPositions
+        })
+      }
+    )
+  },
+
+  syncFromPdf(req, res, next) {
+    if (next == null) {
+      next = function(error) {}
+    }
+    const page = parseInt(req.query.page, 10)
+    const h = parseFloat(req.query.h)
+    const v = parseFloat(req.query.v)
+    const { project_id } = req.params
+    const { user_id } = req.params
+    return CompileManager.syncFromPdf(project_id, user_id, page, h, v, function(
+      error,
+      codePositions
+    ) {
+      if (error != null) {
+        return next(error)
+      }
+      return res.json({
+        code: codePositions
+      })
+    })
+  },
+
+  wordcount(req, res, next) {
+    if (next == null) {
+      next = function(error) {}
+    }
+    const file = req.query.file || 'main.tex'
+    const { project_id } = req.params
+    const { user_id } = req.params
+    const { image } = req.query
+    logger.log({ image, file, project_id }, 'word count request')
+
+    return CompileManager.wordcount(project_id, user_id, file, image, function(
+      error,
+      result
+    ) {
+      if (error != null) {
+        return next(error)
+      }
+      return res.json({
+        texcount: result
+      })
+    })
+  },
+
+  status(req, res, next) {
+    if (next == null) {
+      next = function(error) {}
+    }
+    return res.send('OK')
+  }
+}

+ 705 - 0
services/clsi/app/js/CompileManager.js

@@ -0,0 +1,705 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-return-assign,
+    no-undef,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let CompileManager
+const ResourceWriter = require('./ResourceWriter')
+const LatexRunner = require('./LatexRunner')
+const OutputFileFinder = require('./OutputFileFinder')
+const OutputCacheManager = require('./OutputCacheManager')
+const Settings = require('settings-sharelatex')
+const Path = require('path')
+const logger = require('logger-sharelatex')
+const Metrics = require('./Metrics')
+const child_process = require('child_process')
+const DraftModeManager = require('./DraftModeManager')
+const TikzManager = require('./TikzManager')
+const LockManager = require('./LockManager')
+const fs = require('fs')
+const fse = require('fs-extra')
+const os = require('os')
+const async = require('async')
+const Errors = require('./Errors')
+const CommandRunner = require('./CommandRunner')
+
+const getCompileName = function(project_id, user_id) {
+  if (user_id != null) {
+    return `${project_id}-${user_id}`
+  } else {
+    return project_id
+  }
+}
+
+const getCompileDir = (project_id, user_id) =>
+  Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id))
+
+module.exports = CompileManager = {
+  doCompileWithLock(request, callback) {
+    if (callback == null) {
+      callback = function(error, outputFiles) {}
+    }
+    const compileDir = getCompileDir(request.project_id, request.user_id)
+    const lockFile = Path.join(compileDir, '.project-lock')
+    // use a .project-lock file in the compile directory to prevent
+    // simultaneous compiles
+    return fse.ensureDir(compileDir, function(error) {
+      if (error != null) {
+        return callback(error)
+      }
+      return LockManager.runWithLock(
+        lockFile,
+        releaseLock => CompileManager.doCompile(request, releaseLock),
+        callback
+      )
+    })
+  },
+
+  doCompile(request, callback) {
+    if (callback == null) {
+      callback = function(error, outputFiles) {}
+    }
+    const compileDir = getCompileDir(request.project_id, request.user_id)
+    let timer = new Metrics.Timer('write-to-disk')
+    logger.log(
+      { project_id: request.project_id, user_id: request.user_id },
+      'syncing resources to disk'
+    )
+    return ResourceWriter.syncResourcesToDisk(request, compileDir, function(
+      error,
+      resourceList
+    ) {
+      // NOTE: resourceList is insecure, it should only be used to exclude files from the output list
+      if (error != null && error instanceof Errors.FilesOutOfSyncError) {
+        logger.warn(
+          { project_id: request.project_id, user_id: request.user_id },
+          'files out of sync, please retry'
+        )
+        return callback(error)
+      } else if (error != null) {
+        logger.err(
+          {
+            err: error,
+            project_id: request.project_id,
+            user_id: request.user_id
+          },
+          'error writing resources to disk'
+        )
+        return callback(error)
+      }
+      logger.log(
+        {
+          project_id: request.project_id,
+          user_id: request.user_id,
+          time_taken: Date.now() - timer.start
+        },
+        'written files to disk'
+      )
+      timer.done()
+
+      const injectDraftModeIfRequired = function(callback) {
+        if (request.draft) {
+          return DraftModeManager.injectDraftMode(
+            Path.join(compileDir, request.rootResourcePath),
+            callback
+          )
+        } else {
+          return callback()
+        }
+      }
+
+      const createTikzFileIfRequired = callback =>
+        TikzManager.checkMainFile(
+          compileDir,
+          request.rootResourcePath,
+          resourceList,
+          function(error, needsMainFile) {
+            if (error != null) {
+              return callback(error)
+            }
+            if (needsMainFile) {
+              return TikzManager.injectOutputFile(
+                compileDir,
+                request.rootResourcePath,
+                callback
+              )
+            } else {
+              return callback()
+            }
+          }
+        )
+      // set up environment variables for chktex
+      const env = {}
+      // only run chktex on LaTeX files (not knitr .Rtex files or any others)
+      const isLaTeXFile =
+        request.rootResourcePath != null
+          ? request.rootResourcePath.match(/\.tex$/i)
+          : undefined
+      if (request.check != null && isLaTeXFile) {
+        env.CHKTEX_OPTIONS = '-nall -e9 -e10 -w15 -w16'
+        env.CHKTEX_ULIMIT_OPTIONS = '-t 5 -v 64000'
+        if (request.check === 'error') {
+          env.CHKTEX_EXIT_ON_ERROR = 1
+        }
+        if (request.check === 'validate') {
+          env.CHKTEX_VALIDATE = 1
+        }
+      }
+
+      // apply a series of file modifications/creations for draft mode and tikz
+      return async.series(
+        [injectDraftModeIfRequired, createTikzFileIfRequired],
+        function(error) {
+          if (error != null) {
+            return callback(error)
+          }
+          timer = new Metrics.Timer('run-compile')
+          // find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite)
+          let tag =
+            __guard__(
+              __guard__(
+                request.imageName != null
+                  ? request.imageName.match(/:(.*)/)
+                  : undefined,
+                x1 => x1[1]
+              ),
+              x => x.replace(/\./g, '-')
+            ) || 'default'
+          if (!request.project_id.match(/^[0-9a-f]{24}$/)) {
+            tag = 'other'
+          } // exclude smoke test
+          Metrics.inc('compiles')
+          Metrics.inc(`compiles-with-image.${tag}`)
+          const compileName = getCompileName(
+            request.project_id,
+            request.user_id
+          )
+          return LatexRunner.runLatex(
+            compileName,
+            {
+              directory: compileDir,
+              mainFile: request.rootResourcePath,
+              compiler: request.compiler,
+              timeout: request.timeout,
+              image: request.imageName,
+              flags: request.flags,
+              environment: env
+            },
+            function(error, output, stats, timings) {
+              // request was for validation only
+              let metric_key, metric_value
+              if (request.check === 'validate') {
+                const result = (error != null
+                ? error.code
+                : undefined)
+                  ? 'fail'
+                  : 'pass'
+                error = new Error('validation')
+                error.validate = result
+              }
+              // request was for compile, and failed on validation
+              if (
+                request.check === 'error' &&
+                (error != null ? error.message : undefined) === 'exited'
+              ) {
+                error = new Error('compilation')
+                error.validate = 'fail'
+              }
+              // compile was killed by user, was a validation, or a compile which failed validation
+              if (
+                (error != null ? error.terminated : undefined) ||
+                (error != null ? error.validate : undefined) ||
+                (error != null ? error.timedout : undefined)
+              ) {
+                OutputFileFinder.findOutputFiles(
+                  resourceList,
+                  compileDir,
+                  function(err, outputFiles) {
+                    if (err != null) {
+                      return callback(err)
+                    }
+                    error.outputFiles = outputFiles // return output files so user can check logs
+                    return callback(error)
+                  }
+                )
+                return
+              }
+              // compile completed normally
+              if (error != null) {
+                return callback(error)
+              }
+              Metrics.inc('compiles-succeeded')
+              const object = stats || {}
+              for (metric_key in object) {
+                metric_value = object[metric_key]
+                Metrics.count(metric_key, metric_value)
+              }
+              const object1 = timings || {}
+              for (metric_key in object1) {
+                metric_value = object1[metric_key]
+                Metrics.timing(metric_key, metric_value)
+              }
+              const loadavg =
+                typeof os.loadavg === 'function' ? os.loadavg() : undefined
+              if (loadavg != null) {
+                Metrics.gauge('load-avg', loadavg[0])
+              }
+              const ts = timer.done()
+              logger.log(
+                {
+                  project_id: request.project_id,
+                  user_id: request.user_id,
+                  time_taken: ts,
+                  stats,
+                  timings,
+                  loadavg
+                },
+                'done compile'
+              )
+              if ((stats != null ? stats['latex-runs'] : undefined) > 0) {
+                Metrics.timing('run-compile-per-pass', ts / stats['latex-runs'])
+              }
+              if (
+                (stats != null ? stats['latex-runs'] : undefined) > 0 &&
+                (timings != null ? timings['cpu-time'] : undefined) > 0
+              ) {
+                Metrics.timing(
+                  'run-compile-cpu-time-per-pass',
+                  timings['cpu-time'] / stats['latex-runs']
+                )
+              }
+
+              return OutputFileFinder.findOutputFiles(
+                resourceList,
+                compileDir,
+                function(error, outputFiles) {
+                  if (error != null) {
+                    return callback(error)
+                  }
+                  return OutputCacheManager.saveOutputFiles(
+                    outputFiles,
+                    compileDir,
+                    (error, newOutputFiles) => callback(null, newOutputFiles)
+                  )
+                }
+              )
+            }
+          )
+        }
+      )
+    })
+  },
+
+  stopCompile(project_id, user_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const compileName = getCompileName(project_id, user_id)
+    return LatexRunner.killLatex(compileName, callback)
+  },
+
+  clearProject(project_id, user_id, _callback) {
+    if (_callback == null) {
+      _callback = function(error) {}
+    }
+    const callback = function(error) {
+      _callback(error)
+      return (_callback = function() {})
+    }
+
+    const compileDir = getCompileDir(project_id, user_id)
+
+    return CompileManager._checkDirectory(compileDir, function(err, exists) {
+      if (err != null) {
+        return callback(err)
+      }
+      if (!exists) {
+        return callback()
+      } // skip removal if no directory present
+
+      const proc = child_process.spawn('rm', ['-r', compileDir])
+
+      proc.on('error', callback)
+
+      let stderr = ''
+      proc.stderr.on('data', chunk => (stderr += chunk.toString()))
+
+      return proc.on('close', function(code) {
+        if (code === 0) {
+          return callback(null)
+        } else {
+          return callback(new Error(`rm -r ${compileDir} failed: ${stderr}`))
+        }
+      })
+    })
+  },
+
+  _findAllDirs(callback) {
+    if (callback == null) {
+      callback = function(error, allDirs) {}
+    }
+    const root = Settings.path.compilesDir
+    return fs.readdir(root, function(err, files) {
+      if (err != null) {
+        return callback(err)
+      }
+      const allDirs = Array.from(files).map(file => Path.join(root, file))
+      return callback(null, allDirs)
+    })
+  },
+
+  clearExpiredProjects(max_cache_age_ms, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const now = Date.now()
+    // action for each directory
+    const expireIfNeeded = (checkDir, cb) =>
+      fs.stat(checkDir, function(err, stats) {
+        if (err != null) {
+          return cb()
+        } // ignore errors checking directory
+        const age = now - stats.mtime
+        const hasExpired = age > max_cache_age_ms
+        if (hasExpired) {
+          return fse.remove(checkDir, cb)
+        } else {
+          return cb()
+        }
+      })
+    // iterate over all project directories
+    return CompileManager._findAllDirs(function(error, allDirs) {
+      if (error != null) {
+        return callback()
+      }
+      return async.eachSeries(allDirs, expireIfNeeded, callback)
+    })
+  },
+
+  _checkDirectory(compileDir, callback) {
+    if (callback == null) {
+      callback = function(error, exists) {}
+    }
+    return fs.lstat(compileDir, function(err, stats) {
+      if ((err != null ? err.code : undefined) === 'ENOENT') {
+        return callback(null, false) //  directory does not exist
+      } else if (err != null) {
+        logger.err(
+          { dir: compileDir, err },
+          'error on stat of project directory for removal'
+        )
+        return callback(err)
+      } else if (!(stats != null ? stats.isDirectory() : undefined)) {
+        logger.err(
+          { dir: compileDir, stats },
+          'bad project directory for removal'
+        )
+        return callback(new Error('project directory is not directory'))
+      } else {
+        return callback(null, true)
+      }
+    })
+  }, // directory exists
+
+  syncFromCode(project_id, user_id, file_name, line, column, callback) {
+    // If LaTeX was run in a virtual environment, the file path that synctex expects
+    // might not match the file path on the host. The .synctex.gz file however, will be accessed
+    // wherever it is on the host.
+    if (callback == null) {
+      callback = function(error, pdfPositions) {}
+    }
+    const compileName = getCompileName(project_id, user_id)
+    const base_dir = Settings.path.synctexBaseDir(compileName)
+    const file_path = base_dir + '/' + file_name
+    const compileDir = getCompileDir(project_id, user_id)
+    const synctex_path = `${base_dir}/output.pdf`
+    const command = ['code', synctex_path, file_path, line, column]
+    return fse.ensureDir(compileDir, function(error) {
+      if (error != null) {
+        logger.err(
+          { error, project_id, user_id, file_name },
+          'error ensuring dir for sync from code'
+        )
+        return callback(error)
+      }
+      return CompileManager._runSynctex(project_id, user_id, command, function(
+        error,
+        stdout
+      ) {
+        if (error != null) {
+          return callback(error)
+        }
+        logger.log(
+          { project_id, user_id, file_name, line, column, command, stdout },
+          'synctex code output'
+        )
+        return callback(
+          null,
+          CompileManager._parseSynctexFromCodeOutput(stdout)
+        )
+      })
+    })
+  },
+
+  syncFromPdf(project_id, user_id, page, h, v, callback) {
+    if (callback == null) {
+      callback = function(error, filePositions) {}
+    }
+    const compileName = getCompileName(project_id, user_id)
+    const compileDir = getCompileDir(project_id, user_id)
+    const base_dir = Settings.path.synctexBaseDir(compileName)
+    const synctex_path = `${base_dir}/output.pdf`
+    const command = ['pdf', synctex_path, page, h, v]
+    return fse.ensureDir(compileDir, function(error) {
+      if (error != null) {
+        logger.err(
+          { error, project_id, user_id, file_name },
+          'error ensuring dir for sync to code'
+        )
+        return callback(error)
+      }
+      return CompileManager._runSynctex(project_id, user_id, command, function(
+        error,
+        stdout
+      ) {
+        if (error != null) {
+          return callback(error)
+        }
+        logger.log(
+          { project_id, user_id, page, h, v, stdout },
+          'synctex pdf output'
+        )
+        return callback(
+          null,
+          CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)
+        )
+      })
+    })
+  },
+
+  _checkFileExists(path, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const synctexDir = Path.dirname(path)
+    const synctexFile = Path.join(synctexDir, 'output.synctex.gz')
+    return fs.stat(synctexDir, function(error, stats) {
+      if ((error != null ? error.code : undefined) === 'ENOENT') {
+        return callback(
+          new Errors.NotFoundError('called synctex with no output directory')
+        )
+      }
+      if (error != null) {
+        return callback(error)
+      }
+      return fs.stat(synctexFile, function(error, stats) {
+        if ((error != null ? error.code : undefined) === 'ENOENT') {
+          return callback(
+            new Errors.NotFoundError('called synctex with no output file')
+          )
+        }
+        if (error != null) {
+          return callback(error)
+        }
+        if (!(stats != null ? stats.isFile() : undefined)) {
+          return callback(new Error('not a file'))
+        }
+        return callback()
+      })
+    })
+  },
+
+  _runSynctex(project_id, user_id, command, callback) {
+    if (callback == null) {
+      callback = function(error, stdout) {}
+    }
+    const seconds = 1000
+
+    command.unshift('/opt/synctex')
+
+    const directory = getCompileDir(project_id, user_id)
+    const timeout = 60 * 1000 // increased to allow for large projects
+    const compileName = getCompileName(project_id, user_id)
+    return CommandRunner.run(
+      compileName,
+      command,
+      directory,
+      Settings.clsi != null ? Settings.clsi.docker.image : undefined,
+      timeout,
+      {},
+      function(error, output) {
+        if (error != null) {
+          logger.err(
+            { err: error, command, project_id, user_id },
+            'error running synctex'
+          )
+          return callback(error)
+        }
+        return callback(null, output.stdout)
+      }
+    )
+  },
+
+  _parseSynctexFromCodeOutput(output) {
+    const results = []
+    for (const line of Array.from(output.split('\n'))) {
+      const [node, page, h, v, width, height] = Array.from(line.split('\t'))
+      if (node === 'NODE') {
+        results.push({
+          page: parseInt(page, 10),
+          h: parseFloat(h),
+          v: parseFloat(v),
+          height: parseFloat(height),
+          width: parseFloat(width)
+        })
+      }
+    }
+    return results
+  },
+
+  _parseSynctexFromPdfOutput(output, base_dir) {
+    const results = []
+    for (let line of Array.from(output.split('\n'))) {
+      let column, file_path, node
+      ;[node, file_path, line, column] = Array.from(line.split('\t'))
+      if (node === 'NODE') {
+        const file = file_path.slice(base_dir.length + 1)
+        results.push({
+          file,
+          line: parseInt(line, 10),
+          column: parseInt(column, 10)
+        })
+      }
+    }
+    return results
+  },
+
+  wordcount(project_id, user_id, file_name, image, callback) {
+    if (callback == null) {
+      callback = function(error, pdfPositions) {}
+    }
+    logger.log({ project_id, user_id, file_name, image }, 'running wordcount')
+    const file_path = `$COMPILE_DIR/${file_name}`
+    const command = [
+      'texcount',
+      '-nocol',
+      '-inc',
+      file_path,
+      `-out=${file_path}.wc`
+    ]
+    const compileDir = getCompileDir(project_id, user_id)
+    const timeout = 60 * 1000
+    const compileName = getCompileName(project_id, user_id)
+    return fse.ensureDir(compileDir, function(error) {
+      if (error != null) {
+        logger.err(
+          { error, project_id, user_id, file_name },
+          'error ensuring dir for sync from code'
+        )
+        return callback(error)
+      }
+      return CommandRunner.run(
+        compileName,
+        command,
+        compileDir,
+        image,
+        timeout,
+        {},
+        function(error) {
+          if (error != null) {
+            return callback(error)
+          }
+          return fs.readFile(
+            compileDir + '/' + file_name + '.wc',
+            'utf-8',
+            function(err, stdout) {
+              if (err != null) {
+                // call it node_err so sentry doesn't use random path error as unique id so it can't be ignored
+                logger.err(
+                  { node_err: err, command, compileDir, project_id, user_id },
+                  'error reading word count output'
+                )
+                return callback(err)
+              }
+              const results = CompileManager._parseWordcountFromOutput(stdout)
+              logger.log(
+                { project_id, user_id, wordcount: results },
+                'word count results'
+              )
+              return callback(null, results)
+            }
+          )
+        }
+      )
+    })
+  },
+
+  _parseWordcountFromOutput(output) {
+    const results = {
+      encode: '',
+      textWords: 0,
+      headWords: 0,
+      outside: 0,
+      headers: 0,
+      elements: 0,
+      mathInline: 0,
+      mathDisplay: 0,
+      errors: 0,
+      messages: ''
+    }
+    for (const line of Array.from(output.split('\n'))) {
+      const [data, info] = Array.from(line.split(':'))
+      if (data.indexOf('Encoding') > -1) {
+        results.encode = info.trim()
+      }
+      if (data.indexOf('in text') > -1) {
+        results.textWords = parseInt(info, 10)
+      }
+      if (data.indexOf('in head') > -1) {
+        results.headWords = parseInt(info, 10)
+      }
+      if (data.indexOf('outside') > -1) {
+        results.outside = parseInt(info, 10)
+      }
+      if (data.indexOf('of head') > -1) {
+        results.headers = parseInt(info, 10)
+      }
+      if (data.indexOf('Number of floats/tables/figures') > -1) {
+        results.elements = parseInt(info, 10)
+      }
+      if (data.indexOf('Number of math inlines') > -1) {
+        results.mathInline = parseInt(info, 10)
+      }
+      if (data.indexOf('Number of math displayed') > -1) {
+        results.mathDisplay = parseInt(info, 10)
+      }
+      if (data === '(errors') {
+        // errors reported as (errors:123)
+        results.errors = parseInt(info, 10)
+      }
+      if (line.indexOf('!!! ') > -1) {
+        // errors logged as !!! message !!!
+        results.messages += line + '\n'
+      }
+    }
+    return results
+  }
+}
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 38 - 0
services/clsi/app/js/ContentTypeMapper.js

@@ -0,0 +1,38 @@
+/* eslint-disable
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+let ContentTypeMapper
+const Path = require('path')
+
+// here we coerce html, css and js to text/plain,
+// otherwise choose correct mime type based on file extension,
+// falling back to octet-stream
+module.exports = ContentTypeMapper = {
+  map(path) {
+    switch (Path.extname(path)) {
+      case '.txt':
+      case '.html':
+      case '.js':
+      case '.css':
+      case '.svg':
+        return 'text/plain'
+      case '.csv':
+        return 'text/csv'
+      case '.pdf':
+        return 'application/pdf'
+      case '.png':
+        return 'image/png'
+      case '.jpg':
+      case '.jpeg':
+        return 'image/jpeg'
+      case '.tiff':
+        return 'image/tiff'
+      case '.gif':
+        return 'image/gif'
+      default:
+        return 'application/octet-stream'
+    }
+  }
+}

+ 18 - 0
services/clsi/app/js/DbQueue.js

@@ -0,0 +1,18 @@
+// TODO: This file was created by bulk-decaffeinate.
+// Sanity-check the conversion and remove this comment.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const async = require('async')
+const Settings = require('settings-sharelatex')
+const logger = require('logger-sharelatex')
+const queue = async.queue(
+  (task, cb) => task(cb),
+  Settings.parallelSqlQueryLimit
+)
+
+queue.drain = () => logger.debug('all items have been processed')
+
+module.exports = { queue }

+ 113 - 0
services/clsi/app/js/DockerLockManager.js

@@ -0,0 +1,113 @@
+/* eslint-disable
+    handle-callback-err,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let LockManager
+const logger = require('logger-sharelatex')
+
+const LockState = {} // locks for docker container operations, by container name
+
+module.exports = LockManager = {
+  MAX_LOCK_HOLD_TIME: 15000, // how long we can keep a lock
+  MAX_LOCK_WAIT_TIME: 10000, // how long we wait for a lock
+  LOCK_TEST_INTERVAL: 1000, // retry time
+
+  tryLock(key, callback) {
+    let lockValue
+    if (callback == null) {
+      callback = function(err, gotLock) {}
+    }
+    const existingLock = LockState[key]
+    if (existingLock != null) {
+      // the lock is already taken, check how old it is
+      const lockAge = Date.now() - existingLock.created
+      if (lockAge < LockManager.MAX_LOCK_HOLD_TIME) {
+        return callback(null, false) // we didn't get the lock, bail out
+      } else {
+        logger.error(
+          { key, lock: existingLock, age: lockAge },
+          'taking old lock by force'
+        )
+      }
+    }
+    // take the lock
+    LockState[key] = lockValue = { created: Date.now() }
+    return callback(null, true, lockValue)
+  },
+
+  getLock(key, callback) {
+    let attempt
+    if (callback == null) {
+      callback = function(error, lockValue) {}
+    }
+    const startTime = Date.now()
+    return (attempt = () =>
+      LockManager.tryLock(key, function(error, gotLock, lockValue) {
+        if (error != null) {
+          return callback(error)
+        }
+        if (gotLock) {
+          return callback(null, lockValue)
+        } else if (Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME) {
+          const e = new Error('Lock timeout')
+          e.key = key
+          return callback(e)
+        } else {
+          return setTimeout(attempt, LockManager.LOCK_TEST_INTERVAL)
+        }
+      }))()
+  },
+
+  releaseLock(key, lockValue, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const existingLock = LockState[key]
+    if (existingLock === lockValue) {
+      // lockValue is an object, so we can test by reference
+      delete LockState[key] // our lock, so we can free it
+      return callback()
+    } else if (existingLock != null) {
+      // lock exists but doesn't match ours
+      logger.error(
+        { key, lock: existingLock },
+        'tried to release lock taken by force'
+      )
+      return callback()
+    } else {
+      logger.error(
+        { key, lock: existingLock },
+        'tried to release lock that has gone'
+      )
+      return callback()
+    }
+  },
+
+  runWithLock(key, runner, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return LockManager.getLock(key, function(error, lockValue) {
+      if (error != null) {
+        return callback(error)
+      }
+      return runner((error1, ...args) =>
+        LockManager.releaseLock(key, lockValue, function(error2) {
+          error = error1 || error2
+          if (error != null) {
+            return callback(error)
+          }
+          return callback(null, ...Array.from(args))
+        })
+      )
+    })
+  }
+}

+ 695 - 0
services/clsi/app/js/DockerRunner.js

@@ -0,0 +1,695 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-return-assign,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS205: Consider reworking code to avoid use of IIFEs
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let DockerRunner, oneHour
+const Settings = require('settings-sharelatex')
+const logger = require('logger-sharelatex')
+const Docker = require('dockerode')
+const dockerode = new Docker()
+const crypto = require('crypto')
+const async = require('async')
+const LockManager = require('./DockerLockManager')
+const fs = require('fs')
+const Path = require('path')
+const _ = require('underscore')
+
+logger.info('using docker runner')
+
+const usingSiblingContainers = () =>
+  __guard__(
+    Settings != null ? Settings.path : undefined,
+    x => x.sandboxedCompilesHostDir
+  ) != null
+
+let containerMonitorTimeout
+let containerMonitorInterval
+
+module.exports = DockerRunner = {
+  ERR_NOT_DIRECTORY: new Error('not a directory'),
+  ERR_TERMINATED: new Error('terminated'),
+  ERR_EXITED: new Error('exited'),
+  ERR_TIMED_OUT: new Error('container timed out'),
+
+  run(project_id, command, directory, image, timeout, environment, callback) {
+    let name
+    if (callback == null) {
+      callback = function(error, output) {}
+    }
+    if (usingSiblingContainers()) {
+      const _newPath = Settings.path.sandboxedCompilesHostDir
+      logger.log(
+        { path: _newPath },
+        'altering bind path for sibling containers'
+      )
+      // Server Pro, example:
+      //   '/var/lib/sharelatex/data/compiles/<project-id>'
+      //   ... becomes ...
+      //   '/opt/sharelatex_data/data/compiles/<project-id>'
+      directory = Path.join(
+        Settings.path.sandboxedCompilesHostDir,
+        Path.basename(directory)
+      )
+    }
+
+    const volumes = {}
+    volumes[directory] = '/compile'
+
+    command = Array.from(command).map(arg =>
+      __guardMethod__(arg.toString(), 'replace', o =>
+        o.replace('$COMPILE_DIR', '/compile')
+      )
+    )
+    if (image == null) {
+      ;({ image } = Settings.clsi.docker)
+    }
+
+    if (Settings.texliveImageNameOveride != null) {
+      const img = image.split('/')
+      image = `${Settings.texliveImageNameOveride}/${img[2]}`
+    }
+
+    const options = DockerRunner._getContainerOptions(
+      command,
+      image,
+      volumes,
+      timeout,
+      environment
+    )
+    const fingerprint = DockerRunner._fingerprintContainer(options)
+    options.name = name = `project-${project_id}-${fingerprint}`
+
+    // logOptions = _.clone(options)
+    // logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging"
+    logger.log({ project_id }, 'running docker container')
+    DockerRunner._runAndWaitForContainer(options, volumes, timeout, function(
+      error,
+      output
+    ) {
+      if (error && error.statusCode === 500) {
+        logger.log(
+          { err: error, project_id },
+          'error running container so destroying and retrying'
+        )
+        return DockerRunner.destroyContainer(name, null, true, function(error) {
+          if (error != null) {
+            return callback(error)
+          }
+          return DockerRunner._runAndWaitForContainer(
+            options,
+            volumes,
+            timeout,
+            callback
+          )
+        })
+      } else {
+        return callback(error, output)
+      }
+    })
+
+    return name
+  }, // pass back the container name to allow it to be killed
+
+  kill(container_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    logger.log({ container_id }, 'sending kill signal to container')
+    const container = dockerode.getContainer(container_id)
+    return container.kill(function(error) {
+      if (
+        error != null &&
+        __guardMethod__(error != null ? error.message : undefined, 'match', o =>
+          o.match(/Cannot kill container .* is not running/)
+        )
+      ) {
+        logger.warn(
+          { err: error, container_id },
+          'container not running, continuing'
+        )
+        error = null
+      }
+      if (error != null) {
+        logger.error({ err: error, container_id }, 'error killing container')
+        return callback(error)
+      } else {
+        return callback()
+      }
+    })
+  },
+
+  _runAndWaitForContainer(options, volumes, timeout, _callback) {
+    if (_callback == null) {
+      _callback = function(error, output) {}
+    }
+    const callback = function(...args) {
+      _callback(...Array.from(args || []))
+      // Only call the callback once
+      return (_callback = function() {})
+    }
+
+    const { name } = options
+
+    let streamEnded = false
+    let containerReturned = false
+    let output = {}
+
+    const callbackIfFinished = function() {
+      if (streamEnded && containerReturned) {
+        return callback(null, output)
+      }
+    }
+
+    const attachStreamHandler = function(error, _output) {
+      if (error != null) {
+        return callback(error)
+      }
+      output = _output
+      streamEnded = true
+      return callbackIfFinished()
+    }
+
+    return DockerRunner.startContainer(
+      options,
+      volumes,
+      attachStreamHandler,
+      function(error, containerId) {
+        if (error != null) {
+          return callback(error)
+        }
+
+        return DockerRunner.waitForContainer(name, timeout, function(
+          error,
+          exitCode
+        ) {
+          let err
+          if (error != null) {
+            return callback(error)
+          }
+          if (exitCode === 137) {
+            // exit status from kill -9
+            err = DockerRunner.ERR_TERMINATED
+            err.terminated = true
+            return callback(err)
+          }
+          if (exitCode === 1) {
+            // exit status from chktex
+            err = DockerRunner.ERR_EXITED
+            err.code = exitCode
+            return callback(err)
+          }
+          containerReturned = true
+          __guard__(
+            options != null ? options.HostConfig : undefined,
+            x => (x.SecurityOpt = null)
+          ) // small log line
+          logger.log({ err, exitCode, options }, 'docker container has exited')
+          return callbackIfFinished()
+        })
+      }
+    )
+  },
+
+  _getContainerOptions(command, image, volumes, timeout, environment) {
+    let m, year
+    let key, value, hostVol, dockerVol
+    const timeoutInSeconds = timeout / 1000
+
+    const dockerVolumes = {}
+    for (hostVol in volumes) {
+      dockerVol = volumes[hostVol]
+      dockerVolumes[dockerVol] = {}
+
+      if (volumes[hostVol].slice(-3).indexOf(':r') === -1) {
+        volumes[hostVol] = `${dockerVol}:rw`
+      }
+    }
+
+    // merge settings and environment parameter
+    const env = {}
+    for (const src of [Settings.clsi.docker.env, environment || {}]) {
+      for (key in src) {
+        value = src[key]
+        env[key] = value
+      }
+    }
+    // set the path based on the image year
+    if ((m = image.match(/:([0-9]+)\.[0-9]+/))) {
+      year = m[1]
+    } else {
+      year = '2014'
+    }
+    env.PATH = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/${year}/bin/x86_64-linux/`
+    const options = {
+      Cmd: command,
+      Image: image,
+      Volumes: dockerVolumes,
+      WorkingDir: '/compile',
+      NetworkDisabled: true,
+      Memory: 1024 * 1024 * 1024 * 1024, // 1 Gb
+      User: Settings.clsi.docker.user,
+      Env: (() => {
+        const result = []
+        for (key in env) {
+          value = env[key]
+          result.push(`${key}=${value}`)
+        }
+        return result
+      })(), // convert the environment hash to an array
+      HostConfig: {
+        Binds: (() => {
+          const result1 = []
+          for (hostVol in volumes) {
+            dockerVol = volumes[hostVol]
+            result1.push(`${hostVol}:${dockerVol}`)
+          }
+          return result1
+        })(),
+        LogConfig: { Type: 'none', Config: {} },
+        Ulimits: [
+          {
+            Name: 'cpu',
+            Soft: timeoutInSeconds + 5,
+            Hard: timeoutInSeconds + 10
+          }
+        ],
+        CapDrop: 'ALL',
+        SecurityOpt: ['no-new-privileges']
+      }
+    }
+
+    if (
+      (Settings.path != null ? Settings.path.synctexBinHostPath : undefined) !=
+      null
+    ) {
+      options.HostConfig.Binds.push(
+        `${Settings.path.synctexBinHostPath}:/opt/synctex:ro`
+      )
+    }
+
+    if (Settings.clsi.docker.seccomp_profile != null) {
+      options.HostConfig.SecurityOpt.push(
+        `seccomp=${Settings.clsi.docker.seccomp_profile}`
+      )
+    }
+
+    if (Settings.clsi.docker.runtime) {
+      options.HostConfig.Runtime = Settings.clsi.docker.runtime
+    }
+
+    return options
+  },
+
+  _fingerprintContainer(containerOptions) {
+    // Yay, Hashing!
+    const json = JSON.stringify(containerOptions)
+    return crypto
+      .createHash('md5')
+      .update(json)
+      .digest('hex')
+  },
+
+  startContainer(options, volumes, attachStreamHandler, callback) {
+    return LockManager.runWithLock(
+      options.name,
+      releaseLock =>
+        // Check that volumes exist before starting the container.
+        // When a container is started with volume pointing to a
+        // non-existent directory then docker creates the directory but
+        // with root ownership.
+        DockerRunner._checkVolumes(options, volumes, function(err) {
+          if (err != null) {
+            return releaseLock(err)
+          }
+          return DockerRunner._startContainer(
+            options,
+            volumes,
+            attachStreamHandler,
+            releaseLock
+          )
+        }),
+
+      callback
+    )
+  },
+
+  // Check that volumes exist and are directories
+  _checkVolumes(options, volumes, callback) {
+    if (callback == null) {
+      callback = function(error, containerName) {}
+    }
+    if (usingSiblingContainers()) {
+      // Server Pro, with sibling-containers active, skip checks
+      return callback(null)
+    }
+
+    const checkVolume = (path, cb) =>
+      fs.stat(path, function(err, stats) {
+        if (err != null) {
+          return cb(err)
+        }
+        if (!(stats != null ? stats.isDirectory() : undefined)) {
+          return cb(DockerRunner.ERR_NOT_DIRECTORY)
+        }
+        return cb()
+      })
+    const jobs = []
+    for (const vol in volumes) {
+      ;(vol => jobs.push(cb => checkVolume(vol, cb)))(vol)
+    }
+    return async.series(jobs, callback)
+  },
+
+  _startContainer(options, volumes, attachStreamHandler, callback) {
+    if (callback == null) {
+      callback = function(error, output) {}
+    }
+    callback = _.once(callback)
+    const { name } = options
+
+    logger.log({ container_name: name }, 'starting container')
+    const container = dockerode.getContainer(name)
+
+    const createAndStartContainer = () =>
+      dockerode.createContainer(options, function(error, container) {
+        if (error != null) {
+          return callback(error)
+        }
+        return startExistingContainer()
+      })
+    var startExistingContainer = () =>
+      DockerRunner.attachToContainer(
+        options.name,
+        attachStreamHandler,
+        function(error) {
+          if (error != null) {
+            return callback(error)
+          }
+          return container.start(function(error) {
+            if (
+              error != null &&
+              (error != null ? error.statusCode : undefined) !== 304
+            ) {
+              // already running
+              return callback(error)
+            } else {
+              return callback()
+            }
+          })
+        }
+      )
+    return container.inspect(function(error, stats) {
+      if ((error != null ? error.statusCode : undefined) === 404) {
+        return createAndStartContainer()
+      } else if (error != null) {
+        logger.err(
+          { container_name: name, error },
+          'unable to inspect container to start'
+        )
+        return callback(error)
+      } else {
+        return startExistingContainer()
+      }
+    })
+  },
+
+  attachToContainer(containerId, attachStreamHandler, attachStartCallback) {
+    const container = dockerode.getContainer(containerId)
+    return container.attach({ stdout: 1, stderr: 1, stream: 1 }, function(
+      error,
+      stream
+    ) {
+      if (error != null) {
+        logger.error(
+          { err: error, container_id: containerId },
+          'error attaching to container'
+        )
+        return attachStartCallback(error)
+      } else {
+        attachStartCallback()
+      }
+
+      logger.log({ container_id: containerId }, 'attached to container')
+
+      const MAX_OUTPUT = 1024 * 1024 // limit output to 1MB
+      const createStringOutputStream = function(name) {
+        return {
+          data: '',
+          overflowed: false,
+          write(data) {
+            if (this.overflowed) {
+              return
+            }
+            if (this.data.length < MAX_OUTPUT) {
+              return (this.data += data)
+            } else {
+              logger.error(
+                {
+                  container_id: containerId,
+                  length: this.data.length,
+                  maxLen: MAX_OUTPUT
+                },
+                `${name} exceeds max size`
+              )
+              this.data += `(...truncated at ${MAX_OUTPUT} chars...)`
+              return (this.overflowed = true)
+            }
+          }
+          // kill container if too much output
+          // docker.containers.kill(containerId, () ->)
+        }
+      }
+
+      const stdout = createStringOutputStream('stdout')
+      const stderr = createStringOutputStream('stderr')
+
+      container.modem.demuxStream(stream, stdout, stderr)
+
+      stream.on('error', err =>
+        logger.error(
+          { err, container_id: containerId },
+          'error reading from container stream'
+        )
+      )
+
+      return stream.on('end', () =>
+        attachStreamHandler(null, { stdout: stdout.data, stderr: stderr.data })
+      )
+    })
+  },
+
+  waitForContainer(containerId, timeout, _callback) {
+    if (_callback == null) {
+      _callback = function(error, exitCode) {}
+    }
+    const callback = function(...args) {
+      _callback(...Array.from(args || []))
+      // Only call the callback once
+      return (_callback = function() {})
+    }
+
+    const container = dockerode.getContainer(containerId)
+
+    let timedOut = false
+    const timeoutId = setTimeout(function() {
+      timedOut = true
+      logger.log(
+        { container_id: containerId },
+        'timeout reached, killing container'
+      )
+      return container.kill(function() {})
+    }, timeout)
+
+    logger.log({ container_id: containerId }, 'waiting for docker container')
+    return container.wait(function(error, res) {
+      if (error != null) {
+        clearTimeout(timeoutId)
+        logger.error(
+          { err: error, container_id: containerId },
+          'error waiting for container'
+        )
+        return callback(error)
+      }
+      if (timedOut) {
+        logger.log({ containerId }, 'docker container timed out')
+        error = DockerRunner.ERR_TIMED_OUT
+        error.timedout = true
+        return callback(error)
+      } else {
+        clearTimeout(timeoutId)
+        logger.log(
+          { container_id: containerId, exitCode: res.StatusCode },
+          'docker container returned'
+        )
+        return callback(null, res.StatusCode)
+      }
+    })
+  },
+
+  destroyContainer(containerName, containerId, shouldForce, callback) {
+    // We want the containerName for the lock and, ideally, the
+    // containerId to delete.  There is a bug in the docker.io module
+    // where if you delete by name and there is an error, it throws an
+    // async exception, but if you delete by id it just does a normal
+    // error callback. We fall back to deleting by name if no id is
+    // supplied.
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return LockManager.runWithLock(
+      containerName,
+      releaseLock =>
+        DockerRunner._destroyContainer(
+          containerId || containerName,
+          shouldForce,
+          releaseLock
+        ),
+      callback
+    )
+  },
+
+  _destroyContainer(containerId, shouldForce, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    logger.log({ container_id: containerId }, 'destroying docker container')
+    const container = dockerode.getContainer(containerId)
+    return container.remove({ force: shouldForce === true }, function(error) {
+      if (
+        error != null &&
+        (error != null ? error.statusCode : undefined) === 404
+      ) {
+        logger.warn(
+          { err: error, container_id: containerId },
+          'container not found, continuing'
+        )
+        error = null
+      }
+      if (error != null) {
+        logger.error(
+          { err: error, container_id: containerId },
+          'error destroying container'
+        )
+      } else {
+        logger.log({ container_id: containerId }, 'destroyed container')
+      }
+      return callback(error)
+    })
+  },
+
+  // handle expiry of docker containers
+
+  MAX_CONTAINER_AGE:
+    Settings.clsi.docker.maxContainerAge || (oneHour = 60 * 60 * 1000),
+
+  examineOldContainer(container, callback) {
+    if (callback == null) {
+      callback = function(error, name, id, ttl) {}
+    }
+    const name =
+      container.Name ||
+      (container.Names != null ? container.Names[0] : undefined)
+    const created = container.Created * 1000 // creation time is returned in seconds
+    const now = Date.now()
+    const age = now - created
+    const maxAge = DockerRunner.MAX_CONTAINER_AGE
+    const ttl = maxAge - age
+    logger.log(
+      { containerName: name, created, now, age, maxAge, ttl },
+      'checking whether to destroy container'
+    )
+    return callback(null, name, container.Id, ttl)
+  },
+
+  destroyOldContainers(callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return dockerode.listContainers({ all: true }, function(error, containers) {
+      if (error != null) {
+        return callback(error)
+      }
+      const jobs = []
+      for (const container of Array.from(containers || [])) {
+        ;(container =>
+          DockerRunner.examineOldContainer(container, function(
+            err,
+            name,
+            id,
+            ttl
+          ) {
+            if (name.slice(0, 9) === '/project-' && ttl <= 0) {
+              return jobs.push(cb =>
+                DockerRunner.destroyContainer(name, id, false, () => cb())
+              )
+            }
+          }))(container)
+      }
+      // Ignore errors because some containers get stuck but
+      // will be destroyed next time
+      return async.series(jobs, callback)
+    })
+  },
+
+  startContainerMonitor() {
+    logger.log(
+      { maxAge: DockerRunner.MAX_CONTAINER_AGE },
+      'starting container expiry'
+    )
+
+    // guarantee only one monitor is running
+    DockerRunner.stopContainerMonitor()
+
+    // randomise the start time
+    const randomDelay = Math.floor(Math.random() * 5 * 60 * 1000)
+    containerMonitorTimeout = setTimeout(() => {
+      containerMonitorInterval = setInterval(
+        () => DockerRunner.destroyOldContainers(),
+        (oneHour = 60 * 60 * 1000)
+      )
+    }, randomDelay)
+  },
+
+  stopContainerMonitor() {
+    if (containerMonitorTimeout) {
+      clearTimeout(containerMonitorTimeout)
+      containerMonitorTimeout = undefined
+    }
+    if (containerMonitorInterval) {
+      clearInterval(containerMonitorTimeout)
+      containerMonitorTimeout = undefined
+    }
+  }
+}
+
+DockerRunner.startContainerMonitor()
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}
+function __guardMethod__(obj, methodName, transform) {
+  if (
+    typeof obj !== 'undefined' &&
+    obj !== null &&
+    typeof obj[methodName] === 'function'
+  ) {
+    return transform(obj, methodName)
+  } else {
+    return undefined
+  }
+}

+ 57 - 0
services/clsi/app/js/DraftModeManager.js

@@ -0,0 +1,57 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-useless-escape,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let DraftModeManager
+const fs = require('fs')
+const logger = require('logger-sharelatex')
+
+module.exports = DraftModeManager = {
+  injectDraftMode(filename, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return fs.readFile(filename, 'utf8', function(error, content) {
+      if (error != null) {
+        return callback(error)
+      }
+      // avoid adding draft mode more than once
+      if (
+        (content != null
+          ? content.indexOf('\\documentclass[draft')
+          : undefined) >= 0
+      ) {
+        return callback()
+      }
+      const modified_content = DraftModeManager._injectDraftOption(content)
+      logger.log(
+        {
+          content: content.slice(0, 1024), // \documentclass is normally v near the top
+          modified_content: modified_content.slice(0, 1024),
+          filename
+        },
+        'injected draft class'
+      )
+      return fs.writeFile(filename, modified_content, callback)
+    })
+  },
+
+  _injectDraftOption(content) {
+    return (
+      content
+        // With existing options (must be first, otherwise both are applied)
+        .replace(/\\documentclass\[/g, '\\documentclass[draft,')
+        // Without existing options
+        .replace(/\\documentclass\{/g, '\\documentclass[draft]{')
+    )
+  }
+}

+ 36 - 0
services/clsi/app/js/Errors.js

@@ -0,0 +1,36 @@
+/* eslint-disable
+    no-proto,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+let Errors
+var NotFoundError = function(message) {
+  const error = new Error(message)
+  error.name = 'NotFoundError'
+  error.__proto__ = NotFoundError.prototype
+  return error
+}
+NotFoundError.prototype.__proto__ = Error.prototype
+
+var FilesOutOfSyncError = function(message) {
+  const error = new Error(message)
+  error.name = 'FilesOutOfSyncError'
+  error.__proto__ = FilesOutOfSyncError.prototype
+  return error
+}
+FilesOutOfSyncError.prototype.__proto__ = Error.prototype
+
+var AlreadyCompilingError = function(message) {
+  const error = new Error(message)
+  error.name = 'AlreadyCompilingError'
+  error.__proto__ = AlreadyCompilingError.prototype
+  return error
+}
+AlreadyCompilingError.prototype.__proto__ = Error.prototype
+
+module.exports = Errors = {
+  NotFoundError,
+  FilesOutOfSyncError,
+  AlreadyCompilingError
+}

+ 204 - 0
services/clsi/app/js/LatexRunner.js

@@ -0,0 +1,204 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-return-assign,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let LatexRunner
+const Path = require('path')
+const Settings = require('settings-sharelatex')
+const logger = require('logger-sharelatex')
+const Metrics = require('./Metrics')
+const CommandRunner = require('./CommandRunner')
+
+const ProcessTable = {} // table of currently running jobs (pids or docker container names)
+
+module.exports = LatexRunner = {
+  runLatex(project_id, options, callback) {
+    let command
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    let {
+      directory,
+      mainFile,
+      compiler,
+      timeout,
+      image,
+      environment,
+      flags
+    } = options
+    if (!compiler) {
+      compiler = 'pdflatex'
+    }
+    if (!timeout) {
+      timeout = 60000
+    } // milliseconds
+
+    logger.log(
+      { directory, compiler, timeout, mainFile, environment, flags },
+      'starting compile'
+    )
+
+    // We want to run latexmk on the tex file which we will automatically
+    // generate from the Rtex/Rmd/md file.
+    mainFile = mainFile.replace(/\.(Rtex|md|Rmd)$/, '.tex')
+
+    if (compiler === 'pdflatex') {
+      command = LatexRunner._pdflatexCommand(mainFile, flags)
+    } else if (compiler === 'latex') {
+      command = LatexRunner._latexCommand(mainFile, flags)
+    } else if (compiler === 'xelatex') {
+      command = LatexRunner._xelatexCommand(mainFile, flags)
+    } else if (compiler === 'lualatex') {
+      command = LatexRunner._lualatexCommand(mainFile, flags)
+    } else {
+      return callback(new Error(`unknown compiler: ${compiler}`))
+    }
+
+    if (Settings.clsi != null ? Settings.clsi.strace : undefined) {
+      command = ['strace', '-o', 'strace', '-ff'].concat(command)
+    }
+
+    const id = `${project_id}` // record running project under this id
+
+    return (ProcessTable[id] = CommandRunner.run(
+      project_id,
+      command,
+      directory,
+      image,
+      timeout,
+      environment,
+      function(error, output) {
+        delete ProcessTable[id]
+        if (error != null) {
+          return callback(error)
+        }
+        const runs =
+          __guard__(
+            __guard__(output != null ? output.stderr : undefined, x1 =>
+              x1.match(/^Run number \d+ of .*latex/gm)
+            ),
+            x => x.length
+          ) || 0
+        const failed =
+          __guard__(output != null ? output.stdout : undefined, x2 =>
+            x2.match(/^Latexmk: Errors/m)
+          ) != null
+            ? 1
+            : 0
+        // counters from latexmk output
+        const stats = {}
+        stats['latexmk-errors'] = failed
+        stats['latex-runs'] = runs
+        stats['latex-runs-with-errors'] = failed ? runs : 0
+        stats[`latex-runs-${runs}`] = 1
+        stats[`latex-runs-with-errors-${runs}`] = failed ? 1 : 0
+        // timing information from /usr/bin/time
+        const timings = {}
+        const stderr = output != null ? output.stderr : undefined
+        timings['cpu-percent'] =
+          __guard__(
+            stderr != null
+              ? stderr.match(/Percent of CPU this job got: (\d+)/m)
+              : undefined,
+            x3 => x3[1]
+          ) || 0
+        timings['cpu-time'] =
+          __guard__(
+            stderr != null
+              ? stderr.match(/User time.*: (\d+.\d+)/m)
+              : undefined,
+            x4 => x4[1]
+          ) || 0
+        timings['sys-time'] =
+          __guard__(
+            stderr != null
+              ? stderr.match(/System time.*: (\d+.\d+)/m)
+              : undefined,
+            x5 => x5[1]
+          ) || 0
+        return callback(error, output, stats, timings)
+      }
+    ))
+  },
+
+  killLatex(project_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const id = `${project_id}`
+    logger.log({ id }, 'killing running compile')
+    if (ProcessTable[id] == null) {
+      logger.warn({ id }, 'no such project to kill')
+      return callback(null)
+    } else {
+      return CommandRunner.kill(ProcessTable[id], callback)
+    }
+  },
+
+  _latexmkBaseCommand(flags) {
+    let args = [
+      'latexmk',
+      '-cd',
+      '-f',
+      '-jobname=output',
+      '-auxdir=$COMPILE_DIR',
+      '-outdir=$COMPILE_DIR',
+      '-synctex=1',
+      '-interaction=batchmode'
+    ]
+    if (flags) {
+      args = args.concat(flags)
+    }
+    return (
+      __guard__(
+        Settings != null ? Settings.clsi : undefined,
+        x => x.latexmkCommandPrefix
+      ) || []
+    ).concat(args)
+  },
+
+  _pdflatexCommand(mainFile, flags) {
+    return LatexRunner._latexmkBaseCommand(flags).concat([
+      '-pdf',
+      Path.join('$COMPILE_DIR', mainFile)
+    ])
+  },
+
+  _latexCommand(mainFile, flags) {
+    return LatexRunner._latexmkBaseCommand(flags).concat([
+      '-pdfdvi',
+      Path.join('$COMPILE_DIR', mainFile)
+    ])
+  },
+
+  _xelatexCommand(mainFile, flags) {
+    return LatexRunner._latexmkBaseCommand(flags).concat([
+      '-xelatex',
+      Path.join('$COMPILE_DIR', mainFile)
+    ])
+  },
+
+  _lualatexCommand(mainFile, flags) {
+    return LatexRunner._latexmkBaseCommand(flags).concat([
+      '-lualatex',
+      Path.join('$COMPILE_DIR', mainFile)
+    ])
+  }
+}
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 91 - 0
services/clsi/app/js/LocalCommandRunner.js

@@ -0,0 +1,91 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-return-assign,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let CommandRunner
+const { spawn } = require('child_process')
+const logger = require('logger-sharelatex')
+
+logger.info('using standard command runner')
+
+module.exports = CommandRunner = {
+  run(project_id, command, directory, image, timeout, environment, callback) {
+    let key, value
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    command = Array.from(command).map(arg =>
+      arg.toString().replace('$COMPILE_DIR', directory)
+    )
+    logger.log({ project_id, command, directory }, 'running command')
+    logger.warn('timeouts and sandboxing are not enabled with CommandRunner')
+
+    // merge environment settings
+    const env = {}
+    for (key in process.env) {
+      value = process.env[key]
+      env[key] = value
+    }
+    for (key in environment) {
+      value = environment[key]
+      env[key] = value
+    }
+
+    // run command as detached process so it has its own process group (which can be killed if needed)
+    const proc = spawn(command[0], command.slice(1), { cwd: directory, env })
+
+    let stdout = ''
+    proc.stdout.on('data', data => (stdout += data))
+
+    proc.on('error', function(err) {
+      logger.err(
+        { err, project_id, command, directory },
+        'error running command'
+      )
+      return callback(err)
+    })
+
+    proc.on('close', function(code, signal) {
+      let err
+      logger.info({ code, signal, project_id }, 'command exited')
+      if (signal === 'SIGTERM') {
+        // signal from kill method below
+        err = new Error('terminated')
+        err.terminated = true
+        return callback(err)
+      } else if (code === 1) {
+        // exit status from chktex
+        err = new Error('exited')
+        err.code = code
+        return callback(err)
+      } else {
+        return callback(null, { stdout: stdout })
+      }
+    })
+
+    return proc.pid
+  }, // return process id to allow job to be killed if necessary
+
+  kill(pid, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    try {
+      process.kill(-pid) // kill all processes in group
+    } catch (err) {
+      return callback(err)
+    }
+    return callback()
+  }
+}

+ 72 - 0
services/clsi/app/js/LockManager.js

@@ -0,0 +1,72 @@
+/* eslint-disable
+    handle-callback-err,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let LockManager
+const Settings = require('settings-sharelatex')
+const logger = require('logger-sharelatex')
+const Lockfile = require('lockfile') // from https://github.com/npm/lockfile
+const Errors = require('./Errors')
+const fs = require('fs')
+const Path = require('path')
+module.exports = LockManager = {
+  LOCK_TEST_INTERVAL: 1000, // 50ms between each test of the lock
+  MAX_LOCK_WAIT_TIME: 15000, // 10s maximum time to spend trying to get the lock
+  LOCK_STALE: 5 * 60 * 1000, // 5 mins time until lock auto expires
+
+  runWithLock(path, runner, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const lockOpts = {
+      wait: this.MAX_LOCK_WAIT_TIME,
+      pollPeriod: this.LOCK_TEST_INTERVAL,
+      stale: this.LOCK_STALE
+    }
+    return Lockfile.lock(path, lockOpts, function(error) {
+      if ((error != null ? error.code : undefined) === 'EEXIST') {
+        return callback(new Errors.AlreadyCompilingError('compile in progress'))
+      } else if (error != null) {
+        return fs.lstat(path, (statLockErr, statLock) =>
+          fs.lstat(Path.dirname(path), (statDirErr, statDir) =>
+            fs.readdir(Path.dirname(path), function(readdirErr, readdirDir) {
+              logger.err(
+                {
+                  error,
+                  path,
+                  statLock,
+                  statLockErr,
+                  statDir,
+                  statDirErr,
+                  readdirErr,
+                  readdirDir
+                },
+                'unable to get lock'
+              )
+              return callback(error)
+            })
+          )
+        )
+      } else {
+        return runner((error1, ...args) =>
+          Lockfile.unlock(path, function(error2) {
+            error = error1 || error2
+            if (error != null) {
+              return callback(error)
+            }
+            return callback(null, ...Array.from(args))
+          })
+        )
+      }
+    })
+  }
+}

+ 3 - 0
services/clsi/app/js/Metrics.js

@@ -0,0 +1,3 @@
+// TODO: This file was created by bulk-decaffeinate.
+// Sanity-check the conversion and remove this comment.
+module.exports = require('metrics-sharelatex')

+ 399 - 0
services/clsi/app/js/OutputCacheManager.js

@@ -0,0 +1,399 @@
+/* eslint-disable
+    handle-callback-err,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS104: Avoid inline assignments
+ * DS204: Change includes calls to have a more natural evaluation order
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let OutputCacheManager
+const async = require('async')
+const fs = require('fs')
+const fse = require('fs-extra')
+const Path = require('path')
+const logger = require('logger-sharelatex')
+const _ = require('underscore')
+const Settings = require('settings-sharelatex')
+const crypto = require('crypto')
+
+const OutputFileOptimiser = require('./OutputFileOptimiser')
+
+module.exports = OutputCacheManager = {
+  CACHE_SUBDIR: '.cache/clsi',
+  ARCHIVE_SUBDIR: '.archive/clsi',
+  // build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes
+  // for backwards compatibility, make the randombytes part optional
+  BUILD_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/,
+  CACHE_LIMIT: 2, // maximum number of cache directories
+  CACHE_AGE: 60 * 60 * 1000, // up to one hour old
+
+  path(buildId, file) {
+    // used by static server, given build id return '.cache/clsi/buildId'
+    if (buildId.match(OutputCacheManager.BUILD_REGEX)) {
+      return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId, file)
+    } else {
+      // for invalid build id, return top level
+      return file
+    }
+  },
+
+  generateBuildId(callback) {
+    // generate a secure build id from Date.now() and 8 random bytes in hex
+    if (callback == null) {
+      callback = function(error, buildId) {}
+    }
+    return crypto.randomBytes(8, function(err, buf) {
+      if (err != null) {
+        return callback(err)
+      }
+      const random = buf.toString('hex')
+      const date = Date.now().toString(16)
+      return callback(err, `${date}-${random}`)
+    })
+  },
+
+  saveOutputFiles(outputFiles, compileDir, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return OutputCacheManager.generateBuildId(function(err, buildId) {
+      if (err != null) {
+        return callback(err)
+      }
+      return OutputCacheManager.saveOutputFilesInBuildDir(
+        outputFiles,
+        compileDir,
+        buildId,
+        callback
+      )
+    })
+  },
+
+  saveOutputFilesInBuildDir(outputFiles, compileDir, buildId, callback) {
+    // make a compileDir/CACHE_SUBDIR/build_id directory and
+    // copy all the output files into it
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR)
+    // Put the files into a new cache subdirectory
+    const cacheDir = Path.join(
+      compileDir,
+      OutputCacheManager.CACHE_SUBDIR,
+      buildId
+    )
+    // Is it a per-user compile? check if compile directory is PROJECTID-USERID
+    const perUser = Path.basename(compileDir).match(
+      /^[0-9a-f]{24}-[0-9a-f]{24}$/
+    )
+
+    // Archive logs in background
+    if (
+      (Settings.clsi != null ? Settings.clsi.archive_logs : undefined) ||
+      (Settings.clsi != null ? Settings.clsi.strace : undefined)
+    ) {
+      OutputCacheManager.archiveLogs(outputFiles, compileDir, buildId, function(
+        err
+      ) {
+        if (err != null) {
+          return logger.warn({ err }, 'erroring archiving log files')
+        }
+      })
+    }
+
+    // make the new cache directory
+    return fse.ensureDir(cacheDir, function(err) {
+      if (err != null) {
+        logger.error(
+          { err, directory: cacheDir },
+          'error creating cache directory'
+        )
+        return callback(err, outputFiles)
+      } else {
+        // copy all the output files into the new cache directory
+        const results = []
+        return async.mapSeries(
+          outputFiles,
+          function(file, cb) {
+            // don't send dot files as output, express doesn't serve them
+            if (OutputCacheManager._fileIsHidden(file.path)) {
+              logger.debug(
+                { compileDir, path: file.path },
+                'ignoring dotfile in output'
+              )
+              return cb()
+            }
+            // copy other files into cache directory if valid
+            const newFile = _.clone(file)
+            const [src, dst] = Array.from([
+              Path.join(compileDir, file.path),
+              Path.join(cacheDir, file.path)
+            ])
+            return OutputCacheManager._checkFileIsSafe(src, function(
+              err,
+              isSafe
+            ) {
+              if (err != null) {
+                return cb(err)
+              }
+              if (!isSafe) {
+                return cb()
+              }
+              return OutputCacheManager._checkIfShouldCopy(src, function(
+                err,
+                shouldCopy
+              ) {
+                if (err != null) {
+                  return cb(err)
+                }
+                if (!shouldCopy) {
+                  return cb()
+                }
+                return OutputCacheManager._copyFile(src, dst, function(err) {
+                  if (err != null) {
+                    return cb(err)
+                  }
+                  newFile.build = buildId // attach a build id if we cached the file
+                  results.push(newFile)
+                  return cb()
+                })
+              })
+            })
+          },
+          function(err) {
+            if (err != null) {
+              // pass back the original files if we encountered *any* error
+              callback(err, outputFiles)
+              // clean up the directory we just created
+              return fse.remove(cacheDir, function(err) {
+                if (err != null) {
+                  return logger.error(
+                    { err, dir: cacheDir },
+                    'error removing cache dir after failure'
+                  )
+                }
+              })
+            } else {
+              // pass back the list of new files in the cache
+              callback(err, results)
+              // let file expiry run in the background, expire all previous files if per-user
+              return OutputCacheManager.expireOutputFiles(cacheRoot, {
+                keep: buildId,
+                limit: perUser ? 1 : null
+              })
+            }
+          }
+        )
+      }
+    })
+  },
+
+  archiveLogs(outputFiles, compileDir, buildId, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const archiveDir = Path.join(
+      compileDir,
+      OutputCacheManager.ARCHIVE_SUBDIR,
+      buildId
+    )
+    logger.log({ dir: archiveDir }, 'archiving log files for project')
+    return fse.ensureDir(archiveDir, function(err) {
+      if (err != null) {
+        return callback(err)
+      }
+      return async.mapSeries(
+        outputFiles,
+        function(file, cb) {
+          const [src, dst] = Array.from([
+            Path.join(compileDir, file.path),
+            Path.join(archiveDir, file.path)
+          ])
+          return OutputCacheManager._checkFileIsSafe(src, function(
+            err,
+            isSafe
+          ) {
+            if (err != null) {
+              return cb(err)
+            }
+            if (!isSafe) {
+              return cb()
+            }
+            return OutputCacheManager._checkIfShouldArchive(src, function(
+              err,
+              shouldArchive
+            ) {
+              if (err != null) {
+                return cb(err)
+              }
+              if (!shouldArchive) {
+                return cb()
+              }
+              return OutputCacheManager._copyFile(src, dst, cb)
+            })
+          })
+        },
+        callback
+      )
+    })
+  },
+
+  expireOutputFiles(cacheRoot, options, callback) {
+    // look in compileDir for build dirs and delete if > N or age of mod time > T
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return fs.readdir(cacheRoot, function(err, results) {
+      if (err != null) {
+        if (err.code === 'ENOENT') {
+          return callback(null)
+        } // cache directory is empty
+        logger.error({ err, project_id: cacheRoot }, 'error clearing cache')
+        return callback(err)
+      }
+
+      const dirs = results.sort().reverse()
+      const currentTime = Date.now()
+
+      const isExpired = function(dir, index) {
+        if ((options != null ? options.keep : undefined) === dir) {
+          return false
+        }
+        // remove any directories over the requested (non-null) limit
+        if (
+          (options != null ? options.limit : undefined) != null &&
+          index > options.limit
+        ) {
+          return true
+        }
+        // remove any directories over the hard limit
+        if (index > OutputCacheManager.CACHE_LIMIT) {
+          return true
+        }
+        // we can get the build time from the first part of the directory name DDDD-RRRR
+        // DDDD is date and RRRR is random bytes
+        const dirTime = parseInt(
+          __guard__(dir.split('-'), x => x[0]),
+          16
+        )
+        const age = currentTime - dirTime
+        return age > OutputCacheManager.CACHE_AGE
+      }
+
+      const toRemove = _.filter(dirs, isExpired)
+
+      const removeDir = (dir, cb) =>
+        fse.remove(Path.join(cacheRoot, dir), function(err, result) {
+          logger.log({ cache: cacheRoot, dir }, 'removed expired cache dir')
+          if (err != null) {
+            logger.error({ err, dir }, 'cache remove error')
+          }
+          return cb(err, result)
+        })
+      return async.eachSeries(
+        toRemove,
+        (dir, cb) => removeDir(dir, cb),
+        callback
+      )
+    })
+  },
+
+  _fileIsHidden(path) {
+    return (path != null ? path.match(/^\.|\/\./) : undefined) != null
+  },
+
+  _checkFileIsSafe(src, callback) {
+    // check if we have a valid file to copy into the cache
+    if (callback == null) {
+      callback = function(error, isSafe) {}
+    }
+    return fs.stat(src, function(err, stats) {
+      if ((err != null ? err.code : undefined) === 'ENOENT') {
+        logger.warn(
+          { err, file: src },
+          'file has disappeared before copying to build cache'
+        )
+        return callback(err, false)
+      } else if (err != null) {
+        // some other problem reading the file
+        logger.error({ err, file: src }, 'stat error for file in cache')
+        return callback(err, false)
+      } else if (!stats.isFile()) {
+        // other filetype - reject it
+        logger.warn(
+          { src, stat: stats },
+          'nonfile output - refusing to copy to cache'
+        )
+        return callback(null, false)
+      } else {
+        // it's a plain file, ok to copy
+        return callback(null, true)
+      }
+    })
+  },
+
+  _copyFile(src, dst, callback) {
+    // copy output file into the cache
+    return fse.copy(src, dst, function(err) {
+      if ((err != null ? err.code : undefined) === 'ENOENT') {
+        logger.warn(
+          { err, file: src },
+          'file has disappeared when copying to build cache'
+        )
+        return callback(err, false)
+      } else if (err != null) {
+        logger.error({ err, src, dst }, 'copy error for file in cache')
+        return callback(err)
+      } else {
+        if (
+          Settings.clsi != null ? Settings.clsi.optimiseInDocker : undefined
+        ) {
+          // don't run any optimisations on the pdf when they are done
+          // in the docker container
+          return callback()
+        } else {
+          // call the optimiser for the file too
+          return OutputFileOptimiser.optimiseFile(src, dst, callback)
+        }
+      }
+    })
+  },
+
+  _checkIfShouldCopy(src, callback) {
+    if (callback == null) {
+      callback = function(err, shouldCopy) {}
+    }
+    return callback(null, !Path.basename(src).match(/^strace/))
+  },
+
+  _checkIfShouldArchive(src, callback) {
+    let needle
+    if (callback == null) {
+      callback = function(err, shouldCopy) {}
+    }
+    if (Path.basename(src).match(/^strace/)) {
+      return callback(null, true)
+    }
+    if (
+      (Settings.clsi != null ? Settings.clsi.archive_logs : undefined) &&
+      ((needle = Path.basename(src)),
+      ['output.log', 'output.blg'].includes(needle))
+    ) {
+      return callback(null, true)
+    }
+    return callback(null, false)
+  }
+}
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 115 - 0
services/clsi/app/js/OutputFileFinder.js

@@ -0,0 +1,115 @@
+/* eslint-disable
+    handle-callback-err,
+    no-return-assign,
+    no-unused-vars,
+    no-useless-escape,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let OutputFileFinder
+const async = require('async')
+const fs = require('fs')
+const Path = require('path')
+const { spawn } = require('child_process')
+const logger = require('logger-sharelatex')
+
+module.exports = OutputFileFinder = {
+  findOutputFiles(resources, directory, callback) {
+    if (callback == null) {
+      callback = function(error, outputFiles, allFiles) {}
+    }
+    const incomingResources = {}
+    for (const resource of Array.from(resources)) {
+      incomingResources[resource.path] = true
+    }
+
+    return OutputFileFinder._getAllFiles(directory, function(error, allFiles) {
+      if (allFiles == null) {
+        allFiles = []
+      }
+      if (error != null) {
+        logger.err({ err: error }, 'error finding all output files')
+        return callback(error)
+      }
+      const outputFiles = []
+      for (const file of Array.from(allFiles)) {
+        if (!incomingResources[file]) {
+          outputFiles.push({
+            path: file,
+            type: __guard__(file.match(/\.([^\.]+)$/), x => x[1])
+          })
+        }
+      }
+      return callback(null, outputFiles, allFiles)
+    })
+  },
+
+  _getAllFiles(directory, _callback) {
+    if (_callback == null) {
+      _callback = function(error, fileList) {}
+    }
+    const callback = function(error, fileList) {
+      _callback(error, fileList)
+      return (_callback = function() {})
+    }
+
+    // don't include clsi-specific files/directories in the output list
+    const EXCLUDE_DIRS = [
+      '-name',
+      '.cache',
+      '-o',
+      '-name',
+      '.archive',
+      '-o',
+      '-name',
+      '.project-*'
+    ]
+    const args = [
+      directory,
+      '(',
+      ...Array.from(EXCLUDE_DIRS),
+      ')',
+      '-prune',
+      '-o',
+      '-type',
+      'f',
+      '-print'
+    ]
+    logger.log({ args }, 'running find command')
+
+    const proc = spawn('find', args)
+    let stdout = ''
+    proc.stdout.on('data', chunk => (stdout += chunk.toString()))
+    proc.on('error', callback)
+    return proc.on('close', function(code) {
+      if (code !== 0) {
+        logger.warn(
+          { directory, code },
+          "find returned error, directory likely doesn't exist"
+        )
+        return callback(null, [])
+      }
+      let fileList = stdout.trim().split('\n')
+      fileList = fileList.map(function(file) {
+        // Strip leading directory
+        let path
+        return (path = Path.relative(directory, file))
+      })
+      return callback(null, fileList)
+    })
+  }
+}
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 104 - 0
services/clsi/app/js/OutputFileOptimiser.js

@@ -0,0 +1,104 @@
+/* eslint-disable
+    handle-callback-err,
+    no-return-assign,
+    no-undef,
+    no-unused-vars,
+    node/no-deprecated-api,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let OutputFileOptimiser
+const fs = require('fs')
+const Path = require('path')
+const { spawn } = require('child_process')
+const logger = require('logger-sharelatex')
+const Metrics = require('./Metrics')
+const _ = require('underscore')
+
+module.exports = OutputFileOptimiser = {
+  optimiseFile(src, dst, callback) {
+    // check output file (src) and see if we can optimise it, storing
+    // the result in the build directory (dst)
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    if (src.match(/\/output\.pdf$/)) {
+      return OutputFileOptimiser.checkIfPDFIsOptimised(src, function(
+        err,
+        isOptimised
+      ) {
+        if (err != null || isOptimised) {
+          return callback(null)
+        }
+        return OutputFileOptimiser.optimisePDF(src, dst, callback)
+      })
+    } else {
+      return callback(null)
+    }
+  },
+
+  checkIfPDFIsOptimised(file, callback) {
+    const SIZE = 16 * 1024 // check the header of the pdf
+    const result = new Buffer(SIZE)
+    result.fill(0) // prevent leakage of uninitialised buffer
+    return fs.open(file, 'r', function(err, fd) {
+      if (err != null) {
+        return callback(err)
+      }
+      return fs.read(fd, result, 0, SIZE, 0, (errRead, bytesRead, buffer) =>
+        fs.close(fd, function(errClose) {
+          if (errRead != null) {
+            return callback(errRead)
+          }
+          if (typeof errReadClose !== 'undefined' && errReadClose !== null) {
+            return callback(errClose)
+          }
+          const isOptimised =
+            buffer.toString('ascii').indexOf('/Linearized 1') >= 0
+          return callback(null, isOptimised)
+        })
+      )
+    })
+  },
+
+  optimisePDF(src, dst, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const tmpOutput = dst + '.opt'
+    const args = ['--linearize', src, tmpOutput]
+    logger.log({ args }, 'running qpdf command')
+
+    const timer = new Metrics.Timer('qpdf')
+    const proc = spawn('qpdf', args)
+    let stdout = ''
+    proc.stdout.on('data', chunk => (stdout += chunk.toString()))
+    callback = _.once(callback) // avoid double call back for error and close event
+    proc.on('error', function(err) {
+      logger.warn({ err, args }, 'qpdf failed')
+      return callback(null)
+    }) // ignore the error
+    return proc.on('close', function(code) {
+      timer.done()
+      if (code !== 0) {
+        logger.warn({ code, args }, 'qpdf returned error')
+        return callback(null) // ignore the error
+      }
+      return fs.rename(tmpOutput, dst, function(err) {
+        if (err != null) {
+          logger.warn(
+            { tmpOutput, dst },
+            'failed to rename output of qpdf command'
+          )
+        }
+        return callback(null)
+      })
+    })
+  } // ignore the error
+}

+ 163 - 0
services/clsi/app/js/ProjectPersistenceManager.js

@@ -0,0 +1,163 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let ProjectPersistenceManager
+const UrlCache = require('./UrlCache')
+const CompileManager = require('./CompileManager')
+const db = require('./db')
+const dbQueue = require('./DbQueue')
+const async = require('async')
+const logger = require('logger-sharelatex')
+const oneDay = 24 * 60 * 60 * 1000
+const Settings = require('settings-sharelatex')
+
+module.exports = ProjectPersistenceManager = {
+  EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5,
+
+  markProjectAsJustAccessed(project_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const job = cb =>
+      db.Project.findOrCreate({ where: { project_id } })
+        .spread((project, created) =>
+          project
+            .update({ lastAccessed: new Date() })
+            .then(() => cb())
+            .error(cb)
+        )
+        .error(cb)
+    return dbQueue.queue.push(job, callback)
+  },
+
+  clearExpiredProjects(callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return ProjectPersistenceManager._findExpiredProjectIds(function(
+      error,
+      project_ids
+    ) {
+      if (error != null) {
+        return callback(error)
+      }
+      logger.log({ project_ids }, 'clearing expired projects')
+      const jobs = Array.from(project_ids || []).map(project_id =>
+        (project_id => callback =>
+          ProjectPersistenceManager.clearProjectFromCache(project_id, function(
+            err
+          ) {
+            if (err != null) {
+              logger.error({ err, project_id }, 'error clearing project')
+            }
+            return callback()
+          }))(project_id)
+      )
+      return async.series(jobs, function(error) {
+        if (error != null) {
+          return callback(error)
+        }
+        return CompileManager.clearExpiredProjects(
+          ProjectPersistenceManager.EXPIRY_TIMEOUT,
+          error => callback()
+        )
+      })
+    })
+  }, // ignore any errors from deleting directories
+
+  clearProject(project_id, user_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    logger.log({ project_id, user_id }, 'clearing project for user')
+    return CompileManager.clearProject(project_id, user_id, function(error) {
+      if (error != null) {
+        return callback(error)
+      }
+      return ProjectPersistenceManager.clearProjectFromCache(
+        project_id,
+        function(error) {
+          if (error != null) {
+            return callback(error)
+          }
+          return callback()
+        }
+      )
+    })
+  },
+
+  clearProjectFromCache(project_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    logger.log({ project_id }, 'clearing project from cache')
+    return UrlCache.clearProject(project_id, function(error) {
+      if (error != null) {
+        logger.err({ error, project_id }, 'error clearing project from cache')
+        return callback(error)
+      }
+      return ProjectPersistenceManager._clearProjectFromDatabase(
+        project_id,
+        function(error) {
+          if (error != null) {
+            logger.err(
+              { error, project_id },
+              'error clearing project from database'
+            )
+          }
+          return callback(error)
+        }
+      )
+    })
+  },
+
+  _clearProjectFromDatabase(project_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    logger.log({ project_id }, 'clearing project from database')
+    const job = cb =>
+      db.Project.destroy({ where: { project_id } })
+        .then(() => cb())
+        .error(cb)
+    return dbQueue.queue.push(job, callback)
+  },
+
+  _findExpiredProjectIds(callback) {
+    if (callback == null) {
+      callback = function(error, project_ids) {}
+    }
+    const job = function(cb) {
+      const keepProjectsFrom = new Date(
+        Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT
+      )
+      const q = {}
+      q[db.op.lt] = keepProjectsFrom
+      return db.Project.findAll({ where: { lastAccessed: q } })
+        .then(projects =>
+          cb(
+            null,
+            projects.map(project => project.project_id)
+          )
+        )
+        .error(cb)
+    }
+
+    return dbQueue.queue.push(job, callback)
+  }
+}
+
+logger.log(
+  { EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT },
+  'project assets kept timeout'
+)

+ 217 - 0
services/clsi/app/js/RequestParser.js

@@ -0,0 +1,217 @@
+/* eslint-disable
+    handle-callback-err,
+    no-control-regex,
+    no-throw-literal,
+    no-unused-vars,
+    no-useless-escape,
+    standard/no-callback-literal,
+    valid-typeof,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS205: Consider reworking code to avoid use of IIFEs
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let RequestParser
+const settings = require('settings-sharelatex')
+
+module.exports = RequestParser = {
+  VALID_COMPILERS: ['pdflatex', 'latex', 'xelatex', 'lualatex'],
+  MAX_TIMEOUT: 600,
+
+  parse(body, callback) {
+    let resource
+    if (callback == null) {
+      callback = function(error, data) {}
+    }
+    const response = {}
+
+    if (body.compile == null) {
+      return callback('top level object should have a compile attribute')
+    }
+
+    const { compile } = body
+    if (!compile.options) {
+      compile.options = {}
+    }
+
+    try {
+      response.compiler = this._parseAttribute(
+        'compiler',
+        compile.options.compiler,
+        {
+          validValues: this.VALID_COMPILERS,
+          default: 'pdflatex',
+          type: 'string'
+        }
+      )
+      response.timeout = this._parseAttribute(
+        'timeout',
+        compile.options.timeout,
+        {
+          default: RequestParser.MAX_TIMEOUT,
+          type: 'number'
+        }
+      )
+      response.imageName = this._parseAttribute(
+        'imageName',
+        compile.options.imageName,
+        { type: 'string' }
+      )
+      response.draft = this._parseAttribute('draft', compile.options.draft, {
+        default: false,
+        type: 'boolean'
+      })
+      response.check = this._parseAttribute('check', compile.options.check, {
+        type: 'string'
+      })
+      response.flags = this._parseAttribute('flags', compile.options.flags, {
+        default: [],
+        type: 'object'
+      })
+
+      // The syncType specifies whether the request contains all
+      // resources (full) or only those resources to be updated
+      // in-place (incremental).
+      response.syncType = this._parseAttribute(
+        'syncType',
+        compile.options.syncType,
+        {
+          validValues: ['full', 'incremental'],
+          type: 'string'
+        }
+      )
+
+      // The syncState is an identifier passed in with the request
+      // which has the property that it changes when any resource is
+      // added, deleted, moved or renamed.
+      //
+      // on syncType full the syncState identifier is passed in and
+      // stored
+      //
+      // on syncType incremental the syncState identifier must match
+      // the stored value
+      response.syncState = this._parseAttribute(
+        'syncState',
+        compile.options.syncState,
+        { type: 'string' }
+      )
+
+      if (response.timeout > RequestParser.MAX_TIMEOUT) {
+        response.timeout = RequestParser.MAX_TIMEOUT
+      }
+      response.timeout = response.timeout * 1000 // milliseconds
+
+      response.resources = (() => {
+        const result = []
+        for (resource of Array.from(compile.resources || [])) {
+          result.push(this._parseResource(resource))
+        }
+        return result
+      })()
+
+      const rootResourcePath = this._parseAttribute(
+        'rootResourcePath',
+        compile.rootResourcePath,
+        {
+          default: 'main.tex',
+          type: 'string'
+        }
+      )
+      const originalRootResourcePath = rootResourcePath
+      const sanitizedRootResourcePath = RequestParser._sanitizePath(
+        rootResourcePath
+      )
+      response.rootResourcePath = RequestParser._checkPath(
+        sanitizedRootResourcePath
+      )
+
+      for (resource of Array.from(response.resources)) {
+        if (resource.path === originalRootResourcePath) {
+          resource.path = sanitizedRootResourcePath
+        }
+      }
+    } catch (error1) {
+      const error = error1
+      return callback(error)
+    }
+
+    return callback(null, response)
+  },
+
+  _parseResource(resource) {
+    let modified
+    if (resource.path == null || typeof resource.path !== 'string') {
+      throw 'all resources should have a path attribute'
+    }
+
+    if (resource.modified != null) {
+      modified = new Date(resource.modified)
+      if (isNaN(modified.getTime())) {
+        throw `resource modified date could not be understood: ${resource.modified}`
+      }
+    }
+
+    if (resource.url == null && resource.content == null) {
+      throw 'all resources should have either a url or content attribute'
+    }
+    if (resource.content != null && typeof resource.content !== 'string') {
+      throw 'content attribute should be a string'
+    }
+    if (resource.url != null && typeof resource.url !== 'string') {
+      throw 'url attribute should be a string'
+    }
+
+    return {
+      path: resource.path,
+      modified,
+      url: resource.url,
+      content: resource.content
+    }
+  },
+
+  _parseAttribute(name, attribute, options) {
+    if (attribute != null) {
+      if (options.validValues != null) {
+        if (options.validValues.indexOf(attribute) === -1) {
+          throw `${name} attribute should be one of: ${options.validValues.join(
+            ', '
+          )}`
+        }
+      }
+      if (options.type != null) {
+        if (typeof attribute !== options.type) {
+          throw `${name} attribute should be a ${options.type}`
+        }
+      }
+    } else {
+      if (options.default != null) {
+        return options.default
+      }
+    }
+    return attribute
+  },
+
+  _sanitizePath(path) {
+    // See http://php.net/manual/en/function.escapeshellcmd.php
+    return path.replace(
+      /[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g,
+      ''
+    )
+  },
+
+  _checkPath(path) {
+    // check that the request does not use a relative path
+    for (const dir of Array.from(path.split('/'))) {
+      if (dir === '..') {
+        throw 'relative path in root resource'
+      }
+    }
+    return path
+  }
+}

+ 154 - 0
services/clsi/app/js/ResourceStateManager.js

@@ -0,0 +1,154 @@
+/* eslint-disable
+    handle-callback-err,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS201: Simplify complex destructure assignments
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let ResourceStateManager
+const Path = require('path')
+const fs = require('fs')
+const logger = require('logger-sharelatex')
+const settings = require('settings-sharelatex')
+const Errors = require('./Errors')
+const SafeReader = require('./SafeReader')
+
+module.exports = ResourceStateManager = {
+  // The sync state is an identifier which must match for an
+  // incremental update to be allowed.
+  //
+  // The initial value is passed in and stored on a full
+  // compile, along with the list of resources..
+  //
+  // Subsequent incremental compiles must come with the same value - if
+  // not they will be rejected with a 409 Conflict response. The
+  // previous list of resources is returned.
+  //
+  // An incremental compile can only update existing files with new
+  // content.  The sync state identifier must change if any docs or
+  // files are moved, added, deleted or renamed.
+
+  SYNC_STATE_FILE: '.project-sync-state',
+  SYNC_STATE_MAX_SIZE: 128 * 1024,
+
+  saveProjectState(state, resources, basePath, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
+    if (state == null) {
+      // remove the file if no state passed in
+      logger.log({ state, basePath }, 'clearing sync state')
+      return fs.unlink(stateFile, function(err) {
+        if (err != null && err.code !== 'ENOENT') {
+          return callback(err)
+        } else {
+          return callback()
+        }
+      })
+    } else {
+      logger.log({ state, basePath }, 'writing sync state')
+      const resourceList = Array.from(resources).map(resource => resource.path)
+      return fs.writeFile(
+        stateFile,
+        [...Array.from(resourceList), `stateHash:${state}`].join('\n'),
+        callback
+      )
+    }
+  },
+
+  checkProjectStateMatches(state, basePath, callback) {
+    if (callback == null) {
+      callback = function(error, resources) {}
+    }
+    const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
+    const size = this.SYNC_STATE_MAX_SIZE
+    return SafeReader.readFile(stateFile, size, 'utf8', function(
+      err,
+      result,
+      bytesRead
+    ) {
+      if (err != null) {
+        return callback(err)
+      }
+      if (bytesRead === size) {
+        logger.error(
+          { file: stateFile, size, bytesRead },
+          'project state file truncated'
+        )
+      }
+      const array =
+        __guard__(result != null ? result.toString() : undefined, x =>
+          x.split('\n')
+        ) || []
+      const adjustedLength = Math.max(array.length, 1)
+      const resourceList = array.slice(0, adjustedLength - 1)
+      const oldState = array[adjustedLength - 1]
+      const newState = `stateHash:${state}`
+      logger.log(
+        { state, oldState, basePath, stateMatches: newState === oldState },
+        'checking sync state'
+      )
+      if (newState !== oldState) {
+        return callback(
+          new Errors.FilesOutOfSyncError('invalid state for incremental update')
+        )
+      } else {
+        const resources = Array.from(resourceList).map(path => ({ path }))
+        return callback(null, resources)
+      }
+    })
+  },
+
+  checkResourceFiles(resources, allFiles, basePath, callback) {
+    // check the paths are all relative to current directory
+    let file
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    for (file of Array.from(resources || [])) {
+      for (const dir of Array.from(
+        __guard__(file != null ? file.path : undefined, x => x.split('/'))
+      )) {
+        if (dir === '..') {
+          return callback(new Error('relative path in resource file list'))
+        }
+      }
+    }
+    // check if any of the input files are not present in list of files
+    const seenFile = {}
+    for (file of Array.from(allFiles)) {
+      seenFile[file] = true
+    }
+    const missingFiles = Array.from(resources)
+      .filter(resource => !seenFile[resource.path])
+      .map(resource => resource.path)
+    if ((missingFiles != null ? missingFiles.length : undefined) > 0) {
+      logger.err(
+        { missingFiles, basePath, allFiles, resources },
+        'missing input files for project'
+      )
+      return callback(
+        new Errors.FilesOutOfSyncError(
+          'resource files missing in incremental update'
+        )
+      )
+    } else {
+      return callback()
+    }
+  }
+}
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 352 - 0
services/clsi/app/js/ResourceWriter.js

@@ -0,0 +1,352 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-return-assign,
+    no-unused-vars,
+    no-useless-escape,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let ResourceWriter
+const UrlCache = require('./UrlCache')
+const Path = require('path')
+const fs = require('fs')
+const async = require('async')
+const OutputFileFinder = require('./OutputFileFinder')
+const ResourceStateManager = require('./ResourceStateManager')
+const Metrics = require('./Metrics')
+const logger = require('logger-sharelatex')
+const settings = require('settings-sharelatex')
+
+const parallelFileDownloads = settings.parallelFileDownloads || 1
+
+module.exports = ResourceWriter = {
+  syncResourcesToDisk(request, basePath, callback) {
+    if (callback == null) {
+      callback = function(error, resourceList) {}
+    }
+    if (request.syncType === 'incremental') {
+      logger.log(
+        { project_id: request.project_id, user_id: request.user_id },
+        'incremental sync'
+      )
+      return ResourceStateManager.checkProjectStateMatches(
+        request.syncState,
+        basePath,
+        function(error, resourceList) {
+          if (error != null) {
+            return callback(error)
+          }
+          return ResourceWriter._removeExtraneousFiles(
+            resourceList,
+            basePath,
+            function(error, outputFiles, allFiles) {
+              if (error != null) {
+                return callback(error)
+              }
+              return ResourceStateManager.checkResourceFiles(
+                resourceList,
+                allFiles,
+                basePath,
+                function(error) {
+                  if (error != null) {
+                    return callback(error)
+                  }
+                  return ResourceWriter.saveIncrementalResourcesToDisk(
+                    request.project_id,
+                    request.resources,
+                    basePath,
+                    function(error) {
+                      if (error != null) {
+                        return callback(error)
+                      }
+                      return callback(null, resourceList)
+                    }
+                  )
+                }
+              )
+            }
+          )
+        }
+      )
+    } else {
+      logger.log(
+        { project_id: request.project_id, user_id: request.user_id },
+        'full sync'
+      )
+      return this.saveAllResourcesToDisk(
+        request.project_id,
+        request.resources,
+        basePath,
+        function(error) {
+          if (error != null) {
+            return callback(error)
+          }
+          return ResourceStateManager.saveProjectState(
+            request.syncState,
+            request.resources,
+            basePath,
+            function(error) {
+              if (error != null) {
+                return callback(error)
+              }
+              return callback(null, request.resources)
+            }
+          )
+        }
+      )
+    }
+  },
+
+  saveIncrementalResourcesToDisk(project_id, resources, basePath, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return this._createDirectory(basePath, error => {
+      if (error != null) {
+        return callback(error)
+      }
+      const jobs = Array.from(resources).map(resource =>
+        (resource => {
+          return callback =>
+            this._writeResourceToDisk(project_id, resource, basePath, callback)
+        })(resource)
+      )
+      return async.parallelLimit(jobs, parallelFileDownloads, callback)
+    })
+  },
+
+  saveAllResourcesToDisk(project_id, resources, basePath, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return this._createDirectory(basePath, error => {
+      if (error != null) {
+        return callback(error)
+      }
+      return this._removeExtraneousFiles(resources, basePath, error => {
+        if (error != null) {
+          return callback(error)
+        }
+        const jobs = Array.from(resources).map(resource =>
+          (resource => {
+            return callback =>
+              this._writeResourceToDisk(
+                project_id,
+                resource,
+                basePath,
+                callback
+              )
+          })(resource)
+        )
+        return async.parallelLimit(jobs, parallelFileDownloads, callback)
+      })
+    })
+  },
+
+  _createDirectory(basePath, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return fs.mkdir(basePath, function(err) {
+      if (err != null) {
+        if (err.code === 'EEXIST') {
+          return callback()
+        } else {
+          logger.log({ err, dir: basePath }, 'error creating directory')
+          return callback(err)
+        }
+      } else {
+        return callback()
+      }
+    })
+  },
+
+  _removeExtraneousFiles(resources, basePath, _callback) {
+    if (_callback == null) {
+      _callback = function(error, outputFiles, allFiles) {}
+    }
+    const timer = new Metrics.Timer('unlink-output-files')
+    const callback = function(error, ...result) {
+      timer.done()
+      return _callback(error, ...Array.from(result))
+    }
+
+    return OutputFileFinder.findOutputFiles(resources, basePath, function(
+      error,
+      outputFiles,
+      allFiles
+    ) {
+      if (error != null) {
+        return callback(error)
+      }
+
+      const jobs = []
+      for (const file of Array.from(outputFiles || [])) {
+        ;(function(file) {
+          const { path } = file
+          let should_delete = true
+          if (
+            path.match(/^output\./) ||
+            path.match(/\.aux$/) ||
+            path.match(/^cache\//)
+          ) {
+            // knitr cache
+            should_delete = false
+          }
+          if (path.match(/^output-.*/)) {
+            // Tikz cached figures (default case)
+            should_delete = false
+          }
+          if (path.match(/\.(pdf|dpth|md5)$/)) {
+            // Tikz cached figures (by extension)
+            should_delete = false
+          }
+          if (
+            path.match(/\.(pygtex|pygstyle)$/) ||
+            path.match(/(^|\/)_minted-[^\/]+\//)
+          ) {
+            // minted files/directory
+            should_delete = false
+          }
+          if (
+            path.match(/\.md\.tex$/) ||
+            path.match(/(^|\/)_markdown_[^\/]+\//)
+          ) {
+            // markdown files/directory
+            should_delete = false
+          }
+          if (path.match(/-eps-converted-to\.pdf$/)) {
+            // Epstopdf generated files
+            should_delete = false
+          }
+          if (
+            path === 'output.pdf' ||
+            path === 'output.dvi' ||
+            path === 'output.log' ||
+            path === 'output.xdv'
+          ) {
+            should_delete = true
+          }
+          if (path === 'output.tex') {
+            // created by TikzManager if present in output files
+            should_delete = true
+          }
+          if (should_delete) {
+            return jobs.push(callback =>
+              ResourceWriter._deleteFileIfNotDirectory(
+                Path.join(basePath, path),
+                callback
+              )
+            )
+          }
+        })(file)
+      }
+
+      return async.series(jobs, function(error) {
+        if (error != null) {
+          return callback(error)
+        }
+        return callback(null, outputFiles, allFiles)
+      })
+    })
+  },
+
+  _deleteFileIfNotDirectory(path, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return fs.stat(path, function(error, stat) {
+      if (error != null && error.code === 'ENOENT') {
+        return callback()
+      } else if (error != null) {
+        logger.err(
+          { err: error, path },
+          'error stating file in deleteFileIfNotDirectory'
+        )
+        return callback(error)
+      } else if (stat.isFile()) {
+        return fs.unlink(path, function(error) {
+          if (error != null) {
+            logger.err(
+              { err: error, path },
+              'error removing file in deleteFileIfNotDirectory'
+            )
+            return callback(error)
+          } else {
+            return callback()
+          }
+        })
+      } else {
+        return callback()
+      }
+    })
+  },
+
+  _writeResourceToDisk(project_id, resource, basePath, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return ResourceWriter.checkPath(basePath, resource.path, function(
+      error,
+      path
+    ) {
+      if (error != null) {
+        return callback(error)
+      }
+      return fs.mkdir(Path.dirname(path), { recursive: true }, function(error) {
+        if (error != null) {
+          return callback(error)
+        }
+        // TODO: Don't overwrite file if it hasn't been modified
+        if (resource.url != null) {
+          return UrlCache.downloadUrlToFile(
+            project_id,
+            resource.url,
+            path,
+            resource.modified,
+            function(err) {
+              if (err != null) {
+                logger.err(
+                  {
+                    err,
+                    project_id,
+                    path,
+                    resource_url: resource.url,
+                    modified: resource.modified
+                  },
+                  'error downloading file for resources'
+                )
+                Metrics.inc('download-failed')
+              }
+              return callback()
+            }
+          ) // try and continue compiling even if http resource can not be downloaded at this time
+        } else {
+          const process = require('process')
+          fs.writeFile(path, resource.content, callback)
+          try {
+            let result
+            return (result = fs.lstatSync(path))
+          } catch (e) {}
+        }
+      })
+    })
+  },
+
+  checkPath(basePath, resourcePath, callback) {
+    const path = Path.normalize(Path.join(basePath, resourcePath))
+    if (path.slice(0, basePath.length + 1) !== basePath + '/') {
+      return callback(new Error('resource path is outside root directory'))
+    } else {
+      return callback(null, path)
+    }
+  }
+}

+ 60 - 0
services/clsi/app/js/SafeReader.js

@@ -0,0 +1,60 @@
+/* eslint-disable
+    handle-callback-err,
+    no-unused-vars,
+    node/no-deprecated-api,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let SafeReader
+const fs = require('fs')
+const logger = require('logger-sharelatex')
+
+module.exports = SafeReader = {
+  // safely read up to size bytes from a file and return result as a
+  // string
+
+  readFile(file, size, encoding, callback) {
+    if (callback == null) {
+      callback = function(error, result) {}
+    }
+    return fs.open(file, 'r', function(err, fd) {
+      if (err != null && err.code === 'ENOENT') {
+        return callback()
+      }
+      if (err != null) {
+        return callback(err)
+      }
+
+      // safely return always closing the file
+      const callbackWithClose = (err, ...result) =>
+        fs.close(fd, function(err1) {
+          if (err != null) {
+            return callback(err)
+          }
+          if (err1 != null) {
+            return callback(err1)
+          }
+          return callback(null, ...Array.from(result))
+        })
+      const buff = new Buffer(size, 0) // fill with zeros
+      return fs.read(fd, buff, 0, buff.length, 0, function(
+        err,
+        bytesRead,
+        buffer
+      ) {
+        if (err != null) {
+          return callbackWithClose(err)
+        }
+        const result = buffer.toString(encoding, 0, bytesRead)
+        return callbackWithClose(null, result, bytesRead)
+      })
+    })
+  }
+}

+ 94 - 0
services/clsi/app/js/StaticServerForbidSymlinks.js

@@ -0,0 +1,94 @@
+/* eslint-disable
+    camelcase,
+    no-cond-assign,
+    no-unused-vars,
+    node/no-deprecated-api,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let ForbidSymlinks
+const Path = require('path')
+const fs = require('fs')
+const Settings = require('settings-sharelatex')
+const logger = require('logger-sharelatex')
+const url = require('url')
+
+module.exports = ForbidSymlinks = function(staticFn, root, options) {
+  const expressStatic = staticFn(root, options)
+  const basePath = Path.resolve(root)
+  return function(req, res, next) {
+    let file, project_id, result
+    const path = __guard__(url.parse(req.url), x => x.pathname)
+    // check that the path is of the form /project_id_or_name/path/to/file.log
+    if ((result = path.match(/^\/?([a-zA-Z0-9_-]+)\/(.*)/))) {
+      project_id = result[1]
+      file = result[2]
+    } else {
+      logger.warn({ path }, 'unrecognized file request')
+      return res.sendStatus(404)
+    }
+    // check that the file does not use a relative path
+    for (const dir of Array.from(file.split('/'))) {
+      if (dir === '..') {
+        logger.warn({ path }, 'attempt to use a relative path')
+        return res.sendStatus(404)
+      }
+    }
+    // check that the requested path is normalized
+    const requestedFsPath = `${basePath}/${project_id}/${file}`
+    if (requestedFsPath !== Path.normalize(requestedFsPath)) {
+      logger.error(
+        { path: requestedFsPath },
+        'requestedFsPath is not normalized'
+      )
+      return res.sendStatus(404)
+    }
+    // check that the requested path is not a symlink
+    return fs.realpath(requestedFsPath, function(err, realFsPath) {
+      if (err != null) {
+        if (err.code === 'ENOENT') {
+          return res.sendStatus(404)
+        } else {
+          logger.error(
+            {
+              err,
+              requestedFsPath,
+              realFsPath,
+              path: req.params[0],
+              project_id: req.params.project_id
+            },
+            'error checking file access'
+          )
+          return res.sendStatus(500)
+        }
+      } else if (requestedFsPath !== realFsPath) {
+        logger.warn(
+          {
+            requestedFsPath,
+            realFsPath,
+            path: req.params[0],
+            project_id: req.params.project_id
+          },
+          'trying to access a different file (symlink), aborting'
+        )
+        return res.sendStatus(404)
+      } else {
+        return expressStatic(req, res, next)
+      }
+    })
+  }
+}
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 94 - 0
services/clsi/app/js/TikzManager.js

@@ -0,0 +1,94 @@
+/* eslint-disable
+    handle-callback-err,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let TikzManager
+const fs = require('fs')
+const Path = require('path')
+const ResourceWriter = require('./ResourceWriter')
+const SafeReader = require('./SafeReader')
+const logger = require('logger-sharelatex')
+
+// for \tikzexternalize or pstool to work the main file needs to match the
+// jobname.  Since we set the -jobname to output, we have to create a
+// copy of the main file as 'output.tex'.
+
+module.exports = TikzManager = {
+  checkMainFile(compileDir, mainFile, resources, callback) {
+    // if there's already an output.tex file, we don't want to touch it
+    if (callback == null) {
+      callback = function(error, needsMainFile) {}
+    }
+    for (const resource of Array.from(resources)) {
+      if (resource.path === 'output.tex') {
+        logger.log({ compileDir, mainFile }, 'output.tex already in resources')
+        return callback(null, false)
+      }
+    }
+    // if there's no output.tex, see if we are using tikz/pgf or pstool in the main file
+    return ResourceWriter.checkPath(compileDir, mainFile, function(
+      error,
+      path
+    ) {
+      if (error != null) {
+        return callback(error)
+      }
+      return SafeReader.readFile(path, 65536, 'utf8', function(error, content) {
+        if (error != null) {
+          return callback(error)
+        }
+        const usesTikzExternalize =
+          (content != null
+            ? content.indexOf('\\tikzexternalize')
+            : undefined) >= 0
+        const usesPsTool =
+          (content != null ? content.indexOf('{pstool}') : undefined) >= 0
+        logger.log(
+          { compileDir, mainFile, usesTikzExternalize, usesPsTool },
+          'checked for packages needing main file as output.tex'
+        )
+        const needsMainFile = usesTikzExternalize || usesPsTool
+        return callback(null, needsMainFile)
+      })
+    })
+  },
+
+  injectOutputFile(compileDir, mainFile, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return ResourceWriter.checkPath(compileDir, mainFile, function(
+      error,
+      path
+    ) {
+      if (error != null) {
+        return callback(error)
+      }
+      return fs.readFile(path, 'utf8', function(error, content) {
+        if (error != null) {
+          return callback(error)
+        }
+        logger.log(
+          { compileDir, mainFile },
+          'copied file to output.tex as project uses packages which require it'
+        )
+        // use wx flag to ensure that output file does not already exist
+        return fs.writeFile(
+          Path.join(compileDir, 'output.tex'),
+          content,
+          { flag: 'wx' },
+          callback
+        )
+      })
+    })
+  }
+}

+ 278 - 0
services/clsi/app/js/UrlCache.js

@@ -0,0 +1,278 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-return-assign,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let UrlCache
+const db = require('./db')
+const dbQueue = require('./DbQueue')
+const UrlFetcher = require('./UrlFetcher')
+const Settings = require('settings-sharelatex')
+const crypto = require('crypto')
+const fs = require('fs')
+const logger = require('logger-sharelatex')
+const async = require('async')
+
+module.exports = UrlCache = {
+  downloadUrlToFile(project_id, url, destPath, lastModified, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return UrlCache._ensureUrlIsInCache(
+      project_id,
+      url,
+      lastModified,
+      (error, pathToCachedUrl) => {
+        if (error != null) {
+          return callback(error)
+        }
+        return UrlCache._copyFile(pathToCachedUrl, destPath, function(error) {
+          if (error != null) {
+            return UrlCache._clearUrlDetails(project_id, url, () =>
+              callback(error)
+            )
+          } else {
+            return callback(error)
+          }
+        })
+      }
+    )
+  },
+
+  clearProject(project_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return UrlCache._findAllUrlsInProject(project_id, function(error, urls) {
+      logger.log(
+        { project_id, url_count: urls.length },
+        'clearing project URLs'
+      )
+      if (error != null) {
+        return callback(error)
+      }
+      const jobs = Array.from(urls || []).map(url =>
+        (url => callback =>
+          UrlCache._clearUrlFromCache(project_id, url, function(error) {
+            if (error != null) {
+              logger.error(
+                { err: error, project_id, url },
+                'error clearing project URL'
+              )
+            }
+            return callback()
+          }))(url)
+      )
+      return async.series(jobs, callback)
+    })
+  },
+
+  _ensureUrlIsInCache(project_id, url, lastModified, callback) {
+    if (callback == null) {
+      callback = function(error, pathOnDisk) {}
+    }
+    if (lastModified != null) {
+      // MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds.
+      // So round down to seconds
+      lastModified = new Date(Math.floor(lastModified.getTime() / 1000) * 1000)
+    }
+    return UrlCache._doesUrlNeedDownloading(
+      project_id,
+      url,
+      lastModified,
+      (error, needsDownloading) => {
+        if (error != null) {
+          return callback(error)
+        }
+        if (needsDownloading) {
+          logger.log({ url, lastModified }, 'downloading URL')
+          return UrlFetcher.pipeUrlToFile(
+            url,
+            UrlCache._cacheFilePathForUrl(project_id, url),
+            error => {
+              if (error != null) {
+                return callback(error)
+              }
+              return UrlCache._updateOrCreateUrlDetails(
+                project_id,
+                url,
+                lastModified,
+                error => {
+                  if (error != null) {
+                    return callback(error)
+                  }
+                  return callback(
+                    null,
+                    UrlCache._cacheFilePathForUrl(project_id, url)
+                  )
+                }
+              )
+            }
+          )
+        } else {
+          logger.log({ url, lastModified }, 'URL is up to date in cache')
+          return callback(null, UrlCache._cacheFilePathForUrl(project_id, url))
+        }
+      }
+    )
+  },
+
+  _doesUrlNeedDownloading(project_id, url, lastModified, callback) {
+    if (callback == null) {
+      callback = function(error, needsDownloading) {}
+    }
+    if (lastModified == null) {
+      return callback(null, true)
+    }
+    return UrlCache._findUrlDetails(project_id, url, function(
+      error,
+      urlDetails
+    ) {
+      if (error != null) {
+        return callback(error)
+      }
+      if (
+        urlDetails == null ||
+        urlDetails.lastModified == null ||
+        urlDetails.lastModified.getTime() < lastModified.getTime()
+      ) {
+        return callback(null, true)
+      } else {
+        return callback(null, false)
+      }
+    })
+  },
+
+  _cacheFileNameForUrl(project_id, url) {
+    return (
+      project_id +
+      ':' +
+      crypto
+        .createHash('md5')
+        .update(url)
+        .digest('hex')
+    )
+  },
+
+  _cacheFilePathForUrl(project_id, url) {
+    return `${Settings.path.clsiCacheDir}/${UrlCache._cacheFileNameForUrl(
+      project_id,
+      url
+    )}`
+  },
+
+  _copyFile(from, to, _callback) {
+    if (_callback == null) {
+      _callback = function(error) {}
+    }
+    const callbackOnce = function(error) {
+      if (error != null) {
+        logger.error({ err: error, from, to }, 'error copying file from cache')
+      }
+      _callback(error)
+      return (_callback = function() {})
+    }
+    const writeStream = fs.createWriteStream(to)
+    const readStream = fs.createReadStream(from)
+    writeStream.on('error', callbackOnce)
+    readStream.on('error', callbackOnce)
+    writeStream.on('close', callbackOnce)
+    return writeStream.on('open', () => readStream.pipe(writeStream))
+  },
+
+  _clearUrlFromCache(project_id, url, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return UrlCache._clearUrlDetails(project_id, url, function(error) {
+      if (error != null) {
+        return callback(error)
+      }
+      return UrlCache._deleteUrlCacheFromDisk(project_id, url, function(error) {
+        if (error != null) {
+          return callback(error)
+        }
+        return callback(null)
+      })
+    })
+  },
+
+  _deleteUrlCacheFromDisk(project_id, url, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return fs.unlink(UrlCache._cacheFilePathForUrl(project_id, url), function(
+      error
+    ) {
+      if (error != null && error.code !== 'ENOENT') {
+        // no error if the file isn't present
+        return callback(error)
+      } else {
+        return callback()
+      }
+    })
+  },
+
+  _findUrlDetails(project_id, url, callback) {
+    if (callback == null) {
+      callback = function(error, urlDetails) {}
+    }
+    const job = cb =>
+      db.UrlCache.findOne({ where: { url, project_id } })
+        .then(urlDetails => cb(null, urlDetails))
+        .error(cb)
+    return dbQueue.queue.push(job, callback)
+  },
+
+  _updateOrCreateUrlDetails(project_id, url, lastModified, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const job = cb =>
+      db.UrlCache.findOrCreate({ where: { url, project_id } })
+        .spread((urlDetails, created) =>
+          urlDetails
+            .update({ lastModified })
+            .then(() => cb())
+            .error(cb)
+        )
+        .error(cb)
+    return dbQueue.queue.push(job, callback)
+  },
+
+  _clearUrlDetails(project_id, url, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const job = cb =>
+      db.UrlCache.destroy({ where: { url, project_id } })
+        .then(() => cb(null))
+        .error(cb)
+    return dbQueue.queue.push(job, callback)
+  },
+
+  _findAllUrlsInProject(project_id, callback) {
+    if (callback == null) {
+      callback = function(error, urls) {}
+    }
+    const job = cb =>
+      db.UrlCache.findAll({ where: { project_id } })
+        .then(urlEntries =>
+          cb(
+            null,
+            urlEntries.map(entry => entry.url)
+          )
+        )
+        .error(cb)
+    return dbQueue.queue.push(job, callback)
+  }
+}

+ 120 - 0
services/clsi/app/js/UrlFetcher.js

@@ -0,0 +1,120 @@
+/* eslint-disable
+    handle-callback-err,
+    no-return-assign,
+    no-unused-vars,
+    node/no-deprecated-api,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let UrlFetcher
+const request = require('request').defaults({ jar: false })
+const fs = require('fs')
+const logger = require('logger-sharelatex')
+const settings = require('settings-sharelatex')
+const URL = require('url')
+
+const oneMinute = 60 * 1000
+
+module.exports = UrlFetcher = {
+  pipeUrlToFile(url, filePath, _callback) {
+    if (_callback == null) {
+      _callback = function(error) {}
+    }
+    const callbackOnce = function(error) {
+      if (timeoutHandler != null) {
+        clearTimeout(timeoutHandler)
+      }
+      _callback(error)
+      return (_callback = function() {})
+    }
+
+    if (settings.filestoreDomainOveride != null) {
+      const p = URL.parse(url).path
+      url = `${settings.filestoreDomainOveride}${p}`
+    }
+    var timeoutHandler = setTimeout(
+      function() {
+        timeoutHandler = null
+        logger.error({ url, filePath }, 'Timed out downloading file to cache')
+        return callbackOnce(
+          new Error(`Timed out downloading file to cache ${url}`)
+        )
+      },
+      // FIXME: maybe need to close fileStream here
+      3 * oneMinute
+    )
+
+    logger.log({ url, filePath }, 'started downloading url to cache')
+    const urlStream = request.get({ url, timeout: oneMinute })
+    urlStream.pause() // stop data flowing until we are ready
+
+    // attach handlers before setting up pipes
+    urlStream.on('error', function(error) {
+      logger.error({ err: error, url, filePath }, 'error downloading url')
+      return callbackOnce(
+        error || new Error(`Something went wrong downloading the URL ${url}`)
+      )
+    })
+
+    urlStream.on('end', () =>
+      logger.log({ url, filePath }, 'finished downloading file into cache')
+    )
+
+    return urlStream.on('response', function(res) {
+      if (res.statusCode >= 200 && res.statusCode < 300) {
+        const fileStream = fs.createWriteStream(filePath)
+
+        // attach handlers before setting up pipes
+        fileStream.on('error', function(error) {
+          logger.error(
+            { err: error, url, filePath },
+            'error writing file into cache'
+          )
+          return fs.unlink(filePath, function(err) {
+            if (err != null) {
+              logger.err({ err, filePath }, 'error deleting file from cache')
+            }
+            return callbackOnce(error)
+          })
+        })
+
+        fileStream.on('finish', function() {
+          logger.log({ url, filePath }, 'finished writing file into cache')
+          return callbackOnce()
+        })
+
+        fileStream.on('pipe', () =>
+          logger.log({ url, filePath }, 'piping into filestream')
+        )
+
+        urlStream.pipe(fileStream)
+        return urlStream.resume() // now we are ready to handle the data
+      } else {
+        logger.error(
+          { statusCode: res.statusCode, url, filePath },
+          'unexpected status code downloading url to cache'
+        )
+        // https://nodejs.org/api/http.html#http_class_http_clientrequest
+        // If you add a 'response' event handler, then you must consume
+        // the data from the response object, either by calling
+        // response.read() whenever there is a 'readable' event, or by
+        // adding a 'data' handler, or by calling the .resume()
+        // method. Until the data is consumed, the 'end' event will not
+        // fire. Also, until the data is read it will consume memory
+        // that can eventually lead to a 'process out of memory' error.
+        urlStream.resume() // discard the data
+        return callbackOnce(
+          new Error(
+            `URL returned non-success status code: ${res.statusCode} ${url}`
+          )
+        )
+      }
+    })
+  }
+}

+ 67 - 0
services/clsi/app/js/db.js

@@ -0,0 +1,67 @@
+/* eslint-disable
+    no-console,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Sequelize = require('sequelize')
+const Settings = require('settings-sharelatex')
+const _ = require('underscore')
+const logger = require('logger-sharelatex')
+
+const options = _.extend({ logging: false }, Settings.mysql.clsi)
+
+logger.log({ dbPath: Settings.mysql.clsi.storage }, 'connecting to db')
+
+const sequelize = new Sequelize(
+  Settings.mysql.clsi.database,
+  Settings.mysql.clsi.username,
+  Settings.mysql.clsi.password,
+  options
+)
+
+if (Settings.mysql.clsi.dialect === 'sqlite') {
+  logger.log('running PRAGMA journal_mode=WAL;')
+  sequelize.query('PRAGMA journal_mode=WAL;')
+  sequelize.query('PRAGMA synchronous=OFF;')
+  sequelize.query('PRAGMA read_uncommitted = true;')
+}
+
+module.exports = {
+  UrlCache: sequelize.define(
+    'UrlCache',
+    {
+      url: Sequelize.STRING,
+      project_id: Sequelize.STRING,
+      lastModified: Sequelize.DATE
+    },
+    {
+      indexes: [{ fields: ['url', 'project_id'] }, { fields: ['project_id'] }]
+    }
+  ),
+
+  Project: sequelize.define(
+    'Project',
+    {
+      project_id: { type: Sequelize.STRING, primaryKey: true },
+      lastAccessed: Sequelize.DATE
+    },
+    {
+      indexes: [{ fields: ['lastAccessed'] }]
+    }
+  ),
+
+  op: Sequelize.Op,
+
+  sync() {
+    logger.log({ dbPath: Settings.mysql.clsi.storage }, 'syncing db schema')
+    return sequelize
+      .sync()
+      .then(() => logger.log('db sync complete'))
+      .catch(err => console.log(err, 'error syncing'))
+  }
+}

+ 0 - 21
services/clsi/bin/install_texlive_gce.sh

@@ -1,21 +0,0 @@
-#!/bin/sh
-METADATA=http://metadata.google.internal./computeMetadata/v1
-SVC_ACCT=$METADATA/instance/service-accounts/default
-PROJECT_URL=$METADATA/project/project-id
-ACCESS_TOKEN=$(curl -s -H 'Metadata-Flavor: Google' $SVC_ACCT/token | cut -d'"' -f 4)
-if [ -z "$ACCESS_TOKEN" ]; then
-	echo "No acccess token to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment."
-	exit 0
-fi
-PROJECT=$(curl -s -H 'Metadata-Flavor: Google' $PROJECT_URL)
-if [ -z "$PROJECT" ]; then
-	echo "No project name to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment."
-	exit 0
-fi
-docker login -u '_token' -p $ACCESS_TOKEN https://gcr.io
-docker pull --all-tags gcr.io/$PROJECT/texlive-full
-cp /app/bin/synctex /app/bin/synctex-mount/synctex
-
-echo "Finished downloading texlive-full images"
-
-

+ 6 - 5
services/clsi/buildscript.txt

@@ -1,10 +1,11 @@
 clsi
---language=coffeescript
---node-version=10.15.0
 --acceptance-creds=None
---dependencies=mongo,redis
+--data-dirs=cache,compiles,db
+--dependencies=
 --docker-repos=gcr.io/overleaf-ops
+--env-add=
 --env-pass-through=TEXLIVE_IMAGE
---build-target=docker
---script-version=1.1.24
+--language=es
+--node-version=10.19.0
 --public-repo=True
+--script-version=2.1.0

+ 0 - 72
services/clsi/config/settings.defaults.coffee

@@ -1,72 +0,0 @@
-Path = require "path"
-
-module.exports =
-	# Options are passed to Sequelize.
-	# See http://sequelizejs.com/documentation#usage-options for details
-	mysql:
-		clsi:
-			database: "clsi"
-			username: "clsi"
-			dialect: "sqlite"
-			storage: process.env["SQLITE_PATH"] or Path.resolve(__dirname + "/../db.sqlite")
-			pool:
-				max: 1
-				min: 1
-			retry:
-				max: 10
-
-	compileSizeLimit:  process.env["COMPILE_SIZE_LIMIT"] or "7mb"
-	
-	path:
-		compilesDir:  Path.resolve(__dirname + "/../compiles")
-		clsiCacheDir: Path.resolve(__dirname + "/../cache")
-		synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id)
-
-	internal:
-		clsi:
-			port: 3013
-			host: process.env["LISTEN_ADDRESS"] or "localhost"
-			
-		load_balancer_agent:
-			report_load:true
-			load_port: 3048
-			local_port: 3049
-	apis:
-		clsi:
-			url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013"
-
-			
-	smokeTest: process.env["SMOKE_TEST"] or false
-	project_cache_length_ms: 1000 * 60 * 60 * 24
-	parallelFileDownloads: process.env["FILESTORE_PARALLEL_FILE_DOWNLOADS"] or 1
-	parallelSqlQueryLimit: process.env["FILESTORE_PARALLEL_SQL_QUERY_LIMIT"] or 1
-	filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"]
-	texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"]
-	sentry:
-		dsn: process.env['SENTRY_DSN']
-
-
-if process.env["DOCKER_RUNNER"]
-	module.exports.clsi =
-		dockerRunner: process.env["DOCKER_RUNNER"] == "true"
-		docker:
-			runtime: process.env["DOCKER_RUNTIME"]
-			image: process.env["TEXLIVE_IMAGE"] or "quay.io/sharelatex/texlive-full:2017.1"
-			env:
-				HOME: "/tmp"
-			socketPath: "/var/run/docker.sock"
-			user: process.env["TEXLIVE_IMAGE_USER"] or "tex"
-		expireProjectAfterIdleMs: 24 * 60 * 60 * 1000
-		checkProjectsIntervalMs: 10 * 60 * 1000
-
-	try
-		seccomp_profile_path = Path.resolve(__dirname + "/../seccomp/clsi-profile.json")
-		module.exports.clsi.docker.seccomp_profile = JSON.stringify(JSON.parse(require("fs").readFileSync(seccomp_profile_path)))
-	catch error
-		console.log error, "could not load seccom profile from #{seccomp_profile_path}"
-
-	module.exports.path.synctexBaseDir = -> "/compile"	
-	
-	module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"]
-
-	module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"]

+ 102 - 0
services/clsi/config/settings.defaults.js

@@ -0,0 +1,102 @@
+const Path = require('path')
+
+module.exports = {
+  // Options are passed to Sequelize.
+  // See http://sequelizejs.com/documentation#usage-options for details
+  mysql: {
+    clsi: {
+      database: 'clsi',
+      username: 'clsi',
+      dialect: 'sqlite',
+      storage:
+        process.env.SQLITE_PATH || Path.resolve(__dirname + '/../db/db.sqlite'),
+      pool: {
+        max: 1,
+        min: 1
+      },
+      retry: {
+        max: 10
+      }
+    }
+  },
+
+  compileSizeLimit: process.env.COMPILE_SIZE_LIMIT || '7mb',
+
+  processLifespanLimitMs:
+    parseInt(process.env.PROCESS_LIFE_SPAN_LIMIT_MS) || 60 * 60 * 24 * 1000 * 2,
+
+  path: {
+    compilesDir: Path.resolve(__dirname + '/../compiles'),
+    clsiCacheDir: Path.resolve(__dirname + '/../cache'),
+    synctexBaseDir(project_id) {
+      return Path.join(this.compilesDir, project_id)
+    }
+  },
+
+  internal: {
+    clsi: {
+      port: 3013,
+      host: process.env.LISTEN_ADDRESS || 'localhost'
+    },
+
+    load_balancer_agent: {
+      report_load: true,
+      load_port: 3048,
+      local_port: 3049
+    }
+  },
+  apis: {
+    clsi: {
+      url: `http://${process.env.CLSI_HOST || 'localhost'}:3013`
+    }
+  },
+
+  smokeTest: process.env.SMOKE_TEST || false,
+  project_cache_length_ms: 1000 * 60 * 60 * 24,
+  parallelFileDownloads: process.env.FILESTORE_PARALLEL_FILE_DOWNLOADS || 1,
+  parallelSqlQueryLimit: process.env.FILESTORE_PARALLEL_SQL_QUERY_LIMIT || 1,
+  filestoreDomainOveride: process.env.FILESTORE_DOMAIN_OVERRIDE,
+  texliveImageNameOveride: process.env.TEX_LIVE_IMAGE_NAME_OVERRIDE,
+  sentry: {
+    dsn: process.env.SENTRY_DSN
+  }
+}
+
+if (process.env.DOCKER_RUNNER) {
+  let seccomp_profile_path
+  module.exports.clsi = {
+    dockerRunner: process.env.DOCKER_RUNNER === 'true',
+    docker: {
+      runtime: process.env.DOCKER_RUNTIME,
+      image:
+        process.env.TEXLIVE_IMAGE || 'quay.io/sharelatex/texlive-full:2017.1',
+      env: {
+        HOME: '/tmp'
+      },
+      socketPath: '/var/run/docker.sock',
+      user: process.env.TEXLIVE_IMAGE_USER || 'tex'
+    },
+    expireProjectAfterIdleMs: 24 * 60 * 60 * 1000,
+    checkProjectsIntervalMs: 10 * 60 * 1000
+  }
+
+  try {
+    seccomp_profile_path = Path.resolve(
+      __dirname + '/../seccomp/clsi-profile.json'
+    )
+    module.exports.clsi.docker.seccomp_profile = JSON.stringify(
+      JSON.parse(require('fs').readFileSync(seccomp_profile_path))
+    )
+  } catch (error) {
+    console.log(
+      error,
+      `could not load seccom profile from ${seccomp_profile_path}`
+    )
+  }
+
+  module.exports.path.synctexBaseDir = () => '/compile'
+
+  module.exports.path.sandboxedCompilesHostDir = process.env.COMPILES_HOST_DIR
+
+  module.exports.path.synctexBinHostPath = process.env.SYNCTEX_BIN_HOST_PATH
+}

+ 2 - 0
services/clsi/db/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 1 - 1
services/clsi/docker-compose-config.yml

@@ -1,4 +1,4 @@
-version: "2"
+version: "2.3"
 
 services:
   dev:

+ 1 - 12
services/clsi/docker-compose.ci.yml

@@ -1,9 +1,8 @@
 # This file was auto-generated, do not edit it directly.
 # Instead run bin/update_build_scripts from
 # https://github.com/sharelatex/sharelatex-dev-environment
-# Version: 1.1.24
 
-version: "2"
+version: "2.3"
 
 services:
   test_unit:
@@ -27,13 +26,9 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       NODE_ENV: test
       TEXLIVE_IMAGE:
-    depends_on:
-      - mongo
-      - redis
     command: npm run test:acceptance:_run
 
 
-
   tar:
     build: .
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
@@ -41,9 +36,3 @@ services:
       - ./:/tmp/build/
     command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
     user: root
-
-  redis:
-    image: redis
-
-  mongo:
-    image: mongo:3.4

+ 7 - 24
services/clsi/docker-compose.yml

@@ -1,13 +1,14 @@
 # This file was auto-generated, do not edit it directly.
 # Instead run bin/update_build_scripts from
 # https://github.com/sharelatex/sharelatex-dev-environment
-# Version: 1.1.24
 
-version: "2"
+version: "2.3"
 
 services:
   test_unit:
-    build: .
+    build:
+      context: .
+      target: base
     volumes:
       - .:/app
     working_dir: /app
@@ -17,7 +18,9 @@ services:
     command: npm run test:unit
 
   test_acceptance:
-    build: .
+    build:
+      context: .
+      target: base
     volumes:
       - .:/app
     working_dir: /app
@@ -32,25 +35,5 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ERROR
       NODE_ENV: test
-    depends_on:
-      - mongo
-      - redis
     command: npm run test:acceptance
 
-
-
-  tar:
-    build: .
-    image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
-    volumes:
-      - ./:/tmp/build/
-    command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
-    user: root
-
-  redis:
-    image: redis
-
-  mongo:
-    image: mongo:3.4
-
-

+ 8 - 15
services/clsi/entrypoint.sh

@@ -1,25 +1,18 @@
 #!/bin/sh
 
-echo "Changing permissions of /var/run/docker.sock for sibling containers"
-ls -al /var/run/docker.sock
-docker --version
-cat /etc/passwd
+docker --version >&2
 
+# add the node user to the docker group on the host
 DOCKER_GROUP=$(stat -c '%g' /var/run/docker.sock)
 groupadd --non-unique --gid ${DOCKER_GROUP} dockeronhost
 usermod -aG dockeronhost node
 
-mkdir -p /app/cache
-chown -R node:node /app/cache
+# compatibility: initial volume setup
+chown node:node /app/cache
+chown node:node /app/compiles
+chown node:node /app/db
 
-mkdir -p /app/compiles
-chown -R node:node /app/compiles
+# make synctex available for remount in compiles
+cp /app/bin/synctex /app/bin/synctex-mount/synctex
 
-chown -R node:node /app/bin/synctex
-mkdir -p /app/test/acceptance/fixtures/tmp/
-chown -R node:node /app
-
-chown -R node:node /app/bin
-
-./bin/install_texlive_gce.sh
 exec runuser -u node -- "$@"

+ 3 - 4
services/clsi/nodemon.json

@@ -10,10 +10,9 @@
   },
 
   "watch": [
-    "app/coffee/",
-    "app.coffee",
+    "app/js/",
+    "app.js",
     "config/"
   ],
-  "ext": "coffee"
-
+  "ext": "js"
 }

+ 0 - 3619
services/clsi/npm-shrinkwrap.json

@@ -1,3619 +0,0 @@
-{
-  "name": "node-clsi",
-  "version": "0.1.4",
-  "lockfileVersion": 1,
-  "requires": true,
-  "dependencies": {
-    "@google-cloud/common": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.2.3.tgz",
-      "integrity": "sha512-lvw54mGKn8VqVIy2NzAk0l5fntBFX4UwQhHk6HaqkyCQ7WBl5oz4XhzKMtMilozF/3ObPcDogqwuyEWyZ6rnQQ==",
-      "requires": {
-        "@google-cloud/projectify": "^1.0.0",
-        "@google-cloud/promisify": "^1.0.0",
-        "arrify": "^2.0.0",
-        "duplexify": "^3.6.0",
-        "ent": "^2.2.0",
-        "extend": "^3.0.2",
-        "google-auth-library": "^5.5.0",
-        "retry-request": "^4.0.0",
-        "teeny-request": "^5.2.1"
-      }
-    },
-    "@google-cloud/debug-agent": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-3.2.0.tgz",
-      "integrity": "sha512-fP87kYbS6aeDna08BivwQ1J260mwJGchRi99XdWCgqbRwuFac8ul0OT5i2wEeDSc5QaDX8ZuWQQ0igZvh1rTyQ==",
-      "requires": {
-        "@google-cloud/common": "^0.32.0",
-        "@sindresorhus/is": "^0.15.0",
-        "acorn": "^6.0.0",
-        "coffeescript": "^2.0.0",
-        "console-log-level": "^1.4.0",
-        "extend": "^3.0.1",
-        "findit2": "^2.2.3",
-        "gcp-metadata": "^1.0.0",
-        "lodash.pickby": "^4.6.0",
-        "p-limit": "^2.2.0",
-        "pify": "^4.0.1",
-        "semver": "^6.0.0",
-        "source-map": "^0.6.1",
-        "split": "^1.0.0"
-      },
-      "dependencies": {
-        "@google-cloud/common": {
-          "version": "0.32.1",
-          "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz",
-          "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==",
-          "requires": {
-            "@google-cloud/projectify": "^0.3.3",
-            "@google-cloud/promisify": "^0.4.0",
-            "@types/request": "^2.48.1",
-            "arrify": "^2.0.0",
-            "duplexify": "^3.6.0",
-            "ent": "^2.2.0",
-            "extend": "^3.0.2",
-            "google-auth-library": "^3.1.1",
-            "pify": "^4.0.1",
-            "retry-request": "^4.0.0",
-            "teeny-request": "^3.11.3"
-          }
-        },
-        "@google-cloud/projectify": {
-          "version": "0.3.3",
-          "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz",
-          "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw=="
-        },
-        "@google-cloud/promisify": {
-          "version": "0.4.0",
-          "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz",
-          "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q=="
-        },
-        "coffeescript": {
-          "version": "2.4.1",
-          "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.4.1.tgz",
-          "integrity": "sha512-34GV1aHrsMpTaO3KfMJL40ZNuvKDR/g98THHnE9bQj8HjMaZvSrLik99WWqyMhRtbe8V5hpx5iLgdcSvM/S2wg=="
-        },
-        "debug": {
-          "version": "3.2.6",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "gaxios": {
-          "version": "1.8.4",
-          "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz",
-          "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==",
-          "requires": {
-            "abort-controller": "^3.0.0",
-            "extend": "^3.0.2",
-            "https-proxy-agent": "^2.2.1",
-            "node-fetch": "^2.3.0"
-          }
-        },
-        "gcp-metadata": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz",
-          "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==",
-          "requires": {
-            "gaxios": "^1.0.2",
-            "json-bigint": "^0.3.0"
-          }
-        },
-        "google-auth-library": {
-          "version": "3.1.2",
-          "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz",
-          "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==",
-          "requires": {
-            "base64-js": "^1.3.0",
-            "fast-text-encoding": "^1.0.0",
-            "gaxios": "^1.2.1",
-            "gcp-metadata": "^1.0.0",
-            "gtoken": "^2.3.2",
-            "https-proxy-agent": "^2.2.1",
-            "jws": "^3.1.5",
-            "lru-cache": "^5.0.0",
-            "semver": "^5.5.0"
-          },
-          "dependencies": {
-            "semver": {
-              "version": "5.7.1",
-              "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-              "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
-            }
-          }
-        },
-        "google-p12-pem": {
-          "version": "1.0.4",
-          "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz",
-          "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==",
-          "requires": {
-            "node-forge": "^0.8.0",
-            "pify": "^4.0.0"
-          }
-        },
-        "gtoken": {
-          "version": "2.3.3",
-          "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz",
-          "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==",
-          "requires": {
-            "gaxios": "^1.0.4",
-            "google-p12-pem": "^1.0.0",
-            "jws": "^3.1.5",
-            "mime": "^2.2.0",
-            "pify": "^4.0.0"
-          }
-        },
-        "https-proxy-agent": {
-          "version": "2.2.4",
-          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
-          "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
-          "requires": {
-            "agent-base": "^4.3.0",
-            "debug": "^3.1.0"
-          }
-        },
-        "mime": {
-          "version": "2.4.4",
-          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
-          "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-        },
-        "node-forge": {
-          "version": "0.8.5",
-          "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz",
-          "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q=="
-        },
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        },
-        "teeny-request": {
-          "version": "3.11.3",
-          "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz",
-          "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==",
-          "requires": {
-            "https-proxy-agent": "^2.2.1",
-            "node-fetch": "^2.2.0",
-            "uuid": "^3.3.2"
-          }
-        }
-      }
-    },
-    "@google-cloud/logging": {
-      "version": "5.5.3",
-      "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-5.5.3.tgz",
-      "integrity": "sha512-TZ/DzHod4icaC7wEEBm0PHYfbhvg0CbCVzKLsdAwj11xSD/egGNOsG5optEQcbAQEPrO1B5xBXfsE0wIBBYjpQ==",
-      "requires": {
-        "@google-cloud/common": "^2.2.2",
-        "@google-cloud/paginator": "^2.0.0",
-        "@google-cloud/projectify": "^1.0.0",
-        "@google-cloud/promisify": "^1.0.0",
-        "@opencensus/propagation-stackdriver": "0.0.18",
-        "arrify": "^2.0.0",
-        "dot-prop": "^5.1.0",
-        "eventid": "^0.1.2",
-        "extend": "^3.0.2",
-        "gcp-metadata": "^3.1.0",
-        "google-gax": "^1.7.5",
-        "is": "^3.3.0",
-        "on-finished": "^2.3.0",
-        "protobufjs": "^6.8.8",
-        "pumpify": "^2.0.0",
-        "snakecase-keys": "^3.0.0",
-        "stream-events": "^1.0.4",
-        "through2": "^3.0.0",
-        "type-fest": "^0.8.0"
-      }
-    },
-    "@google-cloud/logging-bunyan": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@google-cloud/logging-bunyan/-/logging-bunyan-2.0.0.tgz",
-      "integrity": "sha512-9W9B8GQNMlBdQSV+c0492+sMMknn4/428EdSO1xv5Hn07P32N/e4T25y4Gnl9jlrItuZHIXRwYPVSHqUyGb1Zg==",
-      "requires": {
-        "@google-cloud/logging": "^5.5.2",
-        "google-auth-library": "^5.0.0"
-      }
-    },
-    "@google-cloud/paginator": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.1.tgz",
-      "integrity": "sha512-HZ6UTGY/gHGNriD7OCikYWL/Eu0sTEur2qqse2w6OVsz+57se3nTkqH14JIPxtf0vlEJ8IJN5w3BdZ22pjCB8g==",
-      "requires": {
-        "arrify": "^2.0.0",
-        "extend": "^3.0.2"
-      }
-    },
-    "@google-cloud/profiler": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-0.2.3.tgz",
-      "integrity": "sha512-rNvtrFtIebIxZEJ/O0t8n7HciZGIXBo8DvHxWqAmsCaeLvkTtsaL6HmPkwxrNQ1IhbYWAxF+E/DwCiHyhKmgTg==",
-      "requires": {
-        "@google-cloud/common": "^0.26.0",
-        "@types/console-log-level": "^1.4.0",
-        "@types/semver": "^5.5.0",
-        "bindings": "^1.2.1",
-        "console-log-level": "^1.4.0",
-        "delay": "^4.0.1",
-        "extend": "^3.0.1",
-        "gcp-metadata": "^0.9.0",
-        "nan": "^2.11.1",
-        "parse-duration": "^0.1.1",
-        "pify": "^4.0.0",
-        "pretty-ms": "^4.0.0",
-        "protobufjs": "~6.8.6",
-        "semver": "^5.5.0",
-        "teeny-request": "^3.3.0"
-      },
-      "dependencies": {
-        "@google-cloud/common": {
-          "version": "0.26.2",
-          "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.26.2.tgz",
-          "integrity": "sha512-xJ2M/q3MrUbnYZuFlpF01caAlEhAUoRn0NXp93Hn3pkFpfSOG8YfbKbpBAHvcKVbBOAKVIwPsleNtuyuabUwLQ==",
-          "requires": {
-            "@google-cloud/projectify": "^0.3.2",
-            "@google-cloud/promisify": "^0.3.0",
-            "@types/duplexify": "^3.5.0",
-            "@types/request": "^2.47.0",
-            "arrify": "^1.0.1",
-            "duplexify": "^3.6.0",
-            "ent": "^2.2.0",
-            "extend": "^3.0.1",
-            "google-auth-library": "^2.0.0",
-            "pify": "^4.0.0",
-            "retry-request": "^4.0.0",
-            "through2": "^3.0.0"
-          }
-        },
-        "@google-cloud/projectify": {
-          "version": "0.3.3",
-          "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz",
-          "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw=="
-        },
-        "@google-cloud/promisify": {
-          "version": "0.3.1",
-          "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.3.1.tgz",
-          "integrity": "sha512-QzB0/IMvB0eFxFK7Eqh+bfC8NLv3E9ScjWQrPOk6GgfNroxcVITdTlT8NRsRrcp5+QQJVPLkRqKG0PUdaWXmHw=="
-        },
-        "arrify": {
-          "version": "1.0.1",
-          "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
-          "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0="
-        },
-        "debug": {
-          "version": "3.2.6",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "gaxios": {
-          "version": "1.8.4",
-          "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz",
-          "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==",
-          "requires": {
-            "abort-controller": "^3.0.0",
-            "extend": "^3.0.2",
-            "https-proxy-agent": "^2.2.1",
-            "node-fetch": "^2.3.0"
-          }
-        },
-        "gcp-metadata": {
-          "version": "0.9.3",
-          "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.9.3.tgz",
-          "integrity": "sha512-caV4S84xAjENtpezLCT/GILEAF5h/bC4cNqZFmt/tjTn8t+JBtTkQrgBrJu3857YdsnlM8rxX/PMcKGtE8hUlw==",
-          "requires": {
-            "gaxios": "^1.0.2",
-            "json-bigint": "^0.3.0"
-          }
-        },
-        "google-auth-library": {
-          "version": "2.0.2",
-          "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-2.0.2.tgz",
-          "integrity": "sha512-FURxmo1hBVmcfLauuMRKOPYAPKht3dGuI2wjeJFalDUThO0HoYVjr4yxt5cgYSFm1dgUpmN9G/poa7ceTFAIiA==",
-          "requires": {
-            "axios": "^0.18.0",
-            "gcp-metadata": "^0.7.0",
-            "gtoken": "^2.3.0",
-            "https-proxy-agent": "^2.2.1",
-            "jws": "^3.1.5",
-            "lru-cache": "^5.0.0",
-            "semver": "^5.5.0"
-          },
-          "dependencies": {
-            "gcp-metadata": {
-              "version": "0.7.0",
-              "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.7.0.tgz",
-              "integrity": "sha512-ffjC09amcDWjh3VZdkDngIo7WoluyC5Ag9PAYxZbmQLOLNI8lvPtoKTSCyU54j2gwy5roZh6sSMTfkY2ct7K3g==",
-              "requires": {
-                "axios": "^0.18.0",
-                "extend": "^3.0.1",
-                "retry-axios": "0.3.2"
-              }
-            }
-          }
-        },
-        "google-p12-pem": {
-          "version": "1.0.4",
-          "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz",
-          "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==",
-          "requires": {
-            "node-forge": "^0.8.0",
-            "pify": "^4.0.0"
-          }
-        },
-        "gtoken": {
-          "version": "2.3.3",
-          "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz",
-          "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==",
-          "requires": {
-            "gaxios": "^1.0.4",
-            "google-p12-pem": "^1.0.0",
-            "jws": "^3.1.5",
-            "mime": "^2.2.0",
-            "pify": "^4.0.0"
-          }
-        },
-        "https-proxy-agent": {
-          "version": "2.2.4",
-          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
-          "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
-          "requires": {
-            "agent-base": "^4.3.0",
-            "debug": "^3.1.0"
-          }
-        },
-        "mime": {
-          "version": "2.4.4",
-          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
-          "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-        },
-        "node-forge": {
-          "version": "0.8.5",
-          "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz",
-          "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q=="
-        },
-        "teeny-request": {
-          "version": "3.11.3",
-          "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz",
-          "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==",
-          "requires": {
-            "https-proxy-agent": "^2.2.1",
-            "node-fetch": "^2.2.0",
-            "uuid": "^3.3.2"
-          }
-        }
-      }
-    },
-    "@google-cloud/projectify": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.1.tgz",
-      "integrity": "sha512-xknDOmsMgOYHksKc1GPbwDLsdej8aRNIA17SlSZgQdyrcC0lx0OGo4VZgYfwoEU1YS8oUxF9Y+6EzDOb0eB7Xg=="
-    },
-    "@google-cloud/promisify": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.2.tgz",
-      "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g=="
-    },
-    "@google-cloud/trace-agent": {
-      "version": "3.6.1",
-      "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-3.6.1.tgz",
-      "integrity": "sha512-KDo85aPN4gSxJ7oEIOlKd7aGENZFXAM1kbIn1Ds+61gh/K1CQWSyepgJo3nUpAwH6D1ezDWV7Iaf8ueoITc8Uw==",
-      "requires": {
-        "@google-cloud/common": "^0.32.1",
-        "builtin-modules": "^3.0.0",
-        "console-log-level": "^1.4.0",
-        "continuation-local-storage": "^3.2.1",
-        "extend": "^3.0.0",
-        "gcp-metadata": "^1.0.0",
-        "hex2dec": "^1.0.1",
-        "is": "^3.2.0",
-        "methods": "^1.1.1",
-        "require-in-the-middle": "^4.0.0",
-        "semver": "^6.0.0",
-        "shimmer": "^1.2.0",
-        "uuid": "^3.0.1"
-      },
-      "dependencies": {
-        "@google-cloud/common": {
-          "version": "0.32.1",
-          "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz",
-          "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==",
-          "requires": {
-            "@google-cloud/projectify": "^0.3.3",
-            "@google-cloud/promisify": "^0.4.0",
-            "@types/request": "^2.48.1",
-            "arrify": "^2.0.0",
-            "duplexify": "^3.6.0",
-            "ent": "^2.2.0",
-            "extend": "^3.0.2",
-            "google-auth-library": "^3.1.1",
-            "pify": "^4.0.1",
-            "retry-request": "^4.0.0",
-            "teeny-request": "^3.11.3"
-          }
-        },
-        "@google-cloud/projectify": {
-          "version": "0.3.3",
-          "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz",
-          "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw=="
-        },
-        "@google-cloud/promisify": {
-          "version": "0.4.0",
-          "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz",
-          "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q=="
-        },
-        "debug": {
-          "version": "3.2.6",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "gaxios": {
-          "version": "1.8.4",
-          "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz",
-          "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==",
-          "requires": {
-            "abort-controller": "^3.0.0",
-            "extend": "^3.0.2",
-            "https-proxy-agent": "^2.2.1",
-            "node-fetch": "^2.3.0"
-          }
-        },
-        "gcp-metadata": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz",
-          "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==",
-          "requires": {
-            "gaxios": "^1.0.2",
-            "json-bigint": "^0.3.0"
-          }
-        },
-        "google-auth-library": {
-          "version": "3.1.2",
-          "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz",
-          "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==",
-          "requires": {
-            "base64-js": "^1.3.0",
-            "fast-text-encoding": "^1.0.0",
-            "gaxios": "^1.2.1",
-            "gcp-metadata": "^1.0.0",
-            "gtoken": "^2.3.2",
-            "https-proxy-agent": "^2.2.1",
-            "jws": "^3.1.5",
-            "lru-cache": "^5.0.0",
-            "semver": "^5.5.0"
-          },
-          "dependencies": {
-            "semver": {
-              "version": "5.7.1",
-              "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-              "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
-            }
-          }
-        },
-        "google-p12-pem": {
-          "version": "1.0.4",
-          "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz",
-          "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==",
-          "requires": {
-            "node-forge": "^0.8.0",
-            "pify": "^4.0.0"
-          }
-        },
-        "gtoken": {
-          "version": "2.3.3",
-          "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz",
-          "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==",
-          "requires": {
-            "gaxios": "^1.0.4",
-            "google-p12-pem": "^1.0.0",
-            "jws": "^3.1.5",
-            "mime": "^2.2.0",
-            "pify": "^4.0.0"
-          }
-        },
-        "https-proxy-agent": {
-          "version": "2.2.4",
-          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
-          "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
-          "requires": {
-            "agent-base": "^4.3.0",
-            "debug": "^3.1.0"
-          }
-        },
-        "mime": {
-          "version": "2.4.4",
-          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
-          "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-        },
-        "node-forge": {
-          "version": "0.8.5",
-          "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz",
-          "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q=="
-        },
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        },
-        "teeny-request": {
-          "version": "3.11.3",
-          "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz",
-          "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==",
-          "requires": {
-            "https-proxy-agent": "^2.2.1",
-            "node-fetch": "^2.2.0",
-            "uuid": "^3.3.2"
-          }
-        }
-      }
-    },
-    "@grpc/grpc-js": {
-      "version": "0.6.9",
-      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.9.tgz",
-      "integrity": "sha512-r1nDOEEiYmAsVYBaS4DPPqdwPOXPw7YhVOnnpPdWhlNtKbYzPash6DqWTTza9gBiYMA5d2Wiq6HzrPqsRaP4yA==",
-      "requires": {
-        "semver": "^6.2.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
-      }
-    },
-    "@grpc/proto-loader": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.2.tgz",
-      "integrity": "sha512-eBKD/FPxQoY1x6QONW2nBd54QUEyzcFP9FenujmoeDPy1rutVSHki1s/wR68F6O1QfCNDx+ayBH1O2CVNMzyyw==",
-      "requires": {
-        "lodash.camelcase": "^4.3.0",
-        "protobufjs": "^6.8.6"
-      }
-    },
-    "@opencensus/core": {
-      "version": "0.0.18",
-      "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.18.tgz",
-      "integrity": "sha512-PgRQXLyb3bLi8Z6pQct9erYFRdnYAZNQXAEVPf6Xq6IMkZaH20wiOTNNPxEckjI31mq5utgstAbwOn4gJiPjBQ==",
-      "requires": {
-        "continuation-local-storage": "^3.2.1",
-        "log-driver": "^1.2.7",
-        "semver": "^6.0.0",
-        "shimmer": "^1.2.0",
-        "uuid": "^3.2.1"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
-      }
-    },
-    "@opencensus/propagation-stackdriver": {
-      "version": "0.0.18",
-      "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.18.tgz",
-      "integrity": "sha512-BLwfszIGAfqN2mqGf/atfEu84cWeoLM/YuXGfXDO1iDN2k5GXz4QFyhS8sz5l63HtsYuQqFuV+Ze7ZM0NvJp2A==",
-      "requires": {
-        "@opencensus/core": "^0.0.18",
-        "hex2dec": "^1.0.1",
-        "uuid": "^3.2.1"
-      }
-    },
-    "@overleaf/o-error": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-2.1.0.tgz",
-      "integrity": "sha512-Zd9sks9LrLw8ErHt/cXeWIkyxWAqNAvNGn7wIjLQJH6TTEEW835PWOhpch+hQwwWsTxWIx/JDj+IpZ3ouw925g=="
-    },
-    "@protobufjs/aspromise": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
-      "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78="
-    },
-    "@protobufjs/base64": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
-      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
-    },
-    "@protobufjs/codegen": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
-      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
-    },
-    "@protobufjs/eventemitter": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
-      "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A="
-    },
-    "@protobufjs/fetch": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
-      "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=",
-      "requires": {
-        "@protobufjs/aspromise": "^1.1.1",
-        "@protobufjs/inquire": "^1.1.0"
-      }
-    },
-    "@protobufjs/float": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
-      "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E="
-    },
-    "@protobufjs/inquire": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
-      "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik="
-    },
-    "@protobufjs/path": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
-      "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0="
-    },
-    "@protobufjs/pool": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
-      "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q="
-    },
-    "@protobufjs/utf8": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
-      "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
-    },
-    "@sindresorhus/is": {
-      "version": "0.15.0",
-      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.15.0.tgz",
-      "integrity": "sha512-lu8BpxjAtRCAo5ifytTpCPCj99LF7o/2Myn+NXyNCBqvPYn7Pjd76AMmUB5l7XF1U6t0hcWrlEM5ESufW7wAeA=="
-    },
-    "@types/caseless": {
-      "version": "0.12.2",
-      "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
-      "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w=="
-    },
-    "@types/console-log-level": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz",
-      "integrity": "sha512-x+OscEQwcx5Biair4enH7ov9W+clcqUWaZRaxn5IkT4yNWWjRr2oiYDkY/x1uXSTVZOQ2xlbFQySaQGB+VdXGQ=="
-    },
-    "@types/duplexify": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz",
-      "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==",
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "@types/geojson": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz",
-      "integrity": "sha1-PgKXJyjGkkjCrwjWCkjLuGgP/98="
-    },
-    "@types/long": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz",
-      "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q=="
-    },
-    "@types/node": {
-      "version": "10.12.15",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz",
-      "integrity": "sha1-IOhWUbYv2GZW5Xycm8dxqxVwvFk="
-    },
-    "@types/request": {
-      "version": "2.48.3",
-      "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.3.tgz",
-      "integrity": "sha512-3Wo2jNYwqgXcIz/rrq18AdOZUQB8cQ34CXZo+LUwPJNpvRAL86+Kc2wwI8mqpz9Cr1V+enIox5v+WZhy/p3h8w==",
-      "requires": {
-        "@types/caseless": "*",
-        "@types/node": "*",
-        "@types/tough-cookie": "*",
-        "form-data": "^2.5.0"
-      },
-      "dependencies": {
-        "form-data": {
-          "version": "2.5.1",
-          "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
-          "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
-          "requires": {
-            "asynckit": "^0.4.0",
-            "combined-stream": "^1.0.6",
-            "mime-types": "^2.1.12"
-          }
-        }
-      }
-    },
-    "@types/semver": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz",
-      "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ=="
-    },
-    "@types/tough-cookie": {
-      "version": "2.3.5",
-      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz",
-      "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg=="
-    },
-    "JSONStream": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
-      "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=",
-      "requires": {
-        "jsonparse": "^1.2.0",
-        "through": ">=2.2.7 <3"
-      }
-    },
-    "abbrev": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
-      "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg="
-    },
-    "abort-controller": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
-      "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
-      "requires": {
-        "event-target-shim": "^5.0.0"
-      }
-    },
-    "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.3.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
-      "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA=="
-    },
-    "agent-base": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
-      "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
-      "requires": {
-        "es6-promisify": "^5.0.0"
-      }
-    },
-    "ajv": {
-      "version": "6.6.2",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
-      "integrity": "sha1-ys7M9HS/P8POOxR0Q3EaJAY8ww0=",
-      "requires": {
-        "fast-deep-equal": "^2.0.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      }
-    },
-    "ansi-regex": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
-    },
-    "aproba": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
-      "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo="
-    },
-    "are-we-there-yet": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
-      "integrity": "sha1-SzXClE8GKov82mZBB2A1D+nd/CE=",
-      "requires": {
-        "delegates": "^1.0.0",
-        "readable-stream": "^2.0.6"
-      }
-    },
-    "array-flatten": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
-      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
-    },
-    "arrify": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
-      "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="
-    },
-    "asn1": {
-      "version": "0.2.4",
-      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
-      "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=",
-      "requires": {
-        "safer-buffer": "~2.1.0"
-      }
-    },
-    "assert-plus": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
-    },
-    "assertion-error": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz",
-      "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=",
-      "dev": true
-    },
-    "async": {
-      "version": "0.2.9",
-      "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz",
-      "integrity": "sha1-32MGD789Myhqdqr21Vophtn/hhk="
-    },
-    "async-listener": {
-      "version": "0.6.10",
-      "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz",
-      "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==",
-      "requires": {
-        "semver": "^5.3.0",
-        "shimmer": "^1.1.0"
-      }
-    },
-    "asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
-    },
-    "aws-sign2": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
-    },
-    "aws4": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
-      "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8="
-    },
-    "axios": {
-      "version": "0.18.1",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
-      "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
-      "requires": {
-        "follow-redirects": "1.5.10",
-        "is-buffer": "^2.0.2"
-      }
-    },
-    "balanced-match": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
-      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
-    },
-    "base64-js": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
-      "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
-    },
-    "bcrypt-pbkdf": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
-      "requires": {
-        "tweetnacl": "^0.14.3"
-      }
-    },
-    "bignumber.js": {
-      "version": "7.2.1",
-      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
-      "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ=="
-    },
-    "bindings": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
-      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
-      "requires": {
-        "file-uri-to-path": "1.0.0"
-      }
-    },
-    "bintrees": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
-      "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
-    },
-    "bl": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
-      "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=",
-      "requires": {
-        "readable-stream": "^2.3.5",
-        "safe-buffer": "^5.1.1"
-      }
-    },
-    "bluebird": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
-      "integrity": "sha1-fQHG+WFsmlGrD4xUmnnf5uwz76c="
-    },
-    "body-parser": {
-      "version": "1.18.3",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
-      "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
-      "requires": {
-        "bytes": "3.0.0",
-        "content-type": "~1.0.4",
-        "debug": "2.6.9",
-        "depd": "~1.1.2",
-        "http-errors": "~1.6.3",
-        "iconv-lite": "0.4.23",
-        "on-finished": "~2.3.0",
-        "qs": "6.5.2",
-        "raw-body": "2.3.3",
-        "type-is": "~1.6.16"
-      }
-    },
-    "brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=",
-      "requires": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "browser-stdout": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
-      "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=",
-      "dev": true
-    },
-    "buffer-alloc": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
-      "integrity": "sha1-iQ3ZDZI6hz4I4Q5f1RpX5bfM4Ow=",
-      "requires": {
-        "buffer-alloc-unsafe": "^1.1.0",
-        "buffer-fill": "^1.0.0"
-      }
-    },
-    "buffer-alloc-unsafe": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
-      "integrity": "sha1-vX3CauKXLQ7aJTvgYdupkjScGfA="
-    },
-    "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-fill": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
-      "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
-    },
-    "buffer-from": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
-      "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8="
-    },
-    "builtin-modules": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
-      "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw=="
-    },
-    "bunyan": {
-      "version": "0.22.3",
-      "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz",
-      "integrity": "sha1-ehncG0yMZF90AkGnQPIkUUfGfsI=",
-      "dev": true,
-      "requires": {
-        "dtrace-provider": "0.2.8",
-        "mv": "~2"
-      }
-    },
-    "buster-core": {
-      "version": "0.6.4",
-      "resolved": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz",
-      "integrity": "sha1-J79rrWdCROpyDzEdkAoMoct4YFA=",
-      "dev": true
-    },
-    "buster-format": {
-      "version": "0.5.6",
-      "resolved": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz",
-      "integrity": "sha1-K4bDIuz14bCubm55Bev884fSq5U=",
-      "dev": true,
-      "requires": {
-        "buster-core": "=0.6.4"
-      }
-    },
-    "bytes": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
-      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
-    },
-    "caseless": {
-      "version": "0.12.0",
-      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
-      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
-    },
-    "chai": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz",
-      "integrity": "sha1-zHeGbV5+vKK9dRRLHtw3Coh4X3I=",
-      "dev": true,
-      "requires": {
-        "assertion-error": "1.0.0",
-        "deep-eql": "0.1.3"
-      }
-    },
-    "chownr": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
-      "integrity": "sha1-VHJri4//TfBTxCGH6AH7RBLfFJQ="
-    },
-    "cls-bluebird": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz",
-      "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=",
-      "requires": {
-        "is-bluebird": "^1.0.2",
-        "shimmer": "^1.1.0"
-      }
-    },
-    "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="
-    },
-    "coffee-script": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz",
-      "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M="
-    },
-    "coffeescript": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.6.0.tgz",
-      "integrity": "sha1-bdTeHrYveE2MjYCWdVLLpUf/2d4=",
-      "dev": true
-    },
-    "combined-stream": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
-      "integrity": "sha1-LR0kMXr7ir6V1tLAsHtXgTU52Cg=",
-      "requires": {
-        "delayed-stream": "~1.0.0"
-      }
-    },
-    "commander": {
-      "version": "2.0.0",
-      "resolved": "http://registry.npmjs.org/commander/-/commander-2.0.0.tgz",
-      "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg="
-    },
-    "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": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=",
-      "requires": {
-        "buffer-from": "^1.0.0",
-        "inherits": "^2.0.3",
-        "readable-stream": "^2.2.2",
-        "typedarray": "^0.0.6"
-      }
-    },
-    "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="
-    },
-    "console-log-level": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.1.tgz",
-      "integrity": "sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ=="
-    },
-    "content-disposition": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
-      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
-    },
-    "content-type": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
-      "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js="
-    },
-    "continuation-local-storage": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz",
-      "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==",
-      "requires": {
-        "async-listener": "^0.6.0",
-        "emitter-listener": "^1.1.1"
-      }
-    },
-    "cookie": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
-      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
-    },
-    "cookie-signature": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
-      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
-    },
-    "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="
-    },
-    "d64": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/d64/-/d64-1.0.0.tgz",
-      "integrity": "sha1-QAKofoUMv8n52XBrYPymE6MzbpA="
-    },
-    "dashdash": {
-      "version": "1.14.1",
-      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
-      "requires": {
-        "assert-plus": "^1.0.0"
-      }
-    },
-    "debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
-      "requires": {
-        "ms": "2.0.0"
-      }
-    },
-    "deep-eql": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
-      "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=",
-      "dev": true,
-      "requires": {
-        "type-detect": "0.1.1"
-      }
-    },
-    "deep-extend": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
-      "integrity": "sha1-xPp8lUBKF6nD6Mp+FTcxK3NjMKw="
-    },
-    "delay": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz",
-      "integrity": "sha512-Lwaf3zVFDMBop1yDuFZ19F9WyGcZcGacsbdlZtWjQmM50tOcMntm1njF/Nb/Vjij3KaSvCF+sEYGKrrjObu2NA=="
-    },
-    "delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
-    },
-    "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="
-    },
-    "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": "1.0.7",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz",
-      "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ="
-    },
-    "docker-modem": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.7.tgz",
-      "integrity": "sha1-aXAqlcBg7rZ3X3nM3Mc05ZRpcqQ=",
-      "requires": {
-        "JSONStream": "1.3.2",
-        "debug": "^3.2.5",
-        "readable-stream": "~1.0.26-4",
-        "split-ca": "^1.0.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.2.6",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-          "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "isarray": {
-          "version": "0.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
-          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
-        },
-        "ms": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
-          "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo="
-        },
-        "readable-stream": {
-          "version": "1.0.34",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
-          "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
-          "requires": {
-            "core-util-is": "~1.0.0",
-            "inherits": "~2.0.1",
-            "isarray": "0.0.1",
-            "string_decoder": "~0.10.x"
-          }
-        },
-        "string_decoder": {
-          "version": "0.10.31",
-          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
-          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
-        }
-      }
-    },
-    "dockerode": {
-      "version": "2.5.7",
-      "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.7.tgz",
-      "integrity": "sha1-E9yewPfzU6wOUSJJ538y0aqhGZ4=",
-      "requires": {
-        "concat-stream": "~1.6.2",
-        "docker-modem": "1.0.x",
-        "tar-fs": "~1.16.3"
-      }
-    },
-    "dot-prop": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.1.1.tgz",
-      "integrity": "sha512-QCHI6Lkf+9fJMpwfAFsTvbiSh6ujoPmhCLiDvD/n4dGtLvHfhuBwPdN6z2x4YSOwwtTcLoO/LP70xELWGF/JVA==",
-      "requires": {
-        "is-obj": "^2.0.0"
-      }
-    },
-    "dottie": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.1.tgz",
-      "integrity": "sha1-aXrZ1yAE23V00h+JJGajwoWJNlk="
-    },
-    "dtrace-provider": {
-      "version": "0.2.8",
-      "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz",
-      "integrity": "sha1-4kPxkhmqlfvw2PL/sH9b1k6U/iA=",
-      "dev": true,
-      "optional": true
-    },
-    "duplexify": {
-      "version": "3.7.1",
-      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
-      "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
-      "requires": {
-        "end-of-stream": "^1.0.0",
-        "inherits": "^2.0.1",
-        "readable-stream": "^2.0.0",
-        "stream-shift": "^1.0.0"
-      }
-    },
-    "ecc-jsbn": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
-      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
-      "requires": {
-        "jsbn": "~0.1.0",
-        "safer-buffer": "^2.1.0"
-      }
-    },
-    "ecdsa-sig-formatter": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
-      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
-      "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="
-    },
-    "emitter-listener": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
-      "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==",
-      "requires": {
-        "shimmer": "^1.2.0"
-      }
-    },
-    "encodeurl": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
-      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
-    },
-    "end-of-stream": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
-      "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=",
-      "requires": {
-        "once": "^1.4.0"
-      }
-    },
-    "ent": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
-      "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
-    },
-    "es6-promise": {
-      "version": "4.2.8",
-      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
-      "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
-    },
-    "es6-promisify": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
-      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
-      "requires": {
-        "es6-promise": "^4.0.3"
-      }
-    },
-    "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",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
-      "dev": true
-    },
-    "etag": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
-      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
-    },
-    "event-target-shim": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
-      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
-    },
-    "eventid": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/eventid/-/eventid-0.1.2.tgz",
-      "integrity": "sha1-CyMtPiROpbHVKJhBQOpprH7IkhU=",
-      "requires": {
-        "d64": "^1.0.0",
-        "uuid": "^3.0.1"
-      }
-    },
-    "express": {
-      "version": "4.16.4",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
-      "integrity": "sha1-/d72GSYQniTFFeqX/S8b2/Yt8S4=",
-      "requires": {
-        "accepts": "~1.3.5",
-        "array-flatten": "1.1.1",
-        "body-parser": "1.18.3",
-        "content-disposition": "0.5.2",
-        "content-type": "~1.0.4",
-        "cookie": "0.3.1",
-        "cookie-signature": "1.0.6",
-        "debug": "2.6.9",
-        "depd": "~1.1.2",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "finalhandler": "1.1.1",
-        "fresh": "0.5.2",
-        "merge-descriptors": "1.0.1",
-        "methods": "~1.1.2",
-        "on-finished": "~2.3.0",
-        "parseurl": "~1.3.2",
-        "path-to-regexp": "0.1.7",
-        "proxy-addr": "~2.0.4",
-        "qs": "6.5.2",
-        "range-parser": "~1.2.0",
-        "safe-buffer": "5.1.2",
-        "send": "0.16.2",
-        "serve-static": "1.13.2",
-        "setprototypeof": "1.1.0",
-        "statuses": "~1.4.0",
-        "type-is": "~1.6.16",
-        "utils-merge": "1.0.1",
-        "vary": "~1.1.2"
-      },
-      "dependencies": {
-        "statuses": {
-          "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
-          "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic="
-        }
-      }
-    },
-    "extend": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-      "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo="
-    },
-    "extsprintf": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
-    },
-    "fast-deep-equal": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
-    },
-    "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",
-      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
-    },
-    "fast-text-encoding": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz",
-      "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ=="
-    },
-    "file-uri-to-path": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
-      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
-    },
-    "finalhandler": {
-      "version": "1.1.1",
-      "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
-      "integrity": "sha1-7r9O2EAHnIP0JJA4ydcDAIMBsQU=",
-      "requires": {
-        "debug": "2.6.9",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "on-finished": "~2.3.0",
-        "parseurl": "~1.3.2",
-        "statuses": "~1.4.0",
-        "unpipe": "~1.0.0"
-      },
-      "dependencies": {
-        "statuses": {
-          "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
-          "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic="
-        }
-      }
-    },
-    "findit2": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz",
-      "integrity": "sha1-WKRmaX34piBc39vzlVNri9d3pfY="
-    },
-    "follow-redirects": {
-      "version": "1.5.10",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
-      "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
-      "requires": {
-        "debug": "=3.1.0"
-      },
-      "dependencies": {
-        "debug": {
-          "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"
-          }
-        }
-      }
-    },
-    "forever-agent": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
-    },
-    "form-data": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
-      "integrity": "sha1-3M5SwF9kTymManq5Nr1yTO/786Y=",
-      "requires": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.6",
-        "mime-types": "^2.1.12"
-      }
-    },
-    "forwarded": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
-      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
-    },
-    "fresh": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
-      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
-    },
-    "fs-constants": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
-      "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0="
-    },
-    "fs-extra": {
-      "version": "0.16.5",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz",
-      "integrity": "sha1-GtZh+myGyWCM0bSe/G/Og0k5p1A=",
-      "requires": {
-        "graceful-fs": "^3.0.5",
-        "jsonfile": "^2.0.0",
-        "rimraf": "^2.2.8"
-      }
-    },
-    "fs-minipass": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
-      "integrity": "sha1-BsJ3IYRU7CiN93raVKA7hwKqy50=",
-      "requires": {
-        "minipass": "^2.2.1"
-      }
-    },
-    "fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
-    },
-    "gauge": {
-      "version": "2.7.4",
-      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
-      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
-      "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"
-      }
-    },
-    "gaxios": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.1.0.tgz",
-      "integrity": "sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg==",
-      "requires": {
-        "abort-controller": "^3.0.0",
-        "extend": "^3.0.2",
-        "https-proxy-agent": "^3.0.0",
-        "is-stream": "^2.0.0",
-        "node-fetch": "^2.3.0"
-      }
-    },
-    "gcp-metadata": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.2.0.tgz",
-      "integrity": "sha512-ympv+yQ6k5QuWCuwQqnGEvFGS7MBKdcQdj1i188v3bW9QLFIchTGaBCEZxSQapT0jffdn1vdt8oJhB5VBWQO1Q==",
-      "requires": {
-        "gaxios": "^2.0.1",
-        "json-bigint": "^0.3.0"
-      }
-    },
-    "generic-pool": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz",
-      "integrity": "sha1-kv9xllINZwg5pnMICSoSqt8valk="
-    },
-    "getpass": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
-      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
-      "requires": {
-        "assert-plus": "^1.0.0"
-      }
-    },
-    "glob": {
-      "version": "7.1.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
-      "integrity": "sha1-OWCDLT8VdBCDQtr9OmezMsCWnfE=",
-      "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"
-      }
-    },
-    "google-auth-library": {
-      "version": "5.5.1",
-      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz",
-      "integrity": "sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg==",
-      "requires": {
-        "arrify": "^2.0.0",
-        "base64-js": "^1.3.0",
-        "fast-text-encoding": "^1.0.0",
-        "gaxios": "^2.1.0",
-        "gcp-metadata": "^3.2.0",
-        "gtoken": "^4.1.0",
-        "jws": "^3.1.5",
-        "lru-cache": "^5.0.0"
-      }
-    },
-    "google-gax": {
-      "version": "1.7.5",
-      "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.7.5.tgz",
-      "integrity": "sha512-Tz2DFs8umzDcCBTi2W1cY4vEgAKaYRj70g6Hh/MiiZaJizrly7PgyxsIYUGi7sOpEuAbARQymYKvy5mNi8hEbg==",
-      "requires": {
-        "@grpc/grpc-js": "0.6.9",
-        "@grpc/proto-loader": "^0.5.1",
-        "abort-controller": "^3.0.0",
-        "duplexify": "^3.6.0",
-        "google-auth-library": "^5.0.0",
-        "is-stream-ended": "^0.1.4",
-        "lodash.at": "^4.6.0",
-        "lodash.has": "^4.5.2",
-        "node-fetch": "^2.6.0",
-        "protobufjs": "^6.8.8",
-        "retry-request": "^4.0.0",
-        "semver": "^6.0.0",
-        "walkdir": "^0.4.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
-      }
-    },
-    "google-p12-pem": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.2.tgz",
-      "integrity": "sha512-UfnEARfJKI6pbmC1hfFFm+UAcZxeIwTiEcHfqKe/drMsXD/ilnVjF7zgOGpHXyhuvX6jNJK3S8A0hOQjwtFxEw==",
-      "requires": {
-        "node-forge": "^0.9.0"
-      }
-    },
-    "graceful-fs": {
-      "version": "3.0.11",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz",
-      "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=",
-      "requires": {
-        "natives": "^1.1.0"
-      }
-    },
-    "growl": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz",
-      "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto="
-    },
-    "gtoken": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.0.tgz",
-      "integrity": "sha512-wqyn2gf5buzEZN4QNmmiiW2i2JkEdZnL7Z/9p44RtZqgt4077m4khRgAYNuu8cBwHWCc6MsP6eDUn/KkF6jFIw==",
-      "requires": {
-        "gaxios": "^2.0.0",
-        "google-p12-pem": "^2.0.0",
-        "jws": "^3.1.5",
-        "mime": "^2.2.0"
-      },
-      "dependencies": {
-        "mime": {
-          "version": "2.4.4",
-          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
-          "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
-        }
-      }
-    },
-    "har-schema": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
-      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
-    },
-    "har-validator": {
-      "version": "5.1.3",
-      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
-      "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=",
-      "requires": {
-        "ajv": "^6.5.5",
-        "har-schema": "^2.0.0"
-      }
-    },
-    "has-flag": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
-      "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
-      "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="
-    },
-    "he": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
-      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
-      "dev": true
-    },
-    "heapdump": {
-      "version": "0.3.12",
-      "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.12.tgz",
-      "integrity": "sha1-ViO+eBaoqSqy1CsbQi+egppY2ok=",
-      "requires": {
-        "nan": "^2.11.1"
-      }
-    },
-    "hex2dec": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.2.tgz",
-      "integrity": "sha512-Yu+q/XWr2fFQ11tHxPq4p4EiNkb2y+lAacJNhAdRXVfRIcDH6gi7htWFnnlIzvqHMHoWeIsfXlNAjZInpAOJDA=="
-    },
-    "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"
-      }
-    },
-    "http-proxy-agent": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
-      "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
-      "requires": {
-        "agent-base": "4",
-        "debug": "3.1.0"
-      },
-      "dependencies": {
-        "debug": {
-          "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"
-          }
-        }
-      }
-    },
-    "http-signature": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
-      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
-      "requires": {
-        "assert-plus": "^1.0.0",
-        "jsprim": "^1.2.2",
-        "sshpk": "^1.7.0"
-      }
-    },
-    "https-proxy-agent": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz",
-      "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==",
-      "requires": {
-        "agent-base": "^4.3.0",
-        "debug": "^3.1.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.2.6",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-        }
-      }
-    },
-    "iconv-lite": {
-      "version": "0.4.23",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
-      "integrity": "sha1-KXhx9jvlB63Pv8pxXQzQ7thOmmM=",
-      "requires": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      }
-    },
-    "ignore-walk": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
-      "integrity": "sha1-qD5i59JyrA47VRqqgoMaGbafgvg=",
-      "requires": {
-        "minimatch": "^3.0.4"
-      }
-    },
-    "inflection": {
-      "version": "1.12.0",
-      "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz",
-      "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY="
-    },
-    "inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "requires": {
-        "once": "^1.3.0",
-        "wrappy": "1"
-      }
-    },
-    "inherits": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
-      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
-    },
-    "ini": {
-      "version": "1.3.5",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
-      "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc="
-    },
-    "ipaddr.js": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
-      "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
-    },
-    "is": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz",
-      "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg=="
-    },
-    "is-bluebird": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz",
-      "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI="
-    },
-    "is-buffer": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
-      "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
-    },
-    "is-fullwidth-code-point": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
-      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
-      "requires": {
-        "number-is-nan": "^1.0.0"
-      }
-    },
-    "is-obj": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
-      "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
-    },
-    "is-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
-      "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
-    },
-    "is-stream-ended": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz",
-      "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw=="
-    },
-    "is-typedarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
-    },
-    "isarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
-    },
-    "isstream": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
-      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
-    },
-    "jade": {
-      "version": "0.26.3",
-      "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
-      "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=",
-      "requires": {
-        "commander": "0.6.1",
-        "mkdirp": "0.3.0"
-      },
-      "dependencies": {
-        "commander": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz",
-          "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY="
-        },
-        "mkdirp": {
-          "version": "0.3.0",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
-          "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4="
-        }
-      }
-    },
-    "jsbn": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
-    },
-    "json-bigint": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz",
-      "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=",
-      "requires": {
-        "bignumber.js": "^7.0.0"
-      }
-    },
-    "json-schema": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
-      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
-    },
-    "json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA="
-    },
-    "json-stringify-safe": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
-    },
-    "jsonfile": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
-      "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
-      "requires": {
-        "graceful-fs": "^4.1.6"
-      },
-      "dependencies": {
-        "graceful-fs": {
-          "version": "4.1.15",
-          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
-          "integrity": "sha1-/7cD4QZuig7qpMi4C6klPu77+wA=",
-          "optional": true
-        }
-      }
-    },
-    "jsonparse": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
-      "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
-    },
-    "jsprim": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
-      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
-      "requires": {
-        "assert-plus": "1.0.0",
-        "extsprintf": "1.3.0",
-        "json-schema": "0.2.3",
-        "verror": "1.10.0"
-      }
-    },
-    "jwa": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
-      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
-      "requires": {
-        "buffer-equal-constant-time": "1.0.1",
-        "ecdsa-sig-formatter": "1.0.11",
-        "safe-buffer": "^5.0.1"
-      }
-    },
-    "jws": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
-      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
-      "requires": {
-        "jwa": "^1.4.1",
-        "safe-buffer": "^5.0.1"
-      }
-    },
-    "lockfile": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz",
-      "integrity": "sha1-B/gZ0lrkj4flOOZXi2lkpJgaVgk=",
-      "requires": {
-        "signal-exit": "^3.0.2"
-      }
-    },
-    "lodash": {
-      "version": "4.17.11",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-      "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40="
-    },
-    "lodash.at": {
-      "version": "4.6.0",
-      "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz",
-      "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g="
-    },
-    "lodash.camelcase": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
-      "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
-    },
-    "lodash.has": {
-      "version": "4.5.2",
-      "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz",
-      "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI="
-    },
-    "lodash.pickby": {
-      "version": "4.6.0",
-      "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
-      "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8="
-    },
-    "log-driver": {
-      "version": "1.2.7",
-      "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz",
-      "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg=="
-    },
-    "logger-sharelatex": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.9.0.tgz",
-      "integrity": "sha512-yVTuha82047IiMOQLgQHCZGKkJo6I2+2KtiFKpgkIooR2yZaoTEvAeoMwBesSDSpGUpvUJ/+9UI+PmRyc+PQKQ==",
-      "requires": {
-        "@google-cloud/logging-bunyan": "^2.0.0",
-        "@overleaf/o-error": "^2.0.0",
-        "bunyan": "1.8.12",
-        "raven": "1.1.3",
-        "request": "2.88.0",
-        "yn": "^3.1.1"
-      },
-      "dependencies": {
-        "bunyan": {
-          "version": "1.8.12",
-          "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
-          "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
-          "requires": {
-            "dtrace-provider": "~0.8",
-            "moment": "^2.10.6",
-            "mv": "~2",
-            "safe-json-stringify": "~1"
-          }
-        },
-        "dtrace-provider": {
-          "version": "0.8.8",
-          "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
-          "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
-          "optional": true,
-          "requires": {
-            "nan": "^2.14.0"
-          }
-        },
-        "nan": {
-          "version": "2.14.0",
-          "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
-          "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
-          "optional": true
-        }
-      }
-    },
-    "long": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
-      "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
-    },
-    "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==",
-      "requires": {
-        "yallist": "^3.0.2"
-      }
-    },
-    "lsmod": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz",
-      "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks="
-    },
-    "lynx": {
-      "version": "0.0.11",
-      "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz",
-      "integrity": "sha1-LPoU5EP9LZKlm3efQVZ84cxpZaM=",
-      "requires": {
-        "mersenne": "~0.0.3",
-        "statsd-parser": "~0.0.4"
-      }
-    },
-    "map-obj": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz",
-      "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g=="
-    },
-    "media-typer": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
-    },
-    "merge-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
-      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
-    },
-    "mersenne": {
-      "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz",
-      "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU="
-    },
-    "methods": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
-      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
-    },
-    "metrics-sharelatex": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.3.0.tgz",
-      "integrity": "sha512-qQv4UhI0Pn89WtIEUkysy4fgaCxmIw2S7+2pUB5b15Q4dzleHNplop5peTEOf8FIcURFshjPPJiLOGCJAYph7Q==",
-      "requires": {
-        "@google-cloud/debug-agent": "^3.0.0",
-        "@google-cloud/profiler": "^0.2.3",
-        "@google-cloud/trace-agent": "^3.2.0",
-        "coffee-script": "1.6.0",
-        "lynx": "~0.1.1",
-        "prom-client": "^11.1.3",
-        "underscore": "~1.6.0",
-        "yn": "^3.1.1"
-      },
-      "dependencies": {
-        "lynx": {
-          "version": "0.1.1",
-          "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz",
-          "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=",
-          "requires": {
-            "mersenne": "~0.0.3",
-            "statsd-parser": "~0.0.4"
-          }
-        },
-        "underscore": {
-          "version": "1.6.0",
-          "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
-          "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag="
-        }
-      }
-    },
-    "mime": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
-      "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY="
-    },
-    "mime-db": {
-      "version": "1.37.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
-      "integrity": "sha1-C2oM5v2+lXbiXx8tL96IMNwK0Ng="
-    },
-    "mime-types": {
-      "version": "2.1.21",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
-      "integrity": "sha1-KJlaoey3cHQv5q5+WPkYHHRLP5Y=",
-      "requires": {
-        "mime-db": "~1.37.0"
-      }
-    },
-    "minimatch": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-      "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
-      "requires": {
-        "brace-expansion": "^1.1.7"
-      }
-    },
-    "minimist": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
-      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
-    },
-    "minipass": {
-      "version": "2.3.5",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
-      "integrity": "sha1-ys6+SSAiSX9law8PUeJoKp7S2Eg=",
-      "requires": {
-        "safe-buffer": "^5.1.2",
-        "yallist": "^3.0.0"
-      }
-    },
-    "minizlib": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
-      "integrity": "sha1-3SfqYTYkPHyIBoToZyuzpF/ZthQ=",
-      "requires": {
-        "minipass": "^2.2.1"
-      }
-    },
-    "mkdirp": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
-      "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc="
-    },
-    "mocha": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz",
-      "integrity": "sha1-fYbPvPNcuCnidUwy4XNV7AUzh5Q=",
-      "dev": true,
-      "requires": {
-        "browser-stdout": "1.3.0",
-        "commander": "2.11.0",
-        "debug": "3.1.0",
-        "diff": "3.3.1",
-        "escape-string-regexp": "1.0.5",
-        "glob": "7.1.2",
-        "growl": "1.10.3",
-        "he": "1.1.1",
-        "mkdirp": "0.5.1",
-        "supports-color": "4.4.0"
-      },
-      "dependencies": {
-        "commander": {
-          "version": "2.11.0",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
-          "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=",
-          "dev": true
-        },
-        "debug": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-          "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=",
-          "dev": true,
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "diff": {
-          "version": "3.3.1",
-          "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz",
-          "integrity": "sha1-qoVnpu7QPFMfyJ0/cRzQ5SWd7HU=",
-          "dev": true
-        },
-        "glob": {
-          "version": "7.1.2",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
-          "dev": true,
-          "requires": {
-            "fs.realpath": "^1.0.0",
-            "inflight": "^1.0.4",
-            "inherits": "2",
-            "minimatch": "^3.0.4",
-            "once": "^1.3.0",
-            "path-is-absolute": "^1.0.0"
-          }
-        },
-        "growl": {
-          "version": "1.10.3",
-          "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
-          "integrity": "sha1-GSa6kM8+3+KttJJ/WIC8IsZseQ8=",
-          "dev": true
-        },
-        "mkdirp": {
-          "version": "0.5.1",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
-          "dev": true,
-          "requires": {
-            "minimist": "0.0.8"
-          }
-        }
-      }
-    },
-    "module-details-from-path": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
-      "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is="
-    },
-    "moment": {
-      "version": "2.23.0",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz",
-      "integrity": "sha1-dZ6kkayX1UusWtd2mW4qWMwbwiU="
-    },
-    "moment-timezone": {
-      "version": "0.5.23",
-      "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz",
-      "integrity": "sha1-fLsA2ywUxxsZMDy0ew+wpthlFGM=",
-      "requires": {
-        "moment": ">= 2.9.0"
-      }
-    },
-    "ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
-    },
-    "mv": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
-      "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
-      "optional": true,
-      "requires": {
-        "mkdirp": "~0.5.1",
-        "ncp": "~2.0.0",
-        "rimraf": "~2.4.0"
-      },
-      "dependencies": {
-        "glob": {
-          "version": "6.0.4",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
-          "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
-          "optional": true,
-          "requires": {
-            "inflight": "^1.0.4",
-            "inherits": "2",
-            "minimatch": "2 || 3",
-            "once": "^1.3.0",
-            "path-is-absolute": "^1.0.0"
-          }
-        },
-        "mkdirp": {
-          "version": "0.5.1",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
-          "optional": true,
-          "requires": {
-            "minimist": "0.0.8"
-          }
-        },
-        "rimraf": {
-          "version": "2.4.5",
-          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
-          "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
-          "optional": true,
-          "requires": {
-            "glob": "^6.0.1"
-          }
-        }
-      }
-    },
-    "mysql": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz",
-      "integrity": "sha1-k3Gd0yT1fUHET7bX+GDPjmOFk7A=",
-      "requires": {
-        "bignumber.js": "2.0.7",
-        "readable-stream": "~1.1.13",
-        "require-all": "~1.0.0"
-      },
-      "dependencies": {
-        "bignumber.js": {
-          "version": "2.0.7",
-          "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz",
-          "integrity": "sha1-husHB89qURCQnSPm6nQ0wU9QDxw="
-        },
-        "isarray": {
-          "version": "0.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
-          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
-        },
-        "readable-stream": {
-          "version": "1.1.14",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
-          "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
-          "requires": {
-            "core-util-is": "~1.0.0",
-            "inherits": "~2.0.1",
-            "isarray": "0.0.1",
-            "string_decoder": "~0.10.x"
-          }
-        },
-        "string_decoder": {
-          "version": "0.10.31",
-          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
-          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
-        }
-      }
-    },
-    "nan": {
-      "version": "2.12.0",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz",
-      "integrity": "sha1-nUQ/214TogdwzF5gLu5ZdgpoWIU="
-    },
-    "natives": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz",
-      "integrity": "sha1-pgO0pJirdxc2ErnqGs3sTZgPALs="
-    },
-    "ncp": {
-      "version": "2.0.0",
-      "resolved": "http://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
-      "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
-      "optional": true
-    },
-    "needle": {
-      "version": "2.2.4",
-      "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz",
-      "integrity": "sha1-UZMb/4JTOxkot9HWngHxsA/9Kk4=",
-      "requires": {
-        "debug": "^2.1.2",
-        "iconv-lite": "^0.4.4",
-        "sax": "^1.2.4"
-      }
-    },
-    "negotiator": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
-      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
-    },
-    "node-fetch": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
-      "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
-    },
-    "node-forge": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz",
-      "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ=="
-    },
-    "node-pre-gyp": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
-      "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
-      "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"
-      },
-      "dependencies": {
-        "mkdirp": {
-          "version": "0.5.1",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
-          "requires": {
-            "minimist": "0.0.8"
-          }
-        }
-      }
-    },
-    "nopt": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
-      "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
-      "requires": {
-        "abbrev": "1",
-        "osenv": "^0.1.4"
-      }
-    },
-    "npm-bundled": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz",
-      "integrity": "sha1-PBcyt7qTazoQMlrvYWRnwMy8yXk="
-    },
-    "npm-packlist": {
-      "version": "1.1.12",
-      "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz",
-      "integrity": "sha1-Ir3i68EucspIKr1nr8UetJN3JDo=",
-      "requires": {
-        "ignore-walk": "^3.0.1",
-        "npm-bundled": "^1.0.1"
-      }
-    },
-    "npmlog": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
-      "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=",
-      "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",
-      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
-      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
-    },
-    "oauth-sign": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
-      "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU="
-    },
-    "object-assign": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
-    },
-    "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",
-      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "requires": {
-        "wrappy": "1"
-      }
-    },
-    "os-homedir": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
-      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
-    },
-    "os-tmpdir": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
-      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
-    },
-    "osenv": {
-      "version": "0.1.5",
-      "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
-      "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=",
-      "requires": {
-        "os-homedir": "^1.0.0",
-        "os-tmpdir": "^1.0.0"
-      }
-    },
-    "p-limit": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
-      "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
-      "requires": {
-        "p-try": "^2.0.0"
-      }
-    },
-    "p-try": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
-    },
-    "parse-duration": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz",
-      "integrity": "sha1-ExFN3JiRwezSgANiRFVN5DZHoiY="
-    },
-    "parse-ms": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
-      "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="
-    },
-    "parseurl": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
-      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
-    },
-    "path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
-    },
-    "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=="
-    },
-    "path-to-regexp": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
-      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
-    },
-    "performance-now": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
-    },
-    "pify": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
-      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
-    },
-    "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==",
-      "requires": {
-        "parse-ms": "^2.0.0"
-      }
-    },
-    "process-nextick-args": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
-      "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o="
-    },
-    "prom-client": {
-      "version": "11.5.3",
-      "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.3.tgz",
-      "integrity": "sha512-iz22FmTbtkyL2vt0MdDFY+kWof+S9UB/NACxSn2aJcewtw+EERsen0urSkZ2WrHseNdydsvcxCTAnPcSMZZv4Q==",
-      "requires": {
-        "tdigest": "^0.1.1"
-      }
-    },
-    "protobufjs": {
-      "version": "6.8.8",
-      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz",
-      "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==",
-      "requires": {
-        "@protobufjs/aspromise": "^1.1.2",
-        "@protobufjs/base64": "^1.1.2",
-        "@protobufjs/codegen": "^2.0.4",
-        "@protobufjs/eventemitter": "^1.1.0",
-        "@protobufjs/fetch": "^1.1.0",
-        "@protobufjs/float": "^1.0.2",
-        "@protobufjs/inquire": "^1.1.0",
-        "@protobufjs/path": "^1.1.2",
-        "@protobufjs/pool": "^1.1.0",
-        "@protobufjs/utf8": "^1.1.0",
-        "@types/long": "^4.0.0",
-        "@types/node": "^10.1.0",
-        "long": "^4.0.0"
-      }
-    },
-    "proxy-addr": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
-      "integrity": "sha1-7PxzO/Iv+Mb0B/onUye5q2fki5M=",
-      "requires": {
-        "forwarded": "~0.1.2",
-        "ipaddr.js": "1.8.0"
-      }
-    },
-    "psl": {
-      "version": "1.1.31",
-      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
-      "integrity": "sha1-6aqG0BAbWxBcvpOsa3hM1UcnYYQ="
-    },
-    "pump": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz",
-      "integrity": "sha1-Xf6DEcM7v2/BgmH580cCxHwIqVQ=",
-      "requires": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "pumpify": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz",
-      "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==",
-      "requires": {
-        "duplexify": "^4.1.1",
-        "inherits": "^2.0.3",
-        "pump": "^3.0.0"
-      },
-      "dependencies": {
-        "duplexify": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz",
-          "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==",
-          "requires": {
-            "end-of-stream": "^1.4.1",
-            "inherits": "^2.0.3",
-            "readable-stream": "^3.1.1",
-            "stream-shift": "^1.0.0"
-          }
-        },
-        "pump": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-          "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-          "requires": {
-            "end-of-stream": "^1.1.0",
-            "once": "^1.3.1"
-          }
-        },
-        "readable-stream": {
-          "version": "3.4.0",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
-          "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
-          "requires": {
-            "inherits": "^2.0.3",
-            "string_decoder": "^1.1.1",
-            "util-deprecate": "^1.0.1"
-          }
-        }
-      }
-    },
-    "punycode": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-      "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew="
-    },
-    "qs": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-      "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY="
-    },
-    "range-parser": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
-      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
-    },
-    "raven": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/raven/-/raven-1.1.3.tgz",
-      "integrity": "sha1-QnPBrm005CMPUbLAEEGjK5Iygio=",
-      "requires": {
-        "cookie": "0.3.1",
-        "json-stringify-safe": "5.0.1",
-        "lsmod": "1.0.0",
-        "stack-trace": "0.0.9",
-        "uuid": "3.0.0"
-      },
-      "dependencies": {
-        "uuid": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz",
-          "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg="
-        }
-      }
-    },
-    "raw-body": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
-      "integrity": "sha1-GzJOzmtXBuFThVvBFIxlu39uoMM=",
-      "requires": {
-        "bytes": "3.0.0",
-        "http-errors": "1.6.3",
-        "iconv-lite": "0.4.23",
-        "unpipe": "1.0.0"
-      }
-    },
-    "rc": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
-      "integrity": "sha1-zZJL9SAKB1uDwYjNa54hG3/A0+0=",
-      "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",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
-          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
-        }
-      }
-    },
-    "readable-stream": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
-      "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=",
-      "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"
-      }
-    },
-    "request": {
-      "version": "2.88.0",
-      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
-      "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=",
-      "requires": {
-        "aws-sign2": "~0.7.0",
-        "aws4": "^1.8.0",
-        "caseless": "~0.12.0",
-        "combined-stream": "~1.0.6",
-        "extend": "~3.0.2",
-        "forever-agent": "~0.6.1",
-        "form-data": "~2.3.2",
-        "har-validator": "~5.1.0",
-        "http-signature": "~1.2.0",
-        "is-typedarray": "~1.0.0",
-        "isstream": "~0.1.2",
-        "json-stringify-safe": "~5.0.1",
-        "mime-types": "~2.1.19",
-        "oauth-sign": "~0.9.0",
-        "performance-now": "^2.1.0",
-        "qs": "~6.5.2",
-        "safe-buffer": "^5.1.2",
-        "tough-cookie": "~2.4.3",
-        "tunnel-agent": "^0.6.0",
-        "uuid": "^3.3.2"
-      },
-      "dependencies": {
-        "uuid": {
-          "version": "3.3.2",
-          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
-          "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE="
-        }
-      }
-    },
-    "require-all": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz",
-      "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic="
-    },
-    "require-in-the-middle": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-4.0.1.tgz",
-      "integrity": "sha512-EfkM2zANyGkrfIExsECMeNn/uzjvHrE9h36yLXSavmrDiH4tgDNvltAmEKnt4PNLbqKPHZz+uszW2wTKrLUX0w==",
-      "requires": {
-        "debug": "^4.1.1",
-        "module-details-from-path": "^1.0.3",
-        "resolve": "^1.12.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-        }
-      }
-    },
-    "require-like": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
-      "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=",
-      "dev": true
-    },
-    "resolve": {
-      "version": "1.12.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
-      "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==",
-      "requires": {
-        "path-parse": "^1.0.6"
-      }
-    },
-    "retry-as-promised": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.3.2.tgz",
-      "integrity": "sha1-zZdO5P2bX+A8vzGHHuSCIcB3N7c=",
-      "requires": {
-        "bluebird": "^3.4.6",
-        "debug": "^2.6.9"
-      }
-    },
-    "retry-axios": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz",
-      "integrity": "sha512-jp4YlI0qyDFfXiXGhkCOliBN1G7fRH03Nqy8YdShzGqbY5/9S2x/IR6C88ls2DFkbWuL3ASkP7QD3pVrNpPgwQ=="
-    },
-    "retry-request": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz",
-      "integrity": "sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ==",
-      "requires": {
-        "debug": "^4.1.1",
-        "through2": "^3.0.1"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-        }
-      }
-    },
-    "rimraf": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
-      "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=",
-      "requires": {
-        "glob": "^7.0.5"
-      }
-    },
-    "safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0="
-    },
-    "safe-json-stringify": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
-      "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
-      "optional": true
-    },
-    "safer-buffer": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo="
-    },
-    "sandboxed-module": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz",
-      "integrity": "sha1-8fvvvYCaT2kHO9B8rm/H2y6vX2o=",
-      "dev": true,
-      "requires": {
-        "require-like": "0.1.2",
-        "stack-trace": "0.0.6"
-      },
-      "dependencies": {
-        "stack-trace": {
-          "version": "0.0.6",
-          "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz",
-          "integrity": "sha1-HnGb1qJin/CcGJ4Xqe+QKpT8XbA=",
-          "dev": true
-        }
-      }
-    },
-    "sax": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
-      "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk="
-    },
-    "semver": {
-      "version": "5.6.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
-      "integrity": "sha1-fnQlb7qknHWqfHogXMInmcrIAAQ="
-    },
-    "send": {
-      "version": "0.16.2",
-      "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
-      "integrity": "sha1-bsyh4PjBVtFBWXVZhI32RzCmu8E=",
-      "requires": {
-        "debug": "2.6.9",
-        "depd": "~1.1.2",
-        "destroy": "~1.0.4",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "fresh": "0.5.2",
-        "http-errors": "~1.6.2",
-        "mime": "1.4.1",
-        "ms": "2.0.0",
-        "on-finished": "~2.3.0",
-        "range-parser": "~1.2.0",
-        "statuses": "~1.4.0"
-      },
-      "dependencies": {
-        "statuses": {
-          "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
-          "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic="
-        }
-      }
-    },
-    "sequelize": {
-      "version": "4.42.0",
-      "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.42.0.tgz",
-      "integrity": "sha1-Q5Rnunv+fVr8xW1is+CRhg+/GPM=",
-      "requires": {
-        "bluebird": "^3.5.0",
-        "cls-bluebird": "^2.1.0",
-        "debug": "^3.1.0",
-        "depd": "^1.1.0",
-        "dottie": "^2.0.0",
-        "generic-pool": "^3.4.0",
-        "inflection": "1.12.0",
-        "lodash": "^4.17.1",
-        "moment": "^2.20.0",
-        "moment-timezone": "^0.5.14",
-        "retry-as-promised": "^2.3.2",
-        "semver": "^5.5.0",
-        "terraformer-wkt-parser": "^1.1.2",
-        "toposort-class": "^1.0.1",
-        "uuid": "^3.2.1",
-        "validator": "^10.4.0",
-        "wkx": "^0.4.1"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.2.6",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-          "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=",
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "ms": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
-          "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo="
-        },
-        "uuid": {
-          "version": "3.3.2",
-          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
-          "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE="
-        }
-      }
-    },
-    "serve-static": {
-      "version": "1.13.2",
-      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
-      "integrity": "sha1-CV6Ecv1bRiN9tQzkhqQ/S4bGzsE=",
-      "requires": {
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "parseurl": "~1.3.2",
-        "send": "0.16.2"
-      }
-    },
-    "set-blocking": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
-    },
-    "setprototypeof": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
-      "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY="
-    },
-    "settings-sharelatex": {
-      "version": "git+https://github.com/sharelatex/settings-sharelatex.git#93f63d029b52fef8825c3a401b2b6a7ba29b4750",
-      "from": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0",
-      "requires": {
-        "coffee-script": "1.6.0"
-      }
-    },
-    "shimmer": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz",
-      "integrity": "sha1-+Wb3VVeJdj502IQRk2haXnhzZmU="
-    },
-    "sigmund": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
-      "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA="
-    },
-    "signal-exit": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
-      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
-    },
-    "sinon": {
-      "version": "1.7.3",
-      "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz",
-      "integrity": "sha1-emnWnNApRYbHQyVO7/G1g6UJl/I=",
-      "dev": true,
-      "requires": {
-        "buster-format": "~0.5"
-      }
-    },
-    "smoke-test-sharelatex": {
-      "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c",
-      "from": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0",
-      "requires": {
-        "mocha": "~1.17.0"
-      },
-      "dependencies": {
-        "glob": {
-          "version": "3.2.3",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz",
-          "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=",
-          "requires": {
-            "graceful-fs": "~2.0.0",
-            "inherits": "2",
-            "minimatch": "~0.2.11"
-          }
-        },
-        "graceful-fs": {
-          "version": "2.0.3",
-          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz",
-          "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA="
-        },
-        "lru-cache": {
-          "version": "2.7.3",
-          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
-          "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI="
-        },
-        "minimatch": {
-          "version": "0.2.14",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
-          "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=",
-          "requires": {
-            "lru-cache": "2",
-            "sigmund": "~1.0.0"
-          }
-        },
-        "mocha": {
-          "version": "1.17.1",
-          "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz",
-          "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=",
-          "requires": {
-            "commander": "2.0.0",
-            "debug": "*",
-            "diff": "1.0.7",
-            "glob": "3.2.3",
-            "growl": "1.7.x",
-            "jade": "0.26.3",
-            "mkdirp": "0.3.5"
-          }
-        }
-      }
-    },
-    "snakecase-keys": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.1.0.tgz",
-      "integrity": "sha512-QM038drLbhdOY5HcRQVjO1ZJ1WR7yV5D5TIBzcOB/g3f5HURHhfpYEnvOyzXet8K+MQsgeIUA7O7vn90nAX6EA==",
-      "requires": {
-        "map-obj": "^4.0.0",
-        "to-snake-case": "^1.0.0"
-      }
-    },
-    "source-map": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
-    },
-    "split": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
-      "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
-      "requires": {
-        "through": "2"
-      }
-    },
-    "split-ca": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
-      "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY="
-    },
-    "sqlite3": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.6.tgz",
-      "integrity": "sha512-EqBXxHdKiwvNMRCgml86VTL5TK1i0IKiumnfxykX0gh6H6jaKijAXvE9O1N7+omfNSawR2fOmIyJZcfe8HYWpw==",
-      "requires": {
-        "nan": "~2.10.0",
-        "node-pre-gyp": "^0.11.0",
-        "request": "^2.87.0"
-      },
-      "dependencies": {
-        "nan": {
-          "version": "2.10.0",
-          "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
-          "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
-        }
-      }
-    },
-    "sshpk": {
-      "version": "1.15.2",
-      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz",
-      "integrity": "sha1-yUbWvZsaOdDoY1dj9SQtbtbctik=",
-      "requires": {
-        "asn1": "~0.2.3",
-        "assert-plus": "^1.0.0",
-        "bcrypt-pbkdf": "^1.0.0",
-        "dashdash": "^1.12.0",
-        "ecc-jsbn": "~0.1.1",
-        "getpass": "^0.1.1",
-        "jsbn": "~0.1.0",
-        "safer-buffer": "^2.0.2",
-        "tweetnacl": "~0.14.0"
-      }
-    },
-    "stack-trace": {
-      "version": "0.0.9",
-      "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz",
-      "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU="
-    },
-    "statsd-parser": {
-      "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz",
-      "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0="
-    },
-    "statuses": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
-      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
-    },
-    "stream-events": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
-      "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
-      "requires": {
-        "stubs": "^3.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="
-    },
-    "string-width": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
-      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
-      "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",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
-      "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
-      "requires": {
-        "safe-buffer": "~5.1.0"
-      }
-    },
-    "strip-ansi": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
-      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
-      "requires": {
-        "ansi-regex": "^2.0.0"
-      }
-    },
-    "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="
-    },
-    "stubs": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
-      "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
-    },
-    "supports-color": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
-      "integrity": "sha1-iD992rwWUUKyphQn8zUt7RldGj4=",
-      "dev": true,
-      "requires": {
-        "has-flag": "^2.0.0"
-      }
-    },
-    "tar": {
-      "version": "4.4.8",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz",
-      "integrity": "sha1-sZ7sP94qluZGZt+f20DFyhvDdH0=",
-      "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"
-      },
-      "dependencies": {
-        "mkdirp": {
-          "version": "0.5.1",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
-          "requires": {
-            "minimist": "0.0.8"
-          }
-        }
-      }
-    },
-    "tar-fs": {
-      "version": "1.16.3",
-      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz",
-      "integrity": "sha1-lmpiiEHaLEAQQGqCFny9Xgxy1Qk=",
-      "requires": {
-        "chownr": "^1.0.1",
-        "mkdirp": "^0.5.1",
-        "pump": "^1.0.0",
-        "tar-stream": "^1.1.2"
-      },
-      "dependencies": {
-        "mkdirp": {
-          "version": "0.5.1",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
-          "requires": {
-            "minimist": "0.0.8"
-          }
-        }
-      }
-    },
-    "tar-stream": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
-      "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=",
-      "requires": {
-        "bl": "^1.0.0",
-        "buffer-alloc": "^1.2.0",
-        "end-of-stream": "^1.0.0",
-        "fs-constants": "^1.0.0",
-        "readable-stream": "^2.3.0",
-        "to-buffer": "^1.1.1",
-        "xtend": "^4.0.0"
-      }
-    },
-    "tdigest": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz",
-      "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=",
-      "requires": {
-        "bintrees": "1.0.1"
-      }
-    },
-    "teeny-request": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.3.0.tgz",
-      "integrity": "sha512-sN9E3JvEBe2CFqB/jpJpw1erWD1C7MxyYCxogHFCQSyZfkHYcdf4wzVQSw7FZxbwcfnS+FP0W9BS0mp6SEOKjg==",
-      "requires": {
-        "http-proxy-agent": "^2.1.0",
-        "https-proxy-agent": "^3.0.0",
-        "node-fetch": "^2.2.0",
-        "stream-events": "^1.0.5",
-        "uuid": "^3.3.2"
-      }
-    },
-    "terraformer": {
-      "version": "1.0.9",
-      "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.9.tgz",
-      "integrity": "sha1-d4Uf70pJyQs0XcU88mgJ/fKdzaY=",
-      "requires": {
-        "@types/geojson": "^1.0.0"
-      }
-    },
-    "terraformer-wkt-parser": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.0.tgz",
-      "integrity": "sha1-ydasPf8l9MC9NE6WH0JpSWGDTDQ=",
-      "requires": {
-        "@types/geojson": "^1.0.0",
-        "terraformer": "~1.0.5"
-      }
-    },
-    "through": {
-      "version": "2.3.8",
-      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
-      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
-    },
-    "through2": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz",
-      "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==",
-      "requires": {
-        "readable-stream": "2 || 3"
-      }
-    },
-    "timekeeper": {
-      "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz",
-      "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=",
-      "dev": true
-    },
-    "to-buffer": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
-      "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA="
-    },
-    "to-no-case": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz",
-      "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo="
-    },
-    "to-snake-case": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/to-snake-case/-/to-snake-case-1.0.0.tgz",
-      "integrity": "sha1-znRpE4l5RgGah+Yu366upMYIq4w=",
-      "requires": {
-        "to-space-case": "^1.0.0"
-      }
-    },
-    "to-space-case": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz",
-      "integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=",
-      "requires": {
-        "to-no-case": "^1.0.0"
-      }
-    },
-    "toposort-class": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
-      "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg="
-    },
-    "tough-cookie": {
-      "version": "2.4.3",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
-      "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=",
-      "requires": {
-        "psl": "^1.1.24",
-        "punycode": "^1.4.1"
-      },
-      "dependencies": {
-        "punycode": {
-          "version": "1.4.1",
-          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
-          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
-        }
-      }
-    },
-    "tunnel-agent": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
-      "requires": {
-        "safe-buffer": "^5.0.1"
-      }
-    },
-    "tweetnacl": {
-      "version": "0.14.5",
-      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
-    },
-    "type-detect": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz",
-      "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=",
-      "dev": true
-    },
-    "type-fest": {
-      "version": "0.8.1",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
-      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="
-    },
-    "type-is": {
-      "version": "1.6.16",
-      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
-      "integrity": "sha1-+JzjQVQcZysl7nrjxz3uOyvlAZQ=",
-      "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="
-    },
-    "underscore": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz",
-      "integrity": "sha1-BtzjSg5op7q8KbNluOdLiSUgOWE="
-    },
-    "unpipe": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
-      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
-    },
-    "uri-js": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
-      "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=",
-      "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="
-    },
-    "utils-merge": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
-      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
-    },
-    "uuid": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
-      "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
-    },
-    "v8-profiler-node8": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/v8-profiler-node8/-/v8-profiler-node8-6.0.1.tgz",
-      "integrity": "sha512-DD7L0c/2KeFFQxs20VaFPHq/CSintttW4YB+QJdJ5ZohxOegbsMCvnZKHRHVA9UZfVYqq6ZsLaywe74+3JpZjw==",
-      "requires": {
-        "nan": "^2.5.1",
-        "node-pre-gyp": "^0.11.0"
-      },
-      "dependencies": {
-        "mkdirp": {
-          "version": "0.5.1",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
-          "requires": {
-            "minimist": "0.0.8"
-          }
-        },
-        "node-pre-gyp": {
-          "version": "0.11.0",
-          "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
-          "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
-          "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"
-          }
-        }
-      }
-    },
-    "validator": {
-      "version": "10.9.0",
-      "resolved": "https://registry.npmjs.org/validator/-/validator-10.9.0.tgz",
-      "integrity": "sha1-0QwRZztQYft8z0wRFEEkEbK6wqg="
-    },
-    "vary": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
-      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
-    },
-    "verror": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
-      "requires": {
-        "assert-plus": "^1.0.0",
-        "core-util-is": "1.0.2",
-        "extsprintf": "^1.2.0"
-      }
-    },
-    "walkdir": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz",
-      "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ=="
-    },
-    "wide-align": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
-      "integrity": "sha1-rgdOa9wMFKQx6ATmJFScYzsABFc=",
-      "requires": {
-        "string-width": "^1.0.2 || 2"
-      }
-    },
-    "wkx": {
-      "version": "0.4.6",
-      "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.6.tgz",
-      "integrity": "sha1-Ioq1kuZFc4Lqb7efyCUFjQf85SM=",
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
-    },
-    "wrench": {
-      "version": "1.5.9",
-      "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz",
-      "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo="
-    },
-    "xtend": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
-      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
-    },
-    "yallist": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
-      "integrity": "sha1-tLBJ4xS+VF486AIjbWzSLNkcPek="
-    },
-    "yn": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
-      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
-    }
-  }
-}

+ 7179 - 0
services/clsi/package-lock.json

@@ -0,0 +1,7179 @@
+{
+  "name": "node-clsi",
+  "version": "0.1.4",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@babel/code-frame": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
+      "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
+      "dev": true,
+      "requires": {
+        "@babel/highlight": "^7.8.3"
+      }
+    },
+    "@babel/generator": {
+      "version": "7.8.7",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz",
+      "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.7",
+        "jsesc": "^2.5.1",
+        "lodash": "^4.17.13",
+        "source-map": "^0.5.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
+      }
+    },
+    "@babel/helper-function-name": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
+      "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-get-function-arity": "^7.8.3",
+        "@babel/template": "^7.8.3",
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-get-function-arity": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
+      "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-split-export-declaration": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz",
+      "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
+      "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.0.0",
+        "esutils": "^2.0.2",
+        "js-tokens": "^4.0.0"
+      }
+    },
+    "@babel/parser": {
+      "version": "7.8.7",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz",
+      "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==",
+      "dev": true
+    },
+    "@babel/runtime": {
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz",
+      "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==",
+      "dev": true,
+      "requires": {
+        "regenerator-runtime": "^0.13.2"
+      }
+    },
+    "@babel/runtime-corejs3": {
+      "version": "7.8.7",
+      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.8.7.tgz",
+      "integrity": "sha512-sc7A+H4I8kTd7S61dgB9RomXu/C+F4IrRr4Ytze4dnfx7AXEpCrejSNpjx7vq6y/Bak9S6Kbk65a/WgMLtg43Q==",
+      "dev": true,
+      "requires": {
+        "core-js-pure": "^3.0.0",
+        "regenerator-runtime": "^0.13.4"
+      },
+      "dependencies": {
+        "regenerator-runtime": {
+          "version": "0.13.4",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz",
+          "integrity": "sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/template": {
+      "version": "7.8.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz",
+      "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@babel/parser": "^7.8.6",
+        "@babel/types": "^7.8.6"
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.8.6",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz",
+      "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@babel/generator": "^7.8.6",
+        "@babel/helper-function-name": "^7.8.3",
+        "@babel/helper-split-export-declaration": "^7.8.3",
+        "@babel/parser": "^7.8.6",
+        "@babel/types": "^7.8.6",
+        "debug": "^4.1.0",
+        "globals": "^11.1.0",
+        "lodash": "^4.17.13"
+      },
+      "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"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/types": {
+      "version": "7.8.7",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz",
+      "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.13",
+        "to-fast-properties": "^2.0.0"
+      }
+    },
+    "@google-cloud/common": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.2.3.tgz",
+      "integrity": "sha512-lvw54mGKn8VqVIy2NzAk0l5fntBFX4UwQhHk6HaqkyCQ7WBl5oz4XhzKMtMilozF/3ObPcDogqwuyEWyZ6rnQQ==",
+      "requires": {
+        "@google-cloud/projectify": "^1.0.0",
+        "@google-cloud/promisify": "^1.0.0",
+        "arrify": "^2.0.0",
+        "duplexify": "^3.6.0",
+        "ent": "^2.2.0",
+        "extend": "^3.0.2",
+        "google-auth-library": "^5.5.0",
+        "retry-request": "^4.0.0",
+        "teeny-request": "^5.2.1"
+      }
+    },
+    "@google-cloud/debug-agent": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-3.2.0.tgz",
+      "integrity": "sha512-fP87kYbS6aeDna08BivwQ1J260mwJGchRi99XdWCgqbRwuFac8ul0OT5i2wEeDSc5QaDX8ZuWQQ0igZvh1rTyQ==",
+      "requires": {
+        "@google-cloud/common": "^0.32.0",
+        "@sindresorhus/is": "^0.15.0",
+        "acorn": "^6.0.0",
+        "coffeescript": "^2.0.0",
+        "console-log-level": "^1.4.0",
+        "extend": "^3.0.1",
+        "findit2": "^2.2.3",
+        "gcp-metadata": "^1.0.0",
+        "lodash.pickby": "^4.6.0",
+        "p-limit": "^2.2.0",
+        "pify": "^4.0.1",
+        "semver": "^6.0.0",
+        "source-map": "^0.6.1",
+        "split": "^1.0.0"
+      },
+      "dependencies": {
+        "@google-cloud/common": {
+          "version": "0.32.1",
+          "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz",
+          "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==",
+          "requires": {
+            "@google-cloud/projectify": "^0.3.3",
+            "@google-cloud/promisify": "^0.4.0",
+            "@types/request": "^2.48.1",
+            "arrify": "^2.0.0",
+            "duplexify": "^3.6.0",
+            "ent": "^2.2.0",
+            "extend": "^3.0.2",
+            "google-auth-library": "^3.1.1",
+            "pify": "^4.0.1",
+            "retry-request": "^4.0.0",
+            "teeny-request": "^3.11.3"
+          }
+        },
+        "@google-cloud/projectify": {
+          "version": "0.3.3",
+          "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz",
+          "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw=="
+        },
+        "@google-cloud/promisify": {
+          "version": "0.4.0",
+          "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz",
+          "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q=="
+        },
+        "coffeescript": {
+          "version": "2.5.1",
+          "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz",
+          "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ=="
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "gaxios": {
+          "version": "1.8.4",
+          "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz",
+          "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==",
+          "requires": {
+            "abort-controller": "^3.0.0",
+            "extend": "^3.0.2",
+            "https-proxy-agent": "^2.2.1",
+            "node-fetch": "^2.3.0"
+          }
+        },
+        "gcp-metadata": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz",
+          "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==",
+          "requires": {
+            "gaxios": "^1.0.2",
+            "json-bigint": "^0.3.0"
+          }
+        },
+        "google-auth-library": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz",
+          "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==",
+          "requires": {
+            "base64-js": "^1.3.0",
+            "fast-text-encoding": "^1.0.0",
+            "gaxios": "^1.2.1",
+            "gcp-metadata": "^1.0.0",
+            "gtoken": "^2.3.2",
+            "https-proxy-agent": "^2.2.1",
+            "jws": "^3.1.5",
+            "lru-cache": "^5.0.0",
+            "semver": "^5.5.0"
+          },
+          "dependencies": {
+            "semver": {
+              "version": "5.7.1",
+              "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+              "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+            }
+          }
+        },
+        "google-p12-pem": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz",
+          "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==",
+          "requires": {
+            "node-forge": "^0.8.0",
+            "pify": "^4.0.0"
+          }
+        },
+        "gtoken": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz",
+          "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==",
+          "requires": {
+            "gaxios": "^1.0.4",
+            "google-p12-pem": "^1.0.0",
+            "jws": "^3.1.5",
+            "mime": "^2.2.0",
+            "pify": "^4.0.0"
+          }
+        },
+        "https-proxy-agent": {
+          "version": "2.2.4",
+          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
+          "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
+          "requires": {
+            "agent-base": "^4.3.0",
+            "debug": "^3.1.0"
+          }
+        },
+        "mime": {
+          "version": "2.4.4",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
+          "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "node-forge": {
+          "version": "0.8.5",
+          "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz",
+          "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q=="
+        },
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+        },
+        "teeny-request": {
+          "version": "3.11.3",
+          "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz",
+          "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==",
+          "requires": {
+            "https-proxy-agent": "^2.2.1",
+            "node-fetch": "^2.2.0",
+            "uuid": "^3.3.2"
+          }
+        }
+      }
+    },
+    "@google-cloud/logging": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-5.5.3.tgz",
+      "integrity": "sha512-TZ/DzHod4icaC7wEEBm0PHYfbhvg0CbCVzKLsdAwj11xSD/egGNOsG5optEQcbAQEPrO1B5xBXfsE0wIBBYjpQ==",
+      "requires": {
+        "@google-cloud/common": "^2.2.2",
+        "@google-cloud/paginator": "^2.0.0",
+        "@google-cloud/projectify": "^1.0.0",
+        "@google-cloud/promisify": "^1.0.0",
+        "@opencensus/propagation-stackdriver": "0.0.18",
+        "arrify": "^2.0.0",
+        "dot-prop": "^5.1.0",
+        "eventid": "^0.1.2",
+        "extend": "^3.0.2",
+        "gcp-metadata": "^3.1.0",
+        "google-gax": "^1.7.5",
+        "is": "^3.3.0",
+        "on-finished": "^2.3.0",
+        "protobufjs": "^6.8.8",
+        "pumpify": "^2.0.0",
+        "snakecase-keys": "^3.0.0",
+        "stream-events": "^1.0.4",
+        "through2": "^3.0.0",
+        "type-fest": "^0.8.0"
+      }
+    },
+    "@google-cloud/logging-bunyan": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@google-cloud/logging-bunyan/-/logging-bunyan-2.0.0.tgz",
+      "integrity": "sha512-9W9B8GQNMlBdQSV+c0492+sMMknn4/428EdSO1xv5Hn07P32N/e4T25y4Gnl9jlrItuZHIXRwYPVSHqUyGb1Zg==",
+      "requires": {
+        "@google-cloud/logging": "^5.5.2",
+        "google-auth-library": "^5.0.0"
+      }
+    },
+    "@google-cloud/paginator": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.1.tgz",
+      "integrity": "sha512-HZ6UTGY/gHGNriD7OCikYWL/Eu0sTEur2qqse2w6OVsz+57se3nTkqH14JIPxtf0vlEJ8IJN5w3BdZ22pjCB8g==",
+      "requires": {
+        "arrify": "^2.0.0",
+        "extend": "^3.0.2"
+      }
+    },
+    "@google-cloud/profiler": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-0.2.3.tgz",
+      "integrity": "sha512-rNvtrFtIebIxZEJ/O0t8n7HciZGIXBo8DvHxWqAmsCaeLvkTtsaL6HmPkwxrNQ1IhbYWAxF+E/DwCiHyhKmgTg==",
+      "requires": {
+        "@google-cloud/common": "^0.26.0",
+        "@types/console-log-level": "^1.4.0",
+        "@types/semver": "^5.5.0",
+        "bindings": "^1.2.1",
+        "console-log-level": "^1.4.0",
+        "delay": "^4.0.1",
+        "extend": "^3.0.1",
+        "gcp-metadata": "^0.9.0",
+        "nan": "^2.11.1",
+        "parse-duration": "^0.1.1",
+        "pify": "^4.0.0",
+        "pretty-ms": "^4.0.0",
+        "protobufjs": "~6.8.6",
+        "semver": "^5.5.0",
+        "teeny-request": "^3.3.0"
+      },
+      "dependencies": {
+        "@google-cloud/common": {
+          "version": "0.26.2",
+          "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.26.2.tgz",
+          "integrity": "sha512-xJ2M/q3MrUbnYZuFlpF01caAlEhAUoRn0NXp93Hn3pkFpfSOG8YfbKbpBAHvcKVbBOAKVIwPsleNtuyuabUwLQ==",
+          "requires": {
+            "@google-cloud/projectify": "^0.3.2",
+            "@google-cloud/promisify": "^0.3.0",
+            "@types/duplexify": "^3.5.0",
+            "@types/request": "^2.47.0",
+            "arrify": "^1.0.1",
+            "duplexify": "^3.6.0",
+            "ent": "^2.2.0",
+            "extend": "^3.0.1",
+            "google-auth-library": "^2.0.0",
+            "pify": "^4.0.0",
+            "retry-request": "^4.0.0",
+            "through2": "^3.0.0"
+          }
+        },
+        "@google-cloud/projectify": {
+          "version": "0.3.3",
+          "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz",
+          "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw=="
+        },
+        "@google-cloud/promisify": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.3.1.tgz",
+          "integrity": "sha512-QzB0/IMvB0eFxFK7Eqh+bfC8NLv3E9ScjWQrPOk6GgfNroxcVITdTlT8NRsRrcp5+QQJVPLkRqKG0PUdaWXmHw=="
+        },
+        "arrify": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+          "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0="
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "gaxios": {
+          "version": "1.8.4",
+          "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz",
+          "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==",
+          "requires": {
+            "abort-controller": "^3.0.0",
+            "extend": "^3.0.2",
+            "https-proxy-agent": "^2.2.1",
+            "node-fetch": "^2.3.0"
+          }
+        },
+        "gcp-metadata": {
+          "version": "0.9.3",
+          "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.9.3.tgz",
+          "integrity": "sha512-caV4S84xAjENtpezLCT/GILEAF5h/bC4cNqZFmt/tjTn8t+JBtTkQrgBrJu3857YdsnlM8rxX/PMcKGtE8hUlw==",
+          "requires": {
+            "gaxios": "^1.0.2",
+            "json-bigint": "^0.3.0"
+          }
+        },
+        "google-auth-library": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-2.0.2.tgz",
+          "integrity": "sha512-FURxmo1hBVmcfLauuMRKOPYAPKht3dGuI2wjeJFalDUThO0HoYVjr4yxt5cgYSFm1dgUpmN9G/poa7ceTFAIiA==",
+          "requires": {
+            "axios": "^0.18.0",
+            "gcp-metadata": "^0.7.0",
+            "gtoken": "^2.3.0",
+            "https-proxy-agent": "^2.2.1",
+            "jws": "^3.1.5",
+            "lru-cache": "^5.0.0",
+            "semver": "^5.5.0"
+          },
+          "dependencies": {
+            "gcp-metadata": {
+              "version": "0.7.0",
+              "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.7.0.tgz",
+              "integrity": "sha512-ffjC09amcDWjh3VZdkDngIo7WoluyC5Ag9PAYxZbmQLOLNI8lvPtoKTSCyU54j2gwy5roZh6sSMTfkY2ct7K3g==",
+              "requires": {
+                "axios": "^0.18.0",
+                "extend": "^3.0.1",
+                "retry-axios": "0.3.2"
+              }
+            }
+          }
+        },
+        "google-p12-pem": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz",
+          "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==",
+          "requires": {
+            "node-forge": "^0.8.0",
+            "pify": "^4.0.0"
+          }
+        },
+        "gtoken": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz",
+          "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==",
+          "requires": {
+            "gaxios": "^1.0.4",
+            "google-p12-pem": "^1.0.0",
+            "jws": "^3.1.5",
+            "mime": "^2.2.0",
+            "pify": "^4.0.0"
+          }
+        },
+        "https-proxy-agent": {
+          "version": "2.2.4",
+          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
+          "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
+          "requires": {
+            "agent-base": "^4.3.0",
+            "debug": "^3.1.0"
+          }
+        },
+        "mime": {
+          "version": "2.4.4",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
+          "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "node-forge": {
+          "version": "0.8.5",
+          "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz",
+          "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q=="
+        },
+        "teeny-request": {
+          "version": "3.11.3",
+          "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz",
+          "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==",
+          "requires": {
+            "https-proxy-agent": "^2.2.1",
+            "node-fetch": "^2.2.0",
+            "uuid": "^3.3.2"
+          }
+        }
+      }
+    },
+    "@google-cloud/projectify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.1.tgz",
+      "integrity": "sha512-xknDOmsMgOYHksKc1GPbwDLsdej8aRNIA17SlSZgQdyrcC0lx0OGo4VZgYfwoEU1YS8oUxF9Y+6EzDOb0eB7Xg=="
+    },
+    "@google-cloud/promisify": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.2.tgz",
+      "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g=="
+    },
+    "@google-cloud/trace-agent": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-3.6.1.tgz",
+      "integrity": "sha512-KDo85aPN4gSxJ7oEIOlKd7aGENZFXAM1kbIn1Ds+61gh/K1CQWSyepgJo3nUpAwH6D1ezDWV7Iaf8ueoITc8Uw==",
+      "requires": {
+        "@google-cloud/common": "^0.32.1",
+        "builtin-modules": "^3.0.0",
+        "console-log-level": "^1.4.0",
+        "continuation-local-storage": "^3.2.1",
+        "extend": "^3.0.0",
+        "gcp-metadata": "^1.0.0",
+        "hex2dec": "^1.0.1",
+        "is": "^3.2.0",
+        "methods": "^1.1.1",
+        "require-in-the-middle": "^4.0.0",
+        "semver": "^6.0.0",
+        "shimmer": "^1.2.0",
+        "uuid": "^3.0.1"
+      },
+      "dependencies": {
+        "@google-cloud/common": {
+          "version": "0.32.1",
+          "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz",
+          "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==",
+          "requires": {
+            "@google-cloud/projectify": "^0.3.3",
+            "@google-cloud/promisify": "^0.4.0",
+            "@types/request": "^2.48.1",
+            "arrify": "^2.0.0",
+            "duplexify": "^3.6.0",
+            "ent": "^2.2.0",
+            "extend": "^3.0.2",
+            "google-auth-library": "^3.1.1",
+            "pify": "^4.0.1",
+            "retry-request": "^4.0.0",
+            "teeny-request": "^3.11.3"
+          }
+        },
+        "@google-cloud/projectify": {
+          "version": "0.3.3",
+          "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz",
+          "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw=="
+        },
+        "@google-cloud/promisify": {
+          "version": "0.4.0",
+          "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz",
+          "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q=="
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "gaxios": {
+          "version": "1.8.4",
+          "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz",
+          "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==",
+          "requires": {
+            "abort-controller": "^3.0.0",
+            "extend": "^3.0.2",
+            "https-proxy-agent": "^2.2.1",
+            "node-fetch": "^2.3.0"
+          }
+        },
+        "gcp-metadata": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz",
+          "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==",
+          "requires": {
+            "gaxios": "^1.0.2",
+            "json-bigint": "^0.3.0"
+          }
+        },
+        "google-auth-library": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz",
+          "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==",
+          "requires": {
+            "base64-js": "^1.3.0",
+            "fast-text-encoding": "^1.0.0",
+            "gaxios": "^1.2.1",
+            "gcp-metadata": "^1.0.0",
+            "gtoken": "^2.3.2",
+            "https-proxy-agent": "^2.2.1",
+            "jws": "^3.1.5",
+            "lru-cache": "^5.0.0",
+            "semver": "^5.5.0"
+          },
+          "dependencies": {
+            "semver": {
+              "version": "5.7.1",
+              "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+              "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+            }
+          }
+        },
+        "google-p12-pem": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz",
+          "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==",
+          "requires": {
+            "node-forge": "^0.8.0",
+            "pify": "^4.0.0"
+          }
+        },
+        "gtoken": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz",
+          "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==",
+          "requires": {
+            "gaxios": "^1.0.4",
+            "google-p12-pem": "^1.0.0",
+            "jws": "^3.1.5",
+            "mime": "^2.2.0",
+            "pify": "^4.0.0"
+          }
+        },
+        "https-proxy-agent": {
+          "version": "2.2.4",
+          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
+          "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
+          "requires": {
+            "agent-base": "^4.3.0",
+            "debug": "^3.1.0"
+          }
+        },
+        "mime": {
+          "version": "2.4.4",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
+          "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "node-forge": {
+          "version": "0.8.5",
+          "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz",
+          "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q=="
+        },
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+        },
+        "teeny-request": {
+          "version": "3.11.3",
+          "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz",
+          "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==",
+          "requires": {
+            "https-proxy-agent": "^2.2.1",
+            "node-fetch": "^2.2.0",
+            "uuid": "^3.3.2"
+          }
+        }
+      }
+    },
+    "@grpc/grpc-js": {
+      "version": "0.6.9",
+      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.9.tgz",
+      "integrity": "sha512-r1nDOEEiYmAsVYBaS4DPPqdwPOXPw7YhVOnnpPdWhlNtKbYzPash6DqWTTza9gBiYMA5d2Wiq6HzrPqsRaP4yA==",
+      "requires": {
+        "semver": "^6.2.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+        }
+      }
+    },
+    "@grpc/proto-loader": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.2.tgz",
+      "integrity": "sha512-eBKD/FPxQoY1x6QONW2nBd54QUEyzcFP9FenujmoeDPy1rutVSHki1s/wR68F6O1QfCNDx+ayBH1O2CVNMzyyw==",
+      "requires": {
+        "lodash.camelcase": "^4.3.0",
+        "protobufjs": "^6.8.6"
+      }
+    },
+    "@opencensus/core": {
+      "version": "0.0.18",
+      "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.18.tgz",
+      "integrity": "sha512-PgRQXLyb3bLi8Z6pQct9erYFRdnYAZNQXAEVPf6Xq6IMkZaH20wiOTNNPxEckjI31mq5utgstAbwOn4gJiPjBQ==",
+      "requires": {
+        "continuation-local-storage": "^3.2.1",
+        "log-driver": "^1.2.7",
+        "semver": "^6.0.0",
+        "shimmer": "^1.2.0",
+        "uuid": "^3.2.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+        }
+      }
+    },
+    "@opencensus/propagation-stackdriver": {
+      "version": "0.0.18",
+      "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.18.tgz",
+      "integrity": "sha512-BLwfszIGAfqN2mqGf/atfEu84cWeoLM/YuXGfXDO1iDN2k5GXz4QFyhS8sz5l63HtsYuQqFuV+Ze7ZM0NvJp2A==",
+      "requires": {
+        "@opencensus/core": "^0.0.18",
+        "hex2dec": "^1.0.1",
+        "uuid": "^3.2.1"
+      }
+    },
+    "@overleaf/o-error": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-2.1.0.tgz",
+      "integrity": "sha512-Zd9sks9LrLw8ErHt/cXeWIkyxWAqNAvNGn7wIjLQJH6TTEEW835PWOhpch+hQwwWsTxWIx/JDj+IpZ3ouw925g=="
+    },
+    "@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78="
+    },
+    "@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+    },
+    "@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+    },
+    "@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A="
+    },
+    "@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=",
+      "requires": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E="
+    },
+    "@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik="
+    },
+    "@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0="
+    },
+    "@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q="
+    },
+    "@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
+    },
+    "@sindresorhus/is": {
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.15.0.tgz",
+      "integrity": "sha512-lu8BpxjAtRCAo5ifytTpCPCj99LF7o/2Myn+NXyNCBqvPYn7Pjd76AMmUB5l7XF1U6t0hcWrlEM5ESufW7wAeA=="
+    },
+    "@sinonjs/commons": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz",
+      "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==",
+      "dev": true,
+      "requires": {
+        "type-detect": "4.0.8"
+      },
+      "dependencies": {
+        "type-detect": {
+          "version": "4.0.8",
+          "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+          "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+          "dev": true
+        }
+      }
+    },
+    "@sinonjs/fake-timers": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.0.tgz",
+      "integrity": "sha512-atR1J/jRXvQAb47gfzSK8zavXy7BcpnYq21ALon0U99etu99vsir0trzIO3wpeLtW+LLVY6X7EkfVTbjGSH8Ww==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.7.0"
+      }
+    },
+    "@sinonjs/formatio": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz",
+      "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1",
+        "@sinonjs/samsam": "^5.0.2"
+      }
+    },
+    "@sinonjs/samsam": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz",
+      "integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.6.0",
+        "lodash.get": "^4.4.2",
+        "type-detect": "^4.0.8"
+      },
+      "dependencies": {
+        "type-detect": {
+          "version": "4.0.8",
+          "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+          "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+          "dev": true
+        }
+      }
+    },
+    "@sinonjs/text-encoding": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
+      "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
+      "dev": true
+    },
+    "@types/caseless": {
+      "version": "0.12.2",
+      "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
+      "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w=="
+    },
+    "@types/color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
+      "dev": true
+    },
+    "@types/console-log-level": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz",
+      "integrity": "sha512-x+OscEQwcx5Biair4enH7ov9W+clcqUWaZRaxn5IkT4yNWWjRr2oiYDkY/x1uXSTVZOQ2xlbFQySaQGB+VdXGQ=="
+    },
+    "@types/duplexify": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz",
+      "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/eslint-visitor-keys": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+      "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
+      "dev": true
+    },
+    "@types/json-schema": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
+      "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
+      "dev": true
+    },
+    "@types/long": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz",
+      "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q=="
+    },
+    "@types/node": {
+      "version": "10.12.15",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz",
+      "integrity": "sha1-IOhWUbYv2GZW5Xycm8dxqxVwvFk="
+    },
+    "@types/request": {
+      "version": "2.48.4",
+      "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.4.tgz",
+      "integrity": "sha512-W1t1MTKYR8PxICH+A4HgEIPuAC3sbljoEVfyZbeFJJDbr30guDspJri2XOaM2E+Un7ZjrihaDi7cf6fPa2tbgw==",
+      "requires": {
+        "@types/caseless": "*",
+        "@types/node": "*",
+        "@types/tough-cookie": "*",
+        "form-data": "^2.5.0"
+      },
+      "dependencies": {
+        "form-data": {
+          "version": "2.5.1",
+          "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
+          "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
+          "requires": {
+            "asynckit": "^0.4.0",
+            "combined-stream": "^1.0.6",
+            "mime-types": "^2.1.12"
+          }
+        }
+      }
+    },
+    "@types/semver": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz",
+      "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ=="
+    },
+    "@types/tough-cookie": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz",
+      "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ=="
+    },
+    "@typescript-eslint/experimental-utils": {
+      "version": "1.13.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
+      "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==",
+      "dev": true,
+      "requires": {
+        "@types/json-schema": "^7.0.3",
+        "@typescript-eslint/typescript-estree": "1.13.0",
+        "eslint-scope": "^4.0.0"
+      },
+      "dependencies": {
+        "eslint-scope": {
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+          "dev": true,
+          "requires": {
+            "esrecurse": "^4.1.0",
+            "estraverse": "^4.1.1"
+          }
+        }
+      }
+    },
+    "@typescript-eslint/parser": {
+      "version": "1.13.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz",
+      "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==",
+      "dev": true,
+      "requires": {
+        "@types/eslint-visitor-keys": "^1.0.0",
+        "@typescript-eslint/experimental-utils": "1.13.0",
+        "@typescript-eslint/typescript-estree": "1.13.0",
+        "eslint-visitor-keys": "^1.0.0"
+      }
+    },
+    "@typescript-eslint/typescript-estree": {
+      "version": "1.13.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz",
+      "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==",
+      "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
+        }
+      }
+    },
+    "abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg="
+    },
+    "abort-controller": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+      "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+      "requires": {
+        "event-target-shim": "^5.0.0"
+      }
+    },
+    "accepts": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+      "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+      "requires": {
+        "mime-types": "~2.1.24",
+        "negotiator": "0.6.2"
+      }
+    },
+    "acorn": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
+      "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA=="
+    },
+    "acorn-jsx": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz",
+      "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==",
+      "dev": true
+    },
+    "agent-base": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
+      "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
+      "requires": {
+        "es6-promisify": "^5.0.0"
+      }
+    },
+    "ajv": {
+      "version": "6.12.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
+      "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ansi-colors": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
+      "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
+      "dev": true
+    },
+    "ansi-escapes": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
+      "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
+      "dev": true,
+      "requires": {
+        "type-fest": "^0.11.0"
+      },
+      "dependencies": {
+        "type-fest": {
+          "version": "0.11.0",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
+          "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
+          "dev": true
+        }
+      }
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
+    },
+    "anymatch": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
+      "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+      "dev": true,
+      "requires": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      }
+    },
+    "aproba": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+      "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo="
+    },
+    "are-we-there-yet": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+      "integrity": "sha1-SzXClE8GKov82mZBB2A1D+nd/CE=",
+      "requires": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^2.0.6"
+      }
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "aria-query": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz",
+      "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=",
+      "dev": true,
+      "requires": {
+        "ast-types-flow": "0.0.7",
+        "commander": "^2.11.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.20.3",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+          "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+          "dev": true
+        }
+      }
+    },
+    "array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+    },
+    "array-includes": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz",
+      "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0",
+        "is-string": "^1.0.5"
+      }
+    },
+    "array.prototype.flat": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz",
+      "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1"
+      }
+    },
+    "arrify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
+      "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="
+    },
+    "asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+    },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true
+    },
+    "ast-types-flow": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
+      "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=",
+      "dev": true
+    },
+    "astral-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+      "dev": true
+    },
+    "async": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
+      "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
+    },
+    "async-listener": {
+      "version": "0.6.10",
+      "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz",
+      "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==",
+      "requires": {
+        "semver": "^5.3.0",
+        "shimmer": "^1.1.0"
+      }
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
+    },
+    "aws4": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
+      "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
+    },
+    "axios": {
+      "version": "0.18.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
+      "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
+      "requires": {
+        "follow-redirects": "1.5.10",
+        "is-buffer": "^2.0.2"
+      }
+    },
+    "axobject-query": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.2.tgz",
+      "integrity": "sha512-ICt34ZmrVt8UQnvPl6TVyDTkmhXmAyAT4Jh5ugfGUX4MOrZ+U/ZY6/sdylRw3qGNr9Ub5AJsaHeDMzNLehRdOQ==",
+      "dev": true
+    },
+    "babel-eslint": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
+      "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "@babel/parser": "^7.7.0",
+        "@babel/traverse": "^7.7.0",
+        "@babel/types": "^7.7.0",
+        "eslint-visitor-keys": "^1.0.0",
+        "resolve": "^1.12.0"
+      }
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+    },
+    "base64-js": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+      "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "bignumber.js": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
+      "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ=="
+    },
+    "binary-extensions": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
+      "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
+      "dev": true
+    },
+    "bindings": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+      "requires": {
+        "file-uri-to-path": "1.0.0"
+      }
+    },
+    "bintrees": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
+      "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
+    },
+    "bl": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.1.tgz",
+      "integrity": "sha512-FL/TdvchukRCuWVxT0YMO/7+L5TNeNrVFvRU2IY63aUyv9mpt8splf2NEr6qXtPo5fya5a66YohQKvGNmLrWNA==",
+      "requires": {
+        "readable-stream": "^3.4.0"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
+    "bluebird": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
+    },
+    "body-parser": {
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+      "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+      "requires": {
+        "bytes": "3.1.0",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
+        "on-finished": "~2.3.0",
+        "qs": "6.7.0",
+        "raw-body": "2.4.0",
+        "type-is": "~1.6.17"
+      },
+      "dependencies": {
+        "iconv-lite": {
+          "version": "0.4.24",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+          "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        }
+      }
+    },
+    "boolify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/boolify/-/boolify-1.0.1.tgz",
+      "integrity": "sha1-tcCeF8rNET0Rt7s+04TMASmU2Gs=",
+      "dev": true
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=",
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
+    "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=="
+    },
+    "builtin-modules": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
+      "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw=="
+    },
+    "bunyan": {
+      "version": "1.8.12",
+      "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
+      "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
+      "dev": true,
+      "requires": {
+        "dtrace-provider": "~0.8",
+        "moment": "^2.10.6",
+        "mv": "~2",
+        "safe-json-stringify": "~1"
+      }
+    },
+    "bytes": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+      "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+    },
+    "callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "dev": true
+    },
+    "camelcase-keys": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.1.2.tgz",
+      "integrity": "sha512-QfFrU0CIw2oltVvpndW32kuJ/9YOJwUnmWrjlXt1nnJZHCaS9i6bfOpg9R4Lw8aZjStkJWM+jc0cdXjWBgVJSw==",
+      "dev": true,
+      "requires": {
+        "camelcase": "^5.3.1",
+        "map-obj": "^4.0.0",
+        "quick-lru": "^4.0.1"
+      }
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+    },
+    "chai": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
+      "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
+      "dev": true,
+      "requires": {
+        "assertion-error": "^1.1.0",
+        "check-error": "^1.0.2",
+        "deep-eql": "^3.0.1",
+        "get-func-name": "^2.0.0",
+        "pathval": "^1.1.0",
+        "type-detect": "^4.0.5"
+      }
+    },
+    "chalk": {
+      "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": {
+        "has-flag": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+          "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+          "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"
+          }
+        }
+      }
+    },
+    "chardet": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+      "dev": true
+    },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
+    },
+    "chokidar": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
+      "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==",
+      "dev": true,
+      "requires": {
+        "anymatch": "~3.1.1",
+        "braces": "~3.0.2",
+        "fsevents": "~2.1.1",
+        "glob-parent": "~5.1.0",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.2.0"
+      }
+    },
+    "chownr": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
+      "integrity": "sha1-VHJri4//TfBTxCGH6AH7RBLfFJQ="
+    },
+    "cli-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+      "dev": true,
+      "requires": {
+        "restore-cursor": "^3.1.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
+    },
+    "cliui": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+      "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+      "dev": true,
+      "requires": {
+        "string-width": "^3.1.0",
+        "strip-ansi": "^5.2.0",
+        "wrap-ansi": "^5.1.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        },
+        "emoji-regex": {
+          "version": "7.0.3",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+          "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "cls-bluebird": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz",
+      "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=",
+      "requires": {
+        "is-bluebird": "^1.0.2",
+        "shimmer": "^1.1.0"
+      }
+    },
+    "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="
+    },
+    "coffee-script": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz",
+      "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M="
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dev": true,
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+      "dev": true
+    },
+    "combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npmjs.org/commander/-/commander-2.0.0.tgz",
+      "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg="
+    },
+    "common-tags": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
+      "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==",
+      "dev": true
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "concat-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+      "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.0.2",
+        "typedarray": "^0.0.6"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
+    "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="
+    },
+    "console-log-level": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.1.tgz",
+      "integrity": "sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ=="
+    },
+    "contains-path": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz",
+      "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=",
+      "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=="
+    },
+    "continuation-local-storage": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz",
+      "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==",
+      "requires": {
+        "async-listener": "^0.6.0",
+        "emitter-listener": "^1.1.1"
+      }
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+    },
+    "core-js": {
+      "version": "3.6.4",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
+      "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==",
+      "dev": true
+    },
+    "core-js-pure": {
+      "version": "3.6.4",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz",
+      "integrity": "sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==",
+      "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==",
+      "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"
+      }
+    },
+    "d64": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/d64/-/d64-1.0.0.tgz",
+      "integrity": "sha1-QAKofoUMv8n52XBrYPymE6MzbpA="
+    },
+    "damerau-levenshtein": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
+      "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==",
+      "dev": true
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "deep-eql": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+      "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+      "dev": true,
+      "requires": {
+        "type-detect": "^4.0.0"
+      }
+    },
+    "deep-extend": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+      "integrity": "sha1-xPp8lUBKF6nD6Mp+FTcxK3NjMKw="
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "define-properties": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "dev": true,
+      "requires": {
+        "object-keys": "^1.0.12"
+      }
+    },
+    "delay": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz",
+      "integrity": "sha512-Lwaf3zVFDMBop1yDuFZ19F9WyGcZcGacsbdlZtWjQmM50tOcMntm1njF/Nb/Vjij3KaSvCF+sEYGKrrjObu2NA=="
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+    },
+    "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="
+    },
+    "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": "1.0.7",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz",
+      "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ="
+    },
+    "dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+      "dev": true
+    },
+    "docker-modem": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.1.tgz",
+      "integrity": "sha512-zSFwYN4AP38LJhTIOpZMjiDbAqSJbv8+u9i/Xq5XABIeTzgp83VF63epu6sVHWxe+6tfhMXqgV+sYjZWh/UzSQ==",
+      "requires": {
+        "debug": "^4.1.1",
+        "readable-stream": "^3.5.0",
+        "split-ca": "^1.0.1",
+        "ssh2": "^0.8.7"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
+    "dockerode": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.1.0.tgz",
+      "integrity": "sha512-E0KknBBTlIVEvtt2XJRZ3he59u2UN8Yr1A08Sey/BKIox+WlwnJp5fL5SKyhPgNmSXgamPEuKYCJxMi31uj0Nw==",
+      "requires": {
+        "concat-stream": "~2.0.0",
+        "docker-modem": "^2.1.0",
+        "tar-fs": "~2.0.0"
+      }
+    },
+    "doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "dot-prop": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.1.1.tgz",
+      "integrity": "sha512-QCHI6Lkf+9fJMpwfAFsTvbiSh6ujoPmhCLiDvD/n4dGtLvHfhuBwPdN6z2x4YSOwwtTcLoO/LP70xELWGF/JVA==",
+      "requires": {
+        "is-obj": "^2.0.0"
+      }
+    },
+    "dottie": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz",
+      "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg=="
+    },
+    "dtrace-provider": {
+      "version": "0.8.8",
+      "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
+      "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "nan": "^2.14.0"
+      },
+      "dependencies": {
+        "nan": {
+          "version": "2.14.0",
+          "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+          "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "duplexify": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+      "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+      "requires": {
+        "end-of-stream": "^1.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "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="
+    },
+    "emitter-listener": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
+      "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==",
+      "requires": {
+        "shimmer": "^1.2.0"
+      }
+    },
+    "emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+    },
+    "end-of-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+      "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=",
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "ent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+      "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
+    },
+    "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"
+      }
+    },
+    "es-abstract": {
+      "version": "1.17.4",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz",
+      "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==",
+      "dev": true,
+      "requires": {
+        "es-to-primitive": "^1.2.1",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "has-symbols": "^1.0.1",
+        "is-callable": "^1.1.5",
+        "is-regex": "^1.0.5",
+        "object-inspect": "^1.7.0",
+        "object-keys": "^1.1.1",
+        "object.assign": "^4.1.0",
+        "string.prototype.trimleft": "^2.1.1",
+        "string.prototype.trimright": "^2.1.1"
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+      "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+      "dev": true,
+      "requires": {
+        "is-callable": "^1.1.4",
+        "is-date-object": "^1.0.1",
+        "is-symbol": "^1.0.2"
+      }
+    },
+    "es6-promise": {
+      "version": "4.2.8",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+      "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
+    },
+    "es6-promisify": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+      "requires": {
+        "es6-promise": "^4.0.3"
+      }
+    },
+    "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",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
+    },
+    "eslint": {
+      "version": "6.8.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
+      "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "ajv": "^6.10.0",
+        "chalk": "^2.1.0",
+        "cross-spawn": "^6.0.5",
+        "debug": "^4.0.1",
+        "doctrine": "^3.0.0",
+        "eslint-scope": "^5.0.0",
+        "eslint-utils": "^1.4.3",
+        "eslint-visitor-keys": "^1.1.0",
+        "espree": "^6.1.2",
+        "esquery": "^1.0.1",
+        "esutils": "^2.0.2",
+        "file-entry-cache": "^5.0.1",
+        "functional-red-black-tree": "^1.0.1",
+        "glob-parent": "^5.0.0",
+        "globals": "^12.1.0",
+        "ignore": "^4.0.6",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "inquirer": "^7.0.0",
+        "is-glob": "^4.0.0",
+        "js-yaml": "^3.13.1",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.3.0",
+        "lodash": "^4.17.14",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.1",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.8.3",
+        "progress": "^2.0.0",
+        "regexpp": "^2.0.1",
+        "semver": "^6.1.2",
+        "strip-ansi": "^5.2.0",
+        "strip-json-comments": "^3.0.1",
+        "table": "^5.2.3",
+        "text-table": "^0.2.0",
+        "v8-compile-cache": "^2.0.3"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.12.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
+          "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "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"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+          "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
+          "dev": true
+        },
+        "globals": {
+          "version": "12.4.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
+          "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
+          "dev": true,
+          "requires": {
+            "type-fest": "^0.8.1"
+          }
+        },
+        "lodash": {
+          "version": "4.17.15",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+          "dev": true
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        },
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
+          "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
+          "dev": true
+        }
+      }
+    },
+    "eslint-config-prettier": {
+      "version": "6.10.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz",
+      "integrity": "sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg==",
+      "dev": true,
+      "requires": {
+        "get-stdin": "^6.0.0"
+      }
+    },
+    "eslint-config-standard": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz",
+      "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==",
+      "dev": true
+    },
+    "eslint-config-standard-jsx": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-8.1.0.tgz",
+      "integrity": "sha512-ULVC8qH8qCqbU792ZOO6DaiaZyHNS/5CZt3hKqHkEhVlhPEPN3nfBqqxJCyp59XrjIBZPu1chMYe9T2DXZ7TMw==",
+      "dev": true
+    },
+    "eslint-config-standard-react": {
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-standard-react/-/eslint-config-standard-react-9.2.0.tgz",
+      "integrity": "sha512-u+KRP2uCtthZ/W4DlLWCC59GZNV/y9k9yicWWammgTs/Omh8ZUUPF3EnYm81MAcbkYQq2Wg0oxutAhi/FQ8mIw==",
+      "dev": true,
+      "requires": {
+        "eslint-config-standard-jsx": "^8.0.0"
+      }
+    },
+    "eslint-import-resolver-node": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz",
+      "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.9",
+        "resolve": "^1.13.1"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.15.1",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
+          "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
+          "dev": true,
+          "requires": {
+            "path-parse": "^1.0.6"
+          }
+        }
+      }
+    },
+    "eslint-module-utils": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz",
+      "integrity": "sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.9",
+        "pkg-dir": "^2.0.0"
+      }
+    },
+    "eslint-plugin-chai-expect": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-chai-expect/-/eslint-plugin-chai-expect-2.1.0.tgz",
+      "integrity": "sha512-rd0/4mjMV6c3i0o4DKkWI4uaFN9DK707kW+/fDphaDI6HVgxXnhML9Xgt5vHnTXmSSnDhupuCFBgsEAEpchXmQ==",
+      "dev": true
+    },
+    "eslint-plugin-chai-friendly": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.5.0.tgz",
+      "integrity": "sha512-Pxe6z8C9fP0pn2X2nGFU/b3GBOCM/5FVus1hsMwJsXP3R7RiXFl7g0ksJbsc0GxiLyidTW4mEFk77qsNn7Tk7g==",
+      "dev": true
+    },
+    "eslint-plugin-es": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.0.tgz",
+      "integrity": "sha512-6/Jb/J/ZvSebydwbBJO1R9E5ky7YeElfK56Veh7e4QGFHCXoIXGH9HhVz+ibJLM3XJ1XjP+T7rKBLUa/Y7eIng==",
+      "dev": true,
+      "requires": {
+        "eslint-utils": "^2.0.0",
+        "regexpp": "^3.0.0"
+      },
+      "dependencies": {
+        "eslint-utils": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz",
+          "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==",
+          "dev": true,
+          "requires": {
+            "eslint-visitor-keys": "^1.1.0"
+          }
+        },
+        "regexpp": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
+          "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
+          "dev": true
+        }
+      }
+    },
+    "eslint-plugin-import": {
+      "version": "2.20.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz",
+      "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==",
+      "dev": true,
+      "requires": {
+        "array-includes": "^3.0.3",
+        "array.prototype.flat": "^1.2.1",
+        "contains-path": "^0.1.0",
+        "debug": "^2.6.9",
+        "doctrine": "1.5.0",
+        "eslint-import-resolver-node": "^0.3.2",
+        "eslint-module-utils": "^2.4.1",
+        "has": "^1.0.3",
+        "minimatch": "^3.0.4",
+        "object.values": "^1.1.0",
+        "read-pkg-up": "^2.0.0",
+        "resolve": "^1.12.0"
+      },
+      "dependencies": {
+        "doctrine": {
+          "version": "1.5.0",
+          "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz",
+          "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=",
+          "dev": true,
+          "requires": {
+            "esutils": "^2.0.2",
+            "isarray": "^1.0.0"
+          }
+        }
+      }
+    },
+    "eslint-plugin-jsx-a11y": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz",
+      "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.4.5",
+        "aria-query": "^3.0.0",
+        "array-includes": "^3.0.3",
+        "ast-types-flow": "^0.0.7",
+        "axobject-query": "^2.0.2",
+        "damerau-levenshtein": "^1.0.4",
+        "emoji-regex": "^7.0.2",
+        "has": "^1.0.3",
+        "jsx-ast-utils": "^2.2.1"
+      },
+      "dependencies": {
+        "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
+        }
+      }
+    },
+    "eslint-plugin-mocha": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-6.3.0.tgz",
+      "integrity": "sha512-Cd2roo8caAyG21oKaaNTj7cqeYRWW1I2B5SfpKRp0Ip1gkfwoR1Ow0IGlPWnNjzywdF4n+kHL8/9vM6zCJUxdg==",
+      "dev": true,
+      "requires": {
+        "eslint-utils": "^2.0.0",
+        "ramda": "^0.27.0"
+      },
+      "dependencies": {
+        "eslint-utils": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz",
+          "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==",
+          "dev": true,
+          "requires": {
+            "eslint-visitor-keys": "^1.1.0"
+          }
+        }
+      }
+    },
+    "eslint-plugin-node": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.0.0.tgz",
+      "integrity": "sha512-chUs/NVID+sknFiJzxoN9lM7uKSOEta8GC8365hw1nDfwIPIjjpRSwwPvQanWv8dt/pDe9EV4anmVSwdiSndNg==",
+      "dev": true,
+      "requires": {
+        "eslint-plugin-es": "^3.0.0",
+        "eslint-utils": "^2.0.0",
+        "ignore": "^5.1.1",
+        "minimatch": "^3.0.4",
+        "resolve": "^1.10.1",
+        "semver": "^6.1.0"
+      },
+      "dependencies": {
+        "eslint-utils": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz",
+          "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==",
+          "dev": true,
+          "requires": {
+            "eslint-visitor-keys": "^1.1.0"
+          }
+        },
+        "ignore": {
+          "version": "5.1.4",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
+          "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
+          "dev": true
+        },
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "dev": true
+        }
+      }
+    },
+    "eslint-plugin-prettier": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz",
+      "integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==",
+      "dev": true,
+      "requires": {
+        "prettier-linter-helpers": "^1.0.0"
+      }
+    },
+    "eslint-plugin-promise": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz",
+      "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==",
+      "dev": true
+    },
+    "eslint-plugin-react": {
+      "version": "7.19.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz",
+      "integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==",
+      "dev": true,
+      "requires": {
+        "array-includes": "^3.1.1",
+        "doctrine": "^2.1.0",
+        "has": "^1.0.3",
+        "jsx-ast-utils": "^2.2.3",
+        "object.entries": "^1.1.1",
+        "object.fromentries": "^2.0.2",
+        "object.values": "^1.1.1",
+        "prop-types": "^15.7.2",
+        "resolve": "^1.15.1",
+        "semver": "^6.3.0",
+        "string.prototype.matchall": "^4.0.2",
+        "xregexp": "^4.3.0"
+      },
+      "dependencies": {
+        "doctrine": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+          "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+          "dev": true,
+          "requires": {
+            "esutils": "^2.0.2"
+          }
+        },
+        "resolve": {
+          "version": "1.15.1",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
+          "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
+          "dev": true,
+          "requires": {
+            "path-parse": "^1.0.6"
+          }
+        },
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "dev": true
+        }
+      }
+    },
+    "eslint-plugin-standard": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz",
+      "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==",
+      "dev": true
+    },
+    "eslint-scope": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
+      "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "eslint-utils": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
+      "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
+      "dev": true,
+      "requires": {
+        "eslint-visitor-keys": "^1.1.0"
+      }
+    },
+    "eslint-visitor-keys": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+      "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+      "dev": true
+    },
+    "espree": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
+      "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
+      "dev": true,
+      "requires": {
+        "acorn": "^7.1.1",
+        "acorn-jsx": "^5.2.0",
+        "eslint-visitor-keys": "^1.1.0"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "7.1.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
+          "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
+          "dev": true
+        },
+        "acorn-jsx": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
+          "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
+          "dev": true
+        }
+      }
+    },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true
+    },
+    "esquery": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz",
+      "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.0.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      }
+    },
+    "estraverse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+    },
+    "event-target-shim": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
+    },
+    "eventid": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/eventid/-/eventid-0.1.2.tgz",
+      "integrity": "sha1-CyMtPiROpbHVKJhBQOpprH7IkhU=",
+      "requires": {
+        "d64": "^1.0.0",
+        "uuid": "^3.0.1"
+      }
+    },
+    "express": {
+      "version": "4.17.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+      "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+      "requires": {
+        "accepts": "~1.3.7",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.19.0",
+        "content-disposition": "0.5.3",
+        "content-type": "~1.0.4",
+        "cookie": "0.4.0",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.1.2",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.5",
+        "qs": "6.7.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.1.2",
+        "send": "0.17.1",
+        "serve-static": "1.14.1",
+        "setprototypeof": "1.1.1",
+        "statuses": "~1.5.0",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.4.0",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+          "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
+        }
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo="
+    },
+    "external-editor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+      "dev": true,
+      "requires": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      },
+      "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"
+          }
+        }
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
+    },
+    "fast-deep-equal": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+      "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
+    },
+    "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",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "fast-text-encoding": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz",
+      "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ=="
+    },
+    "figures": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
+    "file-entry-cache": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
+      "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^2.0.1"
+      }
+    },
+    "file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
+    },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+      "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "statuses": "~1.5.0",
+        "unpipe": "~1.0.0"
+      }
+    },
+    "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"
+      }
+    },
+    "findit2": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz",
+      "integrity": "sha1-WKRmaX34piBc39vzlVNri9d3pfY="
+    },
+    "flat": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
+      "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
+      "dev": true,
+      "requires": {
+        "is-buffer": "~2.0.3"
+      }
+    },
+    "flat-cache": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
+      "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+      "dev": true,
+      "requires": {
+        "flatted": "^2.0.0",
+        "rimraf": "2.6.3",
+        "write": "1.0.3"
+      },
+      "dependencies": {
+        "rimraf": {
+          "version": "2.6.3",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+          "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+          "dev": true,
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        }
+      }
+    },
+    "flatted": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz",
+      "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
+      "dev": true
+    },
+    "follow-redirects": {
+      "version": "1.5.10",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+      "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+      "requires": {
+        "debug": "=3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "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"
+          }
+        }
+      }
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
+    },
+    "form-data": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "forwarded": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+    },
+    "fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+    },
+    "fs-extra": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+      "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+      "requires": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^4.0.0",
+        "universalify": "^0.1.0"
+      }
+    },
+    "fs-minipass": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
+      "integrity": "sha1-BsJ3IYRU7CiN93raVKA7hwKqy50=",
+      "requires": {
+        "minipass": "^2.2.1"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+    },
+    "fsevents": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
+      "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
+      "dev": true,
+      "optional": 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",
+      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+      "dev": true
+    },
+    "gauge": {
+      "version": "2.7.4",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+      "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"
+      }
+    },
+    "gaxios": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.1.0.tgz",
+      "integrity": "sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg==",
+      "requires": {
+        "abort-controller": "^3.0.0",
+        "extend": "^3.0.2",
+        "https-proxy-agent": "^3.0.0",
+        "is-stream": "^2.0.0",
+        "node-fetch": "^2.3.0"
+      }
+    },
+    "gcp-metadata": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.2.0.tgz",
+      "integrity": "sha512-ympv+yQ6k5QuWCuwQqnGEvFGS7MBKdcQdj1i188v3bW9QLFIchTGaBCEZxSQapT0jffdn1vdt8oJhB5VBWQO1Q==",
+      "requires": {
+        "gaxios": "^2.0.1",
+        "json-bigint": "^0.3.0"
+      }
+    },
+    "get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true
+    },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+      "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
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "glob": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+      "integrity": "sha1-OWCDLT8VdBCDQtr9OmezMsCWnfE=",
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
+      "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "globals": {
+      "version": "11.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+      "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+      "dev": true
+    },
+    "google-auth-library": {
+      "version": "5.5.1",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz",
+      "integrity": "sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg==",
+      "requires": {
+        "arrify": "^2.0.0",
+        "base64-js": "^1.3.0",
+        "fast-text-encoding": "^1.0.0",
+        "gaxios": "^2.1.0",
+        "gcp-metadata": "^3.2.0",
+        "gtoken": "^4.1.0",
+        "jws": "^3.1.5",
+        "lru-cache": "^5.0.0"
+      }
+    },
+    "google-gax": {
+      "version": "1.7.5",
+      "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.7.5.tgz",
+      "integrity": "sha512-Tz2DFs8umzDcCBTi2W1cY4vEgAKaYRj70g6Hh/MiiZaJizrly7PgyxsIYUGi7sOpEuAbARQymYKvy5mNi8hEbg==",
+      "requires": {
+        "@grpc/grpc-js": "0.6.9",
+        "@grpc/proto-loader": "^0.5.1",
+        "abort-controller": "^3.0.0",
+        "duplexify": "^3.6.0",
+        "google-auth-library": "^5.0.0",
+        "is-stream-ended": "^0.1.4",
+        "lodash.at": "^4.6.0",
+        "lodash.has": "^4.5.2",
+        "node-fetch": "^2.6.0",
+        "protobufjs": "^6.8.8",
+        "retry-request": "^4.0.0",
+        "semver": "^6.0.0",
+        "walkdir": "^0.4.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+        }
+      }
+    },
+    "google-p12-pem": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.2.tgz",
+      "integrity": "sha512-UfnEARfJKI6pbmC1hfFFm+UAcZxeIwTiEcHfqKe/drMsXD/ilnVjF7zgOGpHXyhuvX6jNJK3S8A0hOQjwtFxEw==",
+      "requires": {
+        "node-forge": "^0.9.0"
+      }
+    },
+    "graceful-fs": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
+      "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
+    },
+    "growl": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz",
+      "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto="
+    },
+    "gtoken": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.0.tgz",
+      "integrity": "sha512-wqyn2gf5buzEZN4QNmmiiW2i2JkEdZnL7Z/9p44RtZqgt4077m4khRgAYNuu8cBwHWCc6MsP6eDUn/KkF6jFIw==",
+      "requires": {
+        "gaxios": "^2.0.0",
+        "google-p12-pem": "^2.0.0",
+        "jws": "^3.1.5",
+        "mime": "^2.2.0"
+      },
+      "dependencies": {
+        "mime": {
+          "version": "2.4.4",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
+          "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
+        }
+      }
+    },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
+    },
+    "har-validator": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+      "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+      "requires": {
+        "ajv": "^6.5.5",
+        "har-schema": "^2.0.0"
+      }
+    },
+    "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-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "has-symbols": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
+      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
+      "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="
+    },
+    "he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true
+    },
+    "heapdump": {
+      "version": "0.3.15",
+      "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz",
+      "integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==",
+      "requires": {
+        "nan": "^2.13.2"
+      },
+      "dependencies": {
+        "nan": {
+          "version": "2.14.0",
+          "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+          "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
+        }
+      }
+    },
+    "hex2dec": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.2.tgz",
+      "integrity": "sha512-Yu+q/XWr2fFQ11tHxPq4p4EiNkb2y+lAacJNhAdRXVfRIcDH6gi7htWFnnlIzvqHMHoWeIsfXlNAjZInpAOJDA=="
+    },
+    "hosted-git-info": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz",
+      "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==",
+      "dev": true
+    },
+    "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"
+      }
+    },
+    "http-proxy-agent": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
+      "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
+      "requires": {
+        "agent-base": "4",
+        "debug": "3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "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"
+          }
+        }
+      }
+    },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      }
+    },
+    "https-proxy-agent": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz",
+      "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==",
+      "requires": {
+        "agent-base": "^4.3.0",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.23",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+      "integrity": "sha1-KXhx9jvlB63Pv8pxXQzQ7thOmmM=",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "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
+    },
+    "ignore-walk": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
+      "integrity": "sha1-qD5i59JyrA47VRqqgoMaGbafgvg=",
+      "requires": {
+        "minimatch": "^3.0.4"
+      }
+    },
+    "import-fresh": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
+      "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
+      "dev": true,
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "dev": true
+    },
+    "inflection": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz",
+      "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc="
+    },
+    "inquirer": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
+      "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "^4.2.1",
+        "chalk": "^3.0.0",
+        "cli-cursor": "^3.1.0",
+        "cli-width": "^2.0.0",
+        "external-editor": "^3.0.3",
+        "figures": "^3.0.0",
+        "lodash": "^4.17.15",
+        "mute-stream": "0.0.8",
+        "run-async": "^2.4.0",
+        "rxjs": "^6.5.3",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "through": "^2.3.6"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "4.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+          "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+          "dev": true,
+          "requires": {
+            "@types/color-name": "^1.1.1",
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+          "dev": true
+        },
+        "lodash": {
+          "version": "4.17.15",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+          "dev": true
+        },
+        "run-async": {
+          "version": "2.4.0",
+          "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz",
+          "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==",
+          "dev": true,
+          "requires": {
+            "is-promise": "^2.1.0"
+          }
+        },
+        "string-width": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+          "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
+    "internal-slot": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz",
+      "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==",
+      "dev": true,
+      "requires": {
+        "es-abstract": "^1.17.0-next.1",
+        "has": "^1.0.3",
+        "side-channel": "^1.0.2"
+      }
+    },
+    "ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
+    },
+    "is": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz",
+      "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg=="
+    },
+    "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": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "^2.0.0"
+      }
+    },
+    "is-bluebird": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz",
+      "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI="
+    },
+    "is-buffer": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+      "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
+    },
+    "is-callable": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
+      "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+      "dev": true
+    },
+    "is-date-object": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
+      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
+      "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",
+      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-glob": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true
+    },
+    "is-obj": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+      "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
+    },
+    "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
+    },
+    "is-regex": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
+      "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.3"
+      }
+    },
+    "is-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+      "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
+    },
+    "is-stream-ended": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz",
+      "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw=="
+    },
+    "is-string": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
+      "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
+      "dev": true
+    },
+    "is-symbol": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+      "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+      "dev": true,
+      "requires": {
+        "has-symbols": "^1.0.1"
+      }
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+    },
+    "jade": {
+      "version": "0.26.3",
+      "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
+      "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=",
+      "requires": {
+        "commander": "0.6.1",
+        "mkdirp": "0.3.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz",
+          "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY="
+        },
+        "mkdirp": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
+          "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4="
+        }
+      }
+    },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "dev": true,
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      }
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
+    },
+    "jsesc": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+      "dev": true
+    },
+    "json-bigint": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz",
+      "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=",
+      "requires": {
+        "bignumber.js": "^7.0.0"
+      }
+    },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA="
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+      "dev": true
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+    },
+    "jsonfile": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+      "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+      "requires": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
+    "jsx-ast-utils": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz",
+      "integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==",
+      "dev": true,
+      "requires": {
+        "array-includes": "^3.0.3",
+        "object.assign": "^4.1.0"
+      }
+    },
+    "just-extend": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz",
+      "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==",
+      "dev": true
+    },
+    "jwa": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+      "requires": {
+        "buffer-equal-constant-time": "1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "jws": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+      "requires": {
+        "jwa": "^1.4.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "load-json-file": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
+      "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "parse-json": "^2.2.0",
+        "pify": "^2.0.0",
+        "strip-bom": "^3.0.0"
+      },
+      "dependencies": {
+        "graceful-fs": {
+          "version": "4.2.3",
+          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
+          "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
+          "dev": true
+        },
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
+    "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"
+      }
+    },
+    "lockfile": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz",
+      "integrity": "sha1-B/gZ0lrkj4flOOZXi2lkpJgaVgk=",
+      "requires": {
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "lodash": {
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+    },
+    "lodash.at": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz",
+      "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g="
+    },
+    "lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
+    },
+    "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.has": {
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz",
+      "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI="
+    },
+    "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.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true
+    },
+    "lodash.pickby": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
+      "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8="
+    },
+    "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
+    },
+    "log-driver": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz",
+      "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg=="
+    },
+    "log-symbols": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+      "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.2"
+      }
+    },
+    "logger-sharelatex": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.9.1.tgz",
+      "integrity": "sha512-9s6JQnH/PN+Js2CmI8+J3MQCTNlRzP2Dh4pcekXrV6Jm5J4HzyPi+6d3zfBskZ4NBmaUVw9hC4p5dmdaRmh4mQ==",
+      "requires": {
+        "@google-cloud/logging-bunyan": "^2.0.0",
+        "@overleaf/o-error": "^2.0.0",
+        "bunyan": "1.8.12",
+        "raven": "1.1.3",
+        "request": "2.88.0",
+        "yn": "^3.1.1"
+      },
+      "dependencies": {
+        "bunyan": {
+          "version": "1.8.12",
+          "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
+          "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
+          "requires": {
+            "dtrace-provider": "~0.8",
+            "moment": "^2.10.6",
+            "mv": "~2",
+            "safe-json-stringify": "~1"
+          }
+        },
+        "dtrace-provider": {
+          "version": "0.8.8",
+          "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
+          "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
+          "optional": true,
+          "requires": {
+            "nan": "^2.14.0"
+          }
+        },
+        "nan": {
+          "version": "2.14.0",
+          "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+          "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
+          "optional": true
+        },
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+        },
+        "request": {
+          "version": "2.88.0",
+          "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+          "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+          "requires": {
+            "aws-sign2": "~0.7.0",
+            "aws4": "^1.8.0",
+            "caseless": "~0.12.0",
+            "combined-stream": "~1.0.6",
+            "extend": "~3.0.2",
+            "forever-agent": "~0.6.1",
+            "form-data": "~2.3.2",
+            "har-validator": "~5.1.0",
+            "http-signature": "~1.2.0",
+            "is-typedarray": "~1.0.0",
+            "isstream": "~0.1.2",
+            "json-stringify-safe": "~5.0.1",
+            "mime-types": "~2.1.19",
+            "oauth-sign": "~0.9.0",
+            "performance-now": "^2.1.0",
+            "qs": "~6.5.2",
+            "safe-buffer": "^5.1.2",
+            "tough-cookie": "~2.4.3",
+            "tunnel-agent": "^0.6.0",
+            "uuid": "^3.3.2"
+          }
+        }
+      }
+    },
+    "loglevel": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz",
+      "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==",
+      "dev": true
+    },
+    "loglevel-colored-level-prefix": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz",
+      "integrity": "sha1-akAhj9x64V/HbD0PPmdsRlOIYD4=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "loglevel": "^1.4.1"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "long": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+      "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+    },
+    "loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "dev": true,
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "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==",
+      "requires": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "lsmod": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz",
+      "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks="
+    },
+    "lynx": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.2.0.tgz",
+      "integrity": "sha1-eeZnRTDaQYPoeVO9aGFx4HDaULk=",
+      "requires": {
+        "mersenne": "~0.0.3",
+        "statsd-parser": "~0.0.4"
+      }
+    },
+    "make-plural": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz",
+      "integrity": "sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==",
+      "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,
+          "optional": true
+        }
+      }
+    },
+    "map-obj": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz",
+      "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g=="
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+    },
+    "mersenne": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz",
+      "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU="
+    },
+    "messageformat": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-2.3.0.tgz",
+      "integrity": "sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==",
+      "dev": true,
+      "requires": {
+        "make-plural": "^4.3.0",
+        "messageformat-formatters": "^2.0.1",
+        "messageformat-parser": "^4.1.2"
+      }
+    },
+    "messageformat-formatters": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz",
+      "integrity": "sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg==",
+      "dev": true
+    },
+    "messageformat-parser": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-4.1.2.tgz",
+      "integrity": "sha512-7dWuifeyldz7vhEuL96Kwq1fhZXBW+TUfbnHN4UCrCxoXQTYjHnR78eI66Gk9LaLLsAvzPNVJBaa66DRfFNaiA==",
+      "dev": true
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+    },
+    "metrics-sharelatex": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.6.0.tgz",
+      "integrity": "sha512-kPWCtgBrRZwLXCxqJVVn3c7g+GHQEBGYBpwCIt0Vqp0NaKvgKiPkJMkoPg9vkCsjsN2AgpGxXcOAdnHAjxfrzA==",
+      "requires": {
+        "@google-cloud/debug-agent": "^3.0.0",
+        "@google-cloud/profiler": "^0.2.3",
+        "@google-cloud/trace-agent": "^3.2.0",
+        "coffee-script": "1.6.0",
+        "lynx": "~0.1.1",
+        "prom-client": "^11.1.3",
+        "underscore": "~1.6.0",
+        "yn": "^3.1.1"
+      },
+      "dependencies": {
+        "lynx": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz",
+          "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=",
+          "requires": {
+            "mersenne": "~0.0.3",
+            "statsd-parser": "~0.0.4"
+          }
+        },
+        "underscore": {
+          "version": "1.6.0",
+          "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
+          "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag="
+        }
+      }
+    },
+    "mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+    },
+    "mime-db": {
+      "version": "1.43.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
+      "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
+    },
+    "mime-types": {
+      "version": "2.1.26",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
+      "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
+      "requires": {
+        "mime-db": "1.43.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+    },
+    "minipass": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
+      "integrity": "sha1-ys6+SSAiSX9law8PUeJoKp7S2Eg=",
+      "requires": {
+        "safe-buffer": "^5.1.2",
+        "yallist": "^3.0.0"
+      }
+    },
+    "minizlib": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
+      "integrity": "sha1-3SfqYTYkPHyIBoToZyuzpF/ZthQ=",
+      "requires": {
+        "minipass": "^2.2.1"
+      }
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "mocha": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.0.tgz",
+      "integrity": "sha512-MymHK8UkU0K15Q/zX7uflZgVoRWiTjy0fXE/QjKts6mowUvGxOdPhZ2qj3b0iZdUrNZlW9LAIMFHB4IW+2b3EQ==",
+      "dev": true,
+      "requires": {
+        "ansi-colors": "3.2.3",
+        "browser-stdout": "1.3.1",
+        "chokidar": "3.3.0",
+        "debug": "3.2.6",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "find-up": "3.0.0",
+        "glob": "7.1.3",
+        "growl": "1.10.5",
+        "he": "1.2.0",
+        "js-yaml": "3.13.1",
+        "log-symbols": "3.0.0",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "ms": "2.1.1",
+        "node-environment-flags": "1.0.6",
+        "object.assign": "4.1.0",
+        "strip-json-comments": "2.0.1",
+        "supports-color": "6.0.0",
+        "which": "1.3.1",
+        "wide-align": "1.1.3",
+        "yargs": "13.3.0",
+        "yargs-parser": "13.1.1",
+        "yargs-unparser": "1.6.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "diff": {
+          "version": "3.5.0",
+          "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+          "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+          "dev": true
+        },
+        "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"
+          }
+        },
+        "growl": {
+          "version": "1.10.5",
+          "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+          "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+          "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"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "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==",
+          "dev": true
+        },
+        "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"
+          }
+        }
+      }
+    },
+    "module-details-from-path": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
+      "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is="
+    },
+    "moment": {
+      "version": "2.23.0",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz",
+      "integrity": "sha1-dZ6kkayX1UusWtd2mW4qWMwbwiU="
+    },
+    "moment-timezone": {
+      "version": "0.5.28",
+      "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.28.tgz",
+      "integrity": "sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw==",
+      "requires": {
+        "moment": ">= 2.9.0"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "mute-stream": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+      "dev": true
+    },
+    "mv": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
+      "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
+      "optional": true,
+      "requires": {
+        "mkdirp": "~0.5.1",
+        "ncp": "~2.0.0",
+        "rimraf": "~2.4.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "6.0.4",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+          "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
+          "optional": true,
+          "requires": {
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "2 || 3",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "optional": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "rimraf": {
+          "version": "2.4.5",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
+          "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
+          "optional": true,
+          "requires": {
+            "glob": "^6.0.1"
+          }
+        }
+      }
+    },
+    "mysql": {
+      "version": "2.18.1",
+      "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
+      "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
+      "requires": {
+        "bignumber.js": "9.0.0",
+        "readable-stream": "2.3.7",
+        "safe-buffer": "5.1.2",
+        "sqlstring": "2.3.1"
+      },
+      "dependencies": {
+        "bignumber.js": {
+          "version": "9.0.0",
+          "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
+          "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
+        },
+        "readable-stream": {
+          "version": "2.3.7",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+          "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+          "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"
+          }
+        }
+      }
+    },
+    "nan": {
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+      "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "dev": true
+    },
+    "ncp": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
+      "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
+      "optional": true
+    },
+    "needle": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz",
+      "integrity": "sha1-UZMb/4JTOxkot9HWngHxsA/9Kk4=",
+      "requires": {
+        "debug": "^2.1.2",
+        "iconv-lite": "^0.4.4",
+        "sax": "^1.2.4"
+      }
+    },
+    "negotiator": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+      "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+    },
+    "nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+      "dev": true
+    },
+    "nise": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz",
+      "integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.7.0",
+        "@sinonjs/fake-timers": "^6.0.0",
+        "@sinonjs/text-encoding": "^0.7.1",
+        "just-extend": "^4.0.2",
+        "path-to-regexp": "^1.7.0"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "path-to-regexp": {
+          "version": "1.8.0",
+          "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+          "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+          "dev": true,
+          "requires": {
+            "isarray": "0.0.1"
+          }
+        }
+      }
+    },
+    "node-environment-flags": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
+      "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==",
+      "dev": true,
+      "requires": {
+        "object.getownpropertydescriptors": "^2.0.3",
+        "semver": "^5.7.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        }
+      }
+    },
+    "node-fetch": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
+      "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
+    },
+    "node-forge": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz",
+      "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ=="
+    },
+    "node-pre-gyp": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
+      "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
+      "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",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
+      "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
+      "requires": {
+        "abbrev": "1",
+        "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
+    },
+    "npm-bundled": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz",
+      "integrity": "sha1-PBcyt7qTazoQMlrvYWRnwMy8yXk="
+    },
+    "npm-packlist": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz",
+      "integrity": "sha1-Ir3i68EucspIKr1nr8UetJN3JDo=",
+      "requires": {
+        "ignore-walk": "^3.0.1",
+        "npm-bundled": "^1.0.1"
+      }
+    },
+    "npmlog": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+      "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=",
+      "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",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+    },
+    "oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+    },
+    "object-inspect": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
+      "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
+      "dev": true
+    },
+    "object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "dev": true
+    },
+    "object.assign": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+      "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "function-bind": "^1.1.1",
+        "has-symbols": "^1.0.0",
+        "object-keys": "^1.0.11"
+      }
+    },
+    "object.entries": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz",
+      "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3"
+      }
+    },
+    "object.fromentries": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz",
+      "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3"
+      }
+    },
+    "object.getownpropertydescriptors": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
+      "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1"
+      }
+    },
+    "object.values": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz",
+      "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3"
+      }
+    },
+    "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",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
+      "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "^2.1.0"
+      }
+    },
+    "optionator": {
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+      "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+      "dev": true,
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.6",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "word-wrap": "~1.2.3"
+      }
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+    },
+    "osenv": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+      "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=",
+      "requires": {
+        "os-homedir": "^1.0.0",
+        "os-tmpdir": "^1.0.0"
+      }
+    },
+    "p-limit": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
+      "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
+      "requires": {
+        "p-try": "^2.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"
+      },
+      "dependencies": {
+        "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-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
+        }
+      }
+    },
+    "p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
+    },
+    "parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "parse-duration": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.2.tgz",
+      "integrity": "sha512-0qfMZyjOUFBeEIvJ5EayfXJqaEXxQ+Oj2b7tWJM3hvEXvXsYCk05EDVI23oYnEw2NaFYUWdABEVPBvBMh8L/pA=="
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "dev": true,
+      "requires": {
+        "error-ex": "^1.2.0"
+      }
+    },
+    "parse-ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
+      "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="
+    },
+    "parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+    },
+    "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": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "dev": true
+    },
+    "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=="
+    },
+    "path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+    },
+    "path-type": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
+      "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
+      "dev": true,
+      "requires": {
+        "pify": "^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
+        }
+      }
+    },
+    "pathval": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
+      "dev": true
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
+    },
+    "picomatch": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz",
+      "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
+      "dev": true
+    },
+    "pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
+    },
+    "pkg-dir": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
+      "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
+      "dev": true,
+      "requires": {
+        "find-up": "^2.1.0"
+      }
+    },
+    "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
+    },
+    "prettier": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
+      "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
+      "dev": true
+    },
+    "prettier-eslint": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-9.0.1.tgz",
+      "integrity": "sha512-KZT65QTosSAqBBqmrC+RpXbsMRe7Os2YSR9cAfFbDlyPAopzA/S5bioiZ3rpziNQNSJaOxmtXSx07EQ+o2Dlug==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/parser": "^1.10.2",
+        "common-tags": "^1.4.0",
+        "core-js": "^3.1.4",
+        "dlv": "^1.1.0",
+        "eslint": "^5.0.0",
+        "indent-string": "^4.0.0",
+        "lodash.merge": "^4.6.0",
+        "loglevel-colored-level-prefix": "^1.0.0",
+        "prettier": "^1.7.0",
+        "pretty-format": "^23.0.1",
+        "require-relative": "^0.8.7",
+        "typescript": "^3.2.1",
+        "vue-eslint-parser": "^2.0.2"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.11.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
+          "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ansi-escapes": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+          "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+          "dev": true
+        },
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^2.0.0"
+          }
+        },
+        "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"
+          }
+        },
+        "eslint": {
+          "version": "5.16.0",
+          "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
+          "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "ajv": "^6.9.1",
+            "chalk": "^2.1.0",
+            "cross-spawn": "^6.0.5",
+            "debug": "^4.0.1",
+            "doctrine": "^3.0.0",
+            "eslint-scope": "^4.0.3",
+            "eslint-utils": "^1.3.1",
+            "eslint-visitor-keys": "^1.0.0",
+            "espree": "^5.0.1",
+            "esquery": "^1.0.1",
+            "esutils": "^2.0.2",
+            "file-entry-cache": "^5.0.1",
+            "functional-red-black-tree": "^1.0.1",
+            "glob": "^7.1.2",
+            "globals": "^11.7.0",
+            "ignore": "^4.0.6",
+            "import-fresh": "^3.0.0",
+            "imurmurhash": "^0.1.4",
+            "inquirer": "^6.2.2",
+            "js-yaml": "^3.13.0",
+            "json-stable-stringify-without-jsonify": "^1.0.1",
+            "levn": "^0.3.0",
+            "lodash": "^4.17.11",
+            "minimatch": "^3.0.4",
+            "mkdirp": "^0.5.1",
+            "natural-compare": "^1.4.0",
+            "optionator": "^0.8.2",
+            "path-is-inside": "^1.0.2",
+            "progress": "^2.0.0",
+            "regexpp": "^2.0.1",
+            "semver": "^5.5.1",
+            "strip-ansi": "^4.0.0",
+            "strip-json-comments": "^2.0.1",
+            "table": "^5.2.3",
+            "text-table": "^0.2.0"
+          }
+        },
+        "eslint-scope": {
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+          "dev": true,
+          "requires": {
+            "esrecurse": "^4.1.0",
+            "estraverse": "^4.1.1"
+          }
+        },
+        "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"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+          "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
+          "dev": true
+        },
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5"
+          }
+        },
+        "inquirer": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
+          "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "^3.2.0",
+            "chalk": "^2.4.2",
+            "cli-cursor": "^2.1.0",
+            "cli-width": "^2.0.0",
+            "external-editor": "^3.0.3",
+            "figures": "^2.0.0",
+            "lodash": "^4.17.12",
+            "mute-stream": "0.0.7",
+            "run-async": "^2.2.0",
+            "rxjs": "^6.4.0",
+            "string-width": "^2.1.0",
+            "strip-ansi": "^5.1.0",
+            "through": "^2.3.6"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "4.1.0",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+              "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+              "dev": true
+            },
+            "lodash": {
+              "version": "4.17.15",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+              "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "5.2.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+              "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^4.1.0"
+              }
+            }
+          }
+        },
+        "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
+        },
+        "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
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        },
+        "mute-stream": {
+          "version": "0.0.7",
+          "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+          "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+          "dev": true
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "^2.0.0",
+            "signal-exit": "^3.0.2"
+          }
+        },
+        "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"
+          }
+        }
+      }
+    },
+    "prettier-eslint-cli": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-eslint-cli/-/prettier-eslint-cli-5.0.0.tgz",
+      "integrity": "sha512-cei9UbN1aTrz3sQs88CWpvY/10PYTevzd76zoG1tdJ164OhmNTFRKPTOZrutVvscoQWzbnLKkviS3gu5JXwvZg==",
+      "dev": true,
+      "requires": {
+        "arrify": "^2.0.1",
+        "boolify": "^1.0.0",
+        "camelcase-keys": "^6.0.0",
+        "chalk": "^2.4.2",
+        "common-tags": "^1.8.0",
+        "core-js": "^3.1.4",
+        "eslint": "^5.0.0",
+        "find-up": "^4.1.0",
+        "get-stdin": "^7.0.0",
+        "glob": "^7.1.4",
+        "ignore": "^5.1.2",
+        "lodash.memoize": "^4.1.2",
+        "loglevel-colored-level-prefix": "^1.0.0",
+        "messageformat": "^2.2.1",
+        "prettier-eslint": "^9.0.0",
+        "rxjs": "^6.5.2",
+        "yargs": "^13.2.4"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.11.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
+          "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ansi-escapes": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+          "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+          "dev": true
+        },
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^2.0.0"
+          }
+        },
+        "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"
+          }
+        },
+        "eslint": {
+          "version": "5.16.0",
+          "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
+          "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "ajv": "^6.9.1",
+            "chalk": "^2.1.0",
+            "cross-spawn": "^6.0.5",
+            "debug": "^4.0.1",
+            "doctrine": "^3.0.0",
+            "eslint-scope": "^4.0.3",
+            "eslint-utils": "^1.3.1",
+            "eslint-visitor-keys": "^1.0.0",
+            "espree": "^5.0.1",
+            "esquery": "^1.0.1",
+            "esutils": "^2.0.2",
+            "file-entry-cache": "^5.0.1",
+            "functional-red-black-tree": "^1.0.1",
+            "glob": "^7.1.2",
+            "globals": "^11.7.0",
+            "ignore": "^4.0.6",
+            "import-fresh": "^3.0.0",
+            "imurmurhash": "^0.1.4",
+            "inquirer": "^6.2.2",
+            "js-yaml": "^3.13.0",
+            "json-stable-stringify-without-jsonify": "^1.0.1",
+            "levn": "^0.3.0",
+            "lodash": "^4.17.11",
+            "minimatch": "^3.0.4",
+            "mkdirp": "^0.5.1",
+            "natural-compare": "^1.4.0",
+            "optionator": "^0.8.2",
+            "path-is-inside": "^1.0.2",
+            "progress": "^2.0.0",
+            "regexpp": "^2.0.1",
+            "semver": "^5.5.1",
+            "strip-ansi": "^4.0.0",
+            "strip-json-comments": "^2.0.1",
+            "table": "^5.2.3",
+            "text-table": "^0.2.0"
+          },
+          "dependencies": {
+            "ignore": {
+              "version": "4.0.6",
+              "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+              "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+              "dev": true
+            }
+          }
+        },
+        "eslint-scope": {
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+          "dev": true,
+          "requires": {
+            "esrecurse": "^4.1.0",
+            "estraverse": "^4.1.1"
+          }
+        },
+        "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"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+          "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
+          "dev": true
+        },
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5"
+          }
+        },
+        "find-up": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^5.0.0",
+            "path-exists": "^4.0.0"
+          }
+        },
+        "get-stdin": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz",
+          "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==",
+          "dev": true
+        },
+        "glob": {
+          "version": "7.1.6",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+          "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "ignore": {
+          "version": "5.1.4",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
+          "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
+          "dev": true
+        },
+        "inquirer": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
+          "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "^3.2.0",
+            "chalk": "^2.4.2",
+            "cli-cursor": "^2.1.0",
+            "cli-width": "^2.0.0",
+            "external-editor": "^3.0.3",
+            "figures": "^2.0.0",
+            "lodash": "^4.17.12",
+            "mute-stream": "0.0.7",
+            "run-async": "^2.2.0",
+            "rxjs": "^6.4.0",
+            "string-width": "^2.1.0",
+            "strip-ansi": "^5.1.0",
+            "through": "^2.3.6"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "4.1.0",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+              "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+              "dev": true
+            },
+            "lodash": {
+              "version": "4.17.15",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+              "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "5.2.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+              "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^4.1.0"
+              }
+            }
+          }
+        },
+        "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
+        },
+        "locate-path": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^4.1.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
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        },
+        "mute-stream": {
+          "version": "0.0.7",
+          "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+          "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+          "dev": true
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.2.0"
+          }
+        },
+        "path-exists": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+          "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+          "dev": true
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "^2.0.0",
+            "signal-exit": "^3.0.2"
+          }
+        },
+        "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"
+          }
+        }
+      }
+    },
+    "prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+      "dev": true,
+      "requires": {
+        "fast-diff": "^1.1.2"
+      }
+    },
+    "pretty-format": {
+      "version": "23.6.0",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz",
+      "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^3.0.0",
+        "ansi-styles": "^3.2.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
+        }
+      }
+    },
+    "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==",
+      "requires": {
+        "parse-ms": "^2.0.0"
+      }
+    },
+    "process-nextick-args": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+      "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o="
+    },
+    "progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "dev": true
+    },
+    "prom-client": {
+      "version": "11.5.3",
+      "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.3.tgz",
+      "integrity": "sha512-iz22FmTbtkyL2vt0MdDFY+kWof+S9UB/NACxSn2aJcewtw+EERsen0urSkZ2WrHseNdydsvcxCTAnPcSMZZv4Q==",
+      "requires": {
+        "tdigest": "^0.1.1"
+      }
+    },
+    "prop-types": {
+      "version": "15.7.2",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+      "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+      "dev": true,
+      "requires": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.8.1"
+      }
+    },
+    "protobufjs": {
+      "version": "6.8.8",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz",
+      "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==",
+      "requires": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/long": "^4.0.0",
+        "@types/node": "^10.1.0",
+        "long": "^4.0.0"
+      }
+    },
+    "proxy-addr": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
+      "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
+      "requires": {
+        "forwarded": "~0.1.2",
+        "ipaddr.js": "1.9.1"
+      }
+    },
+    "psl": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
+      "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ=="
+    },
+    "pump": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "pumpify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz",
+      "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==",
+      "requires": {
+        "duplexify": "^4.1.1",
+        "inherits": "^2.0.3",
+        "pump": "^3.0.0"
+      },
+      "dependencies": {
+        "duplexify": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz",
+          "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==",
+          "requires": {
+            "end-of-stream": "^1.4.1",
+            "inherits": "^2.0.3",
+            "readable-stream": "^3.1.1",
+            "stream-shift": "^1.0.0"
+          }
+        },
+        "pump": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+          "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+          "requires": {
+            "end-of-stream": "^1.1.0",
+            "once": "^1.3.1"
+          }
+        },
+        "readable-stream": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
+          "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew="
+    },
+    "qs": {
+      "version": "6.7.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+      "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+    },
+    "quick-lru": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
+      "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
+      "dev": true
+    },
+    "ramda": {
+      "version": "0.27.0",
+      "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz",
+      "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==",
+      "dev": true
+    },
+    "range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+    },
+    "raven": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/raven/-/raven-1.1.3.tgz",
+      "integrity": "sha1-QnPBrm005CMPUbLAEEGjK5Iygio=",
+      "requires": {
+        "cookie": "0.3.1",
+        "json-stringify-safe": "5.0.1",
+        "lsmod": "1.0.0",
+        "stack-trace": "0.0.9",
+        "uuid": "3.0.0"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz",
+          "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg="
+        }
+      }
+    },
+    "raw-body": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+      "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+      "requires": {
+        "bytes": "3.1.0",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "dependencies": {
+        "iconv-lite": {
+          "version": "0.4.24",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+          "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        }
+      }
+    },
+    "rc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+      "integrity": "sha1-zZJL9SAKB1uDwYjNa54hG3/A0+0=",
+      "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",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        }
+      }
+    },
+    "react-is": {
+      "version": "16.13.0",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz",
+      "integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==",
+      "dev": true
+    },
+    "read-pkg": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
+      "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
+      "dev": true,
+      "requires": {
+        "load-json-file": "^2.0.0",
+        "normalize-package-data": "^2.3.2",
+        "path-type": "^2.0.0"
+      }
+    },
+    "read-pkg-up": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
+      "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
+      "dev": true,
+      "requires": {
+        "find-up": "^2.0.0",
+        "read-pkg": "^2.0.0"
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+      "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=",
+      "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"
+      }
+    },
+    "readdirp": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
+      "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
+      "dev": true,
+      "requires": {
+        "picomatch": "^2.0.4"
+      }
+    },
+    "regenerator-runtime": {
+      "version": "0.13.3",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
+      "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==",
+      "dev": true
+    },
+    "regexp.prototype.flags": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz",
+      "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1"
+      }
+    },
+    "regexpp": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+      "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+      "dev": true
+    },
+    "request": {
+      "version": "2.88.2",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
+      "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "har-validator": "~5.1.3",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.5.0",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^3.3.2"
+      },
+      "dependencies": {
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+        },
+        "tough-cookie": {
+          "version": "2.5.0",
+          "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+          "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+          "requires": {
+            "psl": "^1.1.28",
+            "punycode": "^2.1.1"
+          }
+        }
+      }
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "dev": true
+    },
+    "require-in-the-middle": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-4.0.1.tgz",
+      "integrity": "sha512-EfkM2zANyGkrfIExsECMeNn/uzjvHrE9h36yLXSavmrDiH4tgDNvltAmEKnt4PNLbqKPHZz+uszW2wTKrLUX0w==",
+      "requires": {
+        "debug": "^4.1.1",
+        "module-details-from-path": "^1.0.3",
+        "resolve": "^1.12.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "require-like": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
+      "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=",
+      "dev": true
+    },
+    "require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+      "dev": true
+    },
+    "require-relative": {
+      "version": "0.8.7",
+      "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
+      "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=",
+      "dev": true
+    },
+    "resolve": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
+      "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==",
+      "requires": {
+        "path-parse": "^1.0.6"
+      }
+    },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true
+    },
+    "restore-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+      "dev": true,
+      "requires": {
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "retry-as-promised": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz",
+      "integrity": "sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==",
+      "requires": {
+        "any-promise": "^1.3.0"
+      }
+    },
+    "retry-axios": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz",
+      "integrity": "sha512-jp4YlI0qyDFfXiXGhkCOliBN1G7fRH03Nqy8YdShzGqbY5/9S2x/IR6C88ls2DFkbWuL3ASkP7QD3pVrNpPgwQ=="
+    },
+    "retry-request": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz",
+      "integrity": "sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ==",
+      "requires": {
+        "debug": "^4.1.1",
+        "through2": "^3.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "rimraf": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+      "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=",
+      "requires": {
+        "glob": "^7.0.5"
+      }
+    },
+    "run-async": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+      "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+      "dev": true,
+      "requires": {
+        "is-promise": "^2.1.0"
+      }
+    },
+    "rxjs": {
+      "version": "6.5.4",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
+      "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0="
+    },
+    "safe-json-stringify": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
+      "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
+      "optional": true
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo="
+    },
+    "sandboxed-module": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-2.0.3.tgz",
+      "integrity": "sha1-x+VFkzm7y6KMUwPusz9ug4e/upY=",
+      "dev": true,
+      "requires": {
+        "require-like": "0.1.2",
+        "stack-trace": "0.0.9"
+      }
+    },
+    "sax": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+      "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk="
+    },
+    "semver": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+      "integrity": "sha1-fnQlb7qknHWqfHogXMInmcrIAAQ="
+    },
+    "send": {
+      "version": "0.17.1",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+      "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "destroy": "~1.0.4",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "~1.7.2",
+        "mime": "1.6.0",
+        "ms": "2.1.1",
+        "on-finished": "~2.3.0",
+        "range-parser": "~1.2.1",
+        "statuses": "~1.5.0"
+      },
+      "dependencies": {
+        "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=="
+        }
+      }
+    },
+    "sequelize": {
+      "version": "5.21.5",
+      "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.21.5.tgz",
+      "integrity": "sha512-n9hR5K4uQGmBGK/Y/iqewCeSFmKVsd0TRnh0tfoLoAkmXbKC4tpeK96RhKs7d+TTMtrJlgt2TNLVBaAxEwC4iw==",
+      "requires": {
+        "bluebird": "^3.5.0",
+        "cls-bluebird": "^2.1.0",
+        "debug": "^4.1.1",
+        "dottie": "^2.0.0",
+        "inflection": "1.12.0",
+        "lodash": "^4.17.15",
+        "moment": "^2.24.0",
+        "moment-timezone": "^0.5.21",
+        "retry-as-promised": "^3.2.0",
+        "semver": "^6.3.0",
+        "sequelize-pool": "^2.3.0",
+        "toposort-class": "^1.0.1",
+        "uuid": "^3.3.3",
+        "validator": "^10.11.0",
+        "wkx": "^0.4.8"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "moment": {
+          "version": "2.24.0",
+          "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
+          "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+        }
+      }
+    },
+    "sequelize-pool": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz",
+      "integrity": "sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA=="
+    },
+    "serve-static": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+      "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
+      "requires": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.17.1"
+      }
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
+    },
+    "setprototypeof": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+      "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+    },
+    "settings-sharelatex": {
+      "version": "git+https://github.com/sharelatex/settings-sharelatex.git#93f63d029b52fef8825c3a401b2b6a7ba29b4750",
+      "from": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0",
+      "requires": {
+        "coffee-script": "1.6.0"
+      }
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "dev": true
+    },
+    "shimmer": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz",
+      "integrity": "sha1-+Wb3VVeJdj502IQRk2haXnhzZmU="
+    },
+    "side-channel": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz",
+      "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==",
+      "dev": true,
+      "requires": {
+        "es-abstract": "^1.17.0-next.1",
+        "object-inspect": "^1.7.0"
+      }
+    },
+    "sigmund": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
+      "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA="
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
+    },
+    "sinon": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.1.tgz",
+      "integrity": "sha512-iTTyiQo5T94jrOx7X7QLBZyucUJ2WvL9J13+96HMfm2CGoJYbIPqRfl6wgNcqmzk0DI28jeGx5bUTXizkrqBmg==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.7.0",
+        "@sinonjs/fake-timers": "^6.0.0",
+        "@sinonjs/formatio": "^5.0.1",
+        "@sinonjs/samsam": "^5.0.3",
+        "diff": "^4.0.2",
+        "nise": "^4.0.1",
+        "supports-color": "^7.1.0"
+      },
+      "dependencies": {
+        "diff": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+          "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
+    "slice-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+      "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "astral-regex": "^1.0.0",
+        "is-fullwidth-code-point": "^2.0.0"
+      },
+      "dependencies": {
+        "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
+        }
+      }
+    },
+    "smoke-test-sharelatex": {
+      "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c",
+      "from": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0",
+      "requires": {
+        "mocha": "~1.17.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "3.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz",
+          "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=",
+          "requires": {
+            "graceful-fs": "~2.0.0",
+            "inherits": "2",
+            "minimatch": "~0.2.11"
+          }
+        },
+        "graceful-fs": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz",
+          "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA="
+        },
+        "lru-cache": {
+          "version": "2.7.3",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
+          "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI="
+        },
+        "minimatch": {
+          "version": "0.2.14",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
+          "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=",
+          "requires": {
+            "lru-cache": "2",
+            "sigmund": "~1.0.0"
+          }
+        },
+        "mocha": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz",
+          "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=",
+          "requires": {
+            "commander": "2.0.0",
+            "debug": "*",
+            "diff": "1.0.7",
+            "glob": "3.2.3",
+            "growl": "1.7.x",
+            "jade": "0.26.3",
+            "mkdirp": "0.3.5"
+          },
+          "dependencies": {
+            "mkdirp": {
+              "version": "0.3.5",
+              "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
+              "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc="
+            }
+          }
+        }
+      }
+    },
+    "snakecase-keys": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.1.0.tgz",
+      "integrity": "sha512-QM038drLbhdOY5HcRQVjO1ZJ1WR7yV5D5TIBzcOB/g3f5HURHhfpYEnvOyzXet8K+MQsgeIUA7O7vn90nAX6EA==",
+      "requires": {
+        "map-obj": "^4.0.0",
+        "to-snake-case": "^1.0.0"
+      }
+    },
+    "source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+    },
+    "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.5",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
+      "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
+      "dev": true
+    },
+    "split": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
+      "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
+      "requires": {
+        "through": "2"
+      }
+    },
+    "split-ca": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
+      "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY="
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
+    "sqlite3": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz",
+      "integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==",
+      "requires": {
+        "nan": "^2.12.1",
+        "node-pre-gyp": "^0.11.0",
+        "request": "^2.87.0"
+      }
+    },
+    "sqlstring": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
+      "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A="
+    },
+    "ssh2": {
+      "version": "0.8.8",
+      "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.8.tgz",
+      "integrity": "sha512-egJVQkf3sbjECTY6rCeg8rgV/fab6S/7E5kpYqHT3Fe/YpfJbLYeA1qTcB2d+LRUUAjqKi7rlbfWkaP66YdpAQ==",
+      "requires": {
+        "ssh2-streams": "~0.4.9"
+      }
+    },
+    "ssh2-streams": {
+      "version": "0.4.9",
+      "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.9.tgz",
+      "integrity": "sha512-glMQKeYKuA+rLaH16fJC3nZMV1BWklbxuYCR4C5/LlBSf2yaoNRpPU7Ul702xsi5nvYjIx9XDkKBJwrBjkDynw==",
+      "requires": {
+        "asn1": "~0.2.0",
+        "bcrypt-pbkdf": "^1.0.2",
+        "streamsearch": "~0.1.2"
+      }
+    },
+    "sshpk": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+      "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
+    "stack-trace": {
+      "version": "0.0.9",
+      "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz",
+      "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU="
+    },
+    "statsd-parser": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz",
+      "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0="
+    },
+    "statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+    },
+    "stream-events": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
+      "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
+      "requires": {
+        "stubs": "^3.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="
+    },
+    "streamsearch": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+      "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+    },
+    "string-width": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+      "requires": {
+        "code-point-at": "^1.0.0",
+        "is-fullwidth-code-point": "^1.0.0",
+        "strip-ansi": "^3.0.0"
+      }
+    },
+    "string.prototype.matchall": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz",
+      "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0",
+        "has-symbols": "^1.0.1",
+        "internal-slot": "^1.0.2",
+        "regexp.prototype.flags": "^1.3.0",
+        "side-channel": "^1.0.2"
+      }
+    },
+    "string.prototype.trimleft": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz",
+      "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "function-bind": "^1.1.1"
+      }
+    },
+    "string.prototype.trimright": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz",
+      "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "function-bind": "^1.1.1"
+      }
+    },
+    "string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
+      "requires": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "3.0.1",
+      "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-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+    },
+    "stubs": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
+      "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
+    },
+    "supports-color": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
+      "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "table": {
+      "version": "5.4.6",
+      "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
+      "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.10.2",
+        "lodash": "^4.17.14",
+        "slice-ansi": "^2.1.0",
+        "string-width": "^3.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.11.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
+          "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "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
+        },
+        "fast-deep-equal": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+          "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
+          "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
+        },
+        "lodash": {
+          "version": "4.17.15",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "tar": {
+      "version": "4.4.8",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz",
+      "integrity": "sha1-sZ7sP94qluZGZt+f20DFyhvDdH0=",
+      "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"
+      },
+      "dependencies": {
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        }
+      }
+    },
+    "tar-fs": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz",
+      "integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==",
+      "requires": {
+        "chownr": "^1.1.1",
+        "mkdirp": "^0.5.1",
+        "pump": "^3.0.0",
+        "tar-stream": "^2.0.0"
+      },
+      "dependencies": {
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        }
+      }
+    },
+    "tar-stream": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz",
+      "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==",
+      "requires": {
+        "bl": "^4.0.1",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
+    "tdigest": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz",
+      "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=",
+      "requires": {
+        "bintrees": "1.0.1"
+      }
+    },
+    "teeny-request": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.3.0.tgz",
+      "integrity": "sha512-sN9E3JvEBe2CFqB/jpJpw1erWD1C7MxyYCxogHFCQSyZfkHYcdf4wzVQSw7FZxbwcfnS+FP0W9BS0mp6SEOKjg==",
+      "requires": {
+        "http-proxy-agent": "^2.1.0",
+        "https-proxy-agent": "^3.0.0",
+        "node-fetch": "^2.2.0",
+        "stream-events": "^1.0.5",
+        "uuid": "^3.3.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": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+    },
+    "through2": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz",
+      "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==",
+      "requires": {
+        "readable-stream": "2 || 3"
+      }
+    },
+    "timekeeper": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.2.0.tgz",
+      "integrity": "sha512-W3AmPTJWZkRwu+iSNxPIsLZ2ByADsOLbbLxe46UJyWj3mlYLlwucKiq+/dPm0l9wTzqoF3/2PH0AGFCebjq23A==",
+      "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-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-no-case": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz",
+      "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo="
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "to-snake-case": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/to-snake-case/-/to-snake-case-1.0.0.tgz",
+      "integrity": "sha1-znRpE4l5RgGah+Yu366upMYIq4w=",
+      "requires": {
+        "to-space-case": "^1.0.0"
+      }
+    },
+    "to-space-case": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz",
+      "integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=",
+      "requires": {
+        "to-no-case": "^1.0.0"
+      }
+    },
+    "toidentifier": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+      "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+    },
+    "toposort-class": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
+      "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg="
+    },
+    "tough-cookie": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+      "requires": {
+        "psl": "^1.1.24",
+        "punycode": "^1.4.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+        }
+      }
+    },
+    "tslib": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
+      "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
+      "dev": true
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
+    },
+    "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-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true
+    },
+    "type-fest": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="
+    },
+    "type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      }
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+    },
+    "typescript": {
+      "version": "3.7.5",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz",
+      "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==",
+      "dev": true
+    },
+    "underscore": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz",
+      "integrity": "sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ=="
+    },
+    "universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=",
+      "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="
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+    },
+    "uuid": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
+      "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
+    },
+    "v8-compile-cache": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
+      "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
+      "dev": true
+    },
+    "v8-profiler-node8": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/v8-profiler-node8/-/v8-profiler-node8-6.1.1.tgz",
+      "integrity": "sha512-mKS7TXRRYi70hvbv5c1tk9AbuqNrtbLc+jFLlsZ2TpaC1l5lWryBlDLZKJ1JP6hjSbMEjW1ucjWLSaKsaPnGXg==",
+      "requires": {
+        "nan": "^2.14.0",
+        "node-pre-gyp": "^0.13.0"
+      },
+      "dependencies": {
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "nan": {
+          "version": "2.14.0",
+          "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+          "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
+        },
+        "node-pre-gyp": {
+          "version": "0.13.0",
+          "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz",
+          "integrity": "sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==",
+          "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"
+          }
+        }
+      }
+    },
+    "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"
+      }
+    },
+    "validator": {
+      "version": "10.11.0",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz",
+      "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw=="
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "vue-eslint-parser": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz",
+      "integrity": "sha512-ZezcU71Owm84xVF6gfurBQUGg8WQ+WZGxgDEQu1IHFBZNx7BFZg3L1yHxrCBNNwbwFtE1GuvfJKMtb6Xuwc/Bw==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0",
+        "eslint-scope": "^3.7.1",
+        "eslint-visitor-keys": "^1.0.0",
+        "espree": "^3.5.2",
+        "esquery": "^1.0.0",
+        "lodash": "^4.17.4"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "5.7.3",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+          "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+          "dev": true
+        },
+        "acorn-jsx": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
+          "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
+          "dev": true,
+          "requires": {
+            "acorn": "^3.0.4"
+          },
+          "dependencies": {
+            "acorn": {
+              "version": "3.3.0",
+              "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+              "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
+              "dev": true
+            }
+          }
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "eslint-scope": {
+          "version": "3.7.3",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz",
+          "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==",
+          "dev": true,
+          "requires": {
+            "esrecurse": "^4.1.0",
+            "estraverse": "^4.1.1"
+          }
+        },
+        "espree": {
+          "version": "3.5.4",
+          "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
+          "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
+          "dev": true,
+          "requires": {
+            "acorn": "^5.5.0",
+            "acorn-jsx": "^3.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
+      }
+    },
+    "walkdir": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz",
+      "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ=="
+    },
+    "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"
+      }
+    },
+    "which-module": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+      "dev": true
+    },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha1-rgdOa9wMFKQx6ATmJFScYzsABFc=",
+      "requires": {
+        "string-width": "^1.0.2 || 2"
+      }
+    },
+    "wkx": {
+      "version": "0.4.8",
+      "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.8.tgz",
+      "integrity": "sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "word-wrap": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+      "dev": true
+    },
+    "wrap-ansi": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+      "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "string-width": "^3.0.0",
+        "strip-ansi": "^5.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        },
+        "emoji-regex": {
+          "version": "7.0.3",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+          "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    },
+    "wrench": {
+      "version": "1.5.9",
+      "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz",
+      "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo="
+    },
+    "write": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
+      "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+      "dev": true,
+      "requires": {
+        "mkdirp": "^0.5.1"
+      },
+      "dependencies": {
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        }
+      }
+    },
+    "xregexp": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz",
+      "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime-corejs3": "^7.8.3"
+      }
+    },
+    "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": "sha1-tLBJ4xS+VF486AIjbWzSLNkcPek="
+    },
+    "yargs": {
+      "version": "13.3.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz",
+      "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==",
+      "dev": true,
+      "requires": {
+        "cliui": "^5.0.0",
+        "find-up": "^3.0.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^3.0.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^13.1.1"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "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
+        },
+        "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"
+          }
+        },
+        "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
+        },
+        "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"
+          }
+        },
+        "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"
+          }
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "yargs-parser": {
+      "version": "13.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
+      "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
+      "dev": true,
+      "requires": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      }
+    },
+    "yargs-unparser": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
+      "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
+      "dev": true,
+      "requires": {
+        "flat": "^4.1.0",
+        "lodash": "^4.17.15",
+        "yargs": "^13.3.0"
+      }
+    },
+    "yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
+    }
+  }
+}

+ 49 - 35
services/clsi/package.json

@@ -7,48 +7,62 @@
     "url": "https://github.com/sharelatex/clsi-sharelatex.git"
   },
   "scripts": {
-    "compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')",
-    "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js",
-    "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js",
-    "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
-    "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP",
-    "compile:unit_tests": "[ ! -e test/unit/coffee ] &&  echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee",
-    "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee",
-    "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && npm run compile:smoke_tests",
+    "start": "node $NODE_APP_OPTIONS app.js",
+    "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
+    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
+    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "nodemon --config nodemon.json",
-    "compile:smoke_tests": "[ ! -e test/smoke/coffee ] &&  echo 'No smoke tests to compile' || coffee -o test/smoke/js -c test/smoke/coffee"
+    "lint": "node_modules/.bin/eslint .",
+    "format": "node_modules/.bin/prettier-eslint $PWD'/**/*.js' --list-different",
+    "format:fix": "node_modules/.bin/prettier-eslint $PWD'/**/*.js' --write"
   },
   "author": "James Allen <james@sharelatex.com>",
   "dependencies": {
-    "async": "0.2.9",
-    "body-parser": "^1.2.0",
-    "dockerode": "^2.5.3",
-    "express": "^4.2.0",
-    "fs-extra": "^0.16.3",
-    "heapdump": "^0.3.5",
-    "lockfile": "^1.0.3",
-    "logger-sharelatex": "^1.9.0",
-    "lynx": "0.0.11",
-    "metrics-sharelatex": "^2.3.0",
-    "mkdirp": "0.3.5",
-    "mysql": "2.6.2",
-    "request": "^2.21.0",
-    "sequelize": "^4.38.0",
+    "async": "3.2.0",
+    "body-parser": "^1.19.0",
+    "dockerode": "^3.1.0",
+    "express": "^4.17.1",
+    "fs-extra": "^8.1.0",
+    "heapdump": "^0.3.15",
+    "lockfile": "^1.0.4",
+    "logger-sharelatex": "^1.9.1",
+    "lynx": "0.2.0",
+    "metrics-sharelatex": "^2.6.0",
+    "mysql": "^2.18.1",
+    "request": "^2.88.2",
+    "sequelize": "^5.21.5",
     "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0",
     "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0",
-    "sqlite3": "^4.0.6",
-    "underscore": "^1.8.2",
-    "v8-profiler-node8": "^6.0.1",
-    "wrench": "~1.5.4"
+    "sqlite3": "^4.1.1",
+    "underscore": "^1.9.2",
+    "v8-profiler-node8": "^6.1.1",
+    "wrench": "~1.5.9"
   },
   "devDependencies": {
-    "bunyan": "^0.22.1",
-    "chai": "~1.8.1",
-    "coffeescript": "1.6.0",
-    "mocha": "^4.0.1",
-    "sandboxed-module": "~0.3.0",
-    "sinon": "~1.7.3",
-    "timekeeper": "0.0.4"
+    "babel-eslint": "^10.1.0",
+    "bunyan": "^1.8.12",
+    "chai": "~4.2.0",
+    "eslint": "^6.8.0",
+    "eslint-config-prettier": "^6.10.0",
+    "eslint-config-standard": "^14.1.0",
+    "eslint-config-standard-jsx": "^8.1.0",
+    "eslint-config-standard-react": "^9.2.0",
+    "eslint-plugin-chai-expect": "^2.1.0",
+    "eslint-plugin-chai-friendly": "^0.5.0",
+    "eslint-plugin-import": "^2.20.1",
+    "eslint-plugin-jsx-a11y": "^6.2.3",
+    "eslint-plugin-mocha": "^6.3.0",
+    "eslint-plugin-node": "^11.0.0",
+    "eslint-plugin-prettier": "^3.1.2",
+    "eslint-plugin-promise": "^4.2.1",
+    "eslint-plugin-react": "^7.19.0",
+    "eslint-plugin-standard": "^4.0.1",
+    "mocha": "^7.1.0",
+    "prettier": "^1.19.1",
+    "prettier-eslint-cli": "^5.0.0",
+    "sandboxed-module": "^2.0.3",
+    "sinon": "~9.0.1",
+    "timekeeper": "2.2.0"
   }
 }

+ 0 - 48
services/clsi/test/acceptance/coffee/BrokenLatexFileTests.coffee

@@ -1,48 +0,0 @@
-Client = require "./helpers/Client"
-request = require "request"
-require("chai").should()
-ClsiApp = require "./helpers/ClsiApp"
-
-describe "Broken LaTeX file", ->
-	before (done)->
-		@broken_request =
-			resources: [
-				path: "main.tex"
-				content: '''
-					\\documentclass{articl % :(
-					\\begin{documen % :(
-					Broken
-					\\end{documen % :(
-				'''
-			]
-		@correct_request =
-			resources: [
-				path: "main.tex"
-				content: '''
-					\\documentclass{article}
-					\\begin{document}
-					Hello world
-					\\end{document}
-				'''
-			]
-		ClsiApp.ensureRunning done
-		
-	describe "on first run", ->
-		before (done) ->
-			@project_id = Client.randomId()
-			Client.compile @project_id, @broken_request, (@error, @res, @body) => done()
-
-		it "should return a failure status", ->
-			@body.compile.status.should.equal "failure"
-
-	describe "on second run", ->
-		before (done) ->
-			@project_id = Client.randomId()
-			Client.compile @project_id, @correct_request, () =>
-				Client.compile @project_id, @broken_request, (@error, @res, @body) =>
-					done()
-
-		it "should return a failure status", ->
-			@body.compile.status.should.equal "failure"
-		
-		

+ 0 - 36
services/clsi/test/acceptance/coffee/DeleteOldFilesTest.coffee

@@ -1,36 +0,0 @@
-Client = require "./helpers/Client"
-request = require "request"
-require("chai").should()
-ClsiApp = require "./helpers/ClsiApp"
-
-describe "Deleting Old Files", ->
-	before (done)->
-		@request =
-			resources: [
-				path: "main.tex"
-				content: '''
-					\\documentclass{article}
-					\\begin{document}
-					Hello world
-					\\end{document}
-				'''
-			]
-		ClsiApp.ensureRunning done
-
-	describe "on first run", ->
-		before (done) ->
-			@project_id = Client.randomId()
-			Client.compile @project_id, @request, (@error, @res, @body) => done()
-
-		it "should return a success status", ->
-			@body.compile.status.should.equal "success"
-
-		describe "after file has been deleted", ->
-			before (done) ->
-				@request.resources = []
-				Client.compile @project_id, @request, (@error, @res, @body) =>
-					done()
-
-			it "should return a failure status", ->
-				@body.compile.status.should.equal "failure"
-

+ 0 - 129
services/clsi/test/acceptance/coffee/ExampleDocumentTests.coffee

@@ -1,129 +0,0 @@
-Client = require "./helpers/Client"
-request = require "request"
-require("chai").should()
-fs = require "fs"
-ChildProcess = require "child_process"
-ClsiApp = require "./helpers/ClsiApp"
-logger = require("logger-sharelatex")
-Path = require("path")
-fixturePath = (path) -> Path.normalize(__dirname + "/../fixtures/" + path)
-process = require "process"
-console.log process.pid, process.ppid, process.getuid(),process.getgroups(), "PID"
-try
-	console.log "creating tmp directory", fixturePath("tmp")
-	fs.mkdirSync(fixturePath("tmp"))
-catch err
-	console.log err, fixturePath("tmp"), "unable to create fixture tmp path"
-
-MOCHA_LATEX_TIMEOUT = 60 * 1000
-
-convertToPng = (pdfPath, pngPath, callback = (error) ->) ->
-	command = "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}"
-	console.log "COMMAND"
-	console.log command
-	convert = ChildProcess.exec command
-	stdout = ""
-	convert.stdout.on "data", (chunk) -> console.log "STDOUT", chunk.toString()
-	convert.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString()
-	convert.on "exit", () ->
-		callback()
-
-compare = (originalPath, generatedPath, callback = (error, same) ->) ->
-	diff_file = "#{fixturePath(generatedPath)}-diff.png"
-	proc = ChildProcess.exec "compare -metric mae #{fixturePath(originalPath)} #{fixturePath(generatedPath)} #{diff_file}"
-	stderr = ""
-	proc.stderr.on "data", (chunk) -> stderr += chunk
-	proc.on "exit", () ->
-		if stderr.trim() == "0 (0)"
-			# remove output diff if test matches expected image
-			fs.unlink diff_file, (err) ->
-				if err
-					throw err
-			callback null, true
-		else
-			console.log "compare result", stderr
-			callback null, false
-
-checkPdfInfo = (pdfPath, callback = (error, output) ->) ->
-	proc = ChildProcess.exec "pdfinfo #{fixturePath(pdfPath)}"
-	stdout = ""
-	proc.stdout.on "data", (chunk) -> stdout += chunk
-	proc.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString()
-	proc.on "exit", () ->
-		if stdout.match(/Optimized:\s+yes/)
-			callback null, true
-		else
-			callback null, false
-
-compareMultiplePages = (project_id, callback = (error) ->) ->
-	compareNext = (page_no, callback) ->
-		path = "tmp/#{project_id}-source-#{page_no}.png"
-		fs.stat fixturePath(path), (error, stat) ->
-			if error?
-				callback()
-			else
-				compare  "tmp/#{project_id}-source-#{page_no}.png", "tmp/#{project_id}-generated-#{page_no}.png", (error, same) =>
-					throw error if error?
-					same.should.equal true
-					compareNext page_no + 1, callback
-	compareNext 0, callback
-
-comparePdf = (project_id, example_dir, callback = (error) ->) ->
-	console.log "CONVERT"
-	console.log "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png"
-	convertToPng "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png", (error) =>
-		throw error if error?
-		convertToPng "examples/#{example_dir}/output.pdf", "tmp/#{project_id}-source.png", (error) =>
-			throw error if error?
-			fs.stat fixturePath("tmp/#{project_id}-source-0.png"), (error, stat) =>
-				if error?
-					compare  "tmp/#{project_id}-source.png", "tmp/#{project_id}-generated.png", (error, same) =>
-						throw error if error?
-						same.should.equal true
-						callback()
-				else
-					compareMultiplePages project_id, (error) ->
-						throw error if error?
-						callback()
-
-downloadAndComparePdf = (project_id, example_dir, url, callback = (error) ->) ->
-	writeStream = fs.createWriteStream(fixturePath("tmp/#{project_id}.pdf"))
-	request.get(url).pipe(writeStream)
-	console.log("writing file out", fixturePath("tmp/#{project_id}.pdf"))
-	writeStream.on "close", () =>
-		checkPdfInfo "tmp/#{project_id}.pdf", (error, optimised) =>
-			throw error if error?
-			optimised.should.equal true
-			comparePdf project_id, example_dir, callback
-
-Client.runServer(4242, fixturePath("examples"))
-
-describe "Example Documents", ->
-	before (done) ->
-		ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on "exit", () -> 
-			ClsiApp.ensureRunning done
-
-
-	for example_dir in fs.readdirSync fixturePath("examples")
-		do (example_dir) ->
-			describe example_dir, ->
-				before ->
-					@project_id = Client.randomId() + "_" + example_dir
-
-				it "should generate the correct pdf", (done) ->
-					this.timeout(MOCHA_LATEX_TIMEOUT)
-					Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) =>
-						if error || body?.compile?.status is "failure"
-							console.log "DEBUG: error", error, "body", JSON.stringify(body)
-						pdf = Client.getOutputFile body, "pdf"
-						downloadAndComparePdf(@project_id, example_dir, pdf.url, done)
-
-				it "should generate the correct pdf on the second run as well", (done) ->
-					this.timeout(MOCHA_LATEX_TIMEOUT)
-					Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) =>
-						if error || body?.compile?.status is "failure"
-							console.log "DEBUG: error", error, "body", JSON.stringify(body)
-						pdf = Client.getOutputFile body, "pdf"
-						downloadAndComparePdf(@project_id, example_dir, pdf.url, done)
-
-

+ 0 - 41
services/clsi/test/acceptance/coffee/SimpleLatexFileTests.coffee

@@ -1,41 +0,0 @@
-Client = require "./helpers/Client"
-request = require "request"
-require("chai").should()
-ClsiApp = require "./helpers/ClsiApp"
-
-describe "Simple LaTeX file", ->
-	before (done) ->
-		@project_id = Client.randomId()
-		@request =
-			resources: [
-				path: "main.tex"
-				content: '''
-					\\documentclass{article}
-					\\begin{document}
-					Hello world
-					\\end{document}
-				'''
-			]
-		ClsiApp.ensureRunning =>
-			Client.compile @project_id, @request, (@error, @res, @body) => done()
-
-	it "should return the PDF", ->
-		pdf = Client.getOutputFile(@body, "pdf")
-		pdf.type.should.equal "pdf"
-		
-	it "should return the log", ->
-		log = Client.getOutputFile(@body, "log")
-		log.type.should.equal "log"
-
-	it "should provide the pdf for download", (done) ->
-		pdf = Client.getOutputFile(@body, "pdf")
-		request.get pdf.url, (error, res, body) ->
-			res.statusCode.should.equal 200
-			done()
-		
-	it "should provide the log for download", (done) ->
-		log = Client.getOutputFile(@body, "pdf")
-		request.get log.url, (error, res, body) ->
-			res.statusCode.should.equal 200
-			done()
-	

+ 0 - 41
services/clsi/test/acceptance/coffee/SynctexTests.coffee

@@ -1,41 +0,0 @@
-Client = require "./helpers/Client"
-request = require "request"
-require("chai").should()
-expect = require("chai").expect
-ClsiApp = require "./helpers/ClsiApp"
-crypto = require("crypto")
-
-describe "Syncing", ->
-	before (done) ->
-		content = '''
-					\\documentclass{article}
-					\\begin{document}
-					Hello world
-					\\end{document}
-				'''
-		@request =
-			resources: [
-				path: "main.tex"
-				content: content
-			]
-		@project_id = Client.randomId()
-		ClsiApp.ensureRunning =>
-			Client.compile @project_id, @request, (@error, @res, @body) => done()
-
-	describe "from code to pdf", ->
-		it "should return the correct location", (done) ->
-			Client.syncFromCode @project_id, "main.tex", 3, 5, (error, pdfPositions) ->
-				throw error if error?
-				expect(pdfPositions).to.deep.equal(
-					pdf: [ { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } ]
-				)
-				done()
-
-	describe "from pdf to code", ->
-		it "should return the correct location", (done) ->
-			Client.syncFromPdf @project_id, 1, 100, 200, (error, codePositions) =>
-				throw error if error?
-				expect(codePositions).to.deep.equal(
-					code: [ { file: 'main.tex', line: 3, column: -1 } ]
-				)
-				done()

+ 0 - 34
services/clsi/test/acceptance/coffee/TimeoutTests.coffee

@@ -1,34 +0,0 @@
-Client = require "./helpers/Client"
-request = require "request"
-require("chai").should()
-ClsiApp = require "./helpers/ClsiApp"
-
-
-describe "Timed out compile", ->
-	before (done) ->
-		@request =
-			options:
-				timeout: 10 #seconds
-			resources: [
-				path: "main.tex"
-				content: '''
-					\\documentclass{article}
-					\\begin{document}
-					\\def\\x{Hello!\\par\\x}
-					\\x
-					\\end{document}
-				'''
-			]
-		@project_id = Client.randomId()
-		ClsiApp.ensureRunning =>
-			Client.compile @project_id, @request, (@error, @res, @body) => done()
-
-	it "should return a timeout error", ->
-		@body.compile.error.should.equal "container timed out"
-
-	it "should return a timedout status", ->
-		@body.compile.status.should.equal "timedout"
-
-	it "should return the log output file name", ->
-		outputFilePaths = @body.compile.outputFiles.map((x) => x.path)
-		outputFilePaths.should.include('output.log')

+ 0 - 222
services/clsi/test/acceptance/coffee/UrlCachingTests.coffee

@@ -1,222 +0,0 @@
-Client = require "./helpers/Client"
-request = require "request"
-require("chai").should()
-sinon = require "sinon"
-ClsiApp = require "./helpers/ClsiApp"
-
-host = "localhost"
-
-Server =
-	run: () ->
-		express = require "express"
-		app = express()
-
-		staticServer = express.static __dirname + "/../fixtures/"
-		app.get "/:random_id/*", (req, res, next) =>
-			@getFile(req.url)
-			req.url = "/" + req.params[0]
-			staticServer(req, res, next)
-
-		app.listen 31415, host
-
-	getFile: () ->
-
-	randomId: () ->
-		Math.random().toString(16).slice(2)
-
-Server.run()
-
-describe "Url Caching", ->
-	describe "Downloading an image for the first time", ->
-		before (done) ->
-			@project_id = Client.randomId()
-			@file = "#{Server.randomId()}/lion.png"
-			@request =
-				resources: [{
-					path: "main.tex"
-					content: '''
-						\\documentclass{article}
-						\\usepackage{graphicx}
-						\\begin{document}
-						\\includegraphics{lion.png}
-						\\end{document}
-					'''
-				}, {
-					path: "lion.png"
-					url: "http://#{host}:31415/#{@file}"
-				}]
-
-			sinon.spy Server, "getFile"
-			ClsiApp.ensureRunning =>
-				Client.compile @project_id, @request, (@error, @res, @body) => done()
-
-		afterEach ->
-			Server.getFile.restore()
-
-		it "should download the image", ->
-			Server.getFile
-				.calledWith("/" + @file)
-				.should.equal true
-				
-	describe "When an image is in the cache and the last modified date is unchanged", ->
-		before (done) ->
-			@project_id = Client.randomId()
-			@file = "#{Server.randomId()}/lion.png"
-			@request =
-				resources: [{
-					path: "main.tex"
-					content: '''
-						\\documentclass{article}
-						\\usepackage{graphicx}
-						\\begin{document}
-						\\includegraphics{lion.png}
-						\\end{document}
-					'''
-				}, @image_resource = {
-					path: "lion.png"
-					url: "http://#{host}:31415/#{@file}"
-					modified: Date.now()
-				}]
-
-			Client.compile @project_id, @request, (@error, @res, @body) =>
-				sinon.spy Server, "getFile"
-				Client.compile @project_id, @request, (@error, @res, @body) =>
-					done()
-
-		after ->
-			Server.getFile.restore()
-
-		it "should not download the image again", ->
-			Server.getFile.called.should.equal false
-
-	describe "When an image is in the cache and the last modified date is advanced", ->
-		before (done) ->
-			@project_id = Client.randomId()
-			@file = "#{Server.randomId()}/lion.png"
-			@request =
-				resources: [{
-					path: "main.tex"
-					content: '''
-						\\documentclass{article}
-						\\usepackage{graphicx}
-						\\begin{document}
-						\\includegraphics{lion.png}
-						\\end{document}
-					'''
-				}, @image_resource = {
-					path: "lion.png"
-					url: "http://#{host}:31415/#{@file}"
-					modified: @last_modified = Date.now()
-				}]
-
-			Client.compile @project_id, @request, (@error, @res, @body) =>
-				sinon.spy Server, "getFile"
-				@image_resource.modified = new Date(@last_modified + 3000)
-				Client.compile @project_id, @request, (@error, @res, @body) =>
-					done()
-
-		afterEach ->
-			Server.getFile.restore()
-
-		it "should download the image again", ->
-			Server.getFile.called.should.equal true
-
-	describe "When an image is in the cache and the last modified date is further in the past", ->
-		before (done) ->
-			@project_id = Client.randomId()
-			@file = "#{Server.randomId()}/lion.png"
-			@request =
-				resources: [{
-					path: "main.tex"
-					content: '''
-						\\documentclass{article}
-						\\usepackage{graphicx}
-						\\begin{document}
-						\\includegraphics{lion.png}
-						\\end{document}
-					'''
-				}, @image_resource = {
-					path: "lion.png"
-					url: "http://#{host}:31415/#{@file}"
-					modified: @last_modified = Date.now()
-				}]
-
-			Client.compile @project_id, @request, (@error, @res, @body) =>
-				sinon.spy Server, "getFile"
-				@image_resource.modified = new Date(@last_modified - 3000)
-				Client.compile @project_id, @request, (@error, @res, @body) =>
-					done()
-
-		afterEach ->
-			Server.getFile.restore()
-
-		it "should not download the image again", ->
-			Server.getFile.called.should.equal false
-
-	describe "When an image is in the cache and the last modified date is not specified", ->
-		before (done) ->
-			@project_id = Client.randomId()
-			@file = "#{Server.randomId()}/lion.png"
-			@request =
-				resources: [{
-					path: "main.tex"
-					content: '''
-						\\documentclass{article}
-						\\usepackage{graphicx}
-						\\begin{document}
-						\\includegraphics{lion.png}
-						\\end{document}
-					'''
-				}, @image_resource = {
-					path: "lion.png"
-					url: "http://#{host}:31415/#{@file}"
-					modified: @last_modified = Date.now()
-				}]
-
-			Client.compile @project_id, @request, (@error, @res, @body) =>
-				sinon.spy Server, "getFile"
-				delete @image_resource.modified
-				Client.compile @project_id, @request, (@error, @res, @body) =>
-					done()
-
-		afterEach ->
-			Server.getFile.restore()
-
-		it "should download the image again", ->
-			Server.getFile.called.should.equal true
-		
-	describe "After clearing the cache", ->
-		before (done) ->
-			@project_id = Client.randomId()
-			@file = "#{Server.randomId()}/lion.png"
-			@request =
-				resources: [{
-					path: "main.tex"
-					content: '''
-						\\documentclass{article}
-						\\usepackage{graphicx}
-						\\begin{document}
-						\\includegraphics{lion.png}
-						\\end{document}
-					'''
-				}, @image_resource = {
-					path: "lion.png"
-					url: "http://#{host}:31415/#{@file}"
-					modified: @last_modified = Date.now()
-				}]
-
-			Client.compile @project_id, @request, (error) =>
-				throw error if error?
-				Client.clearCache @project_id, (error, res, body) =>
-					throw error if error?
-					sinon.spy Server, "getFile"
-					Client.compile @project_id, @request, (@error, @res, @body) =>
-						done()
-
-		afterEach ->
-			Server.getFile.restore()
-
-		it "should download the image again", ->
-			Server.getFile.called.should.equal true
-		
-				

+ 0 - 38
services/clsi/test/acceptance/coffee/WordcountTests.coffee

@@ -1,38 +0,0 @@
-Client = require "./helpers/Client"
-request = require "request"
-require("chai").should()
-expect = require("chai").expect
-path = require("path")
-fs = require("fs")
-ClsiApp = require "./helpers/ClsiApp"
-
-describe "Syncing", ->
-	before (done) ->
-		@request =
-			resources: [
-				path: "main.tex"
-				content: fs.readFileSync(path.join(__dirname,"../fixtures/naugty_strings.txt"),"utf-8")
-			]
-		@project_id = Client.randomId()
-		ClsiApp.ensureRunning =>
-			Client.compile @project_id, @request, (@error, @res, @body) => done()
-
-	describe "wordcount file", ->
-		it "should return wordcount info", (done) ->
-			Client.wordcount @project_id, "main.tex", (error, result) ->
-				throw error if error?
-				expect(result).to.deep.equal(
-					texcount: { 
-						encode: "utf8"
-						textWords: 2281
-						headWords: 2
-						outside: 0
-						headers: 2
-						elements: 0
-						mathInline: 6
-						mathDisplay: 0
-						errors: 0
-						messages: ""
-					}
-				)
-				done()

+ 0 - 105
services/clsi/test/acceptance/coffee/helpers/Client.coffee

@@ -1,105 +0,0 @@
-request = require "request"
-fs = require "fs"
-Settings = require "settings-sharelatex"
-
-host = "localhost"
-
-module.exports = Client =
-	host: Settings.apis.clsi.url
-
-	randomId: () ->
-		Math.random().toString(16).slice(2)
-
-	compile: (project_id, data, callback = (error, res, body) ->) ->
-		request.post {
-			url: "#{@host}/project/#{project_id}/compile"
-			json:
-				compile: data
-		}, callback
-
-	clearCache: (project_id, callback = (error, res, body) ->) ->
-		request.del "#{@host}/project/#{project_id}", callback
-
-	getOutputFile: (response, type) ->
-		for file in response.compile.outputFiles
-			if file.type == type and file.url.match("output.#{type}")
-				return file
-		return null
-
-	runServer: (port, directory) ->
-		express = require("express")
-		app = express()
-		app.use express.static(directory)
-		console.log("starting test server on", port, host)
-		app.listen(port, host).on "error", (error) ->
-			console.error "error starting server:", error.message
-			process.exit(1)
-
-
-	syncFromCode: (project_id, file, line, column, callback = (error, pdfPositions) ->) ->
-		request.get {
-			url: "#{@host}/project/#{project_id}/sync/code"
-			qs: {
-				file: file
-				line: line
-				column: column
-			}
-		}, (error, response, body) ->
-			return callback(error) if error?
-			callback null, JSON.parse(body)
-
-	syncFromPdf: (project_id, page, h, v, callback = (error, pdfPositions) ->) ->
-		request.get {
-			url: "#{@host}/project/#{project_id}/sync/pdf"
-			qs: {
-				page: page,
-				h: h, v: v
-			}
-		}, (error, response, body) ->
-			return callback(error) if error?
-			callback null, JSON.parse(body)
-
-	compileDirectory: (project_id, baseDirectory, directory, serverPort, callback = (error, res, body) ->) ->
-		resources = []
-		entities = fs.readdirSync("#{baseDirectory}/#{directory}")
-		rootResourcePath = "main.tex"
-		while (entities.length > 0)
-			entity = entities.pop()
-			stat = fs.statSync("#{baseDirectory}/#{directory}/#{entity}")
-			if stat.isDirectory()
-				entities = entities.concat fs.readdirSync("#{baseDirectory}/#{directory}/#{entity}").map (subEntity) ->
-					if subEntity == "main.tex"
-						rootResourcePath = "#{entity}/#{subEntity}"
-					return "#{entity}/#{subEntity}"
-			else if stat.isFile() and entity != "output.pdf"
-				extension = entity.split(".").pop()
-				if ["tex", "bib", "cls", "sty", "pdf_tex", "Rtex", "ist", "md", "Rmd"].indexOf(extension) > -1
-					resources.push
-						path: entity
-						content: fs.readFileSync("#{baseDirectory}/#{directory}/#{entity}").toString()
-				else if ["eps", "ttf", "png", "jpg", "pdf", "jpeg"].indexOf(extension) > -1
-					resources.push
-						path: entity
-						url: "http://#{host}:#{serverPort}/#{directory}/#{entity}"
-						modified: stat.mtime
-
-		fs.readFile "#{baseDirectory}/#{directory}/options.json", (error, body) =>
-			req =
-				resources: resources
-				rootResourcePath: rootResourcePath
-
-			if !error?
-				body = JSON.parse body
-				req.options = body
-
-			@compile project_id, req, callback
-
-	wordcount: (project_id, file, callback = (error, pdfPositions) ->) ->
-		request.get {
-			url: "#{@host}/project/#{project_id}/wordcount"
-			qs: {
-				file: file
-			}
-		}, (error, response, body) ->
-			return callback(error) if error?
-			callback null, JSON.parse(body)

+ 0 - 24
services/clsi/test/acceptance/coffee/helpers/ClsiApp.coffee

@@ -1,24 +0,0 @@
-app = require('../../../../app')
-require("logger-sharelatex").logger.level("info")
-logger = require("logger-sharelatex")
-Settings = require("settings-sharelatex")
-
-module.exports =
-	running: false
-	initing: false
-	callbacks: []
-	ensureRunning: (callback = (error) ->) ->
-		if @running
-			return callback()
-		else if @initing
-			@callbacks.push callback
-		else
-			@initing = true
-			@callbacks.push callback
-			app.listen Settings.internal?.clsi?.port, "localhost", (error) => 
-				throw error if error?
-				@running = true
-				logger.log("clsi running in dev mode")
-
-				for callback in @callbacks
-					callback()

+ 88 - 0
services/clsi/test/acceptance/js/BrokenLatexFileTests.js

@@ -0,0 +1,88 @@
+/* eslint-disable
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Client = require('./helpers/Client')
+const request = require('request')
+require('chai').should()
+const ClsiApp = require('./helpers/ClsiApp')
+
+describe('Broken LaTeX file', function() {
+  before(function(done) {
+    this.broken_request = {
+      resources: [
+        {
+          path: 'main.tex',
+          content: `\
+\\documentclass{articl % :(
+\\begin{documen % :(
+Broken
+\\end{documen % :(\
+`
+        }
+      ]
+    }
+    this.correct_request = {
+      resources: [
+        {
+          path: 'main.tex',
+          content: `\
+\\documentclass{article}
+\\begin{document}
+Hello world
+\\end{document}\
+`
+        }
+      ]
+    }
+    return ClsiApp.ensureRunning(done)
+  })
+
+  describe('on first run', function() {
+    before(function(done) {
+      this.project_id = Client.randomId()
+      return Client.compile(
+        this.project_id,
+        this.broken_request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          return done()
+        }
+      )
+    })
+
+    return it('should return a failure status', function() {
+      return this.body.compile.status.should.equal('failure')
+    })
+  })
+
+  return describe('on second run', function() {
+    before(function(done) {
+      this.project_id = Client.randomId()
+      return Client.compile(this.project_id, this.correct_request, () => {
+        return Client.compile(
+          this.project_id,
+          this.broken_request,
+          (error, res, body) => {
+            this.error = error
+            this.res = res
+            this.body = body
+            return done()
+          }
+        )
+      })
+    })
+
+    return it('should return a failure status', function() {
+      return this.body.compile.status.should.equal('failure')
+    })
+  })
+})

+ 73 - 0
services/clsi/test/acceptance/js/DeleteOldFilesTest.js

@@ -0,0 +1,73 @@
+/* eslint-disable
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Client = require('./helpers/Client')
+const request = require('request')
+require('chai').should()
+const ClsiApp = require('./helpers/ClsiApp')
+
+describe('Deleting Old Files', function() {
+  before(function(done) {
+    this.request = {
+      resources: [
+        {
+          path: 'main.tex',
+          content: `\
+\\documentclass{article}
+\\begin{document}
+Hello world
+\\end{document}\
+`
+        }
+      ]
+    }
+    return ClsiApp.ensureRunning(done)
+  })
+
+  return describe('on first run', function() {
+    before(function(done) {
+      this.project_id = Client.randomId()
+      return Client.compile(
+        this.project_id,
+        this.request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          return done()
+        }
+      )
+    })
+
+    it('should return a success status', function() {
+      return this.body.compile.status.should.equal('success')
+    })
+
+    return describe('after file has been deleted', function() {
+      before(function(done) {
+        this.request.resources = []
+        return Client.compile(
+          this.project_id,
+          this.request,
+          (error, res, body) => {
+            this.error = error
+            this.res = res
+            this.body = body
+            return done()
+          }
+        )
+      })
+
+      return it('should return a failure status', function() {
+        return this.body.compile.status.should.equal('failure')
+      })
+    })
+  })
+})

+ 285 - 0
services/clsi/test/acceptance/js/ExampleDocumentTests.js

@@ -0,0 +1,285 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-path-concat,
+    no-return-assign,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Client = require('./helpers/Client')
+const request = require('request')
+require('chai').should()
+const fs = require('fs')
+const fsExtra = require('fs-extra')
+const ChildProcess = require('child_process')
+const ClsiApp = require('./helpers/ClsiApp')
+const logger = require('logger-sharelatex')
+const Path = require('path')
+const fixturePath = path => {
+  if (path.slice(0, 3) === 'tmp') {
+    return '/tmp/clsi_acceptance_tests' + path.slice(3)
+  }
+  return Path.normalize(__dirname + '/../fixtures/' + path)
+}
+const process = require('process')
+console.log(
+  process.pid,
+  process.ppid,
+  process.getuid(),
+  process.getgroups(),
+  'PID'
+)
+
+const MOCHA_LATEX_TIMEOUT = 60 * 1000
+
+const convertToPng = function(pdfPath, pngPath, callback) {
+  if (callback == null) {
+    callback = function(error) {}
+  }
+  const command = `convert ${fixturePath(pdfPath)} ${fixturePath(pngPath)}`
+  console.log('COMMAND')
+  console.log(command)
+  const convert = ChildProcess.exec(command)
+  const stdout = ''
+  convert.stdout.on('data', chunk => console.log('STDOUT', chunk.toString()))
+  convert.stderr.on('data', chunk => console.log('STDERR', chunk.toString()))
+  return convert.on('exit', () => callback())
+}
+
+const compare = function(originalPath, generatedPath, callback) {
+  if (callback == null) {
+    callback = function(error, same) {}
+  }
+  const diff_file = `${fixturePath(generatedPath)}-diff.png`
+  const proc = ChildProcess.exec(
+    `compare -metric mae ${fixturePath(originalPath)} ${fixturePath(
+      generatedPath
+    )} ${diff_file}`
+  )
+  let stderr = ''
+  proc.stderr.on('data', chunk => (stderr += chunk))
+  return proc.on('exit', () => {
+    if (stderr.trim() === '0 (0)') {
+      // remove output diff if test matches expected image
+      fs.unlink(diff_file, err => {
+        if (err) {
+          throw err
+        }
+      })
+      return callback(null, true)
+    } else {
+      console.log('compare result', stderr)
+      return callback(null, false)
+    }
+  })
+}
+
+const checkPdfInfo = function(pdfPath, callback) {
+  if (callback == null) {
+    callback = function(error, output) {}
+  }
+  const proc = ChildProcess.exec(`pdfinfo ${fixturePath(pdfPath)}`)
+  let stdout = ''
+  proc.stdout.on('data', chunk => (stdout += chunk))
+  proc.stderr.on('data', chunk => console.log('STDERR', chunk.toString()))
+  return proc.on('exit', () => {
+    if (stdout.match(/Optimized:\s+yes/)) {
+      return callback(null, true)
+    } else {
+      return callback(null, false)
+    }
+  })
+}
+
+const compareMultiplePages = function(project_id, callback) {
+  if (callback == null) {
+    callback = function(error) {}
+  }
+  var compareNext = function(page_no, callback) {
+    const path = `tmp/${project_id}-source-${page_no}.png`
+    return fs.stat(fixturePath(path), (error, stat) => {
+      if (error != null) {
+        return callback()
+      } else {
+        return compare(
+          `tmp/${project_id}-source-${page_no}.png`,
+          `tmp/${project_id}-generated-${page_no}.png`,
+          (error, same) => {
+            if (error != null) {
+              throw error
+            }
+            same.should.equal(true)
+            return compareNext(page_no + 1, callback)
+          }
+        )
+      }
+    })
+  }
+  return compareNext(0, callback)
+}
+
+const comparePdf = function(project_id, example_dir, callback) {
+  if (callback == null) {
+    callback = function(error) {}
+  }
+  console.log('CONVERT')
+  console.log(`tmp/${project_id}.pdf`, `tmp/${project_id}-generated.png`)
+  return convertToPng(
+    `tmp/${project_id}.pdf`,
+    `tmp/${project_id}-generated.png`,
+    error => {
+      if (error != null) {
+        throw error
+      }
+      return convertToPng(
+        `examples/${example_dir}/output.pdf`,
+        `tmp/${project_id}-source.png`,
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return fs.stat(
+            fixturePath(`tmp/${project_id}-source-0.png`),
+            (error, stat) => {
+              if (error != null) {
+                return compare(
+                  `tmp/${project_id}-source.png`,
+                  `tmp/${project_id}-generated.png`,
+                  (error, same) => {
+                    if (error != null) {
+                      throw error
+                    }
+                    same.should.equal(true)
+                    return callback()
+                  }
+                )
+              } else {
+                return compareMultiplePages(project_id, error => {
+                  if (error != null) {
+                    throw error
+                  }
+                  return callback()
+                })
+              }
+            }
+          )
+        }
+      )
+    }
+  )
+}
+
+const downloadAndComparePdf = function(project_id, example_dir, url, callback) {
+  if (callback == null) {
+    callback = function(error) {}
+  }
+  const writeStream = fs.createWriteStream(fixturePath(`tmp/${project_id}.pdf`))
+  request.get(url).pipe(writeStream)
+  console.log('writing file out', fixturePath(`tmp/${project_id}.pdf`))
+  return writeStream.on('close', () => {
+    return checkPdfInfo(`tmp/${project_id}.pdf`, (error, optimised) => {
+      if (error != null) {
+        throw error
+      }
+      optimised.should.equal(true)
+      return comparePdf(project_id, example_dir, callback)
+    })
+  })
+}
+
+Client.runServer(4242, fixturePath('examples'))
+
+describe('Example Documents', function() {
+  before(function(done) {
+    ClsiApp.ensureRunning(done)
+  })
+  before(function(done) {
+    fsExtra.remove(fixturePath('tmp'), done)
+  })
+  before(function(done) {
+    fs.mkdir(fixturePath('tmp'), done)
+  })
+  after(function(done) {
+    fsExtra.remove(fixturePath('tmp'), done)
+  })
+
+  return Array.from(fs.readdirSync(fixturePath('examples'))).map(example_dir =>
+    (example_dir =>
+      describe(example_dir, function() {
+        before(function() {
+          return (this.project_id = Client.randomId() + '_' + example_dir)
+        })
+
+        it('should generate the correct pdf', function(done) {
+          this.timeout(MOCHA_LATEX_TIMEOUT)
+          return Client.compileDirectory(
+            this.project_id,
+            fixturePath('examples'),
+            example_dir,
+            4242,
+            (error, res, body) => {
+              if (
+                error ||
+                __guard__(
+                  body != null ? body.compile : undefined,
+                  x => x.status
+                ) === 'failure'
+              ) {
+                console.log('DEBUG: error', error, 'body', JSON.stringify(body))
+              }
+              const pdf = Client.getOutputFile(body, 'pdf')
+              return downloadAndComparePdf(
+                this.project_id,
+                example_dir,
+                pdf.url,
+                done
+              )
+            }
+          )
+        })
+
+        return it('should generate the correct pdf on the second run as well', function(done) {
+          this.timeout(MOCHA_LATEX_TIMEOUT)
+          return Client.compileDirectory(
+            this.project_id,
+            fixturePath('examples'),
+            example_dir,
+            4242,
+            (error, res, body) => {
+              if (
+                error ||
+                __guard__(
+                  body != null ? body.compile : undefined,
+                  x => x.status
+                ) === 'failure'
+              ) {
+                console.log('DEBUG: error', error, 'body', JSON.stringify(body))
+              }
+              const pdf = Client.getOutputFile(body, 'pdf')
+              return downloadAndComparePdf(
+                this.project_id,
+                example_dir,
+                pdf.url,
+                done
+              )
+            }
+          )
+        })
+      }))(example_dir)
+  )
+})
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 71 - 0
services/clsi/test/acceptance/js/SimpleLatexFileTests.js

@@ -0,0 +1,71 @@
+/* eslint-disable
+    handle-callback-err,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Client = require('./helpers/Client')
+const request = require('request')
+require('chai').should()
+const ClsiApp = require('./helpers/ClsiApp')
+
+describe('Simple LaTeX file', function() {
+  before(function(done) {
+    this.project_id = Client.randomId()
+    this.request = {
+      resources: [
+        {
+          path: 'main.tex',
+          content: `\
+\\documentclass{article}
+\\begin{document}
+Hello world
+\\end{document}\
+`
+        }
+      ]
+    }
+    return ClsiApp.ensureRunning(() => {
+      return Client.compile(
+        this.project_id,
+        this.request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          return done()
+        }
+      )
+    })
+  })
+
+  it('should return the PDF', function() {
+    const pdf = Client.getOutputFile(this.body, 'pdf')
+    return pdf.type.should.equal('pdf')
+  })
+
+  it('should return the log', function() {
+    const log = Client.getOutputFile(this.body, 'log')
+    return log.type.should.equal('log')
+  })
+
+  it('should provide the pdf for download', function(done) {
+    const pdf = Client.getOutputFile(this.body, 'pdf')
+    return request.get(pdf.url, (error, res, body) => {
+      res.statusCode.should.equal(200)
+      return done()
+    })
+  })
+
+  return it('should provide the log for download', function(done) {
+    const log = Client.getOutputFile(this.body, 'pdf')
+    return request.get(log.url, (error, res, body) => {
+      res.statusCode.should.equal(200)
+      return done()
+    })
+  })
+})

+ 91 - 0
services/clsi/test/acceptance/js/SynctexTests.js

@@ -0,0 +1,91 @@
+/* eslint-disable
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Client = require('./helpers/Client')
+const request = require('request')
+require('chai').should()
+const { expect } = require('chai')
+const ClsiApp = require('./helpers/ClsiApp')
+const crypto = require('crypto')
+
+describe('Syncing', function() {
+  before(function(done) {
+    const content = `\
+\\documentclass{article}
+\\begin{document}
+Hello world
+\\end{document}\
+`
+    this.request = {
+      resources: [
+        {
+          path: 'main.tex',
+          content
+        }
+      ]
+    }
+    this.project_id = Client.randomId()
+    return ClsiApp.ensureRunning(() => {
+      return Client.compile(
+        this.project_id,
+        this.request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('from code to pdf', function() {
+    return it('should return the correct location', function(done) {
+      return Client.syncFromCode(
+        this.project_id,
+        'main.tex',
+        3,
+        5,
+        (error, pdfPositions) => {
+          if (error != null) {
+            throw error
+          }
+          expect(pdfPositions).to.deep.equal({
+            pdf: [
+              { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 }
+            ]
+          })
+          return done()
+        }
+      )
+    })
+  })
+
+  return describe('from pdf to code', function() {
+    return it('should return the correct location', function(done) {
+      return Client.syncFromPdf(
+        this.project_id,
+        1,
+        100,
+        200,
+        (error, codePositions) => {
+          if (error != null) {
+            throw error
+          }
+          expect(codePositions).to.deep.equal({
+            code: [{ file: 'main.tex', line: 3, column: -1 }]
+          })
+          return done()
+        }
+      )
+    })
+  })
+})

+ 62 - 0
services/clsi/test/acceptance/js/TimeoutTests.js

@@ -0,0 +1,62 @@
+/* eslint-disable
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Client = require('./helpers/Client')
+const request = require('request')
+require('chai').should()
+const ClsiApp = require('./helpers/ClsiApp')
+
+describe('Timed out compile', function() {
+  before(function(done) {
+    this.request = {
+      options: {
+        timeout: 10
+      }, // seconds
+      resources: [
+        {
+          path: 'main.tex',
+          content: `\
+\\documentclass{article}
+\\begin{document}
+\\def\\x{Hello!\\par\\x}
+\\x
+\\end{document}\
+`
+        }
+      ]
+    }
+    this.project_id = Client.randomId()
+    return ClsiApp.ensureRunning(() => {
+      return Client.compile(
+        this.project_id,
+        this.request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          return done()
+        }
+      )
+    })
+  })
+
+  it('should return a timeout error', function() {
+    return this.body.compile.error.should.equal('container timed out')
+  })
+
+  it('should return a timedout status', function() {
+    return this.body.compile.status.should.equal('timedout')
+  })
+
+  return it('should return the log output file name', function() {
+    const outputFilePaths = this.body.compile.outputFiles.map(x => x.path)
+    return outputFilePaths.should.include('output.log')
+  })
+})

+ 373 - 0
services/clsi/test/acceptance/js/UrlCachingTests.js

@@ -0,0 +1,373 @@
+/* eslint-disable
+    no-path-concat,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Client = require('./helpers/Client')
+const request = require('request')
+require('chai').should()
+const sinon = require('sinon')
+const ClsiApp = require('./helpers/ClsiApp')
+
+const host = 'localhost'
+
+const Server = {
+  run() {
+    const express = require('express')
+    const app = express()
+
+    const staticServer = express.static(__dirname + '/../fixtures/')
+    app.get('/:random_id/*', (req, res, next) => {
+      this.getFile(req.url)
+      req.url = `/${req.params[0]}`
+      return staticServer(req, res, next)
+    })
+
+    return app.listen(31415, host)
+  },
+
+  getFile() {},
+
+  randomId() {
+    return Math.random()
+      .toString(16)
+      .slice(2)
+  }
+}
+
+Server.run()
+
+describe('Url Caching', function() {
+  describe('Downloading an image for the first time', function() {
+    before(function(done) {
+      this.project_id = Client.randomId()
+      this.file = `${Server.randomId()}/lion.png`
+      this.request = {
+        resources: [
+          {
+            path: 'main.tex',
+            content: `\
+\\documentclass{article}
+\\usepackage{graphicx}
+\\begin{document}
+\\includegraphics{lion.png}
+\\end{document}\
+`
+          },
+          {
+            path: 'lion.png',
+            url: `http://${host}:31415/${this.file}`
+          }
+        ]
+      }
+
+      sinon.spy(Server, 'getFile')
+      return ClsiApp.ensureRunning(() => {
+        return Client.compile(
+          this.project_id,
+          this.request,
+          (error, res, body) => {
+            this.error = error
+            this.res = res
+            this.body = body
+            return done()
+          }
+        )
+      })
+    })
+
+    afterEach(function() {
+      return Server.getFile.restore()
+    })
+
+    return it('should download the image', function() {
+      return Server.getFile.calledWith(`/${this.file}`).should.equal(true)
+    })
+  })
+
+  describe('When an image is in the cache and the last modified date is unchanged', function() {
+    before(function(done) {
+      this.project_id = Client.randomId()
+      this.file = `${Server.randomId()}/lion.png`
+      this.request = {
+        resources: [
+          {
+            path: 'main.tex',
+            content: `\
+\\documentclass{article}
+\\usepackage{graphicx}
+\\begin{document}
+\\includegraphics{lion.png}
+\\end{document}\
+`
+          },
+          (this.image_resource = {
+            path: 'lion.png',
+            url: `http://${host}:31415/${this.file}`,
+            modified: Date.now()
+          })
+        ]
+      }
+
+      return Client.compile(
+        this.project_id,
+        this.request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          sinon.spy(Server, 'getFile')
+          return Client.compile(
+            this.project_id,
+            this.request,
+            (error1, res1, body1) => {
+              this.error = error1
+              this.res = res1
+              this.body = body1
+              return done()
+            }
+          )
+        }
+      )
+    })
+
+    after(function() {
+      return Server.getFile.restore()
+    })
+
+    return it('should not download the image again', function() {
+      return Server.getFile.called.should.equal(false)
+    })
+  })
+
+  describe('When an image is in the cache and the last modified date is advanced', function() {
+    before(function(done) {
+      this.project_id = Client.randomId()
+      this.file = `${Server.randomId()}/lion.png`
+      this.request = {
+        resources: [
+          {
+            path: 'main.tex',
+            content: `\
+\\documentclass{article}
+\\usepackage{graphicx}
+\\begin{document}
+\\includegraphics{lion.png}
+\\end{document}\
+`
+          },
+          (this.image_resource = {
+            path: 'lion.png',
+            url: `http://${host}:31415/${this.file}`,
+            modified: (this.last_modified = Date.now())
+          })
+        ]
+      }
+
+      return Client.compile(
+        this.project_id,
+        this.request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          sinon.spy(Server, 'getFile')
+          this.image_resource.modified = new Date(this.last_modified + 3000)
+          return Client.compile(
+            this.project_id,
+            this.request,
+            (error1, res1, body1) => {
+              this.error = error1
+              this.res = res1
+              this.body = body1
+              return done()
+            }
+          )
+        }
+      )
+    })
+
+    afterEach(function() {
+      return Server.getFile.restore()
+    })
+
+    return it('should download the image again', function() {
+      return Server.getFile.called.should.equal(true)
+    })
+  })
+
+  describe('When an image is in the cache and the last modified date is further in the past', function() {
+    before(function(done) {
+      this.project_id = Client.randomId()
+      this.file = `${Server.randomId()}/lion.png`
+      this.request = {
+        resources: [
+          {
+            path: 'main.tex',
+            content: `\
+\\documentclass{article}
+\\usepackage{graphicx}
+\\begin{document}
+\\includegraphics{lion.png}
+\\end{document}\
+`
+          },
+          (this.image_resource = {
+            path: 'lion.png',
+            url: `http://${host}:31415/${this.file}`,
+            modified: (this.last_modified = Date.now())
+          })
+        ]
+      }
+
+      return Client.compile(
+        this.project_id,
+        this.request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          sinon.spy(Server, 'getFile')
+          this.image_resource.modified = new Date(this.last_modified - 3000)
+          return Client.compile(
+            this.project_id,
+            this.request,
+            (error1, res1, body1) => {
+              this.error = error1
+              this.res = res1
+              this.body = body1
+              return done()
+            }
+          )
+        }
+      )
+    })
+
+    afterEach(function() {
+      return Server.getFile.restore()
+    })
+
+    return it('should not download the image again', function() {
+      return Server.getFile.called.should.equal(false)
+    })
+  })
+
+  describe('When an image is in the cache and the last modified date is not specified', function() {
+    before(function(done) {
+      this.project_id = Client.randomId()
+      this.file = `${Server.randomId()}/lion.png`
+      this.request = {
+        resources: [
+          {
+            path: 'main.tex',
+            content: `\
+\\documentclass{article}
+\\usepackage{graphicx}
+\\begin{document}
+\\includegraphics{lion.png}
+\\end{document}\
+`
+          },
+          (this.image_resource = {
+            path: 'lion.png',
+            url: `http://${host}:31415/${this.file}`,
+            modified: (this.last_modified = Date.now())
+          })
+        ]
+      }
+
+      return Client.compile(
+        this.project_id,
+        this.request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          sinon.spy(Server, 'getFile')
+          delete this.image_resource.modified
+          return Client.compile(
+            this.project_id,
+            this.request,
+            (error1, res1, body1) => {
+              this.error = error1
+              this.res = res1
+              this.body = body1
+              return done()
+            }
+          )
+        }
+      )
+    })
+
+    afterEach(function() {
+      return Server.getFile.restore()
+    })
+
+    return it('should download the image again', function() {
+      return Server.getFile.called.should.equal(true)
+    })
+  })
+
+  return describe('After clearing the cache', function() {
+    before(function(done) {
+      this.project_id = Client.randomId()
+      this.file = `${Server.randomId()}/lion.png`
+      this.request = {
+        resources: [
+          {
+            path: 'main.tex',
+            content: `\
+\\documentclass{article}
+\\usepackage{graphicx}
+\\begin{document}
+\\includegraphics{lion.png}
+\\end{document}\
+`
+          },
+          (this.image_resource = {
+            path: 'lion.png',
+            url: `http://${host}:31415/${this.file}`,
+            modified: (this.last_modified = Date.now())
+          })
+        ]
+      }
+
+      return Client.compile(this.project_id, this.request, error => {
+        if (error != null) {
+          throw error
+        }
+        return Client.clearCache(this.project_id, (error, res, body) => {
+          if (error != null) {
+            throw error
+          }
+          sinon.spy(Server, 'getFile')
+          return Client.compile(
+            this.project_id,
+            this.request,
+            (error1, res1, body1) => {
+              this.error = error1
+              this.res = res1
+              this.body = body1
+              return done()
+            }
+          )
+        })
+      })
+    })
+
+    afterEach(function() {
+      return Server.getFile.restore()
+    })
+
+    return it('should download the image again', function() {
+      return Server.getFile.called.should.equal(true)
+    })
+  })
+})

+ 72 - 0
services/clsi/test/acceptance/js/WordcountTests.js

@@ -0,0 +1,72 @@
+/* eslint-disable
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Client = require('./helpers/Client')
+const request = require('request')
+require('chai').should()
+const { expect } = require('chai')
+const path = require('path')
+const fs = require('fs')
+const ClsiApp = require('./helpers/ClsiApp')
+
+describe('Syncing', function() {
+  before(function(done) {
+    this.request = {
+      resources: [
+        {
+          path: 'main.tex',
+          content: fs.readFileSync(
+            path.join(__dirname, '../fixtures/naugty_strings.txt'),
+            'utf-8'
+          )
+        }
+      ]
+    }
+    this.project_id = Client.randomId()
+    return ClsiApp.ensureRunning(() => {
+      return Client.compile(
+        this.project_id,
+        this.request,
+        (error, res, body) => {
+          this.error = error
+          this.res = res
+          this.body = body
+          return done()
+        }
+      )
+    })
+  })
+
+  return describe('wordcount file', function() {
+    return it('should return wordcount info', function(done) {
+      return Client.wordcount(this.project_id, 'main.tex', (error, result) => {
+        if (error != null) {
+          throw error
+        }
+        expect(result).to.deep.equal({
+          texcount: {
+            encode: 'utf8',
+            textWords: 2281,
+            headWords: 2,
+            outside: 0,
+            headers: 2,
+            elements: 0,
+            mathInline: 6,
+            mathDisplay: 0,
+            errors: 0,
+            messages: ''
+          }
+        })
+        return done()
+      })
+    })
+  })
+})

+ 208 - 0
services/clsi/test/acceptance/js/helpers/Client.js

@@ -0,0 +1,208 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let Client
+const request = require('request')
+const fs = require('fs')
+const Settings = require('settings-sharelatex')
+
+const host = 'localhost'
+
+module.exports = Client = {
+  host: Settings.apis.clsi.url,
+
+  randomId() {
+    return Math.random()
+      .toString(16)
+      .slice(2)
+  },
+
+  compile(project_id, data, callback) {
+    if (callback == null) {
+      callback = function(error, res, body) {}
+    }
+    return request.post(
+      {
+        url: `${this.host}/project/${project_id}/compile`,
+        json: {
+          compile: data
+        }
+      },
+      callback
+    )
+  },
+
+  clearCache(project_id, callback) {
+    if (callback == null) {
+      callback = function(error, res, body) {}
+    }
+    return request.del(`${this.host}/project/${project_id}`, callback)
+  },
+
+  getOutputFile(response, type) {
+    for (const file of Array.from(response.compile.outputFiles)) {
+      if (file.type === type && file.url.match(`output.${type}`)) {
+        return file
+      }
+    }
+    return null
+  },
+
+  runServer(port, directory) {
+    const express = require('express')
+    const app = express()
+    app.use(express.static(directory))
+    console.log('starting test server on', port, host)
+    return app.listen(port, host).on('error', error => {
+      console.error('error starting server:', error.message)
+      return process.exit(1)
+    })
+  },
+
+  syncFromCode(project_id, file, line, column, callback) {
+    if (callback == null) {
+      callback = function(error, pdfPositions) {}
+    }
+    return request.get(
+      {
+        url: `${this.host}/project/${project_id}/sync/code`,
+        qs: {
+          file,
+          line,
+          column
+        }
+      },
+      (error, response, body) => {
+        if (error != null) {
+          return callback(error)
+        }
+        return callback(null, JSON.parse(body))
+      }
+    )
+  },
+
+  syncFromPdf(project_id, page, h, v, callback) {
+    if (callback == null) {
+      callback = function(error, pdfPositions) {}
+    }
+    return request.get(
+      {
+        url: `${this.host}/project/${project_id}/sync/pdf`,
+        qs: {
+          page,
+          h,
+          v
+        }
+      },
+      (error, response, body) => {
+        if (error != null) {
+          return callback(error)
+        }
+        return callback(null, JSON.parse(body))
+      }
+    )
+  },
+
+  compileDirectory(project_id, baseDirectory, directory, serverPort, callback) {
+    if (callback == null) {
+      callback = function(error, res, body) {}
+    }
+    const resources = []
+    let entities = fs.readdirSync(`${baseDirectory}/${directory}`)
+    let rootResourcePath = 'main.tex'
+    while (entities.length > 0) {
+      var entity = entities.pop()
+      const stat = fs.statSync(`${baseDirectory}/${directory}/${entity}`)
+      if (stat.isDirectory()) {
+        entities = entities.concat(
+          fs
+            .readdirSync(`${baseDirectory}/${directory}/${entity}`)
+            .map(subEntity => {
+              if (subEntity === 'main.tex') {
+                rootResourcePath = `${entity}/${subEntity}`
+              }
+              return `${entity}/${subEntity}`
+            })
+        )
+      } else if (stat.isFile() && entity !== 'output.pdf') {
+        const extension = entity.split('.').pop()
+        if (
+          [
+            'tex',
+            'bib',
+            'cls',
+            'sty',
+            'pdf_tex',
+            'Rtex',
+            'ist',
+            'md',
+            'Rmd'
+          ].indexOf(extension) > -1
+        ) {
+          resources.push({
+            path: entity,
+            content: fs
+              .readFileSync(`${baseDirectory}/${directory}/${entity}`)
+              .toString()
+          })
+        } else if (
+          ['eps', 'ttf', 'png', 'jpg', 'pdf', 'jpeg'].indexOf(extension) > -1
+        ) {
+          resources.push({
+            path: entity,
+            url: `http://${host}:${serverPort}/${directory}/${entity}`,
+            modified: stat.mtime
+          })
+        }
+      }
+    }
+
+    return fs.readFile(
+      `${baseDirectory}/${directory}/options.json`,
+      (error, body) => {
+        const req = {
+          resources,
+          rootResourcePath
+        }
+
+        if (error == null) {
+          body = JSON.parse(body)
+          req.options = body
+        }
+
+        return this.compile(project_id, req, callback)
+      }
+    )
+  },
+
+  wordcount(project_id, file, callback) {
+    if (callback == null) {
+      callback = function(error, pdfPositions) {}
+    }
+    return request.get(
+      {
+        url: `${this.host}/project/${project_id}/wordcount`,
+        qs: {
+          file
+        }
+      },
+      (error, response, body) => {
+        if (error != null) {
+          return callback(error)
+        }
+        return callback(null, JSON.parse(body))
+      }
+    )
+  }
+}

+ 64 - 0
services/clsi/test/acceptance/js/helpers/ClsiApp.js

@@ -0,0 +1,64 @@
+/* eslint-disable
+    handle-callback-err,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS205: Consider reworking code to avoid use of IIFEs
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const app = require('../../../../app')
+require('logger-sharelatex').logger.level('info')
+const logger = require('logger-sharelatex')
+const Settings = require('settings-sharelatex')
+
+module.exports = {
+  running: false,
+  initing: false,
+  callbacks: [],
+  ensureRunning(callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    if (this.running) {
+      return callback()
+    } else if (this.initing) {
+      return this.callbacks.push(callback)
+    } else {
+      this.initing = true
+      this.callbacks.push(callback)
+      return app.listen(
+        __guard__(
+          Settings.internal != null ? Settings.internal.clsi : undefined,
+          x => x.port
+        ),
+        'localhost',
+        error => {
+          if (error != null) {
+            throw error
+          }
+          this.running = true
+          logger.log('clsi running in dev mode')
+
+          return (() => {
+            const result = []
+            for (callback of Array.from(this.callbacks)) {
+              result.push(callback())
+            }
+            return result
+          })()
+        }
+      )
+    }
+  }
+}
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 0 - 71
services/clsi/test/load/coffee/loadTest.coffee

@@ -1,71 +0,0 @@
-request = require "request"
-Settings = require "settings-sharelatex"
-async = require("async")
-fs = require("fs")
-_ = require("underscore")
-concurentCompiles = 5
-totalCompiles = 50
-
-buildUrl = (path) -> "http://#{Settings.internal.clsi.host}:#{Settings.internal.clsi.port}/#{path}"
-
-mainTexContent = fs.readFileSync("./bulk.tex", "utf-8")
-
-compileTimes = []
-failedCount = 0
-
-getAverageCompileTime = ->
-	totalTime = _.reduce compileTimes, (sum, time)->
-		sum + time
-	, 0
-	return totalTime / compileTimes.length
-
-makeRequest = (compileNumber, callback)->
-	bulkBodyCount = 7
-	bodyContent = ""
-	while --bulkBodyCount
-		bodyContent = bodyContent+=mainTexContent
-
-
-	startTime = new Date()
-	request.post {
-		url: buildUrl("project/loadcompile-#{compileNumber}/compile")
-		json:
-			compile:
-				resources: [
-					path: "main.tex"
-					content: """
-						\\documentclass{article}
-						\\begin{document}
-						#{bodyContent}
-						\\end{document}
-					"""
-				]
-		}, (err, response, body)->
-			if response.statusCode != 200
-				failedCount++
-				return callback("compile #{compileNumber} failed")
-			if err?
-				failedCount++
-				return callback("failed")
-			totalTime = new Date() - startTime
-			console.log totalTime+"ms"
-			compileTimes.push(totalTime)
-			callback(err)
-
-
-jobs = _.map [1..totalCompiles], (i)->
-	return (cb)->
-		makeRequest(i, cb)
-
-startTime = new Date()
-async.parallelLimit jobs, concurentCompiles, (err)->
-	if err?
-		console.error err
-	console.log("total time taken = #{(new Date() - startTime)/1000}s")
-	console.log("total compiles = #{totalCompiles}")
-	console.log("concurent compiles = #{concurentCompiles}")
-	console.log("average time = #{getAverageCompileTime()/1000}s")
-	console.log("max time = #{_.max(compileTimes)/1000}s")
-	console.log("min time = #{_.min(compileTimes)/1000}s")
-	console.log("total failures = #{failedCount}")
-

+ 0 - 0
services/clsi/test/load/coffee/bulk.tex → services/clsi/test/load/js/bulk.tex


+ 103 - 0
services/clsi/test/load/js/loadTest.js

@@ -0,0 +1,103 @@
+/* eslint-disable
+    standard/no-callback-literal,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const request = require('request')
+const Settings = require('settings-sharelatex')
+const async = require('async')
+const fs = require('fs')
+const _ = require('underscore')
+const concurentCompiles = 5
+const totalCompiles = 50
+
+const buildUrl = path =>
+  `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}`
+
+const mainTexContent = fs.readFileSync('./bulk.tex', 'utf-8')
+
+const compileTimes = []
+let failedCount = 0
+
+const getAverageCompileTime = function() {
+  const totalTime = _.reduce(compileTimes, (sum, time) => sum + time, 0)
+  return totalTime / compileTimes.length
+}
+
+const makeRequest = function(compileNumber, callback) {
+  let bulkBodyCount = 7
+  let bodyContent = ''
+  while (--bulkBodyCount) {
+    bodyContent = bodyContent += mainTexContent
+  }
+
+  const startTime = new Date()
+  return request.post(
+    {
+      url: buildUrl(`project/loadcompile-${compileNumber}/compile`),
+      json: {
+        compile: {
+          resources: [
+            {
+              path: 'main.tex',
+              content: `\
+\\documentclass{article}
+\\begin{document}
+${bodyContent}
+\\end{document}\
+`
+            }
+          ]
+        }
+      }
+    },
+    (err, response, body) => {
+      if (response.statusCode !== 200) {
+        failedCount++
+        return callback(`compile ${compileNumber} failed`)
+      }
+      if (err != null) {
+        failedCount++
+        return callback('failed')
+      }
+      const totalTime = new Date() - startTime
+      console.log(totalTime + 'ms')
+      compileTimes.push(totalTime)
+      return callback(err)
+    }
+  )
+}
+
+const jobs = _.map(__range__(1, totalCompiles, true), i => cb =>
+  makeRequest(i, cb)
+)
+
+const startTime = new Date()
+async.parallelLimit(jobs, concurentCompiles, err => {
+  if (err != null) {
+    console.error(err)
+  }
+  console.log(`total time taken = ${(new Date() - startTime) / 1000}s`)
+  console.log(`total compiles = ${totalCompiles}`)
+  console.log(`concurent compiles = ${concurentCompiles}`)
+  console.log(`average time = ${getAverageCompileTime() / 1000}s`)
+  console.log(`max time = ${_.max(compileTimes) / 1000}s`)
+  console.log(`min time = ${_.min(compileTimes) / 1000}s`)
+  return console.log(`total failures = ${failedCount}`)
+})
+
+function __range__(left, right, inclusive) {
+  const range = []
+  const ascending = left < right
+  const end = !inclusive ? right : ascending ? right + 1 : right - 1
+  for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
+    range.push(i)
+  }
+  return range
+}

+ 0 - 64
services/clsi/test/smoke/coffee/SmokeTests.coffee

@@ -1,64 +0,0 @@
-chai = require("chai")
-chai.should() unless Object.prototype.should?
-expect = chai.expect
-request = require "request"
-Settings = require "settings-sharelatex"
-
-buildUrl = (path) -> "http://#{Settings.internal.clsi.host}:#{Settings.internal.clsi.port}/#{path}"
-
-url = buildUrl("project/smoketest-#{process.pid}/compile")
-
-describe "Running a compile", ->
-	before (done) ->
-		request.post {
-			url: url
-			json:
-				compile:
-					resources: [
-						path: "main.tex"
-						content: """
-% Membrane-like surface
-% Author: Yotam Avital
-\\documentclass{article}
-\\usepackage{tikz}
-\\usetikzlibrary{calc,fadings,decorations.pathreplacing}
-\\begin{document}
-\\begin{tikzpicture}
-  \\def\\nuPi{3.1459265}
-  \\foreach \\i in {5,4,...,2}{% This one doesn't matter
-    \\foreach \\j in {3,2,...,0}{% This will crate a membrane
-                               % with the front lipids visible
-      % top layer
-      \\pgfmathsetmacro{\\dx}{rand*0.1}% A random variance in the x coordinate
-      \\pgfmathsetmacro{\\dy}{rand*0.1}% A random variance in the y coordinate,
-                                     % gives a hight fill to the lipid
-      \\pgfmathsetmacro{\\rot}{rand*0.1}% A random variance in the
-                                      % molecule orientation      
-      \\shade[ball color=red] ({\\i+\\dx+\\rot},{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)}) circle(0.45);
-      \\shade[ball color=gray] (\\i+\\dx,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-0.9}) circle(0.45);
-      \\shade[ball color=gray] (\\i+\\dx-\\rot,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-1.8}) circle(0.45);
-      % bottom layer
-      \\pgfmathsetmacro{\\dx}{rand*0.1}
-      \\pgfmathsetmacro{\\dy}{rand*0.1}
-      \\pgfmathsetmacro{\\rot}{rand*0.1}
-      \\shade[ball color=gray] (\\i+\\dx+\\rot,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-2.8}) circle(0.45);
-      \\shade[ball color=gray] (\\i+\\dx,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-3.7}) circle(0.45);
-      \\shade[ball color=red] (\\i+\\dx-\\rot,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-4.6}) circle(0.45);
-    }
-  }
-\\end{tikzpicture}
-\\end{document}
-						"""
-					]
-		}, (@error, @response, @body) =>
-			done()
-
-	it "should return the pdf", ->
-		for file in @body.compile.outputFiles
-			return if file.type == "pdf"
-		throw new Error("no pdf returned")
-	
-	it "should return the log", ->
-		for file in @body.compile.outputFiles
-			return if file.type == "log"
-		throw new Error("no log returned")

Некоторые файлы не были показаны из-за большого количества измененных файлов