mediawiki/services/eventstreams (main)

sourcepatches
$ date
--- stdout ---
Thu Apr 18 12:00:08 UTC 2024

--- end ---
$ git clone file:///srv/git/mediawiki-services-eventstreams.git repo --depth=1 -b master
--- stderr ---
Cloning into 'repo'...
--- stdout ---

--- end ---
$ git config user.name libraryupgrader
--- stdout ---

--- end ---
$ git config user.email tools.libraryupgrader@tools.wmflabs.org
--- stdout ---

--- end ---
$ git submodule update --init
--- stdout ---

--- end ---
$ grr init
--- stdout ---
Installed commit-msg hook.

--- end ---
$ git show-ref refs/heads/master
--- stdout ---
30fa4592e015f38e58740033114cfd72861b9f6c refs/heads/master

--- end ---
$ /usr/bin/npm i --package-lock-only
--- stderr ---
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'eslint-plugin-jsdoc@39.2.2',
npm WARN EBADENGINE   required: { node: '^14 || ^16 || ^17' },
npm WARN EBADENGINE   current: { node: 'v18.19.0', npm: '9.2.0' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '@es-joy/jsdoccomment@0.23.6',
npm WARN EBADENGINE   required: { node: '^12 || ^14 || ^16 || ^17' },
npm WARN EBADENGINE   current: { node: 'v18.19.0', npm: '9.2.0' }
npm WARN EBADENGINE }
--- stdout ---

up to date, audited 582 packages in 6s

67 packages are looking for funding
  run `npm fund` for details

8 vulnerabilities (6 moderate, 2 high)

To address issues that do not require attention, run:
  npm audit fix

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.

--- end ---
$ package-lock-lint package-lock.json
--- stdout ---
Checking package-lock.json

--- end ---
Editing .gitignore to remove package-lock.json
$ /usr/bin/npm audit --json
--- stdout ---
{
  "auditReportVersion": 2,
  "vulnerabilities": {
    "@wikimedia/url-get": {
      "name": "@wikimedia/url-get",
      "severity": "moderate",
      "isDirect": true,
      "via": [
        "preq"
      ],
      "effects": [],
      "range": "*",
      "nodes": [
        "node_modules/@wikimedia/url-get"
      ],
      "fixAvailable": false
    },
    "limitation": {
      "name": "limitation",
      "severity": "moderate",
      "isDirect": false,
      "via": [
        "wikimedia-kad-fork"
      ],
      "effects": [],
      "range": ">=0.2.3",
      "nodes": [
        "node_modules/limitation"
      ],
      "fixAvailable": true
    },
    "ms": {
      "name": "ms",
      "severity": "moderate",
      "isDirect": false,
      "via": [
        {
          "source": 1094419,
          "name": "ms",
          "dependency": "ms",
          "title": "Vercel ms Inefficient Regular Expression Complexity vulnerability",
          "url": "https://github.com/advisories/GHSA-w9mr-4mfr-499f",
          "severity": "moderate",
          "cwe": [
            "CWE-1333"
          ],
          "cvss": {
            "score": 5.3,
            "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L"
          },
          "range": "<2.0.0"
        }
      ],
      "effects": [
        "wikimedia-kad-fork"
      ],
      "range": "<2.0.0",
      "nodes": [
        "node_modules/wikimedia-kad-fork/node_modules/ms"
      ],
      "fixAvailable": true
    },
    "preq": {
      "name": "preq",
      "severity": "high",
      "isDirect": true,
      "via": [
        "request",
        "requestretry"
      ],
      "effects": [
        "@wikimedia/url-get"
      ],
      "range": "*",
      "nodes": [
        "node_modules/preq"
      ],
      "fixAvailable": false
    },
    "request": {
      "name": "request",
      "severity": "moderate",
      "isDirect": false,
      "via": [
        {
          "source": 1096727,
          "name": "request",
          "dependency": "request",
          "title": "Server-Side Request Forgery in Request",
          "url": "https://github.com/advisories/GHSA-p8p7-x288-28g6",
          "severity": "moderate",
          "cwe": [
            "CWE-918"
          ],
          "cvss": {
            "score": 6.1,
            "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
          },
          "range": "<=2.88.2"
        },
        "tough-cookie"
      ],
      "effects": [
        "preq",
        "requestretry"
      ],
      "range": "*",
      "nodes": [
        "node_modules/request"
      ],
      "fixAvailable": false
    },
    "requestretry": {
      "name": "requestretry",
      "severity": "high",
      "isDirect": false,
      "via": [
        {
          "source": 1090420,
          "name": "requestretry",
          "dependency": "requestretry",
          "title": "Cookie exposure in requestretry",
          "url": "https://github.com/advisories/GHSA-hjp8-2cm3-cc45",
          "severity": "high",
          "cwe": [
            "CWE-200"
          ],
          "cvss": {
            "score": 7.5,
            "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
          },
          "range": "<7.0.0"
        },
        "request"
      ],
      "effects": [
        "preq"
      ],
      "range": "*",
      "nodes": [
        "node_modules/requestretry"
      ],
      "fixAvailable": false
    },
    "tough-cookie": {
      "name": "tough-cookie",
      "severity": "moderate",
      "isDirect": false,
      "via": [
        {
          "source": 1096643,
          "name": "tough-cookie",
          "dependency": "tough-cookie",
          "title": "tough-cookie Prototype Pollution vulnerability",
          "url": "https://github.com/advisories/GHSA-72xf-g2v4-qvf3",
          "severity": "moderate",
          "cwe": [
            "CWE-1321"
          ],
          "cvss": {
            "score": 6.5,
            "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N"
          },
          "range": "<4.1.3"
        }
      ],
      "effects": [
        "request"
      ],
      "range": "<4.1.3",
      "nodes": [
        "node_modules/tough-cookie"
      ],
      "fixAvailable": false
    },
    "wikimedia-kad-fork": {
      "name": "wikimedia-kad-fork",
      "severity": "moderate",
      "isDirect": false,
      "via": [
        "ms"
      ],
      "effects": [
        "limitation"
      ],
      "range": "*",
      "nodes": [
        "node_modules/wikimedia-kad-fork"
      ],
      "fixAvailable": true
    }
  },
  "metadata": {
    "vulnerabilities": {
      "info": 0,
      "low": 0,
      "moderate": 6,
      "high": 2,
      "critical": 0,
      "total": 8
    },
    "dependencies": {
      "prod": 210,
      "dev": 361,
      "optional": 12,
      "peer": 0,
      "peerOptional": 0,
      "total": 581
    }
  }
}

--- end ---
Upgrading n:eslint-config-wikimedia from ^0.25.1 -> 0.27.0
$ /usr/bin/npm install
--- stderr ---
npm WARN deprecated kad-fs@0.0.4: This package is no longer maintained.
npm WARN deprecated har-validator@5.1.5: this library is no longer supported
npm WARN deprecated kad-memstore@0.0.1: This package is no longer maintained.
npm WARN deprecated gc-stats@1.4.1: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
npm WARN deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
--- stdout ---

added 608 packages, and audited 609 packages in 2m

75 packages are looking for funding
  run `npm fund` for details

8 vulnerabilities (6 moderate, 2 high)

To address issues that do not require attention, run:
  npm audit fix

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.

--- end ---
$ package-lock-lint package-lock.json
--- stdout ---
Checking package-lock.json

--- end ---
$ package-lock-lint package-lock.json
--- stdout ---
Checking package-lock.json

--- end ---
$ ./node_modules/.bin/eslint . --fix
--- stdout ---

/src/repo/app.js
   66:37  warning  Found non-literal argument to RegExp Constructor  security/detect-non-literal-regexp
  141:1   warning  The type 'Application' is undefined               jsdoc/no-undefined-types
  156:31  warning  Found non-literal argument in require             security/detect-non-literal-require
  195:1   warning  The type 'Application' is undefined               jsdoc/no-undefined-types

/src/repo/config.dev.yaml
  90:1  warning  This line has a length of 179. Maximum allowed is 100  max-len

/src/repo/config.prod.yaml
  91:1  warning  This line has a length of 177. Maximum allowed is 100  max-len

/src/repo/config.yaml
  90:1  warning  This line has a length of 179. Maximum allowed is 100  max-len

/src/repo/lib/eventstreams-util.js
  117:1  warning  This line has a length of 101. Maximum allowed is 100  max-len
  118:1  warning  This line has a length of 104. Maximum allowed is 100  max-len

/src/repo/lib/util.js
   99:1  warning  The type 'Application' is undefined  jsdoc/no-undefined-types
  143:1  warning  The type 'Application' is undefined  jsdoc/no-undefined-types
  223:1  warning  The type 'Router' is undefined       jsdoc/no-undefined-types
  243:1  warning  The type 'Application' is undefined  jsdoc/no-undefined-types

/src/repo/routes/stream.js
   28:1  warning  Missing JSDoc @return declaration                      jsdoc/require-returns
  249:1  warning  Missing JSDoc @return declaration                      jsdoc/require-returns
  424:1  warning  This line has a length of 103. Maximum allowed is 100  max-len

/src/repo/server.js
  1:1  error  ES2023 Hashbang comments are forbidden  es-x/no-hashbang

/src/repo/stream-config.yaml
  5:1  warning  This line has a length of 169. Maximum allowed is 100  max-len

/src/repo/test/features/app/spec.js
   70:25  error    'spec' is already declared in the upper scope on line 11 column 5  no-shadow
  154:14  warning  Found non-literal argument to RegExp Constructor                   security/detect-non-literal-regexp
  265:16  error    'spec' is already declared in the upper scope on line 11 column 5  no-shadow

/src/repo/test/features/info/info.js
  1:11  error  'describe' is already defined as a built-in global variable  no-redeclare
  1:21  error  'it' is already defined as a built-in global variable        no-redeclare
  1:25  error  'before' is already defined as a built-in global variable    no-redeclare
  1:33  error  'after' is already defined as a built-in global variable     no-redeclare

/src/repo/test/utils/assert.js
  23:1  warning  The type 'integer' is undefined  jsdoc/no-undefined-types

/src/repo/test/utils/server.js
   9:9   error  Unexpected dangling '_' in '_spec'     no-underscore-dangle
  13:14  error  Unexpected dangling '_' in '_running'  no-underscore-dangle
  19:30  error  Unexpected dangling '_' in '_impl'     no-underscore-dangle
  19:30  error  Unexpected dangling '_' in '_runner'   no-underscore-dangle
  20:27  error  Unexpected dangling '_' in '_impl'     no-underscore-dangle
  20:27  error  Unexpected dangling '_' in '_runner'   no-underscore-dangle
  22:14  error  Unexpected dangling '_' in '_spec'     no-underscore-dangle
  30:17  error  Unexpected dangling '_' in '_spec'     no-underscore-dangle
  34:17  error  Unexpected dangling '_' in '_spec'     no-underscore-dangle
  40:27  error  Unexpected dangling '_' in '_impl'     no-underscore-dangle
  40:27  error  Unexpected dangling '_' in '_runner'   no-underscore-dangle
  41:27  error  Unexpected dangling '_' in '_spec'     no-underscore-dangle
  49:19  error  Unexpected dangling '_' in '_impl'     no-underscore-dangle
  49:19  error  Unexpected dangling '_' in '_runner'   no-underscore-dangle
  50:19  error  Unexpected dangling '_' in '_spec'     no-underscore-dangle

/src/repo/ui/src/apis/EventStreamsApi.js
  1:1  error  Parsing error: 'import' and 'export' may appear only with 'sourceType: module'

/src/repo/ui/src/config/index.js
  1:1  error  Parsing error: 'import' and 'export' may appear only with 'sourceType: module'

/src/repo/ui/src/main.js
  1:1  error  Parsing error: 'import' and 'export' may appear only with 'sourceType: module'

/src/repo/ui/src/router/index.js
  1:1  error  Parsing error: 'import' and 'export' may appear only with 'sourceType: module'

/src/repo/ui/src/utils/index.js
  57:1  error  Parsing error: 'import' and 'export' may appear only with 'sourceType: module'

✖ 46 problems (27 errors, 19 warnings)


--- end ---
$ ./node_modules/.bin/eslint . -f json
--- stdout ---
[{"filePath":"/src/repo/.eslintrc.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/.travis.yml","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/app.js","messages":[{"ruleId":"security/detect-non-literal-regexp","severity":1,"message":"Found non-literal argument to RegExp Constructor","line":66,"column":37,"nodeType":"NewExpression","endLine":68,"endColumn":28},{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'Application' is undefined.","line":141,"column":1,"nodeType":"Block","endLine":141,"endColumn":1},{"ruleId":"security/detect-non-literal-require","severity":1,"message":"Found non-literal argument in require","line":156,"column":31,"nodeType":"CallExpression","endLine":156,"endColumn":61},{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'Application' is undefined.","line":195,"column":1,"nodeType":"Block","endLine":195,"endColumn":1}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\nconst http = require('http');\nconst BBPromise = require('bluebird');\nconst express = require('express');\n// === EventStreams modification ===\n// const compression = require('compression');\n// === End EventStreams modification ===\nconst bodyParser = require('body-parser');\nconst fs = BBPromise.promisifyAll(require('fs'));\nconst sUtil = require('./lib/util');\nconst packageInfo = require('./package.json');\nconst yaml = require('js-yaml');\nconst addShutdown = require('http-shutdown');\nconst path = require('path');\n\n\n/**\n * Creates an express app and initialises it\n *\n * @param {Object} options the options to initialise the app with\n * @return {bluebird} the promise resolving to the app object\n */\nfunction initApp(options) {\n\n    // the main application object\n    const app = express();\n\n    // get the options and make them available in the app\n    app.logger = options.logger;    // the logging device\n    app.metrics = options.metrics;  // the metrics\n    app.conf = options.config;      // this app's config options\n    app.info = packageInfo;         // this app's package info\n\n    // ensure some sane defaults\n    app.conf.port = app.conf.port || 8888;\n    app.conf.interface = app.conf.interface || '0.0.0.0';\n    app.conf.compression_level =\n      app.conf.compression_level === undefined ? 3 : app.conf.compression_level;\n    app.conf.cors = app.conf.cors === undefined ? '*' : app.conf.cors;\n    if (app.conf.csp === undefined) {\n        app.conf.csp = \"default-src 'self'; object-src 'none'; media-src *; img-src *; style-src *; frame-ancestors 'self'\";\n    }\n\n    // set outgoing proxy\n    if (app.conf.proxy) {\n        process.env.HTTP_PROXY = app.conf.proxy;\n        // if there is a list of domains which should\n        // not be proxied, set it\n        if (app.conf.no_proxy_list) {\n            if (Array.isArray(app.conf.no_proxy_list)) {\n                process.env.NO_PROXY = app.conf.no_proxy_list.join(',');\n            } else {\n                process.env.NO_PROXY = app.conf.no_proxy_list;\n            }\n        }\n    }\n\n    // set up header whitelisting for logging\n    if (!app.conf.log_header_whitelist) {\n        app.conf.log_header_whitelist = [\n            'cache-control', 'content-type', 'content-length', 'if-match',\n            'user-agent', 'x-request-id'\n        ];\n    }\n    app.conf.log_header_whitelist = new RegExp(`^(?:${ app.conf.log_header_whitelist.map((item) => {\n        return item.trim();\n    }).join('|') })$`, 'i');\n\n    // set up the spec\n    if (!app.conf.spec) {\n        app.conf.spec = `${ __dirname }/spec.yaml`;\n    }\n    if (app.conf.spec.constructor !== Object) {\n        try {\n            app.conf.spec = yaml.safeLoad(fs.readFileSync(app.conf.spec));\n        } catch (e) {\n            app.logger.log('warn/spec', `Could not load the spec: ${ e }`);\n            app.conf.spec = {};\n        }\n    }\n    if (!app.conf.spec.openapi) {\n        app.conf.spec.openapi = '3.0.0';\n    }\n    if (!app.conf.spec.info) {\n        app.conf.spec.info = {\n            version: app.info.version,\n            title: app.info.name,\n            description: app.info.description\n        };\n    }\n    app.conf.spec.info.version = app.info.version;\n    if (!app.conf.spec.paths) {\n        app.conf.spec.paths = {};\n    }\n\n    // set the CORS and CSP headers\n    app.all('*', (req, res, next) => {\n        if (app.conf.cors !== false) {\n            res.header('access-control-allow-origin', app.conf.cors);\n            res.header('access-control-allow-headers', 'accept, x-requested-with, content-type');\n            res.header('access-control-expose-headers', 'etag');\n        }\n        if (app.conf.csp !== false) {\n            res.header('x-xss-protection', '1; mode=block');\n            res.header('x-content-type-options', 'nosniff');\n            res.header('x-frame-options', 'SAMEORIGIN');\n            res.header('content-security-policy', app.conf.csp);\n            res.header('x-content-security-policy', app.conf.csp);\n            res.header('x-webkit-csp', app.conf.csp);\n        }\n        sUtil.initAndLogRequest(req, app);\n        next();\n    });\n\n    // set up the user agent header string to use for requests\n    app.conf.user_agent = app.conf.user_agent || app.info.name;\n\n    // disable the X-Powered-By header\n    app.set('x-powered-by', false);\n    // disable the ETag header - users should provide them!\n    app.set('etag', false);\n\n    // === EventStreams modification ===\n    // Don't use compression, streams never end.\n    // app.use(compression({ level: app.conf.compression_level }));\n    // === End EventStreams modification ===\n\n    // use the JSON body parser\n    app.use(bodyParser.json({ limit: app.conf.max_body_size || '100kb' }));\n    // use the application/x-www-form-urlencoded parser\n    app.use(bodyParser.urlencoded({ extended: true }));\n\n    return BBPromise.resolve(app);\n\n}\n\n/**\n * Loads all routes declared in routes/ into the app\n *\n * @param {Application} app the application object to load routes into\n * @param {string} dir routes folder\n * @return {bluebird} a promise resolving to the app object\n */\nfunction loadRoutes(app, dir) {\n\n    // recursively load routes from .js files under routes/\n    return fs.readdirAsync(dir).map((fname) => {\n        return BBPromise.try(() => {\n            const resolvedPath = path.resolve(dir, fname);\n            const isDirectory = fs.statSync(resolvedPath).isDirectory();\n            if (isDirectory) {\n                loadRoutes(app, resolvedPath);\n            } else if (/\\.js$/.test(fname)) {\n                // import the route file\n                const route = require(`${ dir }/${ fname }`);\n                return route(app);\n            }\n        }).then((route) => {\n            if (route === undefined) {\n                return undefined;\n            }\n            // check that the route exports the object we need\n            if (route.constructor !== Object || !route.path || !route.router ||\n                !(route.api_version || route.skip_domain)) {\n                throw new TypeError(`routes/${ fname } does not export the correct object!`);\n            }\n            // normalise the path to be used as the mount point\n            if (route.path[0] !== '/') {\n                route.path = `/${ route.path }`;\n            }\n            if (route.path[route.path.length - 1] !== '/') {\n                route.path = `${ route.path }/`;\n            }\n            if (!route.skip_domain) {\n                route.path = `/:domain/v${ route.api_version }${ route.path }`;\n            }\n            // wrap the route handlers with Promise.try() blocks\n            sUtil.wrapRouteHandlers(route, app);\n            // all good, use that route\n            app.use(route.path, route.router);\n        });\n    }).then(() => {\n        // catch errors\n        sUtil.setErrorHandler(app);\n        // route loading is now complete, return the app object\n        return BBPromise.resolve(app);\n    });\n\n}\n\n/**\n * Creates and start the service's web server\n *\n * @param {Application} app the app object to use in the service\n * @return {bluebird} a promise creating the web server\n */\nfunction createServer(app) {\n\n    // return a promise which creates an HTTP server,\n    // attaches the app to it, and starts accepting\n    // incoming client requests\n    let server;\n    return new BBPromise((resolve) => {\n        server = http.createServer(app).listen(\n            app.conf.port,\n            app.conf.interface,\n            resolve\n        );\n        server = addShutdown(server);\n    }).then(() => {\n        app.logger.log('info',\n            `Worker ${ process.pid } listening on ${ app.conf.interface || '*' }:${ app.conf.port }`);\n\n        // Don't delay incomplete packets for 40ms (Linux default) on\n        // pipelined HTTP sockets. We write in large chunks or buffers, so\n        // lack of coalescing should not be an issue here.\n        server.on('connection', (socket) => {\n            socket.setNoDelay(true);\n        });\n\n        return server;\n    });\n\n}\n\n/**\n * The service's entry point. It takes over the configuration\n * options and the logger- and metrics-reporting objects from\n * service-runner and starts an HTTP server, attaching the application\n * object to it.\n *\n * @param {Object} options the options to initialise the app with\n * @return {bluebird} HTTP server\n */\nmodule.exports = (options) => {\n\n    return initApp(options)\n    .then((app) => loadRoutes(app, `${ __dirname }/routes`))\n    .then((app) => {\n        // serve static files from static/\n        app.use('/static', express.static(`${ __dirname }/static`));\n        return app;\n    }).then(createServer);\n\n};\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/config.dev.yaml","messages":[{"ruleId":"max-len","severity":1,"message":"This line has a length of 179. Maximum allowed is 100.","line":90,"column":1,"nodeType":"Program","messageId":"max","endLine":90,"endColumn":180}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"# Number of worker processes to spawn.\n# Set to 0 to run everything in a single process without clustering.\n# Use 'ncpu' to run as many workers as there are CPU units\nnum_workers: 0\n\n# Log error messages and gracefully restart a worker if v8 reports that it\n# uses more heap (note: not RSS) than this many mb.\nworker_heap_limit_mb: 250\n\n# Logger info\nlogging:\n  level: debug\n  streams:\n    - type: debug\n#  streams:\n#  # Use gelf-stream -> logstash\n#  - type: gelf\n#    host: logstash1003.eqiad.wmnet\n#    port: 12201\n\n# metrics:\n#   - type: prometheus\n#     port: 9102\n\nservices:\n  - name: eventstreams\n    # a relative path or the name of an npm package, if different from name\n    module: ./app.js\n    # optionally, a version constraint of the npm package\n    # version: ^0.4.0\n    # per-service config\n    conf:\n      port: 8092\n      # interface: localhost # uncomment to only listen on localhost\n      # more per-service config settings\n      # the location of the spec, defaults to spec.yaml if not specified\n      spec: ./spec.yaml\n      # allow cross-domain requests to the API (default '*')\n      cors: \"*\"\n\n      # If enabled, a HTML based stream GUI will be made available at /v2/ui.\n      # Defaults to true if undefined.\n      # stream_ui_enabled: true\n\n      # Connection limiting.  EventStreams connections are\n      # expected to be long lived, so we can't use typical\n      # connections per second type ratelimiting.  Instead,\n      # we limit the number of connections per X-Client-IP.\n      # client_ip_connection_limit: 2\n\n      # A URI from which 'stream configuration' will be fetched.\n      #\n      # The object returned by this URI should map a stream name to its config settings,\n      # which must include topics.  If schema_title is set, it will be mentioned\n      # in the generated OpenAPI spec.\n      #\n      stream_config_uri: ./stream-config.yaml\n      # OR:\n      # stream_config_uri: https://meta.wikimedia.org/w/api.php?action=streamconfigs\n\n      # If provided, these options will be passed when opening stream_config_uri\n      # for reading.  If stream_config_uri is a local file path, fs.readFile options should be\n      # given.  If stream_config_uri is a remote http URI, preq.get options should be used.\n      # stream_config_uri_options: {'headers': {'Host': 'streamconfig.service.org'}}\n\n      # If set, the stream configs are expected to live in a subobject\n      # of the result object returned from stream_config_uri. The stream\n      # configs object will be extracted at this path.\n      # This should be a path string that Lodash#get understands.\n      # stream_config_object_path: 'streams'\n\n      # How long in seconds stream configs live in cache before being recached.\n      # 0 or unset means no expiration.\n      # stream_config_ttl: 60\n\n      # Default stream config settings to apply to the configs fetched from stream_config_uri.\n      # EventStreams uses some extra settings:\n      # - stream_aliases\n      #     An array of stream route aliases from which to allow subscription to the stream.\n      #     You might use this if you want to expose a stream at an additional name\n      #     than its canonical stream name.\n      # - description\n      #     Will be added to the OpenAPI spec's description of the stream route.\n      # - $schema\n      #     If set, will be used to augmment the OpenAPI spec's response schema and examples.\n      # stream_config_defaults:\n      #   mediawiki.page-create:\n      #     stream_aliases: [page-create]\n      #     description: |-\n      #       mediawiki.page-create events. This page create stream is just the first revision create event for each page. As such, it reuses the mediawiki/revision/create schema.\n      #   mediawiki.revision-create:\n      #     stream_aliases: [revision-create]\n      #     description: |-\n      #       mediawiki.revision-create events.\n\n      # If set, these URIs will be prepended to any relative schema URI\n      # extracted from the $schema stream config setting, or the URI constructed\n      # using the schema_title stream config setting.. The resulting URLs will\n      # be searched until a schema is found.  Change this\n      # to match paths to your schema repositories.\n      # schema_base_uris: [\n      #     https://schema.wikimedia.org/repositories/primary/jsonschema,\n      #     https://schema.wikimedia.org/repositories/secondary/jsonschema\n      # ]\n      # If provided, these options will be passed when opening schema URIs.\n      # for reading.  If a schema URI is a local file path, fs.readFile options should be\n      # given.  If a schema URI is a remote http URI, preq.get options should be used.\n      # schema_uri_options: {'headers': {'Host': 'schema.service.org'}}\n\n      # If given, and no $schema is found in a stream config, a schema URI will\n      # attempt to be constructed a /${schema_title}/${schema_latest_version}.\n      # If this is not set, no schema URI will be inferred using schema_title.\n      # schema_latest_version: 'latest'\n\n      # Explicit list of allowed streams.  If this is not set, all streams fetched from\n      # the stream_config_uri will be exposed.  Otherwise, only streams in this list\n      # will be exposed.\n      # allowed_streams:\n      #   - mediawiki.page-create\n      #   - mediawiki.revision-create\n\n      # kafka configs go here.\n      kafka:\n        metadata.broker.list: \"localhost:9092\"\n        statistics.interval.ms: 5000\n\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/config.prod.yaml","messages":[{"ruleId":"max-len","severity":1,"message":"This line has a length of 177. Maximum allowed is 100.","line":91,"column":1,"nodeType":"Program","messageId":"max","endLine":91,"endColumn":178}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"# Number of worker processes to spawn.\n# Set to 0 to run everything in a single process without clustering.\n# Use 'ncpu' to run as many workers as there are CPU units\nnum_workers: 0\n\n# Log error messages and gracefully restart a worker if v8 reports that it\n# uses more heap (note: not RSS) than this many mb.\nworker_heap_limit_mb: 500\n\n# Logger info\nlogging:\n  level: info\n  streams:\n    - type: debug\n  # streams:\n  # # Use gelf-stream -> logstash\n  # - type: gelf\n  #   host: logstash1003.eqiad.wmnet\n  #   port: 12201\n\n# Statsd metrics reporter\nmetrics:\n  - type: prometheus\n    port: 9102\n\nservices:\n  - name: eventstreams\n    # a relative path or the name of an npm package, if different from name\n    module: ./app.js\n    # optionally, a version constraint of the npm package\n    # version: ^0.4.0\n    # per-service config\n    conf:\n      port: 8092\n      # interface: localhost # uncomment to only listen on localhost\n      # more per-service config settings\n      # the location of the spec, defaults to spec.yaml if not specified\n      spec: ./spec.yaml\n      # allow cross-domain requests to the API (default '*')\n      cors: \"*\"\n\n      # If enabled, a HTML based stream GUI will be made available at /v2/ui.\n      # Defaults to true if undefined.\n      # stream_ui_enabled: true\n\n      # Connection limiting.  EventStreams connections are\n      # expected to be long lived, so we can't use typical\n      # connections per second type ratelimiting.  Instead,\n      # we limit the number of connections per X-Client-IP.\n      client_ip_connection_limit: 2\n\n      # A URI from which 'stream configuration' will be fetched.\n      #\n      # The object returned by this URI should map a stream name to its config settings,\n      # which must include topics.  If schema_title is set, it will be mentioned\n      # in the generated OpenAPI spec.\n      #\n      # stream_config_uri: ./stream-config.yaml\n      # OR:\n      stream_config_uri: https://meta.wikimedia.org/w/api.php?action=streamconfigs\n\n      # If provided, these options will be passed when opening stream_config_uri\n      # for reading.  If this is a local file path, fs.readFile options should be\n      # given.  If this is a remote http URI, preq.get options should be used.\n      # stream_config_uri_options: {'headers': {'Host': 'streamconfig.service.org'}}\n\n      # If set, the stream configs are expected to live in a subobject\n      # of the result object returned from stream_config_uri. The stream\n      # configs object will be extracted at this path.\n      # This should be a path string that Lodash#get understands.\n      stream_config_object_path: streams\n\n      # How long in seconds stream configs live in cache before being recached.\n      # 0 or unset means no expiration.\n      stream_config_ttl: 300\n\n      # Default stream config settings to apply to the configs fetched from stream_config_uri.\n      # EventStreams uses some extra settings:\n      # - stream_aliases\n      #     An array of stream route aliases from which to allow subscription to the stream.\n      #     You might use this if you want to expose a stream at an additional name\n      #     than its canonical stream name.\n      # - description\n      #     Will be added to the OpenAPI spec's description of the stream route.\n      # - $schema\n      #     If set, will be used to augmment the OpenAPI spec's response schema and examples.\n      stream_config_defaults:\n        mediawiki.page-create:\n          stream_aliases: [ page-create ]\n          description: |-\n            mediawiki.page-create events. This page create stream is just the first revision create event for each page. As such, it reuses the mediawiki/revision/create schema.\n        mediawiki.page-delete:\n          stream_aliases: [ page-delete ]\n        mediawiki.page-move:\n          stream_aliases: [ page-move ]\n        mediawiki.page-undelete:\n          stream_aliases: [ page-undelete ]\n        mediawiki.page-properties-change:\n          stream_aliases: [ page-properties-change ]\n        mediawiki.revision-create:\n          stream_aliases: [ revision-create ]\n        mediawiki.recentchange:\n          stream_aliases: [ recentchange-delete ]\n\n      # If set, these URIs will be prepended to any relative schema URI\n      # extracted from the $schema stream config setting, or the URI constructed\n      # using the schema_title stream config setting.. The resulting URLs will\n      # be searched until a schema is found.  Change this\n      # to match paths to your schema repositories.\n      schema_base_uris:\n        - https://schema.wikimedia.org/repositories/primary/jsonschema\n        - https://schema.wikimedia.org/repositories/secondary/jsonschema\n\n      # If provided, these options will be passed when opening schema URIs.\n      # for reading.  If a schema URI is a local file path, fs.readFile options should be\n      # given.  If a schema URI is a remote http URI, preq.get options should be used.\n      # schema_uri_options: {'headers': {'Host': 'schema.service.org'}}\n\n      # If given, and no $schema is found in a stream config, a schema URI will\n      # attempt to be constructed a /${schema_title}/${schema_latest_version}.\n      # If this is not set, no schema URI will be inferred using schema_title.\n      schema_latest_version: latest\n\n      # Explicit list of allowed streams.  If this is not set, all streams fetched from\n      # the stream_config_uri will be exposed.  Otherwise, only streams in this list\n      # will be exposed.\n      allowed_streams:\n        - mediawiki.page-create\n        - mediawiki.page-delete\n        - mediawiki.page-move\n        - mediawiki.page-undelete\n        - mediawiki.page-properties-change\n        - mediawiki.revision-create\n        - mediawiki.recentchange\n\n      # kafka configs go here.\n      kafka:\n        metadata.broker.list: \"localhost:9092\"\n        statistics.interval.ms: 60000\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/config.yaml","messages":[{"ruleId":"max-len","severity":1,"message":"This line has a length of 179. Maximum allowed is 100.","line":90,"column":1,"nodeType":"Program","messageId":"max","endLine":90,"endColumn":180}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"# Number of worker processes to spawn.\n# Set to 0 to run everything in a single process without clustering.\n# Use 'ncpu' to run as many workers as there are CPU units\nnum_workers: 0\n\n# Log error messages and gracefully restart a worker if v8 reports that it\n# uses more heap (note: not RSS) than this many mb.\nworker_heap_limit_mb: 250\n\n# Logger info\nlogging:\n  level: debug\n  streams:\n    - type: debug\n#  streams:\n#  # Use gelf-stream -> logstash\n#  - type: gelf\n#    host: logstash1003.eqiad.wmnet\n#    port: 12201\n\n# metrics:\n#   - type: prometheus\n#     port: 9102\n\nservices:\n  - name: eventstreams\n    # a relative path or the name of an npm package, if different from name\n    module: ./app.js\n    # optionally, a version constraint of the npm package\n    # version: ^0.4.0\n    # per-service config\n    conf:\n      port: 8092\n      # interface: localhost # uncomment to only listen on localhost\n      # more per-service config settings\n      # the location of the spec, defaults to spec.yaml if not specified\n      spec: ./spec.yaml\n      # allow cross-domain requests to the API (default '*')\n      cors: \"*\"\n\n      # If enabled, a HTML based stream GUI will be made available at /v2/ui.\n      # Defaults to true if undefined.\n      # stream_ui_enabled: true\n\n      # Connection limiting.  EventStreams connections are\n      # expected to be long lived, so we can't use typical\n      # connections per second type ratelimiting.  Instead,\n      # we limit the number of connections per X-Client-IP.\n      # client_ip_connection_limit: 2\n\n      # A URI from which 'stream configuration' will be fetched.\n      #\n      # The object returned by this URI should map a stream name to its config settings,\n      # which must include topics.  If schema_title is set, it will be mentioned\n      # in the generated OpenAPI spec.\n      #\n      stream_config_uri: ./stream-config.yaml\n      # OR:\n      # stream_config_uri: https://meta.wikimedia.org/w/api.php?action=streamconfigs\n\n      # If provided, these options will be passed when opening stream_config_uri\n      # for reading.  If stream_config_uri is a local file path, fs.readFile options should be\n      # given.  If stream_config_uri is a remote http URI, preq.get options should be used.\n      # stream_config_uri_options: {'headers': {'Host': 'streamconfig.service.org'}}\n\n      # If set, the stream configs are expected to live in a subobject\n      # of the result object returned from stream_config_uri. The stream\n      # configs object will be extracted at this path.\n      # This should be a path string that Lodash#get understands.\n      # stream_config_object_path: 'streams'\n\n      # How long in seconds stream configs live in cache before being recached.\n      # 0 or unset means no expiration.\n      # stream_config_ttl: 60\n\n      # Default stream config settings to apply to the configs fetched from stream_config_uri.\n      # EventStreams uses some extra settings:\n      # - stream_aliases\n      #     An array of stream route aliases from which to allow subscription to the stream.\n      #     You might use this if you want to expose a stream at an additional name\n      #     than its canonical stream name.\n      # - description\n      #     Will be added to the OpenAPI spec's description of the stream route.\n      # - $schema\n      #     If set, will be used to augmment the OpenAPI spec's response schema and examples.\n      # stream_config_defaults:\n      #   mediawiki.page-create:\n      #     stream_aliases: [page-create]\n      #     description: |-\n      #       mediawiki.page-create events. This page create stream is just the first revision create event for each page. As such, it reuses the mediawiki/revision/create schema.\n      #   mediawiki.revision-create:\n      #     stream_aliases: [revision-create]\n      #     description: |-\n      #       mediawiki.revision-create events.\n\n      # If set, these URIs will be prepended to any relative schema URI\n      # extracted from the $schema stream config setting, or the URI constructed\n      # using the schema_title stream config setting.. The resulting URLs will\n      # be searched until a schema is found.  Change this\n      # to match paths to your schema repositories.\n      # schema_base_uris: [\n      #     https://schema.wikimedia.org/repositories/primary/jsonschema,\n      #     https://schema.wikimedia.org/repositories/secondary/jsonschema\n      # ]\n      # If provided, these options will be passed when opening schema URIs.\n      # for reading.  If a schema URI is a local file path, fs.readFile options should be\n      # given.  If a schema URI is a remote http URI, preq.get options should be used.\n      # schema_uri_options: {'headers': {'Host': 'schema.service.org'}}\n\n      # If given, and no $schema is found in a stream config, a schema URI will\n      # attempt to be constructed a /${schema_title}/${schema_latest_version}.\n      # If this is not set, no schema URI will be inferred using schema_title.\n      # schema_latest_version: 'latest'\n\n      # Explicit list of allowed streams.  If this is not set, all streams fetched from\n      # the stream_config_uri will be exposed.  Otherwise, only streams in this list\n      # will be exposed.\n      # allowed_streams:\n      #   - mediawiki.page-create\n      #   - mediawiki.revision-create\n\n      # kafka configs go here.\n      kafka:\n        metadata.broker.list: \"localhost:9092\"\n        statistics.interval.ms: 5000\n\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/lib/eventstreams-util.js","messages":[{"ruleId":"max-len","severity":1,"message":"This line has a length of 101. Maximum allowed is 100.","line":117,"column":1,"nodeType":"Program","messageId":"max","endLine":117,"endColumn":102},{"ruleId":"max-len","severity":1,"message":"This line has a length of 104. Maximum allowed is 100.","line":118,"column":1,"nodeType":"Program","messageId":"max","endLine":118,"endColumn":105}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\nconst _        = require('lodash');\n\nconst {\n    objectFactory\n} = require('@wikimedia/url-get');\n\n/**\n * Custom message deserializer for eventstreams.\n * Augments the deserialized message with kafka\n * metadata in the .meta subobject.\n *\n * @param {Object} kafkaMessage\n * @return {Object}\n */\nfunction deserializer(kafkaMessage) {\n    kafkaMessage.message = objectFactory(kafkaMessage.value);\n\n    if (!kafkaMessage.message.meta) {\n        kafkaMessage.message.meta = {};\n    }\n    kafkaMessage.message.meta.topic     = kafkaMessage.topic;\n    kafkaMessage.message.meta.partition = kafkaMessage.partition;\n    kafkaMessage.message.meta.offset    = kafkaMessage.offset;\n    if (kafkaMessage.key) {\n        kafkaMessage.message.meta.key   = kafkaMessage.key;\n    }\n\n    return kafkaMessage;\n}\n\n/**\n * Time sensitive, one off, implementation to support T354456.\n *\n * Redact message content based on deny list of page titles,\n * populated with the full page name.\n *\n * TODO: re-evaluate this approach after integration testing.\n *\n * @param {{string: string[]}} redacted_pages Map of wiki to list of redacted pages.\n * @param {Object} options\n * @param {Object} options.logger Logger\n * @param {string} options.clientIp\n * @param {string} options.userAgent\n * @param {string} options.referer\n * @return {function(Object): Object}\n */\nfunction makeMediaWikiRedactorDeserializer(redacted_pages, options = {}) {\n    // Time sensitive, one off, implementation to support T354456.\n    // Users are expected to provide *normalized* page names. This data is not available\n    // in some legacy streams. Apply some normalization before comparing user configs\n    // to kafka messages. This won't cover all MediaWiki cases, it's used only to have\n    // consistent naming within this method.\n    //\n    // We can't match on page IDs, because not all streams ship it (e.g. mediawiki.recentchange).\n    // We could create lookup up tables by querying the MediaWiki Action/REST APIs at service\n    // startup, but given the use case here we might get away with simple logic (at the cost of\n    // generating false positives).\n    //\n    // 1. Trim any space at the beginning and end of the string. E.g. \" a b \" -> \"a b\".\n    // 2. Replace all space occurrences with an underscore, to match page titles (uri) naming.\n    //    E.g. 'a b    c' -> 'a_b_c'.\n    // 3. Ignore site-specific capitalization rules, and preemptively lower case page titles\n    //    to workaround case sensitivity settings.\n    // We compare with titles regardless of capitalization.\n    // This might increase false positives, but decrease the risk of PII leaks.\n    // References:\n    //  - https://www.mediawiki.org/wiki/Manual:Page_title\n    //  - https://wikitech.wikimedia.org/wiki/Analytics/Data_Lake/Traffic/Pageviews/Redirects\n    const normalizePageTitle = (pageTitle, defaultReturnValue = null) => {\n        if (pageTitle === null || pageTitle === undefined) {\n            return defaultReturnValue;\n        }\n        // Make sure page_title is a string\n        pageTitle = String(pageTitle);\n        pageTitle = pageTitle.trim();\n        return pageTitle.replace(/ +/g, '_').toLowerCase();\n    };\n\n    for (const domain in redacted_pages) {\n        redacted_pages[domain] = redacted_pages[domain].flatMap(\n            (pageTitle) => normalizePageTitle(pageTitle, [])\n        );\n    }\n    const domains = Object.keys(redacted_pages);\n    return (kafkaMessage) => {\n        const km = deserializer(kafkaMessage);\n        const { message } = km;\n        const domain = message.meta.domain;\n        if (!domain || !domains.includes(domain)) {\n            return km;\n        }\n\n        let pageTitle;\n        if ( // case mediawiki.page_change.v1\n            message.meta.stream === 'mediawiki.page_change.v1' &&\n            domains.includes(message.meta.domain) &&\n            redacted_pages[message.meta.domain].includes(\n                normalizePageTitle(message.page?.page_title)\n            )\n        ) {\n            // Performer field is required in the schema.\n            message.performer = {};\n            delete message.revision.editor;\n            delete message.prior_state?.revision?.editor;\n            pageTitle = message.page?.page_title;\n        } else if ( // case recentchange\n            message.meta.stream === 'mediawiki.recentchange' &&\n            domains.includes(message.meta.domain) &&\n            redacted_pages[message.meta.domain].includes(normalizePageTitle(message.title))\n        ) {\n            delete message.user;\n            pageTitle = message.title;\n        } else if ( // case all other streams\n            domains.includes(message.meta?.domain) &&\n            (redacted_pages[message.meta.domain].includes(normalizePageTitle(message?.page_title)) ||\n            redacted_pages[message.meta.domain].includes(normalizePageTitle(message?.page?.page_title)))\n        ) {\n            delete message.performer;\n            pageTitle = message?.page_title ?? message?.page?.page_title;\n        }\n\n        if (options.logger && !!pageTitle) {\n            options.logger.log('info',\n                `Redacted Page. Wiki: ${ domain }, ` +\n                `Stream: ${ message.meta.stream }, ` +\n                `Page: ${ pageTitle }, ` +\n                `ClientIp: ${ options.clientIp ?? '' }, ` +\n                `UserAgent: ${ options.userAgent ?? '' }, ` +\n                `Referrer: ${ options.referer ?? '' }`\n            );\n        }\n\n        km.message = message;\n        return km;\n    };\n}\n\n/**\n * Filter function that will be passed as an option to the\n * event.stats cb function that node-rdkafka-statsd will create\n * to give each new node-rdkafka client instance.\n *\n * We implement a custom filter because we don't care to report\n * some of these rdkafka metrics.  Specifically, we remove\n * metrics about committed offsets, since kafka-sse does not commit.\n */\nconst rdkafkaStatsWhitelist = [\n    // Broker stats\n    'outbuf_cnt',\n    'outbuf_msg_cnt',\n    'waitresp_cnt',\n    'waitresp_msg_cnt',\n    'tx',\n    'txbytes',\n    'txerrs',\n    'txretries',\n    'req_timeouts',\n    'rx',\n    'rxbytes',\n    'rxerrs',\n    'rxcorriderrs',\n    'rxpartial',\n    'rtt',\n    'throttle',\n\n    // Topic partition stats\n    'msgq_cnt',\n    'msgq_bytes',\n    'xmit_msgq_cnt',\n    'xmit_msgq_bytes',\n    'fetchq_cnt',\n    'fetchq_size',\n    'next_offset',\n    'eof_offset',\n    'lo_offset',\n    'hi_offset',\n    'consumer_lag',\n    'txmsgs',\n    'txbytes',\n    'msgs',\n    'rx_ver_drops'\n];\n\n\nfunction rdkafkaStatsFilter(key) {\n    return _.includes(rdkafkaStatsWhitelist, key);\n}\n\n\nmodule.exports = {\n    makeMediaWikiRedactorDeserializer,\n    deserializer,\n    rdkafkaStatsFilter\n};\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/lib/swagger-ui.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/lib/util.js","messages":[{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'Application' is undefined.","line":99,"column":1,"nodeType":"Block","endLine":99,"endColumn":1},{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'Application' is undefined.","line":143,"column":1,"nodeType":"Block","endLine":143,"endColumn":1},{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'Router' is undefined.","line":223,"column":1,"nodeType":"Block","endLine":223,"endColumn":1},{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'Application' is undefined.","line":243,"column":1,"nodeType":"Block","endLine":243,"endColumn":1}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\nconst BBPromise = require('bluebird');\nconst preq = require('preq');\nconst express = require('express');\nconst uuidv1 = require('uuid/v1');\nconst bunyan = require('bunyan');\n\n/**\n * Error instance wrapping HTTP error responses\n */\nclass HTTPError extends Error {\n\n    constructor(response) {\n        super();\n        Error.captureStackTrace(this, HTTPError);\n\n        if (response.constructor !== Object) {\n            // just assume this is just the error message\n            response = {\n                status: 500,\n                type: 'internal_error',\n                title: 'InternalError',\n                detail: response\n            };\n        }\n\n        this.name = this.constructor.name;\n        this.message = `${ response.status }`;\n        if (response.type) {\n            this.message += `: ${ response.type }`;\n        }\n\n        Object.assign(this, response);\n    }\n}\n\n/**\n * Generates an object suitable for logging out of a request object\n *\n * @param {!Request} req          the request\n * @param {?RegExp}  whitelistRE  the RegExp used to filter headers\n * @return {!Object} an object containing the key components of the request\n */\nfunction reqForLog(req, whitelistRE) {\n\n    const ret = {\n        url: req.originalUrl,\n        headers: {},\n        method: req.method,\n        params: req.params,\n        query: req.query,\n        body: req.body,\n        remoteAddress: req.connection.remoteAddress,\n        remotePort: req.connection.remotePort\n    };\n\n    if (req.headers && whitelistRE) {\n        Object.keys(req.headers).forEach((hdr) => {\n            if (whitelistRE.test(hdr)) {\n                ret.headers[hdr] = req.headers[hdr];\n            }\n        });\n    }\n\n    return ret;\n\n}\n\n/**\n * Serialises an error object in a form suitable for logging.\n *\n * @param {!Error} err error to serialise\n * @return {!Object} the serialised version of the error\n */\nfunction errForLog(err) {\n\n    const ret = bunyan.stdSerializers.err(err);\n    ret.status = err.status;\n    ret.type = err.type;\n    ret.detail = err.detail;\n\n    // log the stack trace only for 500 errors\n    if (Number.parseInt(ret.status, 10) !== 500) {\n        ret.stack = undefined;\n    }\n\n    return ret;\n\n}\n\n/**\n * Wraps all of the given router's handler functions with\n * promised try blocks so as to allow catching all errors,\n * regardless of whether a handler returns/uses promises\n * or not.\n *\n * @param {!Object} route the object containing the router and path to bind it to\n * @param {!Application} app the application object\n */\nfunction wrapRouteHandlers(route, app) {\n\n    route.router.stack.forEach((routerLayer) => {\n        const path = (route.path + routerLayer.route.path.slice(1))\n            .replace(/\\/:/g, '/--')\n            .replace(/^\\//, '')\n            .replace(/[/?]+$/, '');\n        routerLayer.route.stack.forEach((layer) => {\n            const origHandler = layer.handle;\n            const metric = app.metrics.makeMetric({\n                type: 'Gauge',\n                name: 'router',\n                prometheus: {\n                    name: 'express_router_request_duration_seconds',\n                    help: 'request duration handled by router in seconds',\n                    staticLabels: { service: app.metrics.getServiceName() }\n                },\n                labels: {\n                    names: ['path', 'method', 'status'],\n                    omitLabelNames: true\n                }\n            });\n            layer.handle = (req, res, next) => {\n                const startTime = Date.now();\n                BBPromise.try(() => origHandler(req, res, next))\n                .catch(next)\n                .finally(() => {\n                    let statusCode = parseInt(res.statusCode, 10) || 500;\n                    if (statusCode < 100 || statusCode > 599) {\n                        statusCode = 500;\n                    }\n                    metric.set(Date.now() - startTime, [path || 'root', req.method, statusCode]);\n                });\n            };\n        });\n    });\n\n}\n\n/**\n * Generates an error handler for the given applications and installs it.\n *\n * @param {!Application} app the application object to add the handler to\n */\nfunction setErrorHandler(app) {\n\n    app.use((err, req, res, next) => {\n        let errObj;\n        // ensure this is an HTTPError object\n        if (err.constructor === HTTPError) {\n            errObj = err;\n        } else if (err instanceof Error) {\n            // is this an HTTPError defined elsewhere? (preq)\n            if (err.constructor.name === 'HTTPError') {\n                const o = { status: err.status };\n                if (err.body && err.body.constructor === Object) {\n                    Object.keys(err.body).forEach((key) => {\n                        o[key] = err.body[key];\n                    });\n                } else {\n                    o.detail = err.body;\n                }\n                o.message = err.message;\n                errObj = new HTTPError(o);\n            } else {\n                // this is a standard error, convert it\n                errObj = new HTTPError({\n                    status: 500,\n                    type: 'internal_error',\n                    title: err.name,\n                    detail: err.detail || err.message,\n                    stack: err.stack\n                });\n            }\n        } else if (err.constructor === Object) {\n            // this is a regular object, suppose it's a response\n            errObj = new HTTPError(err);\n        } else {\n            // just assume this is just the error message\n            errObj = new HTTPError({\n                status: 500,\n                type: 'internal_error',\n                title: 'InternalError',\n                detail: err\n            });\n        }\n        // ensure some important error fields are present\n        errObj.status = errObj.status || 500;\n        errObj.type = errObj.type || 'internal_error';\n        // add the offending URI and method as well\n        errObj.method = errObj.method || req.method;\n        errObj.uri = errObj.uri || req.url;\n        // some set 'message' or 'description' instead of 'detail'\n        errObj.detail = errObj.detail || errObj.message || errObj.description || '';\n        // adjust the log level based on the status code\n        let level = 'error';\n        if (Number.parseInt(errObj.status, 10) < 400) {\n            level = 'trace';\n        } else if (Number.parseInt(errObj.status, 10) < 500) {\n            level = 'info';\n        }\n        // log the error\n        const component = (errObj.component ? errObj.component : errObj.status);\n        (req.logger || app.logger).log(`${ level }/${ component }`, errForLog(errObj));\n        // let through only non-sensitive info\n        const respBody = {\n            status: errObj.status,\n            type: errObj.type,\n            title: errObj.title,\n            detail: errObj.detail,\n            method: errObj.method,\n            uri: errObj.uri\n        };\n        res.status(errObj.status).json(respBody);\n    });\n\n}\n\n/**\n * Creates a new router with some default options.\n *\n * @param {?Object} [opts] additional options to pass to express.Router()\n * @return {!Router} a new router object\n */\nfunction createRouter(opts) {\n\n    const options = {\n        mergeParams: true\n    };\n\n    if (opts && opts.constructor === Object) {\n        Object.assign(options, opts);\n    }\n\n    return new express.Router(options);\n\n}\n\n/**\n * Adds logger to the request and logs it.\n *\n * @param {!*} req request object\n * @param {!Application} app application object\n */\nfunction initAndLogRequest(req, app) {\n    req.headers = req.headers || {};\n    req.headers['x-request-id'] = req.headers['x-request-id'] || uuidv1();\n    req.logger = app.logger.child({\n        request_id: req.headers['x-request-id'],\n        request: reqForLog(req, app.conf.log_header_whitelist)\n    });\n    req.context = { reqId: req.headers['x-request-id'] };\n    req.issueRequest = (request) => {\n        if (!(request.constructor === Object)) {\n            request = { uri: request };\n        }\n        if (request.url) {\n            request.uri = request.url;\n            delete request.url;\n        }\n        if (!request.uri) {\n            return BBPromise.reject(new HTTPError({\n                status: 500,\n                type: 'internal_error',\n                title: 'No request to issue',\n                detail: 'No request has been specified'\n            }));\n        }\n        request.method = request.method || 'get';\n        request.headers = request.headers || {};\n        Object.assign(request.headers, {\n            'user-agent': app.conf.user_agent,\n            'x-request-id': req.context.reqId\n        });\n        req.logger.log('trace/req', { msg: 'Outgoing request', out_request: request });\n        return preq(request);\n    };\n    req.logger.log('trace/req', { msg: 'Incoming request' });\n}\n\nmodule.exports = {\n    HTTPError,\n    initAndLogRequest,\n    wrapRouteHandlers,\n    setErrorHandler,\n    router: createRouter\n};\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/package-lock.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/package.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/routes/info.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/routes/root.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/routes/stream.js","messages":[{"ruleId":"jsdoc/require-returns","severity":1,"message":"Missing JSDoc @return declaration.","line":28,"column":1,"nodeType":"Block","endLine":64,"endColumn":4},{"ruleId":"jsdoc/require-returns","severity":1,"message":"Missing JSDoc @return declaration.","line":249,"column":1,"nodeType":"Block","endLine":253,"endColumn":4},{"ruleId":"max-len","severity":1,"message":"This line has a length of 103. Maximum allowed is 100.","line":424,"column":1,"nodeType":"Program","messageId":"max","endLine":424,"endColumn":104}],"suppressedMessages":[{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_logger'.","line":444,"column":41,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":444,"endColumn":59,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\nconst fs = require('fs');\nconst _ = require('lodash');\nconst kafkaSse = require('kafka-sse');\nconst express = require('express');\n\nconst {\n    urlGetObject,\n    uriGetFirstObject,\n} = require('@wikimedia/url-get');\n\nconst sUtil = require('../lib/util');\nconst eUtil = require('../lib/eventstreams-util');\nconst HTTPError = sUtil.HTTPError;\n\n/**\n * This file exports a function that configures the ExpressJS router to\n * serve /v2/stream/{stream} using information about streams to expose\n * obtained from stream_config_uri.\n */\n\n/**\n * The main router object\n */\nconst router = sUtil.router();\n\n/**\n * Loads stream configs from app.conf.stream_config_uri.\n * Regex stream names are not supported and will be removed.\n * Streams without a topics setting will be removed.\n * If a stream_aliases setting is found for a stream, that stream\n * config will be duplicated and also keyed by each stream alias.\n *\n * Some stream config settings will be used to augment the OpenAPI spec.\n * See updateSpec below.\n *\n * @param {Object} app\n * @param {Object} app.conf\n * @param {string} app.conf.stream_config_uri\n *  A URI from which stream configs will be requested. This URI should resolve to a JSON\n *  object keyed by stream name with stream config settings.\n * @param {string} app.conf.stream_config_object_path\n *  If set, the stream configs are expected to live in a subobject\n *  of the result object returned from stream_config_uri at this dotted path.\n * @param {Object} app.conf.stream_config_uri_options\n * @param {string} app.conf.stream_config_ttl\n *  How long in seconds stream configs live in cache before being recached.\n *  0 or unset means no expiration.\n * @param {string} app.conf.stream_config_defaults\n *  Defaults to use for stream configs.  This will be applied to the object fetched\n *  from stream_config_uri like _.defaultsDeep(streamConfigs, app.conf.stream_config_defaults)\n * @param {Array<string>} app.conf.allowed_streams\n *  If provided, stream configs will be filtered for streams that are in this list.\n * @param {string} app.conf.schema_latest_version\n *  If given, and no $schema is found in a stream config, a schema URI will\n *  attempt to be constructed a /${schema_title}/${schema_latest_version}.\n *  If this is not set, no schema URU will be inferred using schema_title.\n * @param {Array<string>} app.conf.schema_base_uris\n *  If provided, relative $schema urls for a stream will attempt to be resolved from these.\n * @param {Object} app.conf.schema_uri_options\n *  If provided, redacts user from these pages. Map of wiki to pages to redact.\n * @param {Object<string, Array<string>>} app.conf.mediawiki_redacted_pages\n */\nasync function loadStreamConfigs(app) {\n    let streamConfigs;\n    if (_.isUndefined(app.conf.stream_config_uri)) {\n        throw new Error('Must set stream_config_uri with URI of streams to expose.');\n    }\n\n    app.logger.log('info', `Loading stream configs from ${ app.conf.stream_config_uri }`);\n    streamConfigs = await urlGetObject(\n        app.conf.stream_config_uri,\n        app.conf.stream_config_uri_options || {}\n    );\n\n    // If stream_config_object_path was configured,\n    // expect the config settings for stream to exist at that path.\n    if (app.conf.stream_config_object_path) {\n        streamConfigs = _.get(streamConfigs, app.conf.stream_config_object_path);\n    }\n\n    // Merge any stream_config_extra overides with fetched stream config\n    _.defaultsDeep(streamConfigs, app.conf.stream_config_defaults);\n\n    streamConfigs = _.pickBy(streamConfigs, (streamConfig, streamName) => {\n        // eventstreams does not support regex stream keys, so remove those.\n        if (streamName.startsWith('/')) {\n            app.logger.log(\n                'trace',\n                `Regex stream names are not supported, removing ${ streamName }.`\n            );\n            return false;\n        }\n\n        // We need defined topics in order to consume from a stream.\n        if (_.isUndefined(streamConfig.topics)) {\n            app.logger.log('trace', `${ streamName } does not have configured topics, removing.`);\n            return false;\n        }\n\n        // Also if allowed_streams is an array, remove any non allowed streams.\n        if (\n            app.conf.allowed_streams &&\n            !app.conf.allowed_streams.includes(streamName)\n        ) {\n            app.logger.log(\n                'trace',\n                `${ streamName } is not in the list of allowed_streams, removing.`\n            );\n            return false;\n        }\n\n        // Else this stream is allowed.\n        return true;\n    });\n\n    // Augment streamConfigs with extra info from defaults and schema.\n    await Promise.all(_.keys(streamConfigs).map(async (streamName) => {\n        const streamConfig = streamConfigs[streamName];\n\n        // Add a nice default description.\n        if (_.isUndefined(streamConfig.description)) {\n            streamConfig.description = `${ streamName } events.`;\n        }\n\n        if (_.isUndefined(streamConfig.schema)) {\n            if (\n                _.isUndefined(streamConfig.$schema) &&\n                streamConfig.schema_title &&\n                app.conf.schema_base_uris &&\n                app.conf.schema_latest_version\n            ) {\n                streamConfig.$schema =\n                    `/${ streamConfig.schema_title }/${ app.conf.schema_latest_version }`;\n            }\n\n            if (streamConfig.$schema) {\n                try {\n                    app.logger.log(\n                        'debug',\n                        `Fetching schema for ${ streamName } at ${ streamConfig.$schema }`\n                    );\n                    streamConfig.schema = await uriGetFirstObject(\n                        streamConfig.$schema,\n                        app.conf.schema_base_uris,\n                        undefined,\n                        app.conf.schema_uri_options\n                    );\n                } catch (error) {\n                    app.logger.log('warn', {\n                        msg: `Failed fetching schema for ${ streamName } at ` +\n                            `${ streamConfig.$schema }, not augmenting OpenAPI spec with schema.`,\n                        error,\n                    });\n                }\n            }\n        }\n\n        // If this stream has stream_aliases defined, duplicate this stream config\n        // keyed by this name. When updating the dynamic spec below, and when\n        // figuring out what streams are subscribed to, this alias can be used\n        // as a shortcut route for a stream name, but the original stream name is\n        // still exposed as a route too.\n        if (streamConfig.stream_aliases) {\n            streamConfig.stream_aliases.forEach((stream_alias) => {\n                streamConfigs[stream_alias] = _.cloneDeep(streamConfig);\n                streamConfigs[stream_alias].description +=\n                    `\\n\\n(NOTE: This stream is an alias of ${ streamName })`;\n                // Remove the stream_aliases from this copied stream config.\n                // stream_aliases should only be set on the canonical stream config.\n                delete streamConfigs[stream_alias].stream_aliases;\n            });\n        }\n    }));\n\n    // 'sort' streamConfigs by stream name and then return it.\n    // streamConfigs are used to generated the dynamic OpenAPI spec,\n    // and it is nice to see the routes in order in /?doc.\n    return Object.keys(streamConfigs).sort().reduce((r, k) => {\n        r[k] = streamConfigs[k];\n        return r;\n    }, {});\n}\n\n/**\n * Updates the OpenAPI spec at app.conf.spec with path routes exposing\n * the streams declared in streamConfigs.\n *\n * streamConfig.description will be used for the route path description.\n * If streamConfig.schema is given, it will be\n * used as the route path responses schema and example.\n *\n * @param {Object} app\n * @param {Object} streamConfigs\n */\nfunction updateSpec(app, streamConfigs) {\n    // Update the /v2/stream/{stream} path spec with the list of available streams.\n    app.conf.spec.paths['/v2/stream/{streams}'].get.parameters[0].schema.items.enum =\n        _.keys(streamConfigs);\n\n    // Make an OpenAPI spec paths entry for each stream declared in streamConfigs.\n    _.forOwn(streamConfigs, (streamConfig, streamName) => {\n        // The /v2/stream/{streams} route is the only 'real' route, the stream specific ones\n        // are maintained for API clarity and documentation.  Use the /v2/stream/{streams}\n        // route as a base for each of the stream specific routes.\n        const streamRouteSpec = _.cloneDeep(app.conf.spec.paths['/v2/stream/{streams}']);\n        // The stream specific routes do not take a {streams} parameter.  It is the first\n        // listed, so shift the parameters array to remove it.\n        streamRouteSpec.get.parameters.shift();\n        streamRouteSpec.get.summary = `${ streamName } events`;\n\n        if (streamConfig.description) {\n            // Use the streamConfig description in the route description.\n            streamRouteSpec.get.description = streamConfig.description;\n        }\n        if (streamConfig.schema_title) {\n            streamRouteSpec.get.description += `\\n\\nSchema title: ${ streamConfig.schema_title }`;\n        }\n\n        if (streamConfig.schema) {\n            const schema  = _.cloneDeep(streamConfig.schema);\n            const examples = schema.examples;\n            // Remove OpenAPI unsupported JSONSchema keywords.\n            // See: https://swagger.io/docs/specification/data-models/keywords/\n            delete schema.$schema;\n            delete schema.$id;\n            delete schema.examples;\n\n            // Use the schema in the route response\n            streamRouteSpec.get.responses['200'].content['application/json'].schema = schema;\n            streamRouteSpec.get.responses['200'].content['text/event-stream'].schema = schema;\n\n            if (!_.isEmpty(examples)) {\n                // Use the schema examples in the route response.\n                streamRouteSpec.get.responses['200'].content['application/json'].example =\n                    examples[0];\n                streamRouteSpec.get.responses['200'].content['text/event-stream'].example =\n                    examples[0];\n            }\n        }\n\n        // Add the stream specific route to the OpenAPI spec.\n        app.conf.spec.paths[`/v2/stream/${ streamName }`] = streamRouteSpec;\n    });\n\n}\n\n/**\n * Set up the /v2/stream/{stream} route using stream config.\n *\n * @param {Object} app\n */\nmodule.exports = async (app) => {\n    let streamConfigs = await loadStreamConfigs(app);\n    updateSpec(app, streamConfigs);\n    app.logger.log('trace', { msg: 'Loaded stream configs', streamConfigs });\n\n    if (_.isEmpty(streamConfigs)) {\n        throw new Error(\n            `No stream configs were configuerd to be exposed in ${ app.conf.stream_config_uri }`\n        );\n    }\n\n    // If we loaded stream_config_uri and it should have a TTL, referesh it every TTL seconds.\n    if (!app.conf.streams && app.conf.stream_config_uri && app.conf.stream_config_ttl) {\n        setInterval(async () => {\n            try {\n                streamConfigs = await loadStreamConfigs(app);\n                updateSpec(app, streamConfigs);\n                app.logger.log('trace', {\n                    msg: `Reloaded stream configs after ${ app.conf.stream_config_ttl } seconds`,\n                    streamConfigs\n                });\n            } catch (error) {\n                app.logger.log('warn', {\n                    msg: 'Caught error while reloading stream configs. ' +\n                        'Keeping previous stream configs and routes.',\n                    error\n                });\n            }\n        }, app.conf.stream_config_ttl * 1000);\n    }\n\n    // Connected clients per stream and client IP service-runner metric.\n    // This is a guage and indicates the current number of connected clients.\n    const connectedClientsMetric = app.metrics.makeMetric({\n        type: 'Gauge',\n        name: 'connected-clients',\n        prometheus: {\n            name: 'eventstreams_connected_clients',\n            help: 'Connected clients per stream guage',\n            staticLabels: { service: app.metrics.getServiceName() },\n        },\n        labels: {\n            names: ['stream'],\n            omitLabelNames: true,\n        }\n    });\n\n    // This is a counter of the total number of client connections ever made.\n    // We don't care so much about the totals by client IP here, so it is not a label.\n    // Also, since clients can connect to multiple streams at once, we keep track\n    // of total connections per list of streams, not individual streams.\n    // This helps us keep track of how folks connect and use streams together over time.\n    // The streams label will be a sorted comma separated string list\n    // of streams the client has subscribed to.\n    // https://phabricator.wikimedia.org/T238658#5947574\n    const clientConnectionsTotalMetric = app.metrics.makeMetric({\n        type: 'Counter',\n        name: 'client-connections-total',\n        prometheus: {\n            name: 'eventstreams_client_connections_total',\n            help: 'Client connections total per combination of subscribed streams',\n            staticLabels: { service: app.metrics.getServiceName() },\n        },\n        labels: {\n            names: ['streams'],\n            omitLabelNames: true,\n        }\n    });\n\n    // Keep track of currently connected client IPs for poor-man's rate limiting.\n    const connectionCountPerIp = {};\n\n    router.get('/stream/:streams', (req, res) => {\n        const clientIp = req.get('x-client-ip') || 'UNKNOWN';\n        const userAgent = req.get('user-agent') || 'UNKNOWN';\n        const referer = req.get('referer') || 'UNKNOWN';\n\n        // ensure the requesting client hasn't gone over the concurrent connection limits.\n        if (app.conf.client_ip_connection_limit) {\n            if (!req.headers['x-client-ip']) {\n                throw new HTTPError({\n                    status: 400,\n                    type: 'bad_request',\n                    title: 'Missing Required X-Client-IP Header',\n                    detail: 'X-Client-IP is a required request header'\n                });\n            }\n\n            const clientIpConnectionCount = connectionCountPerIp[clientIp] || 0;\n            if (clientIpConnectionCount >= app.conf.client_ip_connection_limit) {\n                throw new HTTPError({\n                    status: 429,\n                    type: 'too_many_requests',\n                    title: 'Too Many Concurrent Connections From Your Client IP',\n                    detail: 'Your HTTP client is likely opening too many concurrent connections.'\n                });\n            }\n        }\n\n        const requestedStreams = req.params.streams.split(',');\n        // Ensure all requested streams are available.\n        const invalidStreams =\n          requestedStreams.filter((s) => !_.includes(_.keys(streamConfigs), s));\n        if (invalidStreams.length > 0) {\n            throw new HTTPError({\n                status: 400,\n                type: 'not_found',\n                title: 'Stream Not Found',\n                detail: `Invalid streams: ${ invalidStreams.join(',') }`\n            });\n        }\n\n        const topics = _.uniq(_.flatMap(\n            _.pick(streamConfigs, requestedStreams),\n            (streamConfig) => streamConfig.topics\n        ));\n\n        // If since param is provided, it will be used to consume from\n        // a point in time in the past, if Last-Event-ID doesn't already\n        // have assignments in it.\n        let atTimestamp = req.query.since;\n        // If not a milliseconds timestamp, attempt to parse it into one.\n        if (atTimestamp && isNaN(atTimestamp)) {\n            atTimestamp = Date.parse(atTimestamp);\n        }\n        // If atTimestamp is defined but is still not a number milliseconds timestamp,\n        // throw HTTPError.\n        if (atTimestamp !== undefined && isNaN(atTimestamp)) {\n            throw new HTTPError({\n                status: 400,\n                type: 'invalid_timestamp',\n                title: 'Invalid timestamp',\n                detail: `since timestamp is not a UTC milliseconds unix epoch and was not parseable: '${ req.query.since }'`\n            });\n        }\n\n        requestedStreams.forEach((stream) => {\n            // Increment the number of current connections for this stream using this key.\n            // NOTE: This is a guage so we have to decrement it when the client is disconnected too.\n            connectedClientsMetric.increment(1, [stream]);\n            app.logger.log('info', `Connected: IP ${ clientIp }, User-Agent: ${ userAgent }, Referer: ${ referer }, stream ${ stream }`);\n        });\n        // Increment the total counter of clients ever connected to this combination of streams.\n        clientConnectionsTotalMetric.increment(1, [requestedStreams.sort().join(',')]);\n\n        // Increment the number of connctions for this clientIp\n        connectionCountPerIp[clientIp] = (connectionCountPerIp[clientIp] || 0) + 1;\n\n        // After the connection is closed, decrement the number\n        // of current connections for these streams.\n        function decrementConnectionCount() {\n            requestedStreams.forEach((stream) => {\n                app.logger.log('info', `Disconnected: IP ${ clientIp }, User-Agent: ${ userAgent }, Referer: ${ referer }, stream ${ stream }`);\n                connectedClientsMetric.decrement(1, [stream]);\n            });\n            // Decrement the number of concurrent connections for this client ip\n            // (never going below 0).\n            connectionCountPerIp[clientIp] = Math.max(connectionCountPerIp[clientIp] - 1, 0);\n        }\n        // I'm not sure why, but the 'close' event is fired when the client closes the connection,\n        // and the 'finish' event is fired when KafkaSSE closes the connection (due to an error).\n        // Register them both.\n        res.on('close', decrementConnectionCount);\n        res.on('finish', decrementConnectionCount);\n\n        // See if redactor is enabled, and if so set it.\n        // This is router.get so we have access to clientIp and userAgent.\n        let deserializer;\n        if (app.conf.mediawiki_redacted_pages) {\n            deserializer = eUtil.makeMediaWikiRedactorDeserializer(\n                app.conf.mediawiki_redacted_pages, { logger: app.logger, clientIp, userAgent, referer }\n            );\n        } else {\n            deserializer = eUtil.deserializer;\n        }\n\n        // Start the SSE EventStream connection with topics.\n        return kafkaSse(req, res, topics,\n            {\n                // Using topics for allowedTopics may seem redundant, but it\n                // prevents requests for /stream/streamA from consuming from topics\n                // that are not configured for streamA by setting other topics\n                // in the Last-Event-ID header.  Last-Event-ID topic, partition, offset\n                // assignments will take precedence over topics parameter.\n                allowedTopics:          topics,\n                // Support multi DC Kafka clusters by using message timestamps\n                // in Last-Event-ID instead of offsets.\n                useTimestampForId:      true,\n                // Give kafkaSse the request bunyan logger to use.\n                // eslint-disable-next-line no-underscore-dangle\n                logger:                 req.logger._logger,\n                kafkaConfig:            app.conf.kafka,\n                // Use the eventstreams custom deserializer to include\n                // kafka message meta data in the deserialized message.meta object\n                // that will be sent to the client as an event.\n                deserializer:           deserializer\n            },\n            atTimestamp\n        );\n    });\n\n    const uiEnabled = app.conf.stream_ui_enabled || true;\n    if (uiEnabled) {\n        if (!fs.existsSync(`${ __dirname }/../ui/dist`)) {\n            throw new Error(\n                'Cannot enable HTML stream GUI at /v2/ui: The ui/dist directory does not exist.' +\n                ' Run `npm run build-ui`.'\n            );\n        }\n\n        app.logger.log('debug', 'Enabling HTML stream GUI at /v2/ui');\n        app.use('/v2/ui', express.static(`${ __dirname }/../ui/dist`));\n        app.use(express.static(`${ __dirname }/../ui/dist`));\n    }\n\n    return {\n        path: '/v2',\n        api_version: 2,\n        skip_domain: true,\n        router\n    };\n};\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/server.js","messages":[{"ruleId":"es-x/no-hashbang","severity":2,"message":"ES2023 Hashbang comments are forbidden.","line":1,"column":1,"nodeType":"Shebang","messageId":"forbidden","endLine":1,"endColumn":20}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"#!/usr/bin/env node\n\n'use strict';\n\n// Service entry point. Try node server --help for commandline options.\n\n// Start the service by running service-runner, which in turn loads the config\n// (config.yaml by default, specify other path with -c). It requires the\n// module(s) specified in the config 'services' section (app.js in this\n// example).\nconst ServiceRunner = require('service-runner');\nnew ServiceRunner().start();\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/spec.template.yaml","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/spec.yaml","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/stream-config.yaml","messages":[{"ruleId":"max-len","severity":1,"message":"This line has a length of 169. Maximum allowed is 100.","line":5,"column":1,"nodeType":"Program","messageId":"max","endLine":5,"endColumn":170}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"mediawiki.page-create:\n  topics: [ eqiad.mediawiki.page-create, codfw.mediawiki.page-delete ]\n  stream_aliases: [ page-create ]\n  description: |-\n    mediawiki.page-create events. This page create stream is just the first revision create event for each page. As such, it reuses the mediawiki/revision/create schema.\n# $schema: https://schema.wikimedia.org/repositories/primary/jsonschema/mediawiki/revision/create/latest\n\nmediawiki.revision-create:\n  topics: [ eqiad.mediawiki.revision-create, codfw.mediawiki.revision-create ]\n  stream_aliases: [ revision-create ]\n  description: |-\n    mediawiki.revision-create events.\n  # $schema: https://schema.wikimedia.org/repositories/primary/jsonschema/mediawiki/revision/create/latest\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/targets.yaml","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/test/.eslintrc.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/test/features/app/app.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/test/features/app/spec.js","messages":[{"ruleId":"no-shadow","severity":2,"message":"'spec' is already declared in the upper scope on line 11 column 5.","line":70,"column":25,"nodeType":"Identifier","messageId":"noShadow","endLine":70,"endColumn":29},{"ruleId":"security/detect-non-literal-regexp","severity":1,"message":"Found non-literal argument to RegExp Constructor","line":154,"column":14,"nodeType":"NewExpression","endLine":154,"endColumn":47},{"ruleId":"no-shadow","severity":2,"message":"'spec' is already declared in the upper scope on line 11 column 5.","line":265,"column":16,"nodeType":"Identifier","messageId":"noShadow","endLine":265,"endColumn":20}],"suppressedMessages":[{"ruleId":"brace-style","severity":2,"message":"Statement inside of curly braces should be on next line.","line":196,"column":19,"nodeType":"Punctuator","messageId":"blockSameLine","endLine":196,"endColumn":20,"fix":{"range":[6105,6105],"text":"\n"},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"max-statements-per-line","severity":2,"message":"This line has 2 statements. Maximum allowed is 1.","line":196,"column":21,"nodeType":"ReturnStatement","messageId":"exceed","endLine":196,"endColumn":33,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"brace-style","severity":2,"message":"Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.","line":196,"column":34,"nodeType":"Punctuator","messageId":"singleLineClose","endLine":196,"endColumn":35,"fix":{"range":[6119,6119],"text":"\n"},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"brace-style","severity":2,"message":"Statement inside of curly braces should be on next line.","line":197,"column":19,"nodeType":"Punctuator","messageId":"blockSameLine","endLine":197,"endColumn":20,"fix":{"range":[6163,6163],"text":"\n"},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"max-statements-per-line","severity":2,"message":"This line has 2 statements. Maximum allowed is 1.","line":197,"column":21,"nodeType":"ReturnStatement","messageId":"exceed","endLine":197,"endColumn":34,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"brace-style","severity":2,"message":"Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.","line":197,"column":35,"nodeType":"Punctuator","messageId":"singleLineClose","endLine":197,"endColumn":36,"fix":{"range":[6178,6178],"text":"\n"},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-prototype-builtins","severity":2,"message":"Do not access Object.prototype method 'hasOwnProperty' from target object.","line":213,"column":38,"nodeType":"CallExpression","messageId":"prototypeBuildIn","endLine":213,"endColumn":52,"suggestions":[{"messageId":"callObjectPrototype","data":{"prop":"hasOwnProperty"},"fix":{"range":[6705,6728],"text":"Object.prototype.hasOwnProperty.call(resBody, "},"desc":"Call Object.prototype.hasOwnProperty explicitly."}],"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"template-curly-spacing","severity":2,"message":"Expected space(s) after '${'.","line":213,"column":77,"nodeType":null,"messageId":"expectedAfter","endLine":213,"endColumn":79,"fix":{"range":[6754,6754],"text":" "},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"template-curly-spacing","severity":2,"message":"Expected space(s) before '}'.","line":213,"column":82,"nodeType":null,"messageId":"expectedBefore","endLine":213,"endColumn":83,"fix":{"range":[6757,6757],"text":" "},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"block-spacing","severity":2,"message":"Requires a space after '{'.","line":235,"column":41,"nodeType":"BlockStatement","messageId":"missing","endLine":235,"endColumn":42,"fix":{"range":[7462,7462],"text":" "},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"brace-style","severity":2,"message":"Statement inside of curly braces should be on next line.","line":235,"column":41,"nodeType":"Punctuator","messageId":"blockSameLine","endLine":235,"endColumn":42,"fix":{"range":[7462,7462],"text":"\n"},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"max-statements-per-line","severity":2,"message":"This line has 2 statements. Maximum allowed is 1.","line":235,"column":42,"nodeType":"ReturnStatement","messageId":"exceed","endLine":235,"endColumn":55,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"brace-style","severity":2,"message":"Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.","line":235,"column":56,"nodeType":"Punctuator","messageId":"singleLineClose","endLine":235,"endColumn":57,"fix":{"range":[7476,7476],"text":"\n"},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-prototype-builtins","severity":2,"message":"Do not access Object.prototype method 'hasOwnProperty' from target object.","line":240,"column":38,"nodeType":"CallExpression","messageId":"prototypeBuildIn","endLine":240,"endColumn":52,"suggestions":[{"messageId":"callObjectPrototype","data":{"prop":"hasOwnProperty"},"fix":{"range":[7655,7682],"text":"Object.prototype.hasOwnProperty.call(res.headers, "},"desc":"Call Object.prototype.hasOwnProperty explicitly."}],"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"template-curly-spacing","severity":2,"message":"Expected space(s) after '${'.","line":240,"column":73,"nodeType":null,"messageId":"expectedAfter","endLine":240,"endColumn":75,"fix":{"range":[7704,7704],"text":" "},"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"template-curly-spacing","severity":2,"message":"Expected space(s) before '}'.","line":240,"column":78,"nodeType":null,"messageId":"expectedBefore","endLine":240,"endColumn":79,"fix":{"range":[7707,7707],"text":" "},"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":2,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\nconst parallel = require('mocha.parallel');\nconst preq   = require('preq');\nconst assert = require('../../utils/assert.js');\nconst Server = require('../../utils/server.js');\nconst URI    = require('swagger-router').URI;\nconst OpenAPISchemaValidator = require('openapi-schema-validator').default;\nconst validator = new OpenAPISchemaValidator({ version: 3 });\n\nlet spec = null;\nlet baseUrl = null;\nconst server = new Server();\n\nfunction validateExamples(pathStr, defParams, mSpec) {\n\n    const uri = new URI(pathStr, {}, true);\n\n    if (!mSpec) {\n        try {\n            uri.expand(defParams);\n            return true;\n        } catch (e) {\n            throw new Error(`Missing parameter for route ${ pathStr } : ${ e.message }`);\n        }\n    }\n\n    if (!Array.isArray(mSpec)) {\n        throw new Error(`Route ${ pathStr } : x-amples must be an array!`);\n    }\n\n    mSpec.forEach((ex, idx) => {\n        if (!ex.title) {\n            throw new Error(`Route ${ pathStr }, example ${ idx }: title missing!`);\n        }\n        ex.request = ex.request || {};\n        try {\n            uri.expand(Object.assign({}, defParams, ex.request.params || {}));\n        } catch (e) {\n            throw new Error(\n                `Route ${ pathStr }, example ${ idx } (${ ex.title }): missing parameter: ${ e.message }`\n            );\n        }\n    });\n\n    return true;\n\n}\n\nfunction constructTestCase(title, path, method, request, response) {\n    return {\n        title,\n        request: {\n            uri: (baseUrl || server.config.uri) + (path[0] === '/' ? path.slice(1) : path),\n            method,\n            headers: request.headers || {},\n            query: request.query,\n            body: request.body,\n            followRedirect: false\n        },\n        response: {\n            status: response.status || 200,\n            headers: response.headers || {},\n            body: response.body\n        }\n    };\n\n}\n\nfunction constructTests(spec) {\n    const ret = [];\n    const paths = spec.paths;\n    const defParams = spec['x-default-params'] || {};\n\n    Object.keys(paths).forEach((pathStr) => {\n        Object.keys(paths[pathStr]).forEach((method) => {\n            const p = paths[pathStr][method];\n            if ({}.hasOwnProperty.call(p, 'x-monitor') && !p['x-monitor']) {\n                return;\n            }\n            const uri = new URI(pathStr, {}, true);\n            if (!p['x-amples']) {\n                ret.push(constructTestCase(\n                    pathStr,\n                    uri.toString({ params: defParams }),\n                    method,\n                    {},\n                    {}\n                ));\n                return;\n            }\n            p['x-amples'].forEach((ex) => {\n                ex.request = ex.request || {};\n                ret.push(constructTestCase(\n                    ex.title,\n                    uri.toString({\n                        params: Object.assign({},\n                            defParams,\n                            ex.request.params || {})\n                    }),\n                    method,\n                    ex.request,\n                    ex.response || {}\n                ));\n            });\n        });\n    });\n\n    return ret;\n}\n\nfunction cmp(result, expected, errMsg) {\n\n    if (expected === null || expected === undefined) {\n        // nothing to expect, so we can return\n        return true;\n    }\n    if (result === null || result === undefined) {\n        result = '';\n    }\n\n    if (expected.constructor === Object) {\n        Object.keys(expected).forEach((key) => {\n            const val = expected[key];\n            assert.deepEqual({}.hasOwnProperty.call(result, key), true,\n                `Body field ${ key } not found in response!`);\n            cmp(result[key], val, `${ key } body field mismatch!`);\n        });\n        return true;\n    } else if (expected.constructor === Array) {\n        if (result.constructor !== Array) {\n            assert.deepEqual(result, expected, errMsg);\n            return true;\n        }\n        // only one item in expected - compare them all\n        if (expected.length === 1 && result.length > 1) {\n            result.forEach((item) => {\n                cmp(item, expected[0], errMsg);\n            });\n            return true;\n        }\n        // more than one item expected, check them one by one\n        if (expected.length !== result.length) {\n            assert.deepEqual(result, expected, errMsg);\n            return true;\n        }\n        expected.forEach((item, idx) => {\n            cmp(result[idx], item, errMsg);\n        });\n        return true;\n    }\n\n    if (expected.length > 1 && expected[0] === '/' && expected[expected.length - 1] === '/') {\n        if ((new RegExp(expected.slice(1, -1))).test(result)) {\n            return true;\n        }\n    } else if (expected.length === 0 && result.length === 0) {\n        return true;\n    } else if (result === expected || result.startsWith(expected)) {\n        return true;\n    }\n\n    assert.deepEqual(result, expected, errMsg);\n    return true;\n\n}\n\nfunction validateArray(val, resVal, key) {\n    assert.deepEqual(Array.isArray(resVal), true, `Body field ${ key } is not an array!`);\n    let arrVal;\n    if (val.length === 1) {\n        // special case: we have specified only one item in the expected body,\n        // but what we really want is to check all of the returned items so\n        // fill the expected array with as many items as the returned one\n        if (resVal.length < 1) {\n            throw new assert.AssertionError({\n                message: `Expected more then one element in the field: ${ key }`\n            });\n        }\n        arrVal = [];\n        while (arrVal.length < resVal.length) {\n            arrVal.push(val[0]);\n        }\n    } else {\n        arrVal = val;\n    }\n    assert.deepEqual(arrVal.length, resVal.length,\n        `Different size of array for field ${ key }, expected ${ arrVal.length\n        } actual ${ resVal.length }`);\n    arrVal.forEach((item, index) => {\n        validateBody(resVal[index], item);\n    });\n}\n\nfunction validateBody(resBody, expBody) {\n    if (!expBody) { return true; } // eslint-disable-line\n    if (!resBody) { return false; } // eslint-disable-line\n\n    if (Buffer.isBuffer(resBody)) {\n        resBody = resBody.toString();\n    }\n    if (expBody.constructor !== resBody.constructor) {\n        if (expBody.constructor === String) {\n            resBody = JSON.stringify(resBody);\n        } else {\n            resBody = JSON.parse(resBody);\n        }\n    }\n    if (expBody.constructor === Object) {\n        Object.keys(expBody).forEach((key) => {\n            const val = expBody[key];\n            // eslint-disable-next-line\n            assert.deepEqual(resBody.hasOwnProperty(key), true, `Body field ${key} not found in response!`);\n            if (val.constructor === Object) {\n                validateBody(resBody[key], val);\n            } else if (val.constructor === Array) {\n                validateArray(val, resBody[key], key);\n            } else {\n                cmp(resBody[key], val, `${ key } body field mismatch!`);\n            }\n        });\n    } else if (Array.isArray(expBody)) {\n        validateArray(expBody, resBody, 'body');\n    } else {\n        cmp(resBody, expBody, 'Body mismatch!');\n    }\n    return true;\n}\n\nfunction validateTestResponse(testCase, res) {\n    const expRes = testCase.response;\n\n    assert.deepEqual(res.status, expRes.status);\n\n    if (expRes.headers && !res.headers) {return false; } // eslint-disable-line\n\n    Object.keys(expRes.headers).forEach((key) => {\n        const val = expRes.headers[key];\n        // eslint-disable-next-line\n        assert.deepEqual(res.headers.hasOwnProperty(key), true, `Header ${key} not found in response!`);\n        cmp(res.headers[key], val, `${ key } header mismatch!`);\n    });\n\n    validateBody(res.body || '', expRes.body);\n}\n\ndescribe('Swagger spec', function () {\n\n    this.timeout(20000);\n\n    before(() => server.start());\n    after(() => server.stop());\n\n    it('get the spec', () => {\n        baseUrl = server.config.uri;\n        return preq.get(`${ baseUrl }?spec`)\n        .then((res) => {\n            assert.status(200);\n            assert.contentType(res, 'application/json');\n            assert.notDeepEqual(res.body, undefined, 'No body received!');\n            // save a copy\n            spec = res.body;\n            return spec;\n        })\n        .then((spec) => {\n            const routeTests = () => {\n                before(() => server.start());\n                after(() => server.stop());\n\n                constructTests(spec).forEach((testCase) => {\n                    it(testCase.title, function (done) {\n                        return preq(testCase.request)\n                        .then((res) => {\n                            assert.status(res, testCase.response.status);\n                            validateTestResponse(testCase, res);\n                        }, (err) => {\n                            assert.status(err, testCase.response.status);\n                            validateTestResponse(testCase, err);\n                        });\n                    });\n                });\n            };\n            parallel('Monitoring routes', routeTests);\n        });\n    });\n\n    it('should expose valid OpenAPI spec', () => {\n        return preq.get({ uri: `${ server.config.uri }?spec` })\n        .then((res) =>  {\n            assert.deepEqual({ errors: [] }, validator.validate(res.body), 'Spec must have no validation errors');\n        });\n    });\n\n    it('spec validation', () => {\n        // check the high-level attributes\n        [ 'info', 'openapi', 'paths' ].forEach((prop) => {\n            assert.deepEqual(!!spec[prop], true, `No ${ prop } field present!`);\n        });\n        // no paths - no love\n        assert.deepEqual(!!Object.keys(spec.paths), true, 'No paths given in the spec!');\n        // now check each path\n        Object.keys(spec.paths).forEach((pathStr) => {\n            assert.deepEqual(!!pathStr, true, 'A path cannot have a length of zero!');\n            const path = spec.paths[pathStr];\n            assert.deepEqual(!!Object.keys(path), true, `No methods defined for path: ${ pathStr }`);\n            Object.keys(path).forEach((method) => {\n                const mSpec = path[method];\n                if ({}.hasOwnProperty.call(mSpec, 'x-monitor') && !mSpec['x-monitor']) {\n                    return;\n                }\n                validateExamples(pathStr, spec['x-default-params'] || {}, mSpec['x-amples']);\n            });\n        });\n    });\n});\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/test/features/info/info.js","messages":[{"ruleId":"no-redeclare","severity":2,"message":"'describe' is already defined as a built-in global variable.","line":1,"column":11,"nodeType":"Block","messageId":"redeclaredAsBuiltin","endLine":1,"endColumn":19},{"ruleId":"no-redeclare","severity":2,"message":"'it' is already defined as a built-in global variable.","line":1,"column":21,"nodeType":"Block","messageId":"redeclaredAsBuiltin","endLine":1,"endColumn":23},{"ruleId":"no-redeclare","severity":2,"message":"'before' is already defined as a built-in global variable.","line":1,"column":25,"nodeType":"Block","messageId":"redeclaredAsBuiltin","endLine":1,"endColumn":31},{"ruleId":"no-redeclare","severity":2,"message":"'after' is already defined as a built-in global variable.","line":1,"column":33,"nodeType":"Block","messageId":"redeclaredAsBuiltin","endLine":1,"endColumn":38}],"suppressedMessages":[],"errorCount":4,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/* global describe, it, before, after */\n\n'use strict';\n\nconst preq   = require('preq');\nconst assert = require('../../utils/assert.js');\nconst Server = require('../../utils/server.js');\n\ndescribe('service information', function () {\n\n    this.timeout(20000);\n\n    let infoUri = null;\n\n    const server = new Server();\n\n    before(() => {\n        return server.start()\n        .then(() => {\n            infoUri = `${ server.config.uri }_info/`;\n        });\n    });\n\n    after(() => server.stop());\n\n    // common function used for generating requests\n    // and checking their return values\n    function checkRet(fieldName) {\n        return preq.get({\n            uri: infoUri + fieldName\n        }).then((res) => {\n            // check the returned Content-Type header\n            assert.contentType(res, 'application/json');\n            // the status as well\n            assert.status(res, 200);\n            // finally, check the body has the specified field\n            assert.notDeepEqual(res.body, undefined, 'No body returned!');\n            assert.notDeepEqual(res.body[fieldName], undefined, `No ${ fieldName } field returned!`);\n        });\n    }\n\n    it('should get the service name', () => {\n        return checkRet('name');\n    });\n\n    it('should get the service version', () => {\n        return checkRet('version');\n    });\n\n    it('should redirect to the service home page', () => {\n        return preq.get({\n            uri: `${ infoUri }home`,\n            followRedirect: false\n        }).then((res) => {\n            // check the status\n            assert.status(res, 301);\n        });\n    });\n\n    it('should get the service info', () => {\n        return preq.get({\n            uri: infoUri\n        }).then((res) => {\n            // check the status\n            assert.status(res, 200);\n            // check the returned Content-Type header\n            assert.contentType(res, 'application/json');\n            // inspect the body\n            assert.notDeepEqual(res.body, undefined, 'No body returned!');\n            assert.notDeepEqual(res.body.name, undefined, 'No name field returned!');\n            assert.notDeepEqual(res.body.version, undefined, 'No version field returned!');\n            assert.notDeepEqual(res.body.description, undefined, 'No description field returned!');\n            assert.notDeepEqual(res.body.home, undefined, 'No home field returned!');\n        });\n    });\n});\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/test/lib/eventstreams-util.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/test/utils/assert.js","messages":[{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'integer' is undefined.","line":23,"column":1,"nodeType":"Block","endLine":23,"endColumn":1}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\n\n'use strict';\n\nconst assert = require('assert');\n\nfunction deepEqual(result, expected, message) {\n\n    try {\n        assert.deepEqual(result, expected, message);\n    } catch (e) {\n        console.log(`Expected:\\n${ JSON.stringify(expected, null, 2) }`);\n        console.log(`Result:\\n${ JSON.stringify(result, null, 2) }`);\n        throw e;\n    }\n\n}\n\n/**\n * Asserts whether the return status was as expected\n *\n * @param {Object} res\n * @param {integer} expected\n */\nfunction status(res, expected) {\n\n    deepEqual(res.status, expected,\n        `Expected status to be ${ expected }, but was ${ res.status }`);\n\n}\n\n/**\n * Asserts whether content type was as expected\n *\n * @param {Object} res\n * @param {string} expectedRegexString\n */\nfunction contentType(res, expectedRegexString) {\n\n    const actual = res.headers['content-type'];\n    assert.ok(RegExp(expectedRegexString).test(actual),\n        `Expected content-type to match ${ expectedRegexString }, but was ${ actual }`);\n\n}\n\nfunction isDeepEqual(result, expected, message) {\n\n    try {\n        assert.deepEqual(result, expected, message);\n        return true;\n    } catch (e) {\n        return false;\n    }\n\n}\n\nfunction notDeepEqual(result, expected, message) {\n\n    try {\n        assert.notDeepEqual(result, expected, message);\n    } catch (e) {\n        console.log(`Not expected:\\n${ JSON.stringify(expected, null, 2) }`);\n        console.log(`Result:\\n${ JSON.stringify(result, null, 2) }`);\n        throw e;\n    }\n\n}\n\nfunction fails(promise, onRejected) {\n\n    let failed = false;\n\n    function trackFailure(e) {\n        failed = true;\n        return onRejected(e);\n    }\n\n    function check() {\n        if (!failed) {\n            throw new Error('expected error was not thrown');\n        }\n    }\n\n    return promise.catch(trackFailure).then(check);\n\n}\n\nmodule.exports.ok             = assert.ok;\nmodule.exports.fails          = fails;\nmodule.exports.deepEqual      = deepEqual;\nmodule.exports.isDeepEqual    = isDeepEqual;\nmodule.exports.notDeepEqual   = notDeepEqual;\nmodule.exports.contentType    = contentType;\nmodule.exports.status         = status;\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/test/utils/server.js","messages":[{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_spec'.","line":9,"column":9,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":9,"endColumn":19},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_running'.","line":13,"column":14,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":13,"endColumn":27},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_impl'.","line":19,"column":30,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":19,"endColumn":48},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_runner'.","line":19,"column":30,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":19,"endColumn":42},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_impl'.","line":20,"column":27,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":20,"endColumn":45},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_runner'.","line":20,"column":27,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":20,"endColumn":39},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_spec'.","line":22,"column":14,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":22,"endColumn":24},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_spec'.","line":30,"column":17,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":30,"endColumn":27},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_spec'.","line":34,"column":17,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":34,"endColumn":27},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_impl'.","line":40,"column":27,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":40,"endColumn":45},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_runner'.","line":40,"column":27,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":40,"endColumn":39},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_spec'.","line":41,"column":27,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":41,"endColumn":37},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_impl'.","line":49,"column":19,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":49,"endColumn":37},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_runner'.","line":49,"column":19,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":49,"endColumn":31},{"ruleId":"no-underscore-dangle","severity":2,"message":"Unexpected dangling '_' in '_spec'.","line":50,"column":19,"nodeType":"MemberExpression","messageId":"unexpectedUnderscore","endLine":50,"endColumn":29}],"suppressedMessages":[],"errorCount":15,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\nconst preq   = require('preq');\nconst TestRunner = require('service-runner/test/TestServer');\n\nclass TestServiceTemplateNodeRunner extends TestRunner {\n    constructor(configPath = `${ __dirname }/../../config.yaml`) {\n        super(configPath);\n        this._spec = null;\n    }\n\n    get config() {\n        if (!this._running) {\n            throw new Error('Accessing test service config before starting the service');\n        }\n\n        // build the API endpoint URI by supposing the actual service\n        // is the last one in the 'services' list in the config file\n        const myServiceIdx = this._runner._impl.config.services.length - 1;\n        const myService = this._runner._impl.config.services[myServiceIdx];\n        const uri = `http://localhost:${ myService.conf.port }/`;\n        if (!this._spec) {\n            // We only want to load this once.\n            preq.get(`${ uri }?spec`)\n            .then((res) => {\n                if (!res.body) {\n                    throw new Error('Failed to get spec');\n                }\n                // save a copy\n                this._spec = res.body;\n            })\n            .catch((err) => {\n                // this error will be detected later, so ignore it\n                this._spec = { paths: {}, 'x-default-params': {} };\n            })\n            .then(() => {\n                return {\n                    uri,\n                    service: myService,\n                    conf: this._runner._impl.config,\n                    spec: this._spec\n                };\n            });\n        }\n\n        return {\n            uri,\n            service: myService,\n            conf: this._runner._impl.config,\n            spec: this._spec\n        };\n\n    }\n}\n\nmodule.exports = TestServiceTemplateNodeRunner;\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/ui/package.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/ui/src/apis/EventStreamsApi.js","messages":[{"ruleId":null,"fatal":true,"severity":2,"message":"Parsing error: 'import' and 'export' may appear only with 'sourceType: module'","line":1,"column":1,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import Config from '@/config';\n\n/**\n * Gets the list of stream names that can be consumed\n * from the configured EventStreams instance.\n * @return {promise} When resolved, will contain the list of stream names\n *                   that can be consumed from the configured EventStreams.\n */\nfunction getAvailableStreams () {\n  return new Promise(function (resolve) {\n    fetch(Config.eventStreamsUri + '?spec')\n      .then(function (response) { return response.json(); })\n      .then(function (data) { resolve(formatAvailableStreams(data)); });\n  });\n}\n\n// Extracts the stream names from the EventStreams spec.\nfunction formatAvailableStreams (spec) {\n  // /v2/stream/{streams} endpoint spec has a list of all available streams.\n  return spec.paths['/v2/stream/{streams}'].get.parameters[0].schema.items.enum;\n}\n\n/**\n * Opens an SSE EventSource that consumes the passed stream names.\n * Subscribes the passed callback to be called whenever an event is consumed.\n * Finally, returns the EventSource object, so it can be closed.\n * @param streamList {array} List of stream names to be consumed.\n * @param onMessage {function} To be called whenever an event is consumed.\n * @returns {object} Corresponding EventSource object.\n */\nfunction consumeStreams (streamList, onMessage) {\n   const streamCSV = streamList.join(',');\n   const streamUrl = Config.eventStreamsUri + \"v2/stream/\" + streamCSV;\n   const eventSource = new EventSource(streamUrl);\n   eventSource.onmessage = onMessage;\n   return eventSource;\n}\n\nexport default {\n  getAvailableStreams,\n  consumeStreams\n};\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/ui/src/config/index.js","messages":[{"ruleId":null,"fatal":true,"severity":2,"message":"Parsing error: 'import' and 'export' may appear only with 'sourceType: module'","line":1,"column":1,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"export default {\n  // Set eventStreamsUri to the base URI of the EventStreams server\n  // you want to view streams of.\n  // A value of '/' will work with the locally running EventStreams service.\n  // If you use this for development, don't use 'npm run serve', but rather\n  // 'npm run build' and browse the ui from 'http://locahost:8092/v2/ui'\n  // (port might change, depending on how you ran EventStreams server.js).\n  // Caveat: You won't be able to consume streams this way locally.\n  eventStreamsUri: '/'\n  // eventStreamsUri: 'https://stream-beta.wmflabs.org/'\n  // eventStreamsUri: 'https://stream.wikimedia.org/'\n};\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/ui/src/main.js","messages":[{"ruleId":null,"fatal":true,"severity":2,"message":"Parsing error: 'import' and 'export' may appear only with 'sourceType: module'","line":1,"column":1,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import Vue from 'vue';\nimport App from './App.vue';\nimport router from './router';\n\n// Import element-ui.\nimport ElementUI from 'element-ui';\nimport 'element-ui/lib/theme-chalk/index.css';\nimport locale from 'element-ui/lib/locale/lang/en';\nVue.use(ElementUI, {locale});\n\nVue.config.productionTip = false;\n\nnew Vue({\n  router,\n  render: h => h(App)\n}).$mount('#app');\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/ui/src/router/index.js","messages":[{"ruleId":null,"fatal":true,"severity":2,"message":"Parsing error: 'import' and 'export' may appear only with 'sourceType: module'","line":1,"column":1,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import Vue from 'vue'\nimport VueRouter from 'vue-router'\nimport Home from '../views/Home.vue'\n\nVue.use(VueRouter)\n\nconst routes = [\n  {\n    path: '/',\n    name: 'Home',\n    component: Home\n  }\n]\n\nconst router = new VueRouter({\n  routes\n})\n\nexport default router\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/ui/src/utils/index.js","messages":[{"ruleId":null,"fatal":true,"severity":2,"message":"Parsing error: 'import' and 'export' may appear only with 'sourceType: module'","line":57,"column":1,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Returns an HTML blob that represents the passed yaml text.\n * The values will be color-highlighted, depending on type.\n */\nfunction yamlToHtml (yamlText) {\n  const lines = yamlText.trim().split('\\n');\n\n  const formattedLines = lines.map(function (line) {\n    if (/^\\s*-/.test(line)) {\n      // Line is a list item, thus is entirely a value (no label).\n      const indentation = line.indexOf('-');\n      const value = line.substr(indentation + 1).trim();\n      return '&nbsp;'.repeat(indentation) + '- ' + valueToHtml(value);\n    } else {\n      // Line has a label and optionally a value.\n      const indentation = /^(\\s*)/g.exec(line)[1].length;\n      const colonIndex = line.indexOf(':');\n      const label = line.substr(indentation, colonIndex - indentation);\n      const value = line.substr(colonIndex + 1).trim();\n      return '&nbsp;'.repeat(indentation) + label + ': ' + valueToHtml(value);\n    }\n  });\n  return formattedLines.join('<br>');\n}\n\n// Returns the passed value withing an HTML span,\n// color-highlighting it depending on type.\nfunction valueToHtml (value) {\n  var color;\n\n  if (value === 'true' || value === 'false') {\n    color = 'blue';\n  } else if (isNaN(Number(value))) {\n    color = 'green';\n  } else {\n    color = 'red';\n  }\n\n  const htmlValue = value.replaceAll('<', '&lt;').replaceAll('>', '&gt;');\n  return `<span style=\"color:${color}\">${htmlValue}</span>`;\n}\n\n/**\n * Triggers a download of the given text\n * within a file with the specified filename.\n */\nfunction downloadText (text, filename) {\n  const element = document.createElement('a');\n  element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));\n  element.setAttribute('download', filename);\n  element.style.display = 'none';\n  document.body.appendChild(element);\n  element.click();\n  document.body.removeChild(element);\n}\n\nexport default {\n  yamlToHtml,\n  downloadText\n};\n","usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]}]

--- end ---
Traceback (most recent call last):
  File "/venv/lib/python3.11/site-packages/runner-0.1.0-py3.11.egg/runner/__init__.py", line 1534, in main
    libup.run(args.repo, args.output, args.branch)
  File "/venv/lib/python3.11/site-packages/runner-0.1.0-py3.11.egg/runner/__init__.py", line 1472, in run
    self.npm_upgrade(plan)
  File "/venv/lib/python3.11/site-packages/runner-0.1.0-py3.11.egg/runner/__init__.py", line 1054, in npm_upgrade
    hook(update)
  File "/venv/lib/python3.11/site-packages/runner-0.1.0-py3.11.egg/runner/__init__.py", line 1290, in _handle_eslint
    prefix = self._add_eslint_disables(fname, to_disable)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/venv/lib/python3.11/site-packages/runner-0.1.0-py3.11.egg/runner/__init__.py", line 1230, in _add_eslint_disables
    raise ValueError(f"Did not find eslint config for {fname}!")
ValueError: Did not find eslint config for /src/repo/server.js!

npm dependencies

Dependencies
Development dependencies

Logs

Source code is licensed under the AGPL.