diff --git a/meet/server/.eslintrc.json b/meet/server/.eslintrc.json
new file mode 100644
index 00000000..b4a49d19
--- /dev/null
+++ b/meet/server/.eslintrc.json
@@ -0,0 +1,177 @@
+{
+ "env":
+ {
+ "es6": true,
+ "node": true
+ },
+ "extends":
+ [
+ "eslint:recommended"
+ ],
+ "settings": {},
+ "parserOptions":
+ {
+ "ecmaVersion": 2018,
+ "sourceType": "module",
+ "ecmaFeatures":
+ {
+ "impliedStrict": true
+ }
+ },
+ "rules":
+ {
+ "array-bracket-spacing": [ 2, "always",
+ {
+ "objectsInArrays": true,
+ "arraysInArrays": true
+ }],
+ "arrow-parens": [ 2, "always" ],
+ "arrow-spacing": 2,
+ "block-spacing": [ 2, "always" ],
+ "brace-style": [ 2, "allman", { "allowSingleLine": true } ],
+ "camelcase": 2,
+ "comma-dangle": 2,
+ "comma-spacing": [ 2, { "before": false, "after": true } ],
+ "comma-style": 2,
+ "computed-property-spacing": 2,
+ "constructor-super": 2,
+ "func-call-spacing": 2,
+ "generator-star-spacing": 2,
+ "guard-for-in": 2,
+ "indent": [ 2, "tab", { "SwitchCase": 1 } ],
+ "key-spacing": [ 2,
+ {
+ "singleLine":
+ {
+ "beforeColon": false,
+ "afterColon": true
+ },
+ "multiLine":
+ {
+ "beforeColon": true,
+ "afterColon": true,
+ "align": "colon"
+ }
+ }],
+ "keyword-spacing": 2,
+ "linebreak-style": [ 2, "unix" ],
+ "lines-around-comment": [ 2,
+ {
+ "allowBlockStart": true,
+ "allowObjectStart": true,
+ "beforeBlockComment": true,
+ "beforeLineComment": false
+ }],
+ "max-len": [ 2, 90,
+ {
+ "tabWidth": 2,
+ "comments": 90,
+ "ignoreUrls": true,
+ "ignoreStrings": true,
+ "ignoreTemplateLiterals": true,
+ "ignoreRegExpLiterals": true
+ }],
+ "newline-after-var": 2,
+ "newline-before-return": 2,
+ "newline-per-chained-call": 2,
+ "no-alert": 2,
+ "no-caller": 2,
+ "no-case-declarations": 2,
+ "no-catch-shadow": 2,
+ "no-class-assign": 2,
+ "no-confusing-arrow": 2,
+ "no-console": 2,
+ "no-const-assign": 2,
+ "no-debugger": 2,
+ "no-dupe-args": 2,
+ "no-dupe-keys": 2,
+ "no-duplicate-case": 2,
+ "no-div-regex": 2,
+ "no-empty": [ 2, { "allowEmptyCatch": true } ],
+ "no-empty-pattern": 2,
+ "no-else-return": 0,
+ "no-eval": 2,
+ "no-extend-native": 2,
+ "no-ex-assign": 2,
+ "no-extra-bind": 2,
+ "no-extra-boolean-cast": 2,
+ "no-extra-label": 2,
+ "no-extra-semi": 2,
+ "no-fallthrough": 2,
+ "no-func-assign": 2,
+ "no-global-assign": 2,
+ "no-implicit-coercion": 2,
+ "no-implicit-globals": 2,
+ "no-inner-declarations": 2,
+ "no-invalid-regexp": 2,
+ "no-invalid-this": 2,
+ "no-irregular-whitespace": 2,
+ "no-trailing-spaces": [
+ "error",
+ {
+ "ignoreComments": true
+ }
+ ],
+ "no-lonely-if": 2,
+ "no-mixed-operators": 2,
+ "no-mixed-spaces-and-tabs": 2,
+ "no-multi-spaces": 2,
+ "no-multi-str": 2,
+ "no-multiple-empty-lines": [ 1, { "max": 1, "maxEOF": 0, "maxBOF": 0 } ],
+ "no-native-reassign": 2,
+ "no-negated-in-lhs": 2,
+ "no-new": 2,
+ "no-new-func": 2,
+ "no-new-wrappers": 2,
+ "no-obj-calls": 2,
+ "no-proto": 2,
+ "no-prototype-builtins": 0,
+ "no-redeclare": 2,
+ "no-regex-spaces": 2,
+ "no-restricted-imports": 2,
+ "no-return-assign": 2,
+ "no-self-assign": 2,
+ "no-self-compare": 2,
+ "no-sequences": 2,
+ "no-shadow": 2,
+ "no-shadow-restricted-names": 2,
+ "no-spaced-func": 2,
+ "no-sparse-arrays": 2,
+ "no-this-before-super": 2,
+ "no-throw-literal": 2,
+ "no-undef": 2,
+ "no-unexpected-multiline": 2,
+ "no-unmodified-loop-condition": 2,
+ "no-unreachable": 2,
+ "no-unused-vars": [ 1, { "vars": "all", "args": "after-used" }],
+ "no-use-before-define": [ 2, { "functions": false } ],
+ "no-useless-call": 2,
+ "no-useless-computed-key": 2,
+ "no-useless-concat": 2,
+ "no-useless-rename": 2,
+ "no-var": 2,
+ "no-whitespace-before-property": 2,
+ "object-curly-newline": 0,
+ "object-curly-spacing": [ 2, "always" ],
+ "object-property-newline": [ 2, { "allowMultiplePropertiesPerLine": true } ],
+ "prefer-const": 2,
+ "prefer-rest-params": 2,
+ "prefer-spread": 2,
+ "prefer-template": 2,
+ "quotes": [ 2, "single", { "avoidEscape": true } ],
+ "semi": [ 2, "always" ],
+ "semi-spacing": 2,
+ "space-before-blocks": 2,
+ "space-before-function-paren": [ 2,
+ {
+ "anonymous" : "never",
+ "named" : "never",
+ "asyncArrow" : "always"
+ }],
+ "space-in-parens": [ 2, "never" ],
+ "spaced-comment": [ 2, "always" ],
+ "strict": 2,
+ "valid-typeof": 2,
+ "yoda": 2
+ }
+}
\ No newline at end of file
diff --git a/meet/server/.npmrc b/meet/server/.npmrc
new file mode 100644
index 00000000..43c97e71
--- /dev/null
+++ b/meet/server/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/meet/server/access.js b/meet/server/access.js
new file mode 100644
index 00000000..479e9e27
--- /dev/null
+++ b/meet/server/access.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // The role(s) will gain access to the room
+ // even if it is locked (!)
+ BYPASS_ROOM_LOCK : 'BYPASS_ROOM_LOCK',
+ // The role(s) will gain access to the room without
+ // going into the lobby. If you want to restrict access to your
+ // server to only directly allow authenticated users, you could
+ // add the userRoles.AUTHENTICATED to the user in the userMapping
+ // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
+ BYPASS_LOBBY : 'BYPASS_LOBBY'
+};
\ No newline at end of file
diff --git a/meet/server/certs/mediasoup-demo.localhost.cert.pem b/meet/server/certs/mediasoup-demo.localhost.cert.pem
new file mode 100644
index 00000000..0d627583
--- /dev/null
+++ b/meet/server/certs/mediasoup-demo.localhost.cert.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDTTCCAjWgAwIBAgIEWPyBpzANBgkqhkiG9w0BAQUFADBQMSEwHwYDVQQDDBht
+ZWRpYXNvdXAtZGVtby5sb2NhbGhvc3QxFzAVBgNVBAoMDm1lZGlhc291cC1kZW1v
+MRIwEAYDVQQLDAltZWRpYXNvdXAwHhcNMTcwNDIyMTAyNzU1WhcNMzcwNDE4MTAy
+NzU1WjBQMSEwHwYDVQQDDBhtZWRpYXNvdXAtZGVtby5sb2NhbGhvc3QxFzAVBgNV
+BAoMDm1lZGlhc291cC1kZW1vMRIwEAYDVQQLDAltZWRpYXNvdXAwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCynyO1szmonG3dk+SSQflM5DqzBNTI8ufA
+9Za8ltCmq211y6N9cjhKexHS/Eiu8x907QFNgzcagVgrPAA7XJJdckoupKsf4Qrk
+wWrpW7s/nJV2H04oIShAdWWbVckRhMLzdz+VWV0rM4AtjBxYu89B3OH9C1p4uYGH
+3i4/E147gmk+NaYdddUhYbKYTBhjtjrC2IN/lHT+VfGX8yJ0q0J9Pv6B+17pYJ1P
+QAyGhgzmvvi500t1Ke42EI7QOYAGzOw7S/zNl7lBVmXdQGmpGipD7sMVg56txNmt
+7RRETaaQ5uNpCxkBcJdIX/DzGV9xNKFoMLm1GUEdTY1RnM7jN0HNAgMBAAGjLzAt
+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFDazfBdo/7PNIfarcfMY6txrxQgoMA0G
+CSqGSIb3DQEBBQUAA4IBAQCtqv4Wsnp658HxYyDBxX6CnFpnNfMDqeE8scFeihmX
+X3AtJwWMWZJpOX26eOlVqee1h3QyTmvnITau+1Sphttt6EYoHBBHC5It4sCV/kwm
+6iiKKah0uxlXUyoj0ylRMwBA16b922OXm8ozDzo3FQWASLstYaUQf1kJtLQimGrH
+a4YYiQtRkCO7NvGjaHS8zwmkUdOy8mE1sXol8CiiwCJPGF5vUQMQzj1zqOhQEPLM
+44XCmM1CawTfFLhwmgZpPPzYCDMfEz1tF5M/ODOtSTytGoa0H2q4YpXVCiftAQV5
+fpSOlyqYaVk7oBkrHS6I6n58MATfuKcPn5YMJ8S/64u1
+-----END CERTIFICATE-----
diff --git a/meet/server/certs/mediasoup-demo.localhost.key.pem b/meet/server/certs/mediasoup-demo.localhost.key.pem
new file mode 100644
index 00000000..15840209
--- /dev/null
+++ b/meet/server/certs/mediasoup-demo.localhost.key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAsp8jtbM5qJxt3ZPkkkH5TOQ6swTUyPLnwPWWvJbQpqttdcuj
+fXI4SnsR0vxIrvMfdO0BTYM3GoFYKzwAO1ySXXJKLqSrH+EK5MFq6Vu7P5yVdh9O
+KCEoQHVlm1XJEYTC83c/lVldKzOALYwcWLvPQdzh/QtaeLmBh94uPxNeO4JpPjWm
+HXXVIWGymEwYY7Y6wtiDf5R0/lXxl/MidKtCfT7+gfte6WCdT0AMhoYM5r74udNL
+dSnuNhCO0DmABszsO0v8zZe5QVZl3UBpqRoqQ+7DFYOercTZre0URE2mkObjaQsZ
+AXCXSF/w8xlfcTShaDC5tRlBHU2NUZzO4zdBzQIDAQABAoIBABLK2WfxbjyGEK0C
+NUcJ99+WF3LkLDrkC2vqqqw2tccDPCXrgczd6nwzjIGFF2SIoaOcl8l+55o7R3ps
++p1ENQXt004q9vIIrCu7CbN5ei7MG5Fs470nF+QINeNs2BWmwRf6UM82sq2r4m1o
+U0cmozyLr57+xcrzwWP5BSaPtBdQiPjzML7E4PIg9WHbUhYjtgc90a0ruHhp9rlR
+QbFsxX9KMcTeJN+pQA+dJWlJrP4EuQurIyupl2zx+XLBzb9j4pL9wlklu3IFI6v+
+k+FVTVKXqjndanCbeOvPTli01ILng5UNUEsleWbuvFLuiXlSkduhDVBWECmNxJIR
+VCP46EECgYEA6HYNBSyb1NtxXx0Pv2Itg4TbRSP1vQOoQjJzBjrE6FYQJf6lOgMn
+sQJFGmZMXiKcyG729Ntw3gvFjnk25V4DNsK1mbLy9cBO8ETMpm2NfAt9QnH40rmp
+nd9cgu6v3AFvlBwNJAGsGRFDgshExeQlY28aQy5FevsHE/wc3UpDfRECgYEAxLVw
+ocJX/PfvhwW8S0neGtJ4n8h3MLczOxAbHulG44aTwijSIBRxS1E0h+w0jG8Lwr/O
+518RpevKhcoGQtf0XuRu1TP2UAtF/rSflCg8a/zHUBen5N2loOWc7Pd9S71klDoi
+en7d1NIUZq4Cljb1D1UYW9Ek6wQ9tQFe5EtaKP0CgYAB+N5raNF5oNL5Z5m2mfKg
+5wOlNoTjMaC/zwXCy8TX48MHT32/XD999PL5Il0Lf2etG6Pkt+fhOmBWsRiSIZYN
+ZOF9iFMfWp5Q04SY9Nz6bG6HncfqocCaokZ6pePADhMQQpyp7Ym0PL1B4skSlLjs
+ewjSARZ90JtixATKq9KewQKBgB1T294SJqItqQWdgkxLUBT5qkhQUAzwU3AL369F
+Im+Lwf3hripgQd/z1HwraE5DxCIeDNAMKYpuVDyMOVC/98wqDKg23hNjCuWFsoEZ
+WqDTCDhVvo9tyGLruPDPmVuweg1reXZ/8bzoMWh5qyMQQIsvqbkOvo1XjYeuE6K/
+5UpVAoGBAIvYtZi1a2UFhJmKNaa0dnOWhLAtWjsOh7+k/nM36Zgl1/W7veWD3yiA
+HTbyFYK0Rq696OAlVemEVNVEm4bgS2reEJHWYwrQBc07iYVww+qWkocKUVfNmuvd
+BUx/QnIKZAhFpDFYLoLUddnxOsLJd4CiuXeVEaLsLZ+eZcIlzWPc
+-----END RSA PRIVATE KEY-----
diff --git a/meet/server/config/config.example.js b/meet/server/config/config.example.js
new file mode 100644
index 00000000..41b5de0b
--- /dev/null
+++ b/meet/server/config/config.example.js
@@ -0,0 +1,479 @@
+const os = require('os');
+// const fs = require('fs');
+
+const userRoles = require('../userRoles');
+
+const {
+ BYPASS_ROOM_LOCK,
+ BYPASS_LOBBY
+} = require('../access');
+
+const {
+ CHANGE_ROOM_LOCK,
+ PROMOTE_PEER,
+ MODIFY_ROLE,
+ SEND_CHAT,
+ MODERATE_CHAT,
+ SHARE_AUDIO,
+ SHARE_VIDEO,
+ SHARE_SCREEN,
+ EXTRA_VIDEO,
+ SHARE_FILE,
+ MODERATE_FILES,
+ MODERATE_ROOM
+} = require('../permissions');
+
+// const AwaitQueue = require('awaitqueue');
+// const axios = require('axios');
+
+module.exports =
+{
+
+ // Auth conf
+ /*
+ auth :
+ {
+ // Always enabled if configured
+ lti :
+ {
+ consumerKey : 'key',
+ consumerSecret : 'secret'
+ },
+
+ // Auth strategy to use (default oidc)
+ strategy : 'oidc',
+ oidc :
+ {
+ // The issuer URL for OpenID Connect discovery
+ // The OpenID Provider Configuration Document
+ // could be discovered on:
+ // issuerURL + '/.well-known/openid-configuration'
+
+ // e.g. google OIDC config
+ // Follow this guide to get credential:
+ // https://developers.google.com/identity/protocols/oauth2/openid-connect
+ // use this issuerURL
+ // issuerURL : 'https://accounts.google.com/',
+
+ issuerURL : 'https://example.com',
+ clientOptions :
+ {
+ client_id : '',
+ client_secret : '',
+ scope : 'openid email profile',
+ // where client.example.com is your edumeet server
+ redirect_uri : 'https://client.example.com/auth/callback'
+ }
+
+ },
+ saml :
+ {
+ // where edumeet.example.com is your edumeet server
+ callbackUrl : 'https://edumeet.example.com/auth/callback',
+ issuer : 'https://edumeet.example.com',
+ entryPoint : 'https://openidp.feide.no/simplesaml/saml2/idp/SSOService.php',
+ privateCert : fs.readFileSync('config/saml_privkey.pem', 'utf-8'),
+ signingCert : fs.readFileSync('config/saml_cert.pem', 'utf-8'),
+ decryptionPvk : fs.readFileSync('config/saml_privkey.pem', 'utf-8'),
+ decryptionCert : fs.readFileSync('config/saml_cert.pem', 'utf-8'),
+ // Federation cert
+ cert : fs.readFileSync('config/federation_cert.pem', 'utf-8')
+ },
+
+ // to create password hash use: node server/utils/password_encode.js cleartextpassword
+ local :
+ {
+ users : [
+ {
+ id : 1,
+ username : 'alice',
+ passwordHash : '$2b$10$PAXXw.6cL3zJLd7ZX.AnL.sFg2nxjQPDmMmGSOQYIJSa0TrZ9azG6',
+ displayName : 'Alice',
+ emails : [ { value: 'alice@atlanta.com' } ]
+ },
+ {
+ id : 2,
+ username : 'bob',
+ passwordHash : '$2b$10$BzAkXcZ54JxhHTqCQcFn8.H6klY/G48t4jDBeTE2d2lZJk/.tvv0G',
+ displayName : 'Bob',
+ emails : [ { value: 'bob@biloxi.com' } ]
+ }
+ ]
+ }
+ },
+ */
+ // URI and key for requesting geoip-based TURN server closest to the client
+ turnAPIKey : 'examplekey',
+ turnAPIURI : 'https://example.com/api/turn',
+ turnAPIparams : {
+ 'uri_schema' : 'turn',
+ 'transport' : 'tcp',
+ 'ip_ver' : 'ipv4',
+ 'servercount' : '2'
+ },
+ turnAPITimeout : 2 * 1000,
+ // Backup turnservers if REST fails or is not configured
+ backupTurnServers : [
+ {
+ urls : [
+ 'turn:turn.example.com:443?transport=tcp'
+ ],
+ username : 'example',
+ credential : 'example'
+ }
+ ],
+ // bittorrent tracker: please replace this if you want a more private file sharing service inside eduMEET
+ // have a look at https://github.com/webtorrent/bittorrent-tracker for setup your own tracker
+ fileTracker : 'wss://tracker.openwebtorrent.com',
+ // redis server options
+ redisOptions : {},
+ // session cookie secret
+ cookieSecret : 'T0P-S3cR3t_cook!e',
+ cookieName : 'edumeet.sid',
+ // if you use encrypted private key the set the passphrase
+ tls :
+ {
+ cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
+ // passphrase: 'key_password'
+ key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem`
+ },
+ // listening Host or IP
+ // If omitted listens on every IP. ("0.0.0.0" and "::")
+ // listeningHost: 'localhost',
+ // Listening port for https server.
+ listeningPort : 443,
+ // Any http request is redirected to https.
+ // Listening port for http server.
+ listeningRedirectPort : 80,
+ // Listens only on http, only on listeningPort
+ // listeningRedirectPort disabled
+ // use case: loadbalancer backend
+ httpOnly : false,
+ // WebServer/Express trust proxy config for httpOnly mode
+ // You can find more info:
+ // - https://expressjs.com/en/guide/behind-proxies.html
+ // - https://www.npmjs.com/package/proxy-addr
+ // use case: loadbalancer backend
+ trustProxy : '',
+ // This logger class will have the log function
+ // called every time there is a room created or destroyed,
+ // or peer created or destroyed. This would then be able
+ // to log to a file or external service.
+ /* StatusLogger : class
+ {
+ constructor()
+ {
+ this._queue = new AwaitQueue();
+ }
+
+ // rooms: rooms object
+ // peers: peers object
+ // eslint-disable-next-line no-unused-vars
+ async log({ rooms, peers })
+ {
+ this._queue.push(async () =>
+ {
+ // Do your logging in here, use queue to keep correct order
+
+ // eslint-disable-next-line no-console
+ console.log('Number of rooms: ', rooms.size);
+ // eslint-disable-next-line no-console
+ console.log('Number of peers: ', peers.size);
+ })
+ .catch((error) =>
+ {
+ // eslint-disable-next-line no-console
+ console.log('error in log', error);
+ });
+ }
+ }, */
+ // This function will be called on successful login through oidc.
+ // Use this function to map your oidc userinfo to the Peer object.
+ // The roomId is equal to the room name.
+ // See examples below.
+ // Examples:
+ /*
+ // All authenicated users will be MODERATOR and AUTHENTICATED
+ userMapping : async ({ peer, room, roomId, userinfo }) =>
+ {
+ peer.addRole(userRoles.MODERATOR);
+ peer.addRole(userRoles.AUTHENTICATED);
+ },
+ // All authenicated users will be AUTHENTICATED,
+ // and those with the moderator role set in the userinfo
+ // will also be MODERATOR
+ userMapping : async ({ peer, room, roomId, userinfo }) =>
+ {
+ if (
+ Array.isArray(userinfo.meet_roles) &&
+ userinfo.meet_roles.includes('moderator')
+ )
+ {
+ peer.addRole(userRoles.MODERATOR);
+ }
+
+ if (
+ Array.isArray(userinfo.meet_roles) &&
+ userinfo.meet_roles.includes('meetingadmin')
+ )
+ {
+ peer.addRole(userRoles.ADMIN);
+ }
+
+ peer.addRole(userRoles.AUTHENTICATED);
+ },
+ // First authenticated user will be moderator,
+ // all others will be AUTHENTICATED
+ userMapping : async ({ peer, room, roomId, userinfo }) =>
+ {
+ if (room)
+ {
+ const peers = room.getJoinedPeers();
+
+ if (peers.some((_peer) => _peer.authenticated))
+ peer.addRole(userRoles.AUTHENTICATED);
+ else
+ {
+ peer.addRole(userRoles.MODERATOR);
+ peer.addRole(userRoles.AUTHENTICATED);
+ }
+ }
+ },
+ // All authenicated users will be AUTHENTICATED,
+ // and those with email ending with @example.com
+ // will also be MODERATOR
+ userMapping : async ({ peer, room, roomId, userinfo }) =>
+ {
+ if (userinfo.email && userinfo.email.endsWith('@example.com'))
+ {
+ peer.addRole(userRoles.MODERATOR);
+ }
+
+ peer.addRole(userRoles.AUTHENTICATED);
+ },
+ // All authenicated users will be AUTHENTICATED,
+ // and those with email ending with @example.com
+ // will also be MODERATOR
+ userMapping : async ({ peer, room, roomId, userinfo }) =>
+ {
+ if (userinfo.email && userinfo.email.endsWith('@example.com'))
+ {
+ peer.addRole(userRoles.MODERATOR);
+ }
+
+ peer.addRole(userRoles.AUTHENTICATED);
+ },
+ */
+ // eslint-disable-next-line no-unused-vars
+ userMapping : async ({ peer, room, roomId, userinfo }) =>
+ {
+ if (userinfo.picture != null)
+ {
+ if (!userinfo.picture.match(/^http/g))
+ {
+ peer.picture = `data:image/jpeg;base64, ${userinfo.picture}`;
+ }
+ else
+ {
+ peer.picture = userinfo.picture;
+ }
+ }
+ if (userinfo['urn:oid:0.9.2342.19200300.100.1.60'] != null)
+ {
+ peer.picture = `data:image/jpeg;base64, ${userinfo['urn:oid:0.9.2342.19200300.100.1.60']}`;
+ }
+
+ if (userinfo.nickname != null)
+ {
+ peer.displayName = userinfo.nickname;
+ }
+
+ if (userinfo.name != null)
+ {
+ peer.displayName = userinfo.name;
+ }
+
+ if (userinfo.displayName != null)
+ {
+ peer.displayName = userinfo.displayName;
+ }
+
+ if (userinfo['urn:oid:2.16.840.1.113730.3.1.241'] != null)
+ {
+ peer.displayName = userinfo['urn:oid:2.16.840.1.113730.3.1.241'];
+ }
+
+ if (userinfo.email != null)
+ {
+ peer.email = userinfo.email;
+ }
+ },
+ // All users have the role "NORMAL" by default. Other roles need to be
+ // added in the "userMapping" function. The following accesses and
+ // permissions are arrays of roles. Roles can be changed in userRoles.js
+ //
+ // Example:
+ // [ userRoles.MODERATOR, userRoles.AUTHENTICATED ]
+ accessFromRoles : {
+ // The role(s) will gain access to the room
+ // even if it is locked (!)
+ [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ],
+ // The role(s) will gain access to the room without
+ // going into the lobby. If you want to restrict access to your
+ // server to only directly allow authenticated users, you could
+ // add the userRoles.AUTHENTICATED to the user in the userMapping
+ // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
+ [BYPASS_LOBBY] : [ userRoles.NORMAL ]
+ },
+ permissionsFromRoles : {
+ // The role(s) have permission to lock/unlock a room
+ [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ],
+ // The role(s) have permission to promote a peer from the lobby
+ [PROMOTE_PEER] : [ userRoles.NORMAL ],
+ // The role(s) have permission to give/remove other peers roles
+ [MODIFY_ROLE] : [ userRoles.NORMAL ],
+ // The role(s) have permission to send chat messages
+ [SEND_CHAT] : [ userRoles.NORMAL ],
+ // The role(s) have permission to moderate chat
+ [MODERATE_CHAT] : [ userRoles.MODERATOR ],
+ // The role(s) have permission to share audio
+ [SHARE_AUDIO] : [ userRoles.NORMAL ],
+ // The role(s) have permission to share video
+ [SHARE_VIDEO] : [ userRoles.NORMAL ],
+ // The role(s) have permission to share screen
+ [SHARE_SCREEN] : [ userRoles.NORMAL ],
+ // The role(s) have permission to produce extra video
+ [EXTRA_VIDEO] : [ userRoles.NORMAL ],
+ // The role(s) have permission to share files
+ [SHARE_FILE] : [ userRoles.NORMAL ],
+ // The role(s) have permission to moderate files
+ [MODERATE_FILES] : [ userRoles.MODERATOR ],
+ // The role(s) have permission to moderate room (e.g. kick user)
+ [MODERATE_ROOM] : [ userRoles.MODERATOR ]
+ },
+ // Array of permissions. If no peer with the permission in question
+ // is in the room, all peers are permitted to do the action. The peers
+ // that are allowed because of this rule will not be able to do this
+ // action as soon as a peer with the permission joins. In this example
+ // everyone will be able to lock/unlock room until a MODERATOR joins.
+ allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ],
+ // When truthy, the room will be open to all users when as long as there
+ // are allready users in the room
+ activateOnHostJoin : true,
+ // When set, maxUsersPerRoom defines how many users can join
+ // a single room. If not set, there is no limit.
+ // maxUsersPerRoom : 20,
+ // Room size before spreading to new router
+ routerScaleSize : 40,
+ // Socket timout value
+ requestTimeout : 20000,
+ // Socket retries when timeout
+ requestRetries : 3,
+ // Mediasoup settings
+ mediasoup :
+ {
+ numWorkers : Object.keys(os.cpus()).length,
+ // mediasoup Worker settings.
+ worker :
+ {
+ logLevel : 'warn',
+ logTags :
+ [
+ 'info',
+ 'ice',
+ 'dtls',
+ 'rtp',
+ 'srtp',
+ 'rtcp'
+ ],
+ rtcMinPort : 40000,
+ rtcMaxPort : 49999
+ },
+ // mediasoup Router settings.
+ router :
+ {
+ // Router media codecs.
+ mediaCodecs :
+ [
+ {
+ kind : 'audio',
+ mimeType : 'audio/opus',
+ clockRate : 48000,
+ channels : 2
+ },
+ {
+ kind : 'video',
+ mimeType : 'video/VP8',
+ clockRate : 90000,
+ parameters :
+ {
+ 'x-google-start-bitrate' : 1000
+ }
+ },
+ {
+ kind : 'video',
+ mimeType : 'video/VP9',
+ clockRate : 90000,
+ parameters :
+ {
+ 'profile-id' : 2,
+ 'x-google-start-bitrate' : 1000
+ }
+ },
+ {
+ kind : 'video',
+ mimeType : 'video/h264',
+ clockRate : 90000,
+ parameters :
+ {
+ 'packetization-mode' : 1,
+ 'profile-level-id' : '4d0032',
+ 'level-asymmetry-allowed' : 1,
+ 'x-google-start-bitrate' : 1000
+ }
+ },
+ {
+ kind : 'video',
+ mimeType : 'video/h264',
+ clockRate : 90000,
+ parameters :
+ {
+ 'packetization-mode' : 1,
+ 'profile-level-id' : '42e01f',
+ 'level-asymmetry-allowed' : 1,
+ 'x-google-start-bitrate' : 1000
+ }
+ }
+ ]
+ },
+ // mediasoup WebRtcTransport settings.
+ webRtcTransport :
+ {
+ listenIps :
+ [
+ // change 192.0.2.1 IPv4 to your server's IPv4 address!!
+ { ip: '192.0.2.1', announcedIp: null }
+
+ // Can have multiple listening interfaces
+ // change 2001:DB8::1 IPv6 to your server's IPv6 address!!
+ // { ip: '2001:DB8::1', announcedIp: null }
+ ],
+ initialAvailableOutgoingBitrate : 1000000,
+ minimumAvailableOutgoingBitrate : 600000,
+ // Additional options that are not part of WebRtcTransportOptions.
+ maxIncomingBitrate : 1500000
+ }
+ }
+
+ /*
+ ,
+ // Prometheus exporter
+ prometheus : {
+ deidentify : false, // deidentify IP addresses
+ // listen : 'localhost', // exporter listens on this address
+ numeric : false, // show numeric IP addresses
+ port : 8889, // allocated port
+ quiet : false // include fewer labels
+ }
+ */
+};
diff --git a/meet/server/connect.js b/meet/server/connect.js
new file mode 100755
index 00000000..e5c93e70
--- /dev/null
+++ b/meet/server/connect.js
@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+
+const interactiveClient = require('./lib/interactiveClient');
+
+interactiveClient();
diff --git a/meet/server/httpHelper.js b/meet/server/httpHelper.js
new file mode 100644
index 00000000..7948e960
--- /dev/null
+++ b/meet/server/httpHelper.js
@@ -0,0 +1,41 @@
+exports.loginHelper = function(data)
+{
+ const html = `
+
+
+
+ edumeet
+
+
+
+
+ `;
+
+ return html;
+};
+
+exports.logoutHelper = function()
+{
+ const html = `
+
+
+
+ edumeet
+
+
+
+
+ `;
+
+ return html;
+};
\ No newline at end of file
diff --git a/meet/server/lib/Lobby.js b/meet/server/lib/Lobby.js
new file mode 100644
index 00000000..510316fc
--- /dev/null
+++ b/meet/server/lib/Lobby.js
@@ -0,0 +1,222 @@
+const EventEmitter = require('events').EventEmitter;
+const Logger = require('./Logger');
+
+const logger = new Logger('Lobby');
+
+class Lobby extends EventEmitter
+{
+ constructor()
+ {
+ logger.info('constructor()');
+
+ super();
+
+ // Closed flag.
+ this._closed = false;
+
+ this._peers = {};
+ }
+
+ close()
+ {
+ logger.info('close()');
+
+ this._closed = true;
+
+ // Close the peers.
+ for (const peer in this._peers)
+ {
+ if (!this._peers[peer].closed)
+ this._peers[peer].close();
+ }
+
+ this._peers = null;
+ }
+
+ checkEmpty()
+ {
+ logger.info('checkEmpty()');
+
+ return Object.keys(this._peers).length === 0;
+ }
+
+ peerList()
+ {
+ logger.info('peerList()');
+
+ return Object.values(this._peers).map((peer) =>
+ ({
+ id : peer.id,
+ displayName : peer.displayName,
+ picture : peer.picture
+ }));
+ }
+
+ hasPeer(peerId)
+ {
+ return this._peers[peerId] != null;
+ }
+
+ promoteAllPeers()
+ {
+ logger.info('promoteAllPeers()');
+
+ for (const peer in this._peers)
+ {
+ if (!this._peers[peer].closed)
+ this.promotePeer(peer);
+ }
+ }
+
+ promotePeer(peerId)
+ {
+ logger.info('promotePeer() [peer:"%s"]', peerId);
+
+ const peer = this._peers[peerId];
+
+ if (peer)
+ {
+ peer.socket.removeListener('request', peer.socketRequestHandler);
+ peer.removeListener('gotRole', peer.gotRoleHandler);
+ peer.removeListener('displayNameChanged', peer.displayNameChangeHandler);
+ peer.removeListener('pictureChanged', peer.pictureChangeHandler);
+ peer.removeListener('close', peer.closeHandler);
+
+ peer.socketRequestHandler = null;
+ peer.gotRoleHandler = null;
+ peer.displayNameChangeHandler = null;
+ peer.pictureChangeHandler = null;
+ peer.closeHandler = null;
+
+ this.emit('promotePeer', peer);
+ delete this._peers[peerId];
+ }
+ }
+
+ parkPeer(peer)
+ {
+ logger.info('parkPeer() [peer:"%s"]', peer.id);
+
+ if (this._closed)
+ return;
+
+ peer.socketRequestHandler = (request, cb) =>
+ {
+ logger.debug(
+ 'Peer "request" event [method:"%s", peer:"%s"]',
+ request.method, peer.id);
+
+ if (this._closed)
+ return;
+
+ this._handleSocketRequest(peer, request, cb)
+ .catch((error) =>
+ {
+ logger.error('request failed [error:"%o"]', error);
+
+ cb(error);
+ });
+ };
+
+ peer.gotRoleHandler = () =>
+ {
+ logger.info('parkPeer() | rolesChange [peer:"%s"]', peer.id);
+
+ this.emit('peerRolesChanged', peer);
+ };
+
+ peer.displayNameChangeHandler = () =>
+ {
+ logger.info('parkPeer() | displayNameChange [peer:"%s"]', peer.id);
+
+ this.emit('changeDisplayName', peer);
+ };
+
+ peer.pictureChangeHandler = () =>
+ {
+ logger.info('parkPeer() | pictureChange [peer:"%s"]', peer.id);
+
+ this.emit('changePicture', peer);
+ };
+
+ peer.closeHandler = () =>
+ {
+ logger.debug('Peer "close" event [peer:"%s"]', peer.id);
+
+ if (this._closed)
+ return;
+
+ this.emit('peerClosed', peer);
+
+ delete this._peers[peer.id];
+
+ if (this.checkEmpty())
+ this.emit('lobbyEmpty');
+ };
+
+ this._peers[peer.id] = peer;
+
+ peer.on('gotRole', peer.gotRoleHandler);
+ peer.on('displayNameChanged', peer.displayNameChangeHandler);
+ peer.on('pictureChanged', peer.pictureChangeHandler);
+
+ peer.socket.on('request', peer.socketRequestHandler);
+
+ peer.on('close', peer.closeHandler);
+
+ this._notification(peer.socket, 'enteredLobby');
+ }
+
+ async _handleSocketRequest(peer, request, cb)
+ {
+ logger.debug(
+ '_handleSocketRequest [peer:"%s"], [request:"%s"]',
+ peer.id,
+ request.method
+ );
+
+ if (this._closed)
+ return;
+
+ switch (request.method)
+ {
+ case 'changeDisplayName':
+ {
+ const { displayName } = request.data;
+
+ peer.displayName = displayName;
+
+ cb();
+
+ break;
+ }
+
+ case 'changePicture':
+ {
+ const { picture } = request.data;
+
+ peer.picture = picture;
+
+ cb();
+
+ break;
+ }
+ }
+ }
+
+ _notification(socket, method, data = {}, broadcast = false)
+ {
+ if (broadcast)
+ {
+ socket.broadcast.to(this._roomId).emit(
+ 'notification', { method, data }
+ );
+ }
+ else
+ {
+ socket.emit('notification', { method, data });
+ }
+ }
+}
+
+module.exports = Lobby;
\ No newline at end of file
diff --git a/meet/server/lib/Logger.js b/meet/server/lib/Logger.js
new file mode 100644
index 00000000..d51f2b70
--- /dev/null
+++ b/meet/server/lib/Logger.js
@@ -0,0 +1,53 @@
+const debug = require('debug');
+
+const APP_NAME = 'edumeet-server';
+
+class Logger
+{
+ constructor(prefix)
+ {
+ if (prefix)
+ {
+ this._debug = debug(`${APP_NAME}:${prefix}`);
+ this._info = debug(`${APP_NAME}:INFO:${prefix}`);
+ this._warn = debug(`${APP_NAME}:WARN:${prefix}`);
+ this._error = debug(`${APP_NAME}:ERROR:${prefix}`);
+ }
+ else
+ {
+ this._debug = debug(APP_NAME);
+ this._info = debug(`${APP_NAME}:INFO`);
+ this._warn = debug(`${APP_NAME}:WARN`);
+ this._error = debug(`${APP_NAME}:ERROR`);
+ }
+
+ /* eslint-disable no-console */
+ this._debug.log = console.info.bind(console);
+ this._info.log = console.info.bind(console);
+ this._warn.log = console.warn.bind(console);
+ this._error.log = console.error.bind(console);
+ /* eslint-enable no-console */
+ }
+
+ get debug()
+ {
+ return this._debug;
+ }
+
+ get info()
+ {
+ return this._info;
+ }
+
+ get warn()
+ {
+ return this._warn;
+ }
+
+ get error()
+ {
+ return this._error;
+ }
+}
+
+module.exports = Logger;
diff --git a/meet/server/lib/Peer.js b/meet/server/lib/Peer.js
new file mode 100644
index 00000000..3b575e24
--- /dev/null
+++ b/meet/server/lib/Peer.js
@@ -0,0 +1,399 @@
+const EventEmitter = require('events').EventEmitter;
+const userRoles = require('../userRoles');
+const Logger = require('./Logger');
+
+const logger = new Logger('Peer');
+
+class Peer extends EventEmitter
+{
+ constructor({ id, roomId, socket })
+ {
+ logger.info('constructor() [id:"%s"]', id);
+ super();
+
+ this._id = id;
+
+ this._roomId = roomId;
+
+ this._authId = null;
+
+ this._socket = socket;
+
+ this._closed = false;
+
+ this._joined = false;
+
+ this._joinedTimestamp = null;
+
+ this._inLobby = false;
+
+ this._authenticated = false;
+
+ this._authenticatedTimestamp = null;
+
+ this._roles = [ userRoles.NORMAL ];
+
+ this._displayName = false;
+
+ this._picture = null;
+
+ this._email = null;
+
+ this._routerId = null;
+
+ this._rtpCapabilities = null;
+
+ this._raisedHand = false;
+
+ this._raisedHandTimestamp = null;
+
+ this._transports = new Map();
+
+ this._producers = new Map();
+
+ this._consumers = new Map();
+
+ this._handlePeer();
+ }
+
+ close()
+ {
+ logger.info('close()');
+
+ this._closed = true;
+
+ // Iterate and close all mediasoup Transport associated to this Peer, so all
+ // its Producers and Consumers will also be closed.
+ for (const transport of this.transports.values())
+ {
+ transport.close();
+ }
+
+ if (this.socket)
+ this.socket.disconnect(true);
+
+ this.emit('close');
+ }
+
+ _handlePeer()
+ {
+ if (this.socket)
+ {
+ this.socket.on('disconnect', () =>
+ {
+ if (this.closed)
+ return;
+
+ logger.debug('"disconnect" event [id:%s]', this.id);
+
+ this.close();
+ });
+ }
+ }
+
+ get id()
+ {
+ return this._id;
+ }
+
+ set id(id)
+ {
+ this._id = id;
+ }
+
+ get roomId()
+ {
+ return this._roomId;
+ }
+
+ set roomId(roomId)
+ {
+ this._roomId = roomId;
+ }
+
+ get authId()
+ {
+ return this._authId;
+ }
+
+ set authId(authId)
+ {
+ this._authId = authId;
+ }
+
+ get socket()
+ {
+ return this._socket;
+ }
+
+ set socket(socket)
+ {
+ this._socket = socket;
+ }
+
+ get closed()
+ {
+ return this._closed;
+ }
+
+ get joined()
+ {
+ return this._joined;
+ }
+
+ set joined(joined)
+ {
+ joined ?
+ this._joinedTimestamp = Date.now() :
+ this._joinedTimestamp = null;
+
+ this._joined = joined;
+ }
+
+ get joinedTimestamp()
+ {
+ return this._joinedTimestamp;
+ }
+
+ get inLobby()
+ {
+ return this._inLobby;
+ }
+
+ set inLobby(inLobby)
+ {
+ this._inLobby = inLobby;
+ }
+
+ get authenticated()
+ {
+ return this._authenticated;
+ }
+
+ set authenticated(authenticated)
+ {
+ if (authenticated !== this._authenticated)
+ {
+ authenticated ?
+ this._authenticatedTimestamp = Date.now() :
+ this._authenticatedTimestamp = null;
+
+ const oldAuthenticated = this._authenticated;
+
+ this._authenticated = authenticated;
+
+ this.emit('authenticationChanged', { oldAuthenticated });
+ }
+ }
+
+ get authenticatedTimestamp()
+ {
+ return this._authenticatedTimestamp;
+ }
+
+ get roles()
+ {
+ return this._roles;
+ }
+
+ get displayName()
+ {
+ return this._displayName;
+ }
+
+ set displayName(displayName)
+ {
+ if (displayName !== this._displayName)
+ {
+ const oldDisplayName = this._displayName;
+
+ this._displayName = displayName;
+
+ this.emit('displayNameChanged', { oldDisplayName });
+ }
+ }
+
+ get picture()
+ {
+ return this._picture;
+ }
+
+ set picture(picture)
+ {
+ if (picture !== this._picture)
+ {
+ const oldPicture = this._picture;
+
+ this._picture = picture;
+
+ this.emit('pictureChanged', { oldPicture });
+ }
+ }
+
+ get email()
+ {
+ return this._email;
+ }
+
+ set email(email)
+ {
+ this._email = email;
+ }
+
+ get routerId()
+ {
+ return this._routerId;
+ }
+
+ set routerId(routerId)
+ {
+ this._routerId = routerId;
+ }
+
+ get rtpCapabilities()
+ {
+ return this._rtpCapabilities;
+ }
+
+ set rtpCapabilities(rtpCapabilities)
+ {
+ this._rtpCapabilities = rtpCapabilities;
+ }
+
+ get raisedHand()
+ {
+ return this._raisedHand;
+ }
+
+ set raisedHand(raisedHand)
+ {
+ raisedHand ?
+ this._raisedHandTimestamp = Date.now() :
+ this._raisedHandTimestamp = null;
+
+ this._raisedHand = raisedHand;
+ }
+
+ get raisedHandTimestamp()
+ {
+ return this._raisedHandTimestamp;
+ }
+
+ get transports()
+ {
+ return this._transports;
+ }
+
+ get producers()
+ {
+ return this._producers;
+ }
+
+ get consumers()
+ {
+ return this._consumers;
+ }
+
+ addRole(newRole)
+ {
+ if (
+ !this._roles.some((role) => role.id === newRole.id) &&
+ newRole.id !== userRoles.NORMAL.id // Can not add NORMAL
+ )
+ {
+ this._roles.push(newRole);
+
+ logger.info('addRole() | [newRole:"%s]"', newRole);
+
+ this.emit('gotRole', { newRole });
+ }
+ }
+
+ removeRole(oldRole)
+ {
+ if (
+ this._roles.some((role) => role.id === oldRole.id) &&
+ oldRole.id !== userRoles.NORMAL.id // Can not remove NORMAL
+ )
+ {
+ this._roles = this._roles.filter((role) => role.id !== oldRole.id);
+
+ logger.info('removeRole() | [oldRole:"%s]"', oldRole);
+
+ this.emit('lostRole', { oldRole });
+ }
+ }
+
+ hasRole(role)
+ {
+ return this._roles.some((myRole) => myRole.id === role.id);
+ }
+
+ addTransport(id, transport)
+ {
+ this.transports.set(id, transport);
+ }
+
+ getTransport(id)
+ {
+ return this.transports.get(id);
+ }
+
+ getConsumerTransport()
+ {
+ return Array.from(this.transports.values())
+ .find((t) => t.appData.consuming);
+ }
+
+ removeTransport(id)
+ {
+ this.transports.delete(id);
+ }
+
+ addProducer(id, producer)
+ {
+ this.producers.set(id, producer);
+ }
+
+ getProducer(id)
+ {
+ return this.producers.get(id);
+ }
+
+ removeProducer(id)
+ {
+ this.producers.delete(id);
+ }
+
+ addConsumer(id, consumer)
+ {
+ this.consumers.set(id, consumer);
+ }
+
+ getConsumer(id)
+ {
+ return this.consumers.get(id);
+ }
+
+ removeConsumer(id)
+ {
+ this.consumers.delete(id);
+ }
+
+ get peerInfo()
+ {
+ const peerInfo =
+ {
+ id : this.id,
+ displayName : this.displayName,
+ picture : this.picture,
+ roles : this.roles.map((role) => role.id),
+ raisedHand : this.raisedHand,
+ raisedHandTimestamp : this.raisedHandTimestamp
+ };
+
+ return peerInfo;
+ }
+}
+
+module.exports = Peer;
diff --git a/meet/server/lib/Room.js b/meet/server/lib/Room.js
new file mode 100644
index 00000000..1836c5f3
--- /dev/null
+++ b/meet/server/lib/Room.js
@@ -0,0 +1,2113 @@
+const EventEmitter = require('events').EventEmitter;
+const AwaitQueue = require('awaitqueue');
+const axios = require('axios');
+const Logger = require('./Logger');
+const Lobby = require('./Lobby');
+const { SocketTimeoutError } = require('./errors');
+const { v4: uuidv4 } = require('uuid');
+const jwt = require('jsonwebtoken');
+const userRoles = require('../userRoles');
+
+const {
+ BYPASS_ROOM_LOCK,
+ BYPASS_LOBBY
+} = require('../access');
+
+const permissions = require('../permissions'), {
+ CHANGE_ROOM_LOCK,
+ PROMOTE_PEER,
+ MODIFY_ROLE,
+ SEND_CHAT,
+ MODERATE_CHAT,
+ SHARE_AUDIO,
+ SHARE_VIDEO,
+ SHARE_SCREEN,
+ EXTRA_VIDEO,
+ SHARE_FILE,
+ MODERATE_FILES,
+ MODERATE_ROOM
+} = permissions;
+
+const config = require('../config/config');
+
+const logger = new Logger('Room');
+
+// In case they are not configured properly
+const roomAccess =
+{
+ [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ],
+ [BYPASS_LOBBY] : [ userRoles.NORMAL ],
+ ...config.accessFromRoles
+};
+
+const roomPermissions =
+{
+ [CHANGE_ROOM_LOCK] : [ userRoles.NORMAL ],
+ [PROMOTE_PEER] : [ userRoles.NORMAL ],
+ [MODIFY_ROLE] : [ userRoles.MODERATOR ],
+ [SEND_CHAT] : [ userRoles.NORMAL ],
+ [MODERATE_CHAT] : [ userRoles.MODERATOR ],
+ [SHARE_AUDIO] : [ userRoles.NORMAL ],
+ [SHARE_VIDEO] : [ userRoles.NORMAL ],
+ [SHARE_SCREEN] : [ userRoles.NORMAL ],
+ [EXTRA_VIDEO] : [ userRoles.NORMAL ],
+ [SHARE_FILE] : [ userRoles.NORMAL ],
+ [MODERATE_FILES] : [ userRoles.MODERATOR ],
+ [MODERATE_ROOM] : [ userRoles.MODERATOR ],
+ ...config.permissionsFromRoles
+};
+
+const roomAllowWhenRoleMissing = config.allowWhenRoleMissing || [];
+
+const ROUTER_SCALE_SIZE = config.routerScaleSize || 40;
+
+class Room extends EventEmitter
+{
+
+ static getLeastLoadedRouter(mediasoupWorkers, peers, mediasoupRouters)
+ {
+
+ const routerLoads = new Map();
+
+ const workerLoads = new Map();
+
+ const pipedRoutersIds = new Set();
+
+ for (const peer of peers.values())
+ {
+ const routerId = peer.routerId;
+
+ if (routerId)
+ {
+ if (mediasoupRouters.has(routerId))
+ {
+ pipedRoutersIds.add(routerId);
+ }
+
+ if (routerLoads.has(routerId))
+ {
+ routerLoads.set(routerId, routerLoads.get(routerId) + 1);
+ }
+ else
+ {
+ routerLoads.set(routerId, 1);
+ }
+ }
+ }
+
+ for (const worker of mediasoupWorkers)
+ {
+ for (const router of worker._routers)
+ {
+ const routerId = router._internal.routerId;
+
+ if (workerLoads.has(worker._pid))
+ {
+ workerLoads.set(worker._pid, workerLoads.get(worker._pid) +
+ (routerLoads.has(routerId)?routerLoads.get(routerId):0));
+ }
+ else
+ {
+ workerLoads.set(worker._pid,
+ (routerLoads.has(routerId)?routerLoads.get(routerId):0));
+ }
+ }
+ }
+
+ const sortedWorkerLoads = new Map([ ...workerLoads.entries() ].sort(
+ (a, b) => a[1] - b[1]));
+
+ // we don't care about if router is piped, just choose the least loaded worker
+ if (pipedRoutersIds.size === 0 ||
+ pipedRoutersIds.size === mediasoupRouters.size)
+ {
+ const workerId = sortedWorkerLoads.keys().next().value;
+
+ for (const worker of mediasoupWorkers)
+ {
+ if (worker._pid === workerId)
+ {
+ for (const router of worker._routers)
+ {
+ const routerId = router._internal.routerId;
+
+ if (mediasoupRouters.has(routerId))
+ {
+ return routerId;
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ // find if there is a piped router that is on a worker that is below limit
+ for (const [ workerId, workerLoad ] of sortedWorkerLoads.entries())
+ {
+ for (const worker of mediasoupWorkers)
+ {
+ if (worker._pid === workerId)
+ {
+ for (const router of worker._routers)
+ {
+ const routerId = router._internal.routerId;
+
+ // on purpose we check if the worker load is below the limit,
+ // as in reality the worker load is imortant,
+ // not the router load
+ if (mediasoupRouters.has(routerId) &&
+ pipedRoutersIds.has(routerId) &&
+ workerLoad < ROUTER_SCALE_SIZE)
+ {
+ return routerId;
+ }
+ }
+ }
+ }
+ }
+
+ // no piped router found, we need to return router from least loaded worker
+ const workerId = sortedWorkerLoads.keys().next().value;
+
+ for (const worker of mediasoupWorkers)
+ {
+ if (worker._pid === workerId)
+ {
+ for (const router of worker._routers)
+ {
+ const routerId = router._internal.routerId;
+
+ if (mediasoupRouters.has(routerId))
+ {
+ return routerId;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Factory function that creates and returns Room instance.
+ *
+ * @async
+ *
+ * @param {mediasoup.Worker} mediasoupWorkers - The mediasoup Worker in which a new
+ * mediasoup Router must be created.
+ * @param {String} roomId - Id of the Room instance.
+ */
+ static async create({ mediasoupWorkers, roomId, peers })
+ {
+ logger.info('create() [roomId:"%s"]', roomId);
+
+ // Router media codecs.
+ const mediaCodecs = config.mediasoup.router.mediaCodecs;
+
+ const mediasoupRouters = new Map();
+
+ for (const worker of mediasoupWorkers)
+ {
+ const router = await worker.createRouter({ mediaCodecs });
+
+ mediasoupRouters.set(router.id, router);
+ }
+
+ const firstRouter = mediasoupRouters.get(Room.getLeastLoadedRouter(
+ mediasoupWorkers, peers, mediasoupRouters));
+
+ // Create a mediasoup AudioLevelObserver on first router
+ const audioLevelObserver = await firstRouter.createAudioLevelObserver(
+ {
+ maxEntries : 1,
+ threshold : -80,
+ interval : 800
+ });
+
+ return new Room({
+ roomId,
+ mediasoupRouters,
+ audioLevelObserver,
+ mediasoupWorkers,
+ peers
+ });
+ }
+
+ constructor({
+ roomId,
+ mediasoupRouters,
+ audioLevelObserver,
+ mediasoupWorkers,
+ peers
+ })
+ {
+ logger.info('constructor() [roomId:"%s"]', roomId);
+
+ super();
+ this.setMaxListeners(Infinity);
+
+ this._uuid = uuidv4();
+
+ this._mediasoupWorkers = mediasoupWorkers;
+
+ this._allPeers = peers;
+
+ // Room ID.
+ this._roomId = roomId;
+
+ // Closed flag.
+ this._closed = false;
+
+ // Joining queue
+ this._queue = new AwaitQueue();
+
+ // Locked flag.
+ this._locked = false;
+
+ // if true: accessCode is a possibility to open the room
+ this._joinByAccesCode = true;
+
+ // access code to the room,
+ // applicable if ( _locked == true and _joinByAccessCode == true )
+ this._accessCode = '';
+
+ this._lobby = new Lobby();
+
+ this._chatHistory = [];
+
+ this._fileHistory = [];
+
+ this._lastN = [];
+
+ this._peers = {};
+
+ this._selfDestructTimeout = null;
+
+ // Array of mediasoup Router instances.
+ this._mediasoupRouters = mediasoupRouters;
+
+ // mediasoup AudioLevelObserver.
+ this._audioLevelObserver = audioLevelObserver;
+
+ // Current active speaker.
+ this._currentActiveSpeaker = null;
+
+ this._handleLobby();
+ this._handleAudioLevelObserver();
+ }
+
+ isLocked()
+ {
+ return this._locked;
+ }
+
+ close()
+ {
+ logger.debug('close()');
+
+ this._closed = true;
+
+ this._queue.close();
+
+ this._queue = null;
+
+ if (this._selfDestructTimeout)
+ clearTimeout(this._selfDestructTimeout);
+
+ this._selfDestructTimeout = null;
+
+ this._chatHistory = null;
+
+ this._fileHistory = null;
+
+ this._lobby.close();
+
+ this._lobby = null;
+
+ // Close the peers.
+ for (const peer in this._peers)
+ {
+ if (!this._peers[peer].closed)
+ this._peers[peer].close();
+ }
+
+ this._peers = null;
+
+ // Close the mediasoup Routers.
+ for (const router of this._mediasoupRouters.values())
+ {
+ router.close();
+ }
+
+ this._allPeers = null;
+
+ this._mediasoupWorkers = null;
+
+ this._mediasoupRouters.clear();
+
+ this._audioLevelObserver = null;
+
+ // Emit 'close' event.
+ this.emit('close');
+ }
+
+ verifyPeer({ id, token })
+ {
+ try
+ {
+ const decoded = jwt.verify(token, this._uuid);
+
+ logger.info('verifyPeer() [decoded:"%o"]', decoded);
+
+ return decoded.id === id;
+ }
+ catch (err)
+ {
+ logger.warn('verifyPeer() | invalid token');
+ }
+
+ return false;
+ }
+
+ handlePeer({ peer, returning })
+ {
+ logger.info('handlePeer() [peer:"%s", roles:"%s", returning:"%s"]', peer.id, peer.roles, returning);
+
+ // Should not happen
+ if (this._peers[peer.id])
+ {
+ logger.warn(
+ 'handleConnection() | there is already a peer with same peerId [peer:"%s"]',
+ peer.id);
+ }
+
+ // Returning user
+ if (returning)
+ this._peerJoining(peer, true);
+ // Has a role that is allowed to bypass room lock
+ else if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
+ this._peerJoining(peer);
+ else if (
+ 'maxUsersPerRoom' in config &&
+ (
+ Object.keys(this._peers).length +
+ this._lobby.peerList().length
+ ) >= config.maxUsersPerRoom)
+ {
+ this._handleOverRoomLimit(peer);
+ }
+ else if (this._locked)
+ this._parkPeer(peer);
+ else
+ {
+ // Has a role that is allowed to bypass lobby
+ this._hasAccess(peer, BYPASS_LOBBY) ?
+ this._peerJoining(peer) :
+ this._handleGuest(peer);
+ }
+ }
+
+ _handleOverRoomLimit(peer)
+ {
+ this._notification(peer.socket, 'overRoomLimit');
+ }
+
+ _handleGuest(peer)
+ {
+ if (config.activateOnHostJoin && !this.checkEmpty())
+ this._peerJoining(peer);
+ else
+ {
+ this._parkPeer(peer);
+ this._notification(peer.socket, 'signInRequired');
+ }
+ }
+
+ _handleLobby()
+ {
+ this._lobby.on('promotePeer', (promotedPeer) =>
+ {
+ logger.info('promotePeer() [promotedPeer:"%s"]', promotedPeer.id);
+
+ const { id } = promotedPeer;
+
+ this._peerJoining(promotedPeer);
+
+ for (const peer of this._getAllowedPeers(PROMOTE_PEER))
+ {
+ this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id });
+ }
+ });
+
+ this._lobby.on('peerRolesChanged', (peer) =>
+ {
+ // Has a role that is allowed to bypass room lock
+ if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
+ {
+ this._lobby.promotePeer(peer.id);
+
+ return;
+ }
+
+ if ( // Has a role that is allowed to bypass lobby
+ !this._locked &&
+ this._hasAccess(peer, BYPASS_LOBBY)
+ )
+ {
+ this._lobby.promotePeer(peer.id);
+
+ return;
+ }
+ });
+
+ this._lobby.on('changeDisplayName', (changedPeer) =>
+ {
+ const { id, displayName } = changedPeer;
+
+ for (const peer of this._getAllowedPeers(PROMOTE_PEER))
+ {
+ this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName });
+ }
+ });
+
+ this._lobby.on('changePicture', (changedPeer) =>
+ {
+ const { id, picture } = changedPeer;
+
+ for (const peer of this._getAllowedPeers(PROMOTE_PEER))
+ {
+ this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture });
+ }
+ });
+
+ this._lobby.on('peerClosed', (closedPeer) =>
+ {
+ logger.info('peerClosed() [closedPeer:"%s"]', closedPeer.id);
+
+ const { id } = closedPeer;
+
+ for (const peer of this._getAllowedPeers(PROMOTE_PEER))
+ {
+ this._notification(peer.socket, 'lobby:peerClosed', { peerId: id });
+ }
+ });
+
+ // If nobody left in lobby we should check if room is empty too and initiating
+ // rooms selfdestruction sequence
+ this._lobby.on('lobbyEmpty', () =>
+ {
+ if (this.checkEmpty())
+ {
+ this.selfDestructCountdown();
+ }
+ });
+ }
+
+ _handleAudioLevelObserver()
+ {
+ // Set audioLevelObserver events.
+ this._audioLevelObserver.on('volumes', (volumes) =>
+ {
+ const { producer, volume } = volumes[0];
+
+ // Notify all Peers.
+ for (const peer of this.getJoinedPeers())
+ {
+ this._notification(
+ peer.socket,
+ 'activeSpeaker',
+ {
+ peerId : producer.appData.peerId,
+ volume : volume
+ });
+ }
+ });
+
+ this._audioLevelObserver.on('silence', () =>
+ {
+ // Notify all Peers.
+ for (const peer of this.getJoinedPeers())
+ {
+ this._notification(
+ peer.socket,
+ 'activeSpeaker',
+ { peerId: null }
+ );
+ }
+ });
+ }
+
+ logStatus()
+ {
+ logger.info(
+ 'logStatus() [room id:"%s", peers:"%s"]',
+ this._roomId,
+ Object.keys(this._peers).length
+ );
+ }
+
+ dump()
+ {
+ return {
+ roomId : this._roomId,
+ peers : Object.keys(this._peers).length
+ };
+ }
+
+ get id()
+ {
+ return this._roomId;
+ }
+
+ selfDestructCountdown()
+ {
+ logger.debug('selfDestructCountdown() started');
+
+ if (this._selfDestructTimeout)
+ clearTimeout(this._selfDestructTimeout);
+
+ this._selfDestructTimeout = setTimeout(() =>
+ {
+ if (this._closed)
+ return;
+
+ if (this.checkEmpty() && this._lobby.checkEmpty())
+ {
+ logger.info(
+ 'Room deserted for some time, closing the room [roomId:"%s"]',
+ this._roomId);
+ this.close();
+ }
+ else
+ logger.debug('selfDestructCountdown() aborted; room is not empty!');
+ }, 10000);
+ }
+
+ checkEmpty()
+ {
+ return Object.keys(this._peers).length === 0;
+ }
+
+ _parkPeer(parkPeer)
+ {
+ this._lobby.parkPeer(parkPeer);
+
+ for (const peer of this._getAllowedPeers(PROMOTE_PEER))
+ {
+ this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id });
+ }
+ }
+
+ _peerJoining(peer, returning = false)
+ {
+ this._queue.push(async () =>
+ {
+ peer.socket.join(this._roomId);
+
+ // If we don't have this peer, add to end
+ !this._lastN.includes(peer.id) && this._lastN.push(peer.id);
+
+ this._peers[peer.id] = peer;
+
+ // Assign routerId
+ peer.routerId = await this._getRouterId();
+
+ this._handlePeer(peer);
+
+ if (returning)
+ {
+ this._notification(peer.socket, 'roomBack');
+ }
+ else
+ {
+ const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true });
+
+ peer.socket.handshake.session.token = token;
+
+ peer.socket.handshake.session.save();
+
+ let turnServers;
+
+ if ('turnAPIURI' in config)
+ {
+ try
+ {
+ const { data } = await axios.get(
+ config.turnAPIURI,
+ {
+ timeout : config.turnAPITimeout || 2000,
+ params : {
+ ...config.turnAPIparams,
+ 'api_key' : config.turnAPIKey,
+ 'ip' : peer.socket.request.connection.remoteAddress
+ }
+ });
+
+ turnServers = [ {
+ urls : data.uris,
+ username : data.username,
+ credential : data.password
+ } ];
+ }
+ catch (error)
+ {
+ if ('backupTurnServers' in config)
+ turnServers = config.backupTurnServers;
+
+ logger.error('_peerJoining() | error on REST turn [error:"%o"]', error);
+ }
+ }
+ else if ('backupTurnServers' in config)
+ {
+ turnServers = config.backupTurnServers;
+ }
+
+ this._notification(peer.socket, 'roomReady', { turnServers });
+
+ if (config.activateOnHostJoin && this._lobby.peerList().length > 0 &&
+ !this._locked && peer.roles.some((role) =>
+ config.permissionsFromRoles.PROMOTE_PEER.includes(role)))
+ {
+ this._lobby.promoteAllPeers();
+ }
+ }
+ })
+ .catch((error) =>
+ {
+ logger.error('_peerJoining() [error:"%o"]', error);
+ });
+ }
+
+ _handlePeer(peer)
+ {
+ logger.debug('_handlePeer() [peer:"%s"]', peer.id);
+
+ peer.on('close', () =>
+ {
+ this._handlePeerClose(peer);
+ });
+
+ peer.on('displayNameChanged', ({ oldDisplayName }) =>
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ return;
+
+ // Spread to others
+ this._notification(peer.socket, 'changeDisplayName', {
+ peerId : peer.id,
+ displayName : peer.displayName,
+ oldDisplayName : oldDisplayName
+ }, true);
+ });
+
+ peer.on('pictureChanged', () =>
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ return;
+
+ // Spread to others
+ this._notification(peer.socket, 'changePicture', {
+ peerId : peer.id,
+ picture : peer.picture
+ }, true);
+ });
+
+ peer.on('gotRole', ({ newRole }) =>
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ return;
+
+ // Spread to others
+ this._notification(peer.socket, 'gotRole', {
+ peerId : peer.id,
+ roleId : newRole.id
+ }, true, true);
+
+ // Got permission to promote peers, notify peer of
+ // peers in lobby
+ if (roomPermissions.PROMOTE_PEER.some((role) => role.id === newRole.id))
+ {
+ const lobbyPeers = this._lobby.peerList();
+
+ lobbyPeers.length > 0 && this._notification(peer.socket, 'parkedPeers', {
+ lobbyPeers
+ });
+ }
+ });
+
+ peer.on('lostRole', ({ oldRole }) =>
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ return;
+
+ // Spread to others
+ this._notification(peer.socket, 'lostRole', {
+ peerId : peer.id,
+ roleId : oldRole.id
+ }, true, true);
+ });
+
+ peer.socket.on('request', (request, cb) =>
+ {
+ logger.debug(
+ 'Peer "request" event [method:"%s", peerId:"%s"]',
+ request.method, peer.id);
+
+ this._handleSocketRequest(peer, request, cb)
+ .catch((error) =>
+ {
+ logger.error('"request" failed [error:"%o"]', error);
+
+ cb(error);
+ });
+ });
+
+ // Peer left before we were done joining
+ if (peer.closed)
+ this._handlePeerClose(peer);
+ }
+
+ _handlePeerClose(peer)
+ {
+ logger.debug('_handlePeerClose() [peer:"%s"]', peer.id);
+
+ if (this._closed)
+ return;
+
+ // If the Peer was joined, notify all Peers.
+ if (peer.joined)
+ this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true);
+
+ // Remove from lastN
+ this._lastN = this._lastN.filter((id) => id !== peer.id);
+
+ // Need this to know if this peer was the last with PROMOTE_PEER
+ const hasPromotePeer = peer.roles.some((role) =>
+ roomPermissions[PROMOTE_PEER].some((roomRole) => role.id === roomRole.id)
+ );
+
+ delete this._peers[peer.id];
+
+ // No peers left with PROMOTE_PEER, might need to give
+ // lobbyPeers to peers that are left.
+ if (
+ hasPromotePeer &&
+ !this._lobby.checkEmpty() &&
+ roomAllowWhenRoleMissing.includes(PROMOTE_PEER) &&
+ this._getPeersWithPermission(PROMOTE_PEER).length === 0
+ )
+ {
+ const lobbyPeers = this._lobby.peerList();
+
+ for (const allowedPeer of this._getAllowedPeers(PROMOTE_PEER))
+ {
+ this._notification(allowedPeer.socket, 'parkedPeers', { lobbyPeers });
+ }
+ }
+
+ // If this is the last Peer in the room and
+ // lobby is empty, close the room after a while.
+ if (this.checkEmpty() && this._lobby.checkEmpty())
+ this.selfDestructCountdown();
+ }
+
+ async _handleSocketRequest(peer, request, cb)
+ {
+ const router =
+ this._mediasoupRouters.get(peer.routerId);
+
+ switch (request.method)
+ {
+ case 'getRouterRtpCapabilities':
+ {
+ cb(null, router.rtpCapabilities);
+
+ break;
+ }
+
+ case 'join':
+ {
+ // Ensure the Peer is not already joined.
+ if (peer.joined)
+ throw new Error('Peer already joined');
+
+ const {
+ displayName,
+ picture,
+ rtpCapabilities
+ } = request.data;
+
+ // Store client data into the Peer data object.
+ peer.displayName = displayName;
+ peer.picture = picture;
+ peer.rtpCapabilities = rtpCapabilities;
+
+ // Tell the new Peer about already joined Peers.
+ // And also create Consumers for existing Producers.
+
+ const joinedPeers = this.getJoinedPeers(peer);
+
+ const peerInfos = joinedPeers
+ .map((joinedPeer) => (joinedPeer.peerInfo));
+
+ let lobbyPeers = [];
+
+ // Allowed to promote peers, notify about lobbypeers
+ if (this._hasPermission(peer, PROMOTE_PEER))
+ lobbyPeers = this._lobby.peerList();
+
+ cb(null, {
+ roles : peer.roles.map((role) => role.id),
+ peers : peerInfos,
+ tracker : config.fileTracker,
+ authenticated : peer.authenticated,
+ roomPermissions : roomPermissions,
+ userRoles : userRoles,
+ allowWhenRoleMissing : roomAllowWhenRoleMissing,
+ chatHistory : this._chatHistory,
+ fileHistory : this._fileHistory,
+ lastNHistory : this._lastN,
+ locked : this._locked,
+ lobbyPeers : lobbyPeers,
+ accessCode : this._accessCode
+ });
+
+ // Mark the new Peer as joined.
+ peer.joined = true;
+
+ for (const joinedPeer of joinedPeers)
+ {
+ // Create Consumers for existing Producers.
+ for (const producer of joinedPeer.producers.values())
+ {
+ this._createConsumer(
+ {
+ consumerPeer : peer,
+ producerPeer : joinedPeer,
+ producer
+ });
+ }
+ }
+
+ // Notify the new Peer to all other Peers.
+ for (const otherPeer of this.getJoinedPeers(peer))
+ {
+ this._notification(
+ otherPeer.socket,
+ 'newPeer',
+ peer.peerInfo
+ );
+ }
+
+ logger.debug(
+ 'peer joined [peer: "%s", displayName: "%s", picture: "%s"]',
+ peer.id, displayName, picture);
+
+ break;
+ }
+
+ case 'createWebRtcTransport':
+ {
+ // NOTE: Don't require that the Peer is joined here, so the client can
+ // initiate mediasoup Transports and be ready when he later joins.
+
+ const { forceTcp, producing, consuming } = request.data;
+
+ const webRtcTransportOptions =
+ {
+ ...config.mediasoup.webRtcTransport,
+ appData : { producing, consuming }
+ };
+
+ webRtcTransportOptions.enableTcp = true;
+
+ if (forceTcp)
+ webRtcTransportOptions.enableUdp = false;
+ else
+ {
+ webRtcTransportOptions.enableUdp = true;
+ webRtcTransportOptions.preferUdp = true;
+ }
+
+ const transport = await router.createWebRtcTransport(
+ webRtcTransportOptions
+ );
+
+ transport.on('dtlsstatechange', (dtlsState) =>
+ {
+ if (dtlsState === 'failed' || dtlsState === 'closed')
+ logger.warn('WebRtcTransport "dtlsstatechange" event [dtlsState:%s]', dtlsState);
+ });
+
+ // Store the WebRtcTransport into the Peer data Object.
+ peer.addTransport(transport.id, transport);
+
+ cb(
+ null,
+ {
+ id : transport.id,
+ iceParameters : transport.iceParameters,
+ iceCandidates : transport.iceCandidates,
+ dtlsParameters : transport.dtlsParameters
+ });
+
+ const { maxIncomingBitrate } = config.mediasoup.webRtcTransport;
+
+ // If set, apply max incoming bitrate limit.
+ if (maxIncomingBitrate)
+ {
+ try { await transport.setMaxIncomingBitrate(maxIncomingBitrate); }
+ catch (error) {}
+ }
+
+ break;
+ }
+
+ case 'connectWebRtcTransport':
+ {
+ const { transportId, dtlsParameters } = request.data;
+ const transport = peer.getTransport(transportId);
+
+ if (!transport)
+ throw new Error(`transport with id "${transportId}" not found`);
+
+ await transport.connect({ dtlsParameters });
+
+ cb();
+
+ break;
+ }
+
+ case 'restartIce':
+ {
+ const { transportId } = request.data;
+ const transport = peer.getTransport(transportId);
+
+ if (!transport)
+ throw new Error(`transport with id "${transportId}" not found`);
+
+ const iceParameters = await transport.restartIce();
+
+ cb(null, iceParameters);
+
+ break;
+ }
+
+ case 'produce':
+ {
+ let { appData } = request.data;
+
+ if (
+ !appData.source ||
+ ![ 'mic', 'webcam', 'screen', 'extravideo' ]
+ .includes(appData.source)
+ )
+ throw new Error('invalid producer source');
+
+ if (
+ appData.source === 'mic' &&
+ !this._hasPermission(peer, SHARE_AUDIO)
+ )
+ throw new Error('peer not authorized');
+
+ if (
+ appData.source === 'webcam' &&
+ !this._hasPermission(peer, SHARE_VIDEO)
+ )
+ throw new Error('peer not authorized');
+
+ if (
+ appData.source === 'screen' &&
+ !this._hasPermission(peer, SHARE_SCREEN)
+ )
+ throw new Error('peer not authorized');
+
+ if (
+ appData.source === 'extravideo' &&
+ !this._hasPermission(peer, EXTRA_VIDEO)
+ )
+ throw new Error('peer not authorized');
+
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { transportId, kind, rtpParameters } = request.data;
+ const transport = peer.getTransport(transportId);
+
+ if (!transport)
+ throw new Error(`transport with id "${transportId}" not found`);
+
+ // Add peerId into appData to later get the associated Peer during
+ // the 'loudest' event of the audioLevelObserver.
+ appData = { ...appData, peerId: peer.id };
+
+ const producer =
+ await transport.produce({ kind, rtpParameters, appData });
+
+ const pipeRouters = this._getRoutersToPipeTo(peer.routerId);
+
+ for (const [ routerId, destinationRouter ] of this._mediasoupRouters)
+ {
+ if (pipeRouters.includes(routerId))
+ {
+ await router.pipeToRouter({
+ producerId : producer.id,
+ router : destinationRouter
+ });
+ }
+ }
+
+ // Store the Producer into the Peer data Object.
+ peer.addProducer(producer.id, producer);
+
+ // Set Producer events.
+ producer.on('score', (score) =>
+ {
+ this._notification(peer.socket, 'producerScore', { producerId: producer.id, score });
+ });
+
+ producer.on('videoorientationchange', (videoOrientation) =>
+ {
+ logger.debug(
+ 'producer "videoorientationchange" event [producerId:"%s", videoOrientation:"%o"]',
+ producer.id, videoOrientation);
+ });
+
+ cb(null, { id: producer.id });
+
+ // Optimization: Create a server-side Consumer for each Peer.
+ for (const otherPeer of this.getJoinedPeers(peer))
+ {
+ this._createConsumer(
+ {
+ consumerPeer : otherPeer,
+ producerPeer : peer,
+ producer
+ });
+ }
+
+ // Add into the audioLevelObserver.
+ if (kind === 'audio')
+ {
+ this._audioLevelObserver.addProducer({ producerId: producer.id })
+ .catch(() => {});
+ }
+
+ break;
+ }
+
+ case 'closeProducer':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { producerId } = request.data;
+ const producer = peer.getProducer(producerId);
+
+ if (!producer)
+ throw new Error(`producer with id "${producerId}" not found`);
+
+ producer.close();
+
+ // Remove from its map.
+ peer.removeProducer(producer.id);
+
+ cb();
+
+ break;
+ }
+
+ case 'pauseProducer':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { producerId } = request.data;
+ const producer = peer.getProducer(producerId);
+
+ if (!producer)
+ throw new Error(`producer with id "${producerId}" not found`);
+
+ await producer.pause();
+
+ cb();
+
+ break;
+ }
+
+ case 'resumeProducer':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { producerId } = request.data;
+ const producer = peer.getProducer(producerId);
+
+ if (!producer)
+ throw new Error(`producer with id "${producerId}" not found`);
+
+ await producer.resume();
+
+ cb();
+
+ break;
+ }
+
+ case 'pauseConsumer':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { consumerId } = request.data;
+ const consumer = peer.getConsumer(consumerId);
+
+ if (!consumer)
+ throw new Error(`consumer with id "${consumerId}" not found`);
+
+ await consumer.pause();
+
+ cb();
+
+ break;
+ }
+
+ case 'resumeConsumer':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { consumerId } = request.data;
+ const consumer = peer.getConsumer(consumerId);
+
+ if (!consumer)
+ throw new Error(`consumer with id "${consumerId}" not found`);
+
+ await consumer.resume();
+
+ cb();
+
+ break;
+ }
+
+ case 'setConsumerPreferedLayers':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { consumerId, spatialLayer, temporalLayer } = request.data;
+ const consumer = peer.getConsumer(consumerId);
+
+ if (!consumer)
+ throw new Error(`consumer with id "${consumerId}" not found`);
+
+ await consumer.setPreferredLayers({ spatialLayer, temporalLayer });
+
+ cb();
+
+ break;
+ }
+
+ case 'setConsumerPriority':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { consumerId, priority } = request.data;
+ const consumer = peer.getConsumer(consumerId);
+
+ if (!consumer)
+ throw new Error(`consumer with id "${consumerId}" not found`);
+
+ await consumer.setPriority(priority);
+
+ cb();
+
+ break;
+ }
+
+ case 'requestConsumerKeyFrame':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { consumerId } = request.data;
+ const consumer = peer.getConsumer(consumerId);
+
+ if (!consumer)
+ throw new Error(`consumer with id "${consumerId}" not found`);
+
+ await consumer.requestKeyFrame();
+
+ cb();
+
+ break;
+ }
+
+ case 'getTransportStats':
+ {
+ const { transportId } = request.data;
+ const transport = peer.getTransport(transportId);
+
+ if (!transport)
+ throw new Error(`transport with id "${transportId}" not found`);
+
+ const stats = await transport.getStats();
+
+ cb(null, stats);
+
+ break;
+ }
+
+ case 'getProducerStats':
+ {
+ const { producerId } = request.data;
+ const producer = peer.getProducer(producerId);
+
+ if (!producer)
+ throw new Error(`producer with id "${producerId}" not found`);
+
+ const stats = await producer.getStats();
+
+ cb(null, stats);
+
+ break;
+ }
+
+ case 'getConsumerStats':
+ {
+ const { consumerId } = request.data;
+ const consumer = peer.getConsumer(consumerId);
+
+ if (!consumer)
+ throw new Error(`consumer with id "${consumerId}" not found`);
+
+ const stats = await consumer.getStats();
+
+ cb(null, stats);
+
+ break;
+ }
+
+ case 'changeDisplayName':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { displayName } = request.data;
+
+ peer.displayName = displayName;
+
+ // This will be spread through events from the peer object
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ /* case 'changePicture':
+ {
+ // Ensure the Peer is joined.
+ if (!peer.joined)
+ throw new Error('Peer not yet joined');
+
+ const { picture } = request.data;
+
+ peer.picture = picture;
+
+ // Spread to others
+ this._notification(peer.socket, 'changePicture', {
+ peerId : peer.id,
+ picture : picture
+ }, true);
+
+ // Return no error
+ cb();
+
+ break;
+ } */
+
+ case 'chatMessage':
+ {
+ if (!this._hasPermission(peer, SEND_CHAT))
+ throw new Error('peer not authorized');
+
+ const { chatMessage } = request.data;
+
+ this._chatHistory.push(chatMessage);
+
+ // Spread to others
+ this._notification(peer.socket, 'chatMessage', {
+ peerId : peer.id,
+ chatMessage : chatMessage
+ }, true);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'moderator:giveRole':
+ {
+ if (!this._hasPermission(peer, MODIFY_ROLE))
+ throw new Error('peer not authorized');
+
+ const { peerId, roleId } = request.data;
+
+ const userRole = Object.values(userRoles).find((role) => role.id === roleId);
+
+ if (!userRole || !userRole.promotable)
+ throw new Error('no such role');
+
+ if (!peer.roles.some((role) => role.level >= userRole.level))
+ throw new Error('peer not authorized for this level');
+
+ const giveRolePeer = this._peers[peerId];
+
+ if (!giveRolePeer)
+ throw new Error(`peer with id "${peerId}" not found`);
+
+ // This will propagate the event automatically
+ giveRolePeer.addRole(userRole);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'moderator:removeRole':
+ {
+ if (!this._hasPermission(peer, MODIFY_ROLE))
+ throw new Error('peer not authorized');
+
+ const { peerId, roleId } = request.data;
+
+ const userRole = Object.values(userRoles).find((role) => role.id === roleId);
+
+ if (!userRole || !userRole.promotable)
+ throw new Error('no such role');
+
+ if (!peer.roles.some((role) => role.level >= userRole.level))
+ throw new Error('peer not authorized for this level');
+
+ const removeRolePeer = this._peers[peerId];
+
+ if (!removeRolePeer)
+ throw new Error(`peer with id "${peerId}" not found`);
+
+ // This will propagate the event automatically
+ removeRolePeer.removeRole(userRole);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'moderator:clearChat':
+ {
+ if (!this._hasPermission(peer, MODERATE_CHAT))
+ throw new Error('peer not authorized');
+
+ this._chatHistory = [];
+
+ // Spread to others
+ this._notification(peer.socket, 'moderator:clearChat', null, true);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'lockRoom':
+ {
+ if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
+ throw new Error('peer not authorized');
+
+ this._locked = true;
+
+ // Spread to others
+ this._notification(peer.socket, 'lockRoom', {
+ peerId : peer.id
+ }, true);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'unlockRoom':
+ {
+ if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
+ throw new Error('peer not authorized');
+
+ this._locked = false;
+
+ // Spread to others
+ this._notification(peer.socket, 'unlockRoom', {
+ peerId : peer.id
+ }, true);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'setAccessCode':
+ {
+ const { accessCode } = request.data;
+
+ this._accessCode = accessCode;
+
+ // Spread to others
+ // if (request.public) {
+ this._notification(peer.socket, 'setAccessCode', {
+ peerId : peer.id,
+ accessCode : accessCode
+ }, true);
+ // }
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'setJoinByAccessCode':
+ {
+ const { joinByAccessCode } = request.data;
+
+ this._joinByAccessCode = joinByAccessCode;
+
+ // Spread to others
+ this._notification(peer.socket, 'setJoinByAccessCode', {
+ peerId : peer.id,
+ joinByAccessCode : joinByAccessCode
+ }, true);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'promotePeer':
+ {
+ if (!this._hasPermission(peer, PROMOTE_PEER))
+ throw new Error('peer not authorized');
+
+ const { peerId } = request.data;
+
+ this._lobby.promotePeer(peerId);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'promoteAllPeers':
+ {
+ if (!this._hasPermission(peer, PROMOTE_PEER))
+ throw new Error('peer not authorized');
+
+ this._lobby.promoteAllPeers();
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'sendFile':
+ {
+ if (!this._hasPermission(peer, SHARE_FILE))
+ throw new Error('peer not authorized');
+
+ const { magnetUri } = request.data;
+
+ this._fileHistory.push({ peerId: peer.id, magnetUri: magnetUri });
+
+ // Spread to others
+ this._notification(peer.socket, 'sendFile', {
+ peerId : peer.id,
+ magnetUri : magnetUri
+ }, true);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'moderator:clearFileSharing':
+ {
+ if (!this._hasPermission(peer, MODERATE_FILES))
+ throw new Error('peer not authorized');
+
+ this._fileHistory = [];
+
+ // Spread to others
+ this._notification(peer.socket, 'moderator:clearFileSharing', null, true);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'raisedHand':
+ {
+ const { raisedHand } = request.data;
+
+ peer.raisedHand = raisedHand;
+
+ // Spread to others
+ this._notification(peer.socket, 'raisedHand', {
+ peerId : peer.id,
+ raisedHand : raisedHand,
+ raisedHandTimestamp : peer.raisedHandTimestamp
+ }, true);
+
+ // Return no error
+ cb();
+
+ break;
+ }
+
+ case 'moderator:mute':
+ {
+ if (!this._hasPermission(peer, MODERATE_ROOM))
+ throw new Error('peer not authorized');
+
+ const { peerId } = request.data;
+
+ const mutePeer = this._peers[peerId];
+
+ if (!mutePeer)
+ throw new Error(`peer with id "${peerId}" not found`);
+
+ this._notification(mutePeer.socket, 'moderator:mute');
+
+ cb();
+
+ break;
+ }
+
+ case 'moderator:muteAll':
+ {
+ if (!this._hasPermission(peer, MODERATE_ROOM))
+ throw new Error('peer not authorized');
+
+ // Spread to others
+ this._notification(peer.socket, 'moderator:mute', null, true);
+
+ cb();
+
+ break;
+ }
+
+ case 'moderator:stopVideo':
+ {
+ if (!this._hasPermission(peer, MODERATE_ROOM))
+ throw new Error('peer not authorized');
+
+ const { peerId } = request.data;
+
+ const stopVideoPeer = this._peers[peerId];
+
+ if (!stopVideoPeer)
+ throw new Error(`peer with id "${peerId}" not found`);
+
+ this._notification(stopVideoPeer.socket, 'moderator:stopVideo');
+
+ cb();
+
+ break;
+ }
+
+ case 'moderator:stopAllVideo':
+ {
+ if (!this._hasPermission(peer, MODERATE_ROOM))
+ throw new Error('peer not authorized');
+
+ // Spread to others
+ this._notification(peer.socket, 'moderator:stopVideo', null, true);
+
+ cb();
+
+ break;
+ }
+
+ case 'moderator:stopAllScreenSharing':
+ {
+ if (!this._hasPermission(peer, MODERATE_ROOM))
+ throw new Error('peer not authorized');
+
+ // Spread to others
+ this._notification(peer.socket, 'moderator:stopScreenSharing', null, true);
+
+ cb();
+
+ break;
+ }
+
+ case 'moderator:stopScreenSharing':
+ {
+ if (!this._hasPermission(peer, MODERATE_ROOM))
+ throw new Error('peer not authorized');
+
+ const { peerId } = request.data;
+
+ const stopVideoPeer = this._peers[peerId];
+
+ if (!stopVideoPeer)
+ throw new Error(`peer with id "${peerId}" not found`);
+
+ this._notification(stopVideoPeer.socket, 'moderator:stopScreenSharing');
+
+ cb();
+
+ break;
+ }
+
+ case 'moderator:closeMeeting':
+ {
+ if (!this._hasPermission(peer, MODERATE_ROOM))
+ throw new Error('peer not authorized');
+
+ this._notification(peer.socket, 'moderator:kick', null, true);
+
+ cb();
+
+ // Close the room
+ this.close();
+
+ break;
+ }
+
+ case 'moderator:kickPeer':
+ {
+ if (!this._hasPermission(peer, MODERATE_ROOM))
+ throw new Error('peer not authorized');
+
+ const { peerId } = request.data;
+
+ const kickPeer = this._peers[peerId];
+
+ if (!kickPeer)
+ throw new Error(`peer with id "${peerId}" not found`);
+
+ this._notification(kickPeer.socket, 'moderator:kick');
+
+ kickPeer.close();
+
+ cb();
+
+ break;
+ }
+
+ case 'moderator:lowerHand':
+ {
+ if (!this._hasPermission(peer, MODERATE_ROOM))
+ throw new Error('peer not authorized');
+
+ const { peerId } = request.data;
+
+ const lowerPeer = this._peers[peerId];
+
+ if (!lowerPeer)
+ throw new Error(`peer with id "${peerId}" not found`);
+
+ this._notification(lowerPeer.socket, 'moderator:lowerHand');
+
+ cb();
+
+ break;
+ }
+
+ default:
+ {
+ logger.error('unknown request.method "%s"', request.method);
+
+ cb(500, `unknown request.method "${request.method}"`);
+ }
+ }
+ }
+
+ /**
+ * Creates a mediasoup Consumer for the given mediasoup Producer.
+ *
+ * @async
+ */
+ async _createConsumer({ consumerPeer, producerPeer, producer })
+ {
+ logger.debug(
+ '_createConsumer() [consumerPeer:"%s", producerPeer:"%s", producer:"%s"]',
+ consumerPeer.id,
+ producerPeer.id,
+ producer.id
+ );
+
+ const router = this._mediasoupRouters.get(producerPeer.routerId);
+
+ // Optimization:
+ // - Create the server-side Consumer. If video, do it paused.
+ // - Tell its Peer about it and wait for its response.
+ // - Upon receipt of the response, resume the server-side Consumer.
+ // - If video, this will mean a single key frame requested by the
+ // server-side Consumer (when resuming it).
+
+ // NOTE: Don't create the Consumer if the remote Peer cannot consume it.
+ if (
+ !consumerPeer.rtpCapabilities ||
+ !router.canConsume(
+ {
+ producerId : producer.id,
+ rtpCapabilities : consumerPeer.rtpCapabilities
+ })
+ )
+ {
+ return;
+ }
+
+ // Must take the Transport the remote Peer is using for consuming.
+ const transport = consumerPeer.getConsumerTransport();
+
+ // This should not happen.
+ if (!transport)
+ {
+ logger.warn('_createConsumer() | Transport for consuming not found');
+
+ return;
+ }
+
+ // Create the Consumer in paused mode.
+ let consumer;
+
+ try
+ {
+ consumer = await transport.consume(
+ {
+ producerId : producer.id,
+ rtpCapabilities : consumerPeer.rtpCapabilities,
+ paused : producer.kind === 'video'
+ });
+
+ if (producer.kind === 'audio')
+ await consumer.setPriority(255);
+ }
+ catch (error)
+ {
+ logger.warn('_createConsumer() | [error:"%o"]', error);
+
+ return;
+ }
+
+ // Store the Consumer into the consumerPeer data Object.
+ consumerPeer.addConsumer(consumer.id, consumer);
+
+ // Set Consumer events.
+ consumer.on('transportclose', () =>
+ {
+ // Remove from its map.
+ consumerPeer.removeConsumer(consumer.id);
+ });
+
+ consumer.on('producerclose', () =>
+ {
+ // Remove from its map.
+ consumerPeer.removeConsumer(consumer.id);
+
+ this._notification(consumerPeer.socket, 'consumerClosed', { consumerId: consumer.id });
+ });
+
+ consumer.on('producerpause', () =>
+ {
+ this._notification(consumerPeer.socket, 'consumerPaused', { consumerId: consumer.id });
+ });
+
+ consumer.on('producerresume', () =>
+ {
+ this._notification(consumerPeer.socket, 'consumerResumed', { consumerId: consumer.id });
+ });
+
+ consumer.on('score', (score) =>
+ {
+ this._notification(consumerPeer.socket, 'consumerScore', { consumerId: consumer.id, score });
+ });
+
+ consumer.on('layerschange', (layers) =>
+ {
+ this._notification(
+ consumerPeer.socket,
+ 'consumerLayersChanged',
+ {
+ consumerId : consumer.id,
+ spatialLayer : layers ? layers.spatialLayer : null,
+ temporalLayer : layers ? layers.temporalLayer : null
+ }
+ );
+ });
+
+ // Send a request to the remote Peer with Consumer parameters.
+ try
+ {
+ await this._request(
+ consumerPeer.socket,
+ 'newConsumer',
+ {
+ peerId : producerPeer.id,
+ kind : consumer.kind,
+ producerId : producer.id,
+ id : consumer.id,
+ rtpParameters : consumer.rtpParameters,
+ type : consumer.type,
+ appData : producer.appData,
+ producerPaused : consumer.producerPaused
+ }
+ );
+
+ // Now that we got the positive response from the remote Peer and, if
+ // video, resume the Consumer to ask for an efficient key frame.
+ await consumer.resume();
+
+ this._notification(
+ consumerPeer.socket,
+ 'consumerScore',
+ {
+ consumerId : consumer.id,
+ score : consumer.score
+ }
+ );
+ }
+ catch (error)
+ {
+ logger.warn('_createConsumer() | [error:"%o"]', error);
+ }
+ }
+
+ _hasPermission(peer, permission)
+ {
+ const hasPermission = peer.roles.some((role) =>
+ roomPermissions[permission].some((roomRole) => role.id === roomRole.id)
+ );
+
+ if (hasPermission)
+ return true;
+
+ // Allow if config is set, and no one is present
+ if (
+ roomAllowWhenRoleMissing.includes(permission) &&
+ this._getPeersWithPermission(permission).length === 0
+ )
+ return true;
+
+ return false;
+ }
+
+ _hasAccess(peer, access)
+ {
+ return peer.roles.some((role) =>
+ roomAccess[access].some((roomRole) => role.id === roomRole.id)
+ );
+ }
+
+ /**
+ * Get the list of joined peers.
+ */
+ getJoinedPeers(excludePeer = undefined)
+ {
+ return Object.values(this._peers)
+ .filter((peer) => peer.joined && peer !== excludePeer);
+ }
+
+ _getAllowedPeers(permission = null, excludePeer = undefined, joined = true)
+ {
+ const peers = this._getPeersWithPermission(permission, excludePeer, joined);
+
+ if (peers.length > 0)
+ return peers;
+
+ // Allow if config is set, and no one is present
+ if (roomAllowWhenRoleMissing.includes(permission))
+ return Object.values(this._peers);
+
+ return peers;
+ }
+
+ _getPeersWithPermission(permission = null, excludePeer = undefined, joined = true)
+ {
+ return Object.values(this._peers)
+ .filter(
+ (peer) =>
+ peer.joined === joined &&
+ peer !== excludePeer &&
+ peer.roles.some(
+ (role) =>
+ roomPermissions[permission].some((roomRole) =>
+ role.id === roomRole.id)
+ )
+ );
+ }
+
+ _timeoutCallback(callback)
+ {
+ let called = false;
+
+ const interval = setTimeout(
+ () =>
+ {
+ if (called)
+ return;
+ called = true;
+ callback(new SocketTimeoutError('Request timed out'));
+ },
+ config.requestTimeout || 20000
+ );
+
+ return (...args) =>
+ {
+ if (called)
+ return;
+ called = true;
+ clearTimeout(interval);
+
+ callback(...args);
+ };
+ }
+
+ _sendRequest(socket, method, data = {})
+ {
+ return new Promise((resolve, reject) =>
+ {
+ socket.emit(
+ 'request',
+ { method, data },
+ this._timeoutCallback((err, response) =>
+ {
+ if (err)
+ {
+ reject(err);
+ }
+ else
+ {
+ resolve(response);
+ }
+ })
+ );
+ });
+ }
+
+ async _request(socket, method, data)
+ {
+ logger.debug('_request() [method:"%s", data:"%o"]', method, data);
+
+ const {
+ requestRetries = 3
+ } = config;
+
+ for (let tries = 0; tries < requestRetries; tries++)
+ {
+ try
+ {
+ return await this._sendRequest(socket, method, data);
+ }
+ catch (error)
+ {
+ if (
+ error instanceof SocketTimeoutError &&
+ tries < requestRetries
+ )
+ logger.warn('_request() | timeout, retrying [attempt:"%s"]', tries);
+ else
+ throw error;
+ }
+ }
+ }
+
+ _notification(socket, method, data = {}, broadcast = false, includeSender = false)
+ {
+ if (broadcast)
+ {
+ socket.broadcast.to(this._roomId).emit(
+ 'notification', { method, data }
+ );
+
+ if (includeSender)
+ socket.emit('notification', { method, data });
+ }
+ else
+ {
+ socket.emit('notification', { method, data });
+ }
+ }
+
+ async _pipeProducersToRouter(routerId)
+ {
+ const router = this._mediasoupRouters.get(routerId);
+
+ const peersToPipe =
+ Object.values(this._peers)
+ .filter((peer) => peer.routerId !== routerId && peer.routerId !== null);
+
+ for (const peer of peersToPipe)
+ {
+ const srcRouter = this._mediasoupRouters.get(peer.routerId);
+
+ for (const producerId of peer.producers.keys())
+ {
+ if (router._producers.has(producerId))
+ {
+ continue;
+ }
+
+ await srcRouter.pipeToRouter({
+ producerId : producerId,
+ router : router
+ });
+ }
+ }
+ }
+
+ async _getRouterId()
+ {
+ const routerId = Room.getLeastLoadedRouter(
+ this._mediasoupWorkers, this._allPeers, this._mediasoupRouters);
+
+ await this._pipeProducersToRouter(routerId);
+
+ return routerId;
+ }
+
+ // Returns an array of router ids we need to pipe to
+ _getRoutersToPipeTo(originRouterId)
+ {
+ return Object.values(this._peers)
+ .map((peer) => peer.routerId)
+ .filter((routerId, index, self) =>
+ routerId !== originRouterId && self.indexOf(routerId) === index
+ );
+ }
+
+}
+
+module.exports = Room;
diff --git a/meet/server/lib/errors.js b/meet/server/lib/errors.js
new file mode 100644
index 00000000..379838d7
--- /dev/null
+++ b/meet/server/lib/errors.js
@@ -0,0 +1,22 @@
+/**
+ * Error produced when a socket request has a timeout.
+ */
+class SocketTimeoutError extends Error
+{
+ constructor(message)
+ {
+ super(message);
+
+ this.name = 'SocketTimeoutError';
+
+ if (Error.hasOwnProperty('captureStackTrace')) // Just in V8.
+ Error.captureStackTrace(this, SocketTimeoutError);
+ else
+ this.stack = (new Error(message)).stack;
+ }
+}
+
+module.exports =
+{
+ SocketTimeoutError
+};
\ No newline at end of file
diff --git a/meet/server/lib/interactiveClient.js b/meet/server/lib/interactiveClient.js
new file mode 100644
index 00000000..dbeaaee1
--- /dev/null
+++ b/meet/server/lib/interactiveClient.js
@@ -0,0 +1,27 @@
+const net = require('net');
+const os = require('os');
+const path = require('path');
+
+const SOCKET_PATH_UNIX = '/tmp/edumeet-server.sock';
+const SOCKET_PATH_WIN = path.join('\\\\?\\pipe', process.cwd(), 'edumeet-server');
+const SOCKET_PATH = os.platform() === 'win32'? SOCKET_PATH_WIN : SOCKET_PATH_UNIX;
+
+module.exports = async function()
+{
+ const socket = net.connect(SOCKET_PATH);
+
+ process.stdin.pipe(socket);
+ socket.pipe(process.stdout);
+
+ socket.on('connect', () => process.stdin.setRawMode(true));
+
+ socket.on('close', () => process.exit(0));
+ socket.on('exit', () => socket.end());
+
+ if (process.argv && process.argv[2] === '--stats')
+ {
+ await socket.write('stats\n');
+
+ socket.end();
+ }
+};
diff --git a/meet/server/lib/interactiveServer.js b/meet/server/lib/interactiveServer.js
new file mode 100644
index 00000000..78eaf92c
--- /dev/null
+++ b/meet/server/lib/interactiveServer.js
@@ -0,0 +1,699 @@
+const os = require('os');
+const path = require('path');
+const repl = require('repl');
+const readline = require('readline');
+const net = require('net');
+const fs = require('fs');
+const mediasoup = require('mediasoup');
+const colors = require('colors/safe');
+const pidusage = require('pidusage');
+
+const SOCKET_PATH_UNIX = '/tmp/edumeet-server.sock';
+const SOCKET_PATH_WIN = path.join('\\\\?\\pipe', process.cwd(), 'edumeet-server');
+const SOCKET_PATH = os.platform() === 'win32' ? SOCKET_PATH_WIN : SOCKET_PATH_UNIX;
+
+// Maps to store all mediasoup objects.
+const workers = new Map();
+const routers = new Map();
+const transports = new Map();
+const producers = new Map();
+const consumers = new Map();
+const dataProducers = new Map();
+const dataConsumers = new Map();
+
+class Interactive
+{
+ constructor(socket)
+ {
+ this._socket = socket;
+
+ this._isTerminalOpen = false;
+ }
+
+ openCommandConsole()
+ {
+ const cmd = readline.createInterface(
+ {
+ input : this._socket,
+ output : this._socket,
+ terminal : true
+ });
+
+ cmd.on('close', () =>
+ {
+ if (this._isTerminalOpen)
+ return;
+
+ this.log('\nexiting...');
+
+ this._socket.end();
+ });
+
+ const readStdin = () =>
+ {
+ cmd.question('cmd> ', async (input) =>
+ {
+ const params = input.split(/[\s\t]+/);
+ const command = params.shift();
+
+ switch (command)
+ {
+ case '':
+ {
+ readStdin();
+ break;
+ }
+
+ case 'h':
+ case 'help':
+ {
+ this.log('');
+ this.log('available commands:');
+ this.log('- h, help : show this message');
+ this.log('- usage : show CPU and memory usage of the Node.js and mediasoup-worker processes');
+ this.log('- logLevel level : changes logLevel in all mediasoup Workers');
+ this.log('- logTags [tag] [tag] : changes logTags in all mediasoup Workers (values separated by space)');
+ this.log('- dumpRooms : dump all rooms');
+ this.log('- dumpPeers : dump all peers');
+ this.log('- dw, dumpWorkers : dump mediasoup Workers');
+ this.log('- dr, dumpRouter [id] : dump mediasoup Router with given id (or the latest created one)');
+ this.log('- dt, dumpTransport [id] : dump mediasoup Transport with given id (or the latest created one)');
+ this.log('- dp, dumpProducer [id] : dump mediasoup Producer with given id (or the latest created one)');
+ this.log('- dc, dumpConsumer [id] : dump mediasoup Consumer with given id (or the latest created one)');
+ this.log('- ddp, dumpDataProducer [id] : dump mediasoup DataProducer with given id (or the latest created one)');
+ this.log('- ddc, dumpDataConsumer [id] : dump mediasoup DataConsumer with given id (or the latest created one)');
+ this.log('- st, statsTransport [id] : get stats for mediasoup Transport with given id (or the latest created one)');
+ this.log('- sp, statsProducer [id] : get stats for mediasoup Producer with given id (or the latest created one)');
+ this.log('- sc, statsConsumer [id] : get stats for mediasoup Consumer with given id (or the latest created one)');
+ this.log('- sdp, statsDataProducer [id] : get stats for mediasoup DataProducer with given id (or the latest created one)');
+ this.log('- sdc, statsDataConsumer [id] : get stats for mediasoup DataConsumer with given id (or the latest created one)');
+ this.log('- t, terminal : open Node REPL Terminal');
+ this.log('');
+ readStdin();
+
+ break;
+ }
+
+ case 'u':
+ case 'usage':
+ {
+ let usage = await pidusage(process.pid);
+
+ this.log(`Node.js process [pid:${process.pid}]:\n${JSON.stringify(usage, null, ' ')}`);
+
+ for (const worker of workers.values())
+ {
+ usage = await pidusage(worker.pid);
+
+ this.log(`mediasoup-worker process [pid:${worker.pid}]:\n${JSON.stringify(usage, null, ' ')}`);
+ }
+
+ break;
+ }
+
+ case 'logLevel':
+ {
+ const level = params[0];
+ const promises = [];
+
+ for (const worker of workers.values())
+ {
+ promises.push(worker.updateSettings({ logLevel: level }));
+ }
+
+ try
+ {
+ await Promise.all(promises);
+
+ this.log('done');
+ }
+ catch (error)
+ {
+ this.error(String(error));
+ }
+
+ break;
+ }
+
+ case 'logTags':
+ {
+ const tags = params;
+ const promises = [];
+
+ for (const worker of workers.values())
+ {
+ promises.push(worker.updateSettings({ logTags: tags }));
+ }
+
+ try
+ {
+ await Promise.all(promises);
+
+ this.log('done');
+ }
+ catch (error)
+ {
+ this.error(String(error));
+ }
+
+ break;
+ }
+
+ case 'stats':
+ {
+ this.log(`rooms:${global.rooms.size}\npeers:${global.peers.size}`);
+
+ break;
+ }
+
+ case 'dumpRooms':
+ {
+ for (const room of global.rooms.values())
+ {
+ try
+ {
+ const dump = await room.dump();
+
+ this.log(`room.dump():\n${JSON.stringify(dump, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`room.dump() failed: ${error}`);
+ }
+ }
+
+ break;
+ }
+
+ case 'dumpPeers':
+ {
+ for (const peer of global.peers.values())
+ {
+ try
+ {
+ const dump = await peer.peerInfo;
+
+ this.log(`peer.peerInfo():\n${JSON.stringify(dump, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`peer.peerInfo() failed: ${error}`);
+ }
+ }
+
+ break;
+ }
+
+ case 'dw':
+ case 'dumpWorkers':
+ {
+ for (const worker of workers.values())
+ {
+ try
+ {
+ const dump = await worker.dump();
+
+ this.log(`worker.dump():\n${JSON.stringify(dump, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`worker.dump() failed: ${error}`);
+ }
+ }
+
+ break;
+ }
+
+ case 'dr':
+ case 'dumpRouter':
+ {
+ const id = params[0] || Array.from(routers.keys()).pop();
+ const router = routers.get(id);
+
+ if (!router)
+ {
+ this.error('Router not found');
+
+ break;
+ }
+
+ try
+ {
+ const dump = await router.dump();
+
+ this.log(`router.dump():\n${JSON.stringify(dump, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`router.dump() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'dt':
+ case 'dumpTransport':
+ {
+ const id = params[0] || Array.from(transports.keys()).pop();
+ const transport = transports.get(id);
+
+ if (!transport)
+ {
+ this.error('Transport not found');
+
+ break;
+ }
+
+ try
+ {
+ const dump = await transport.dump();
+
+ this.log(`transport.dump():\n${JSON.stringify(dump, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`transport.dump() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'dp':
+ case 'dumpProducer':
+ {
+ const id = params[0] || Array.from(producers.keys()).pop();
+ const producer = producers.get(id);
+
+ if (!producer)
+ {
+ this.error('Producer not found');
+
+ break;
+ }
+
+ try
+ {
+ const dump = await producer.dump();
+
+ this.log(`producer.dump():\n${JSON.stringify(dump, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`producer.dump() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'dc':
+ case 'dumpConsumer':
+ {
+ const id = params[0] || Array.from(consumers.keys()).pop();
+ const consumer = consumers.get(id);
+
+ if (!consumer)
+ {
+ this.error('Consumer not found');
+
+ break;
+ }
+
+ try
+ {
+ const dump = await consumer.dump();
+
+ this.log(`consumer.dump():\n${JSON.stringify(dump, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`consumer.dump() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'ddp':
+ case 'dumpDataProducer':
+ {
+ const id = params[0] || Array.from(dataProducers.keys()).pop();
+ const dataProducer = dataProducers.get(id);
+
+ if (!dataProducer)
+ {
+ this.error('DataProducer not found');
+
+ break;
+ }
+
+ try
+ {
+ const dump = await dataProducer.dump();
+
+ this.log(`dataProducer.dump():\n${JSON.stringify(dump, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`dataProducer.dump() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'ddc':
+ case 'dumpDataConsumer':
+ {
+ const id = params[0] || Array.from(dataConsumers.keys()).pop();
+ const dataConsumer = dataConsumers.get(id);
+
+ if (!dataConsumer)
+ {
+ this.error('DataConsumer not found');
+
+ break;
+ }
+
+ try
+ {
+ const dump = await dataConsumer.dump();
+
+ this.log(`dataConsumer.dump():\n${JSON.stringify(dump, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`dataConsumer.dump() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'st':
+ case 'statsTransport':
+ {
+ const id = params[0] || Array.from(transports.keys()).pop();
+ const transport = transports.get(id);
+
+ if (!transport)
+ {
+ this.error('Transport not found');
+
+ break;
+ }
+
+ try
+ {
+ const stats = await transport.getStats();
+
+ this.log(`transport.getStats():\n${JSON.stringify(stats, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`transport.getStats() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'sp':
+ case 'statsProducer':
+ {
+ const id = params[0] || Array.from(producers.keys()).pop();
+ const producer = producers.get(id);
+
+ if (!producer)
+ {
+ this.error('Producer not found');
+
+ break;
+ }
+
+ try
+ {
+ const stats = await producer.getStats();
+
+ this.log(`producer.getStats():\n${JSON.stringify(stats, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`producer.getStats() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'sc':
+ case 'statsConsumer':
+ {
+ const id = params[0] || Array.from(consumers.keys()).pop();
+ const consumer = consumers.get(id);
+
+ if (!consumer)
+ {
+ this.error('Consumer not found');
+
+ break;
+ }
+
+ try
+ {
+ const stats = await consumer.getStats();
+
+ this.log(`consumer.getStats():\n${JSON.stringify(stats, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`consumer.getStats() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'sdp':
+ case 'statsDataProducer':
+ {
+ const id = params[0] || Array.from(dataProducers.keys()).pop();
+ const dataProducer = dataProducers.get(id);
+
+ if (!dataProducer)
+ {
+ this.error('DataProducer not found');
+
+ break;
+ }
+
+ try
+ {
+ const stats = await dataProducer.getStats();
+
+ this.log(`dataProducer.getStats():\n${JSON.stringify(stats, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`dataProducer.getStats() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 'sdc':
+ case 'statsDataConsumer':
+ {
+ const id = params[0] || Array.from(dataConsumers.keys()).pop();
+ const dataConsumer = dataConsumers.get(id);
+
+ if (!dataConsumer)
+ {
+ this.error('DataConsumer not found');
+
+ break;
+ }
+
+ try
+ {
+ const stats = await dataConsumer.getStats();
+
+ this.log(`dataConsumer.getStats():\n${JSON.stringify(stats, null, ' ')}`);
+ }
+ catch (error)
+ {
+ this.error(`dataConsumer.getStats() failed: ${error}`);
+ }
+
+ break;
+ }
+
+ case 't':
+ case 'terminal':
+ {
+ this._isTerminalOpen = true;
+
+ cmd.close();
+ this.openTerminal();
+
+ return;
+ }
+
+ default:
+ {
+ this.error(`unknown command '${command}'`);
+ this.log('press \'h\' or \'help\' to get the list of available commands');
+ }
+ }
+
+ readStdin();
+ });
+ };
+
+ readStdin();
+ }
+
+ openTerminal()
+ {
+ this.log('\n[opening Node REPL Terminal...]');
+ this.log('here you have access to workers, routers, transports, producers, consumers, dataProducers and dataConsumers ES6 maps');
+
+ const terminal = repl.start(
+ {
+ input : this._socket,
+ output : this._socket,
+ terminal : true,
+ prompt : 'terminal> ',
+ useColors : true,
+ useGlobal : true,
+ ignoreUndefined : false
+ });
+
+ this._isTerminalOpen = true;
+
+ terminal.on('exit', () =>
+ {
+ this.log('\n[exiting Node REPL Terminal...]');
+
+ this._isTerminalOpen = false;
+
+ this.openCommandConsole();
+ });
+ }
+
+ log(msg)
+ {
+ try
+ {
+ this._socket.write(`${colors.green(msg)}\n`);
+ }
+ catch (error)
+ {}
+ }
+
+ error(msg)
+ {
+ try
+ {
+ this._socket.write(`${colors.red.bold('ERROR: ')}${colors.red(msg)}\n`);
+ }
+ catch (error)
+ {}
+ }
+}
+
+function runMediasoupObserver()
+{
+ mediasoup.observer.on('newworker', (worker) =>
+ {
+ // Store the latest worker in a global variable.
+ global.worker = worker;
+
+ workers.set(worker.pid, worker);
+ worker.observer.on('close', () => workers.delete(worker.pid));
+
+ worker.observer.on('newrouter', (router) =>
+ {
+ // Store the latest router in a global variable.
+ global.router = router;
+
+ routers.set(router.id, router);
+ router.observer.on('close', () => routers.delete(router.id));
+
+ router.observer.on('newtransport', (transport) =>
+ {
+ // Store the latest transport in a global variable.
+ global.transport = transport;
+
+ transports.set(transport.id, transport);
+ transport.observer.on('close', () => transports.delete(transport.id));
+
+ transport.observer.on('newproducer', (producer) =>
+ {
+ // Store the latest producer in a global variable.
+ global.producer = producer;
+
+ producers.set(producer.id, producer);
+ producer.observer.on('close', () => producers.delete(producer.id));
+ });
+
+ transport.observer.on('newconsumer', (consumer) =>
+ {
+ // Store the latest consumer in a global variable.
+ global.consumer = consumer;
+
+ consumers.set(consumer.id, consumer);
+ consumer.observer.on('close', () => consumers.delete(consumer.id));
+ });
+
+ transport.observer.on('newdataproducer', (dataProducer) =>
+ {
+ // Store the latest dataProducer in a global variable.
+ global.dataProducer = dataProducer;
+
+ dataProducers.set(dataProducer.id, dataProducer);
+ dataProducer.observer.on('close', () => dataProducers.delete(dataProducer.id));
+ });
+
+ transport.observer.on('newdataconsumer', (dataConsumer) =>
+ {
+ // Store the latest dataConsumer in a global variable.
+ global.dataConsumer = dataConsumer;
+
+ dataConsumers.set(dataConsumer.id, dataConsumer);
+ dataConsumer.observer.on('close', () => dataConsumers.delete(dataConsumer.id));
+ });
+ });
+ });
+ });
+}
+
+module.exports = async function(rooms, peers)
+{
+ try
+ {
+ // Run the mediasoup observer API.
+ runMediasoupObserver();
+
+ // Make maps global so they can be used during the REPL terminal.
+ global.rooms = rooms;
+ global.peers = peers;
+ global.workers = workers;
+ global.routers = routers;
+ global.transports = transports;
+ global.producers = producers;
+ global.consumers = consumers;
+ global.dataProducers = dataProducers;
+ global.dataConsumers = dataConsumers;
+
+ const server = net.createServer((socket) =>
+ {
+ const interactive = new Interactive(socket);
+
+ interactive.openCommandConsole();
+ });
+
+ await new Promise((resolve) =>
+ {
+ try { fs.unlinkSync(SOCKET_PATH); }
+ catch (error) {}
+
+ server.listen(SOCKET_PATH, resolve);
+ });
+ }
+ catch (error)
+ {}
+};
diff --git a/meet/server/lib/promExporter.js b/meet/server/lib/promExporter.js
new file mode 100644
index 00000000..82bb7a13
--- /dev/null
+++ b/meet/server/lib/promExporter.js
@@ -0,0 +1,286 @@
+const { Resolver } = require('dns').promises;
+const express = require('express');
+const mediasoup = require('mediasoup');
+const prom = require('prom-client');
+
+const Logger = require('./Logger');
+
+const logger = new Logger('prom');
+const resolver = new Resolver();
+const workers = new Map();
+
+const labelNames = [
+ 'pid', 'room_id', 'peer_id', 'display_name', 'user_agent', 'transport_id',
+ 'proto', 'local_addr', 'remote_addr', 'id', 'kind', 'codec', 'type'
+];
+
+const metadata = {
+ 'byteCount' : { metricType: prom.Counter, unit: 'bytes' },
+ 'score' : { metricType: prom.Gauge }
+};
+
+module.exports = async function(rooms, peers, config)
+{
+ const collect = async function(registry)
+ {
+ const newMetrics = function(subsystem)
+ {
+ const namespace = 'mediasoup';
+ const metrics = new Map();
+
+ for (const key in metadata)
+ {
+ if (Object.prototype.hasOwnProperty.call(metadata, key))
+ {
+ const value = metadata[key];
+ const name = key.split(/(?=[A-Z])/).join('_')
+ .toLowerCase();
+ const unit = value.unit;
+ const metricType = value.metricType;
+ let s = `${namespace}_${subsystem}_${name}`;
+
+ if (unit)
+ {
+ s += `_${unit}`;
+ }
+ const m = new metricType({
+ name : s, help : `${subsystem}.${key}`, labelNames : labelNames, registers : [ registry ] });
+
+ metrics.set(key, m);
+ }
+ }
+
+ return metrics;
+ };
+
+ const commonLabels = function(both, fn)
+ {
+ for (const roomId of rooms.keys())
+ {
+ for (const [ peerId, peer ] of peers)
+ {
+ if (fn(peer))
+ {
+ const displayName = peer._displayName;
+ const userAgent = peer._socket.client.request.headers['user-agent'];
+ const kind = both.kind;
+ const codec = both.rtpParameters.codecs[0].mimeType.split('/')[1];
+
+ return { roomId, peerId, displayName, userAgent, kind, codec };
+ }
+ }
+ }
+ throw new Error('cannot find common labels');
+ };
+
+ const addr = async function(ip, port)
+ {
+ if (config.deidentify)
+ {
+ const a = ip.split('.');
+
+ for (let i = 0; i < a.length - 2; i++)
+ {
+ a[i] = 'xx';
+ }
+
+ return `${a.join('.')}:${port}`;
+ }
+ else if (config.numeric)
+ {
+ return `${ip}:${port}`;
+ }
+ else
+ {
+ try
+ {
+ const a = await resolver.reverse(ip);
+
+ ip = a[0];
+ }
+ catch (err)
+ {
+ logger.error(`reverse DNS query failed: ${ip} ${err.code}`);
+ }
+
+ return `${ip}:${port}`;
+ }
+ };
+
+ const quiet = function(s)
+ {
+ return config.quiet ? '' : s;
+ };
+
+ const setValue = function(key, m, labels, v)
+ {
+ logger.debug(`setValue key=${key} v=${v}`);
+ switch (metadata[key].metricType)
+ {
+ case prom.Counter:
+ m.inc(labels, v);
+ break;
+ case prom.Gauge:
+ m.set(labels, v);
+ break;
+ default:
+ throw new Error(`unexpected metric: ${m}`);
+ }
+ };
+
+ logger.debug('collect');
+ const mRooms = new prom.Gauge({ name: 'edumeet_rooms', help: '#rooms', registers: [ registry ] });
+
+ mRooms.set(rooms.size);
+ const mPeers = new prom.Gauge({ name: 'edumeet_peers', help: '#peers', labelNames: [ 'room_id' ], registers: [ registry ] });
+
+ for (const [ roomId, room ] of rooms)
+ {
+ mPeers.labels(roomId).set(Object.keys(room._peers).length);
+ }
+
+ const mConsumer = newMetrics('consumer');
+ const mProducer = newMetrics('producer');
+
+ for (const [ pid, worker ] of workers)
+ {
+ logger.debug(`visiting worker ${pid}`);
+ for (const router of worker._routers)
+ {
+ logger.debug(`visiting router ${router.id}`);
+ for (const [ transportId, transport ] of router._transports)
+ {
+ logger.debug(`visiting transport ${transportId}`);
+ const transportJson = await transport.dump();
+
+ if (transportJson.iceState != 'completed')
+ {
+ logger.debug(`skipping transport ${transportId}}: ${transportJson.iceState}`);
+ continue;
+ }
+ const iceSelectedTuple = transportJson.iceSelectedTuple;
+ const proto = iceSelectedTuple.protocol;
+ const localAddr = await addr(iceSelectedTuple.localIp,
+ iceSelectedTuple.localPort);
+ const remoteAddr = await addr(iceSelectedTuple.remoteIp,
+ iceSelectedTuple.remotePort);
+
+ for (const [ producerId, producer ] of transport._producers)
+ {
+ logger.debug(`visiting producer ${producerId}`);
+ const { roomId, peerId, displayName, userAgent, kind, codec } =
+ commonLabels(producer, (peer) => peer._producers.has(producerId));
+ const a = await producer.getStats();
+
+ for (const x of a)
+ {
+ const type = x.type;
+ const labels = {
+ 'pid' : pid,
+ 'room_id' : roomId,
+ 'peer_id' : peerId,
+ 'display_name' : displayName,
+ 'user_agent' : userAgent,
+ 'transport_id' : quiet(transportId),
+ 'proto' : proto,
+ 'local_addr' : localAddr,
+ 'remote_addr' : remoteAddr,
+ 'id' : quiet(producerId),
+ 'kind' : kind,
+ 'codec' : codec,
+ 'type' : type
+ };
+
+ for (const [ key, m ] of mProducer)
+ {
+ setValue(key, m, labels, x[key]);
+ }
+ }
+ }
+ for (const [ consumerId, consumer ] of transport._consumers)
+ {
+ logger.debug(`visiting consumer ${consumerId}`);
+ const { roomId, peerId, displayName, userAgent, kind, codec } =
+ commonLabels(consumer, (peer) => peer._consumers.has(consumerId));
+ const a = await consumer.getStats();
+
+ for (const x of a)
+ {
+ if (x.type == 'inbound-rtp')
+ {
+ continue;
+ }
+ const type = x.type;
+ const labels =
+ {
+ 'pid' : pid,
+ 'room_id' : roomId,
+ 'peer_id' : peerId,
+ 'display_name' : displayName,
+ 'user_agent' : userAgent,
+ 'transport_id' : quiet(transportId),
+ 'proto' : proto,
+ 'local_addr' : localAddr,
+ 'remote_addr' : remoteAddr,
+ 'id' : quiet(consumerId),
+ 'kind' : kind,
+ 'codec' : codec,
+ 'type' : type
+ };
+
+ for (const [ key, m ] of mConsumer)
+ {
+ setValue(key, m, labels, x[key]);
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ try
+ {
+ logger.debug(`config.deidentify=${config.deidentify}`);
+ logger.debug(`config.listen=${config.listen}`);
+ logger.debug(`config.numeric=${config.numeric}`);
+ logger.debug(`config.port=${config.port}`);
+ logger.debug(`config.quiet=${config.quiet}`);
+
+ mediasoup.observer.on('newworker', (worker) =>
+ {
+ logger.debug(`observing newworker ${worker.pid} #${workers.size}`);
+ workers.set(worker.pid, worker);
+ worker.observer.on('close', () =>
+ {
+ logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`);
+ workers.delete(worker.pid);
+ });
+ });
+
+ const app = express();
+
+ app.get('/', async (req, res) =>
+ {
+ logger.debug(`GET ${req.originalUrl}`);
+ const registry = new prom.Registry();
+
+ await collect(registry);
+ res.set('Content-Type', registry.contentType);
+ const data = registry.metrics();
+
+ res.end(data);
+ });
+ const server = app.listen(config.port || 8889,
+ config.listen || undefined, () =>
+ {
+ const address = server.address();
+
+ logger.info(`listening ${address.address}:${address.port}`);
+ });
+ }
+ catch (err)
+ {
+ logger.error(err);
+ }
+};
diff --git a/meet/server/package-lock.json b/meet/server/package-lock.json
new file mode 100644
index 00000000..6573ead1
--- /dev/null
+++ b/meet/server/package-lock.json
@@ -0,0 +1,3605 @@
+{
+ "name": "edumeet-server",
+ "version": "3.3.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
+ "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.8.3"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.9.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz",
+ "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==",
+ "dev": true
+ },
+ "@babel/highlight": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz",
+ "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.9.0",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@panva/asn1.js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
+ "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw=="
+ },
+ "@sindresorhus/is": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
+ "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
+ },
+ "@szmarczak/http-timer": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
+ "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
+ "requires": {
+ "defer-to-connect": "^1.0.1"
+ }
+ },
+ "@types/color-name": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
+ "dev": true
+ },
+ "@types/got": {
+ "version": "9.6.10",
+ "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.10.tgz",
+ "integrity": "sha512-owBY1cgHUIXjObzY+vs+J9Cpw0czvfksJX+qEkgxRojFutFq7n1tKoj6Ekg57DhvXMk0vGQ7FbinvS9I/1wxcg==",
+ "requires": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "form-data": "^2.5.0"
+ },
+ "dependencies": {
+ "form-data": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
+ "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ }
+ }
+ }
+ },
+ "@types/node": {
+ "version": "13.13.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.1.tgz",
+ "integrity": "sha512-uysqysLJ+As9jqI5yqjwP3QJrhOcUwBjHUlUxPxjbplwKoILvXVsmYWEhfmAQlrPfbRZmhJB007o4L9sKqtHqQ=="
+ },
+ "@types/tough-cookie": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz",
+ "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A=="
+ },
+ "accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+ "requires": {
+ "mime-types": "~2.1.24",
+ "negotiator": "0.6.2"
+ }
+ },
+ "acorn": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
+ "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
+ "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
+ "dev": true
+ },
+ "after": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+ "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
+ },
+ "aggregate-error": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
+ "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==",
+ "requires": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "dependencies": {
+ "indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="
+ }
+ }
+ },
+ "ajv": {
+ "version": "6.12.2",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
+ "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-escapes": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
+ "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.11.0"
+ },
+ "dependencies": {
+ "type-fest": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
+ "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
+ "dev": true
+ }
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "array-find-index": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+ "optional": true
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "arraybuffer.slice": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+ "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
+ },
+ "asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "optional": true,
+ "requires": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "optional": true
+ },
+ "astral-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+ "dev": true
+ },
+ "async-limiter": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
+ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+ },
+ "awaitqueue": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-1.0.1.tgz",
+ "integrity": "sha512-v5XFR8slds87u7WkjWUtuMXUBEaqfg1WJ8yxrUDbo8aodUQLEVag0MZYBptRP/TmMGOYn0YHS6ar8aqVCJVynA=="
+ },
+ "aws-sign2": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+ "optional": true
+ },
+ "aws4": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz",
+ "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==",
+ "optional": true
+ },
+ "axios": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
+ "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
+ "requires": {
+ "follow-redirects": "1.5.10"
+ }
+ },
+ "backo2": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+ "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "base-64": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
+ "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
+ },
+ "base64-arraybuffer": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+ "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
+ },
+ "base64-js": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+ },
+ "base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
+ },
+ "base64url": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
+ "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "optional": true,
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "better-assert": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+ "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+ "requires": {
+ "callsite": "1.0.0"
+ }
+ },
+ "bintrees": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
+ "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
+ },
+ "blob": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+ "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
+ },
+ "body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+ "requires": {
+ "bytes": "3.1.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "bowser": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz",
+ "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA=="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "buffer": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
+ "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
+ "requires": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4"
+ }
+ },
+ "buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
+ },
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+ },
+ "cacheable-request": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
+ "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+ "requires": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^3.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^4.1.0",
+ "responselike": "^1.0.2"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
+ "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
+ }
+ }
+ },
+ "callsite": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+ "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+ "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+ "optional": true
+ },
+ "camelcase-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+ "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+ "optional": true,
+ "requires": {
+ "camelcase": "^2.0.0",
+ "map-obj": "^1.0.0"
+ }
+ },
+ "camelize": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
+ "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+ "optional": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
+ "clang-tools-prebuilt": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/clang-tools-prebuilt/-/clang-tools-prebuilt-0.1.4.tgz",
+ "integrity": "sha1-8gINNlN2CMDPrQeuvglNmXMFkLM=",
+ "optional": true,
+ "requires": {
+ "home-path": "^0.1.1",
+ "mkdirp": "^0.5.0",
+ "nugget": "^1.5.1",
+ "path-exists": "^1.0.0"
+ }
+ },
+ "clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "cli-width": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
+ "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
+ "dev": true
+ },
+ "clone-response": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
+ "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "colors": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
+ },
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "component-bind": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+ "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
+ },
+ "component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+ },
+ "component-inherit": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+ "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
+ },
+ "compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "requires": {
+ "mime-db": ">= 1.43.0 < 2"
+ }
+ },
+ "compression": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "requires": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "connect-redis": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-4.0.4.tgz",
+ "integrity": "sha512-aXk7btMlG0J5LqtPNRpFKa5fglzlTzukYNx+Fq8cghbUIQHN/gyK9c3+b0XEROMwiSxMoZDADqjp9tdpUoZLAg=="
+ },
+ "content-disposition": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
+ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
+ },
+ "content-security-policy-builder": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz",
+ "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ=="
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
+ },
+ "cookie-parser": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
+ "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==",
+ "requires": {
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6"
+ }
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "crc": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
+ "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
+ "requires": {
+ "buffer": "^5.1.0"
+ }
+ },
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "currently-unhandled": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+ "optional": true,
+ "requires": {
+ "array-find-index": "^1.0.1"
+ }
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "dasherize": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz",
+ "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg="
+ },
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "requires": {
+ "ms": "^2.1.1"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "optional": true
+ },
+ "decompress-response": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+ "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+ "dev": true
+ },
+ "defer-to-connect": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
+ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "detect-node": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
+ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw=="
+ },
+ "dns-prefetch-control": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz",
+ "integrity": "sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q=="
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "dont-sniff-mimetype": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz",
+ "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug=="
+ },
+ "double-ended-queue": {
+ "version": "2.1.0-0",
+ "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
+ "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
+ },
+ "duplexer3": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
+ },
+ "ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+ "optional": true,
+ "requires": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "engine.io": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.1.tgz",
+ "integrity": "sha512-8MfIfF1/IIfxuc2gv5K+XlFZczw/BpTvqBdl0E2fBLkYQp4miv4LuDTVtYt4yMyaIFLEr4vtaSgV4mjvll8Crw==",
+ "requires": {
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "0.3.1",
+ "debug": "~4.1.0",
+ "engine.io-parser": "~2.2.0",
+ "ws": "^7.1.2"
+ },
+ "dependencies": {
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+ }
+ }
+ },
+ "engine.io-client": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.1.tgz",
+ "integrity": "sha512-RJNmA+A9Js+8Aoq815xpGAsgWH1VoSYM//2VgIiu9lNOaHFfLpTjH4tOzktBpjIs5lvOfiNY1dwf+NuU6D38Mw==",
+ "requires": {
+ "component-emitter": "1.2.1",
+ "component-inherit": "0.0.3",
+ "debug": "~4.1.0",
+ "engine.io-parser": "~2.2.0",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "ws": "~6.1.0",
+ "xmlhttprequest-ssl": "~1.5.4",
+ "yeast": "0.1.2"
+ },
+ "dependencies": {
+ "ws": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
+ "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
+ "requires": {
+ "async-limiter": "~1.0.0"
+ }
+ }
+ }
+ },
+ "engine.io-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz",
+ "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==",
+ "requires": {
+ "after": "0.8.2",
+ "arraybuffer.slice": "~0.0.7",
+ "base64-arraybuffer": "0.1.5",
+ "blob": "0.0.5",
+ "has-binary2": "~1.0.2"
+ }
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "optional": true,
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "eslint": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
+ "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "ajv": "^6.10.0",
+ "chalk": "^2.1.0",
+ "cross-spawn": "^6.0.5",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "eslint-scope": "^5.0.0",
+ "eslint-utils": "^1.4.3",
+ "eslint-visitor-keys": "^1.1.0",
+ "espree": "^6.1.2",
+ "esquery": "^1.0.1",
+ "esutils": "^2.0.2",
+ "file-entry-cache": "^5.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.0.0",
+ "globals": "^12.1.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "inquirer": "^7.0.0",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.3.0",
+ "lodash": "^4.17.14",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.1",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.8.3",
+ "progress": "^2.0.0",
+ "regexpp": "^2.0.1",
+ "semver": "^6.1.2",
+ "strip-ansi": "^5.2.0",
+ "strip-json-comments": "^3.0.1",
+ "table": "^5.2.3",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-scope": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
+ "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
+ "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^1.1.0"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+ "dev": true
+ },
+ "espree": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
+ "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.1.1",
+ "acorn-jsx": "^5.2.0",
+ "eslint-visitor-keys": "^1.1.0"
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "esquery": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz",
+ "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.1.0"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz",
+ "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==",
+ "dev": true
+ }
+ }
+ },
+ "esrecurse": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^4.1.0"
+ }
+ },
+ "estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "expect-ct": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.2.0.tgz",
+ "integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g=="
+ },
+ "express": {
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+ "requires": {
+ "accepts": "~1.3.7",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.1.2",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "express-session": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz",
+ "integrity": "sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==",
+ "requires": {
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.0.2",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.2.0",
+ "uid-safe": "~2.1.5"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ },
+ "safe-buffer": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+ "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
+ }
+ }
+ },
+ "express-socket.io-session": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/express-socket.io-session/-/express-socket.io-session-1.3.5.tgz",
+ "integrity": "sha512-ila9jN7Pu9OuNIDzkuW+ZChR2Y0TzyyFITT7xiOWCjuGCDUWioD382zqxI7HOaa8kIhfs3wTLOZMU9h6buuOFw==",
+ "requires": {
+ "cookie-parser": "~1.3.3",
+ "crc": "^3.3.0",
+ "debug": "~2.6.0"
+ },
+ "dependencies": {
+ "cookie": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz",
+ "integrity": "sha1-5zSlwUF/zkctWu+Cw4HKu2TRpDU="
+ },
+ "cookie-parser": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz",
+ "integrity": "sha1-nXVVcPtdF4kHcSJ6AjFNm+fPg1Y=",
+ "requires": {
+ "cookie": "0.1.3",
+ "cookie-signature": "1.0.6"
+ }
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "optional": true
+ },
+ "external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "requires": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ }
+ },
+ "extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+ "optional": true
+ },
+ "fast-deep-equal": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+ "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "feature-policy": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz",
+ "integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ=="
+ },
+ "figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "file-entry-cache": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
+ "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^2.0.1"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "find-up": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+ "optional": true,
+ "requires": {
+ "path-exists": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "dependencies": {
+ "path-exists": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+ "optional": true,
+ "requires": {
+ "pinkie-promise": "^2.0.0"
+ }
+ }
+ }
+ },
+ "flat-cache": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
+ "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+ "dev": true,
+ "requires": {
+ "flatted": "^2.0.0",
+ "rimraf": "2.6.3",
+ "write": "1.0.3"
+ }
+ },
+ "flatted": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
+ "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
+ "dev": true
+ },
+ "follow-redirects": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+ "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+ "requires": {
+ "debug": "=3.1.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+ "optional": true
+ },
+ "form-data": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+ "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+ "optional": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "frameguard": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.1.0.tgz",
+ "integrity": "sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g=="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "get-stdin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+ "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+ "optional": true
+ },
+ "get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+ "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "globals": {
+ "version": "12.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
+ "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.8.1"
+ }
+ },
+ "got": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
+ "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
+ "requires": {
+ "@sindresorhus/is": "^0.14.0",
+ "@szmarczak/http-timer": "^1.1.2",
+ "cacheable-request": "^6.0.0",
+ "decompress-response": "^3.3.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^4.1.0",
+ "lowercase-keys": "^1.0.1",
+ "mimic-response": "^1.0.1",
+ "p-cancelable": "^1.0.0",
+ "to-readable-stream": "^1.0.0",
+ "url-parse-lax": "^3.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
+ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
+ "optional": true
+ },
+ "h264-profile-level-id": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz",
+ "integrity": "sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q==",
+ "requires": {
+ "debug": "^4.1.1"
+ }
+ },
+ "handle-thing": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
+ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg=="
+ },
+ "har-schema": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+ "optional": true
+ },
+ "har-validator": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+ "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+ "optional": true,
+ "requires": {
+ "ajv": "^6.5.5",
+ "har-schema": "^2.0.0"
+ }
+ },
+ "has-binary2": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+ "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+ "requires": {
+ "isarray": "2.0.1"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+ }
+ }
+ },
+ "has-cors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+ "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "helmet": {
+ "version": "3.22.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.22.0.tgz",
+ "integrity": "sha512-Xrqicn2nm1ZIUxP3YGuTBmbDL04neKsIT583Sjh0FkiwKDXYCMUqGqC88w3NUvVXtA75JyR2Jn6jw6ZEMOD+ZA==",
+ "requires": {
+ "depd": "2.0.0",
+ "dns-prefetch-control": "0.2.0",
+ "dont-sniff-mimetype": "1.1.0",
+ "expect-ct": "0.2.0",
+ "feature-policy": "0.3.0",
+ "frameguard": "3.1.0",
+ "helmet-crossdomain": "0.4.0",
+ "helmet-csp": "2.10.0",
+ "hide-powered-by": "1.1.0",
+ "hpkp": "2.0.0",
+ "hsts": "2.2.0",
+ "ienoopen": "1.1.0",
+ "nocache": "2.1.0",
+ "referrer-policy": "1.2.0",
+ "x-xss-protection": "1.3.0"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ }
+ }
+ },
+ "helmet-crossdomain": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz",
+ "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA=="
+ },
+ "helmet-csp": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.10.0.tgz",
+ "integrity": "sha512-Rz953ZNEFk8sT2XvewXkYN0Ho4GEZdjAZy4stjiEQV3eN7GDxg1QKmYggH7otDyIA7uGA6XnUMVSgeJwbR5X+w==",
+ "requires": {
+ "bowser": "2.9.0",
+ "camelize": "1.0.0",
+ "content-security-policy-builder": "2.1.0",
+ "dasherize": "2.0.0"
+ }
+ },
+ "hide-powered-by": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz",
+ "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg=="
+ },
+ "home-path": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/home-path/-/home-path-0.1.2.tgz",
+ "integrity": "sha1-PbJsojrcFE/uqPHi18j2yJDL/io=",
+ "optional": true
+ },
+ "hosted-git-info": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+ "optional": true
+ },
+ "hpack.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+ "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=",
+ "requires": {
+ "inherits": "^2.0.1",
+ "obuf": "^1.0.0",
+ "readable-stream": "^2.0.1",
+ "wbuf": "^1.1.0"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "hpkp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz",
+ "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI="
+ },
+ "hsts": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz",
+ "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==",
+ "requires": {
+ "depd": "2.0.0"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ }
+ }
+ },
+ "http-cache-semantics": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
+ },
+ "http-deceiver": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+ "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc="
+ },
+ "http-errors": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ }
+ },
+ "http-signature": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ieee754": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
+ },
+ "ienoopen": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.1.0.tgz",
+ "integrity": "sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ=="
+ },
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
+ "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "ims-lti": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/ims-lti/-/ims-lti-3.0.2.tgz",
+ "integrity": "sha1-inOBxiKrgTvjvxilqiTryoyyhgs=",
+ "requires": {
+ "node-uuid": "~1.4.0",
+ "xml2js": "~0.4.0",
+ "xmlbuilder": "~2.4.0"
+ },
+ "dependencies": {
+ "node-uuid": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
+ "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc="
+ }
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true
+ },
+ "indent-string": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+ "optional": true,
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
+ "indexof": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+ "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "inquirer": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
+ "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^3.0.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.15",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.4.0",
+ "rxjs": "^6.5.3",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "requires": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "optional": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-finite": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
+ "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==",
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+ "optional": true
+ },
+ "is-utf8": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+ "optional": true
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "optional": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+ "optional": true
+ },
+ "jose": {
+ "version": "1.26.0",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-1.26.0.tgz",
+ "integrity": "sha512-mXZAgJIX/mDKjbnX/arEVpJaFs3g5cfZMpcsGlhLQ6X+CCL0YV46FVNa3wHKVe3iZD1qVtAdj3fLhbXqI+6/dQ==",
+ "requires": {
+ "@panva/asn1.js": "^1.0.0"
+ }
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+ "optional": true
+ },
+ "json-buffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
+ "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+ "optional": true
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+ "optional": true
+ },
+ "jsonwebtoken": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
+ "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
+ "requires": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^5.6.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
+ "jsprim": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+ "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ }
+ },
+ "jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "requires": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "requires": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "keyv": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
+ "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
+ "requires": {
+ "json-buffer": "3.0.0"
+ }
+ },
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "load-json-file": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+ "optional": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0",
+ "strip-bom": "^2.0.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+ },
+ "lodash-node": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/lodash-node/-/lodash-node-2.4.1.tgz",
+ "integrity": "sha1-6oL3sQDHM9GkKvdoAeUGEF4qgOw="
+ },
+ "lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
+ },
+ "lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
+ },
+ "lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
+ },
+ "lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
+ },
+ "lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+ },
+ "lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
+ },
+ "lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+ },
+ "loud-rejection": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+ "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+ "optional": true,
+ "requires": {
+ "currently-unhandled": "^0.4.1",
+ "signal-exit": "^3.0.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
+ },
+ "lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "requires": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+ },
+ "map-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+ "optional": true
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "mediasoup": {
+ "version": "3.6.12",
+ "resolved": "https://registry.npmjs.org/mediasoup/-/mediasoup-3.6.12.tgz",
+ "integrity": "sha512-F5q8nmW8xR6d6R60r9B51PuhyD8QOlJBTo16v3+BGcAnoson3Ld4BmS3cYQs6DnazixrR4poovrIXMq7psk19w==",
+ "requires": {
+ "@types/node": "^14.0.20",
+ "awaitqueue": "^2.2.3",
+ "clang-tools-prebuilt": "^0.1.4",
+ "debug": "^4.1.1",
+ "h264-profile-level-id": "^1.0.1",
+ "netstring": "^0.3.0",
+ "random-number": "^0.0.9",
+ "supports-color": "^7.1.0",
+ "uuid": "^8.2.0"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "14.0.22",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.22.tgz",
+ "integrity": "sha512-emeGcJvdiZ4Z3ohbmw93E/64jRzUHAItSHt8nF7M4TGgQTiWqFVGB8KNpLGFmUHmHLvjvBgFwVlqNcq+VuGv9g=="
+ },
+ "awaitqueue": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-2.2.3.tgz",
+ "integrity": "sha512-vKY8hHHt1FT05UBxaTKWoA8+A3APnyzcOO1UBY5wQ7ENzXCFi3Yy4wSKsqrO0ncDD/5CI6IZDN1nVZQKziWIqg=="
+ },
+ "uuid": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
+ "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
+ }
+ }
+ },
+ "meow": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+ "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+ "optional": true,
+ "requires": {
+ "camelcase-keys": "^2.0.0",
+ "decamelize": "^1.1.2",
+ "loud-rejection": "^1.0.0",
+ "map-obj": "^1.0.1",
+ "minimist": "^1.1.3",
+ "normalize-package-data": "^2.3.4",
+ "object-assign": "^4.0.1",
+ "read-pkg-up": "^1.0.1",
+ "redent": "^1.0.0",
+ "trim-newlines": "^1.0.0"
+ }
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+ },
+ "mime-db": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
+ "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
+ },
+ "mime-types": {
+ "version": "2.1.26",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
+ "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
+ "requires": {
+ "mime-db": "1.43.0"
+ }
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
+ },
+ "minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "negotiator": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+ },
+ "netstring": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/netstring/-/netstring-0.3.0.tgz",
+ "integrity": "sha1-ho3FsgxY0/cwVTHUk2jqqr0ZtxI="
+ },
+ "nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "nocache": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz",
+ "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q=="
+ },
+ "normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "optional": true,
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "normalize-url": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
+ "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ=="
+ },
+ "nugget": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz",
+ "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=",
+ "optional": true,
+ "requires": {
+ "debug": "^2.1.3",
+ "minimist": "^1.1.0",
+ "pretty-bytes": "^1.0.2",
+ "progress-stream": "^1.1.0",
+ "request": "^2.45.0",
+ "single-line-log": "^0.4.1",
+ "throttleit": "0.0.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "optional": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "oauth-sign": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+ "optional": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "optional": true
+ },
+ "object-component": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+ "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
+ },
+ "object-hash": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz",
+ "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg=="
+ },
+ "object-keys": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz",
+ "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=",
+ "optional": true
+ },
+ "obuf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
+ },
+ "oidc-token-hash": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz",
+ "integrity": "sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg=="
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
+ "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "openid-client": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-3.14.2.tgz",
+ "integrity": "sha512-ga9YL3H7lNYEO1T0zRqovVqzwlljI9CgpBCYESrb/xhRbj/LOIQRAoipCvvjZj2JGgvSR1H6PFwgzcx1Cd1z8A==",
+ "requires": {
+ "@types/got": "^9.6.9",
+ "base64url": "^3.0.1",
+ "got": "^9.6.0",
+ "jose": "^1.25.0",
+ "lodash": "^4.17.15",
+ "lru-cache": "^5.1.1",
+ "make-error": "^1.3.6",
+ "object-hash": "^2.0.1",
+ "oidc-token-hash": "^5.0.0",
+ "p-any": "^3.0.0"
+ }
+ },
+ "optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "dev": true,
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true
+ },
+ "p-any": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-any/-/p-any-3.0.0.tgz",
+ "integrity": "sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w==",
+ "requires": {
+ "p-cancelable": "^2.0.0",
+ "p-some": "^5.0.0"
+ },
+ "dependencies": {
+ "p-cancelable": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz",
+ "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg=="
+ }
+ }
+ },
+ "p-cancelable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
+ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="
+ },
+ "p-some": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-some/-/p-some-5.0.0.tgz",
+ "integrity": "sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig==",
+ "requires": {
+ "aggregate-error": "^3.0.0",
+ "p-cancelable": "^2.0.0"
+ },
+ "dependencies": {
+ "p-cancelable": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz",
+ "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg=="
+ }
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "parse-json": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+ "optional": true,
+ "requires": {
+ "error-ex": "^1.2.0"
+ }
+ },
+ "parseqs": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+ "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+ "requires": {
+ "better-assert": "~1.0.0"
+ }
+ },
+ "parseuri": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+ "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+ "requires": {
+ "better-assert": "~1.0.0"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ },
+ "passport": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz",
+ "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==",
+ "requires": {
+ "passport-strategy": "1.x.x",
+ "pause": "0.0.1"
+ }
+ },
+ "passport-local": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
+ "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=",
+ "requires": {
+ "passport-strategy": "1.x.x"
+ }
+ },
+ "passport-lti": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/passport-lti/-/passport-lti-0.0.7.tgz",
+ "integrity": "sha1-ef4meKnkNCCq+Feiatn91AdJmog=",
+ "requires": {
+ "passport-strategy": "1.0.x"
+ }
+ },
+ "passport-strategy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+ "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
+ },
+ "path-exists": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz",
+ "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=",
+ "optional": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "optional": true
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "path-type": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+ "optional": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "pause": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+ "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
+ },
+ "performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+ "optional": true
+ },
+ "pidusage": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.18.tgz",
+ "integrity": "sha512-Y/VfKfh3poHjMEINxU+gJTeVOBjiThQeFAmzR7z56HSNiMx+etl+yBhk42nRPciPYt/VZl8DQLVXNC6P5vH11A==",
+ "requires": {
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "optional": true
+ },
+ "pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+ "optional": true
+ },
+ "pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+ "optional": true,
+ "requires": {
+ "pinkie": "^2.0.0"
+ }
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+ "dev": true
+ },
+ "prepend-http": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
+ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
+ },
+ "pretty-bytes": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz",
+ "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=",
+ "optional": true,
+ "requires": {
+ "get-stdin": "^4.0.1",
+ "meow": "^3.1.0"
+ }
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
+ "progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true
+ },
+ "progress-stream": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz",
+ "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=",
+ "optional": true,
+ "requires": {
+ "speedometer": "~0.1.2",
+ "through2": "~0.2.3"
+ }
+ },
+ "prom-client": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-12.0.0.tgz",
+ "integrity": "sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ==",
+ "requires": {
+ "tdigest": "^0.1.1"
+ }
+ },
+ "proxy-addr": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
+ "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
+ "requires": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.9.1"
+ }
+ },
+ "psl": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
+ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
+ "optional": true
+ },
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
+ },
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+ },
+ "random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
+ },
+ "random-number": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/random-number/-/random-number-0.0.9.tgz",
+ "integrity": "sha512-ipG3kRCREi/YQpi2A5QGcvDz1KemohovWmH6qGfboVyyGdR2t/7zQz0vFxrfxpbHQgPPdtVlUDaks3aikD1Ljw=="
+ },
+ "range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+ },
+ "raw-body": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+ "requires": {
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ }
+ },
+ "read-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+ "optional": true,
+ "requires": {
+ "load-json-file": "^1.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^1.0.0"
+ }
+ },
+ "read-pkg-up": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+ "optional": true,
+ "requires": {
+ "find-up": "^1.0.0",
+ "read-pkg": "^1.0.0"
+ }
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+ "optional": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "redent": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+ "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+ "optional": true,
+ "requires": {
+ "indent-string": "^2.1.0",
+ "strip-indent": "^1.0.1"
+ }
+ },
+ "redis": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
+ "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
+ "requires": {
+ "double-ended-queue": "^2.1.0-0",
+ "redis-commands": "^1.2.0",
+ "redis-parser": "^2.6.0"
+ }
+ },
+ "redis-commands": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz",
+ "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg=="
+ },
+ "redis-parser": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
+ "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs="
+ },
+ "referrer-policy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz",
+ "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA=="
+ },
+ "regexpp": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+ "dev": true
+ },
+ "repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "optional": true,
+ "requires": {
+ "is-finite": "^1.0.0"
+ }
+ },
+ "request": {
+ "version": "2.88.2",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
+ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
+ "optional": true,
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.5.0",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ },
+ "dependencies": {
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "optional": true
+ },
+ "uuid": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+ "optional": true
+ }
+ }
+ },
+ "resolve": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
+ "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
+ "optional": true,
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
+ },
+ "responselike": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
+ "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
+ "requires": {
+ "lowercase-keys": "^1.0.0"
+ }
+ },
+ "restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "run-async": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "dev": true
+ },
+ "rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
+ "select-hose": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+ "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo="
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ },
+ "send": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ }
+ }
+ },
+ "serve-static": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+ "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.1"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
+ },
+ "single-line-log": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz",
+ "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=",
+ "optional": true
+ },
+ "slice-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+ "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "astral-regex": "^1.0.0",
+ "is-fullwidth-code-point": "^2.0.0"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ }
+ }
+ },
+ "socket.io": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz",
+ "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==",
+ "requires": {
+ "debug": "~4.1.0",
+ "engine.io": "~3.4.0",
+ "has-binary2": "~1.0.2",
+ "socket.io-adapter": "~1.1.0",
+ "socket.io-client": "2.3.0",
+ "socket.io-parser": "~3.4.0"
+ }
+ },
+ "socket.io-adapter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz",
+ "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g=="
+ },
+ "socket.io-client": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz",
+ "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==",
+ "requires": {
+ "backo2": "1.0.2",
+ "base64-arraybuffer": "0.1.5",
+ "component-bind": "1.0.0",
+ "component-emitter": "1.2.1",
+ "debug": "~4.1.0",
+ "engine.io-client": "~3.4.0",
+ "has-binary2": "~1.0.2",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "object-component": "0.0.3",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "socket.io-parser": "~3.3.0",
+ "to-array": "0.1.4"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+ },
+ "socket.io-parser": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz",
+ "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==",
+ "requires": {
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "isarray": "2.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ }
+ }
+ },
+ "socket.io-parser": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.0.tgz",
+ "integrity": "sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ==",
+ "requires": {
+ "component-emitter": "1.2.1",
+ "debug": "~4.1.0",
+ "isarray": "2.0.1"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+ }
+ }
+ },
+ "spdx-correct": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
+ "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
+ "optional": true,
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+ "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+ "optional": true
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "optional": true,
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
+ "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
+ "optional": true
+ },
+ "spdy": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+ "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
+ "requires": {
+ "debug": "^4.1.0",
+ "handle-thing": "^2.0.0",
+ "http-deceiver": "^1.2.7",
+ "select-hose": "^2.0.0",
+ "spdy-transport": "^3.0.0"
+ }
+ },
+ "spdy-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
+ "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
+ "requires": {
+ "debug": "^4.1.0",
+ "detect-node": "^2.0.4",
+ "hpack.js": "^2.1.6",
+ "obuf": "^1.1.2",
+ "readable-stream": "^3.0.6",
+ "wbuf": "^1.7.3"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+ "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ }
+ }
+ },
+ "speedometer": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz",
+ "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=",
+ "optional": true
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "sshpk": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+ "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+ "optional": true,
+ "requires": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ }
+ },
+ "statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+ "optional": true
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ }
+ }
+ },
+ "strip-bom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+ "optional": true,
+ "requires": {
+ "is-utf8": "^0.2.0"
+ }
+ },
+ "strip-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+ "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+ "optional": true,
+ "requires": {
+ "get-stdin": "^4.0.1"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz",
+ "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "table": {
+ "version": "5.4.6",
+ "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
+ "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.10.2",
+ "lodash": "^4.17.14",
+ "slice-ansi": "^2.1.0",
+ "string-width": "^3.0.0"
+ },
+ "dependencies": {
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ }
+ }
+ },
+ "tdigest": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz",
+ "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=",
+ "requires": {
+ "bintrees": "1.0.1"
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "throttleit": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz",
+ "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=",
+ "optional": true
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "through2": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz",
+ "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=",
+ "optional": true,
+ "requires": {
+ "readable-stream": "~1.1.9",
+ "xtend": "~2.1.1"
+ }
+ },
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ },
+ "to-array": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+ "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
+ },
+ "to-readable-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
+ "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q=="
+ },
+ "toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+ },
+ "tough-cookie": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "optional": true,
+ "requires": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ }
+ },
+ "trim-newlines": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+ "optional": true
+ },
+ "tslib": {
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz",
+ "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==",
+ "dev": true
+ },
+ "tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "optional": true,
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "optional": true
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ },
+ "type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true
+ },
+ "type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ }
+ },
+ "uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "requires": {
+ "random-bytes": "~1.0.0"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "uri-js": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "url-parse-lax": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
+ "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
+ "requires": {
+ "prepend-http": "^2.0.0"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "uuid": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
+ "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="
+ },
+ "v8-compile-cache": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
+ "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
+ "dev": true
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "optional": true,
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ },
+ "verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "wbuf": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+ "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+ "requires": {
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "write": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
+ "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^0.5.1"
+ }
+ },
+ "ws": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz",
+ "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ=="
+ },
+ "x-xss-protection": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz",
+ "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg=="
+ },
+ "xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "requires": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "dependencies": {
+ "xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
+ }
+ }
+ },
+ "xmlbuilder": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.4.6.tgz",
+ "integrity": "sha1-QsZk8TWIZOW+sUYbQ0auyHtjRQ8=",
+ "requires": {
+ "lodash-node": "~2.4.1"
+ }
+ },
+ "xmlhttprequest-ssl": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+ "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
+ },
+ "xtend": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz",
+ "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=",
+ "optional": true,
+ "requires": {
+ "object-keys": "~0.4.0"
+ }
+ },
+ "yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
+ },
+ "yeast": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+ }
+ }
+}
diff --git a/meet/server/package.json b/meet/server/package.json
new file mode 100644
index 00000000..2dd31057
--- /dev/null
+++ b/meet/server/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "edumeet-server",
+ "version": "3.3.4",
+ "private": true,
+ "description": "edumeet server",
+ "author": "Håvar Aambø Fosstveit ",
+ "contributors": [
+ "Stefan Otto",
+ "Mészáros Mihály",
+ "Roman Drozd",
+ "Rémai Gábor László",
+ "Piotr Pawałowski"
+ ],
+ "license": "MIT",
+ "main": "lib/index.js",
+ "scripts": {
+ "start": "node server.js",
+ "connect": "node connect.js",
+ "lint": "eslint -c .eslintrc.json --ext .js *.js lib/"
+ },
+ "dependencies": {
+ "awaitqueue": "^1.0.0",
+ "axios": "^0.19.2",
+ "base-64": "^0.1.0",
+ "bcrypt": "^5.0.0",
+ "body-parser": "^1.19.0",
+ "colors": "^1.4.0",
+ "compression": "^1.7.4",
+ "connect-redis": "^4.0.3",
+ "cookie-parser": "^1.4.4",
+ "debug": "^4.1.1",
+ "express": "^4.17.1",
+ "express-session": "^1.17.0",
+ "express-socket.io-session": "^1.3.5",
+ "helmet": "^3.21.2",
+ "ims-lti": "^3.0.2",
+ "jsonwebtoken": "^8.5.1",
+ "mediasoup": "^3.5.14",
+ "openid-client": "^3.7.3",
+ "passport": "^0.4.0",
+ "passport-local": "^1.0.0",
+ "passport-lti": "0.0.7",
+ "passport-saml": "^1.3.5",
+ "pidusage": "^2.0.17",
+ "prom-client": ">=12.0.0",
+ "redis": "^2.8.0",
+ "socket.io": "^2.3.0",
+ "spdy": "^4.0.1",
+ "uuid": "^7.0.2"
+ },
+ "devDependencies": {
+ "eslint": "6.8.0"
+ }
+}
diff --git a/meet/server/permissions.js b/meet/server/permissions.js
new file mode 100644
index 00000000..12a5ef52
--- /dev/null
+++ b/meet/server/permissions.js
@@ -0,0 +1,26 @@
+module.exports = {
+ // The role(s) have permission to lock/unlock a room
+ CHANGE_ROOM_LOCK : 'CHANGE_ROOM_LOCK',
+ // The role(s) have permission to promote a peer from the lobby
+ PROMOTE_PEER : 'PROMOTE_PEER',
+ // The role(s) have permission to give/remove other peers roles
+ MODIFY_ROLE : 'MODIFY_ROLE',
+ // The role(s) have permission to send chat messages
+ SEND_CHAT : 'SEND_CHAT',
+ // The role(s) have permission to moderate chat
+ MODERATE_CHAT : 'MODERATE_CHAT',
+ // The role(s) have permission to share audio
+ SHARE_AUDIO : 'SHARE_AUDIO',
+ // The role(s) have permission to share video
+ SHARE_VIDEO : 'SHARE_VIDEO',
+ // The role(s) have permission to share screen
+ SHARE_SCREEN : 'SHARE_SCREEN',
+ // The role(s) have permission to produce extra video
+ EXTRA_VIDEO : 'EXTRA_VIDEO',
+ // The role(s) have permission to share files
+ SHARE_FILE : 'SHARE_FILE',
+ // The role(s) have permission to moderate files
+ MODERATE_FILES : 'MODERATE_FILES',
+ // The role(s) have permission to moderate room (e.g. kick user)
+ MODERATE_ROOM : 'MODERATE_ROOM'
+};
\ No newline at end of file
diff --git a/meet/server/server.js b/meet/server/server.js
new file mode 100755
index 00000000..2ce12e96
--- /dev/null
+++ b/meet/server/server.js
@@ -0,0 +1,826 @@
+#!/usr/bin/env node
+
+process.title = 'edumeet-server';
+
+const bcrypt = require('bcrypt');
+const config = require('./config/config');
+const fs = require('fs');
+const http = require('http');
+const spdy = require('spdy');
+const express = require('express');
+const bodyParser = require('body-parser');
+const cookieParser = require('cookie-parser');
+const compression = require('compression');
+const mediasoup = require('mediasoup');
+const AwaitQueue = require('awaitqueue');
+const Logger = require('./lib/Logger');
+const Room = require('./lib/Room');
+const Peer = require('./lib/Peer');
+const base64 = require('base-64');
+const helmet = require('helmet');
+const userRoles = require('./userRoles');
+const {
+ loginHelper,
+ logoutHelper
+} = require('./httpHelper');
+// auth
+const passport = require('passport');
+const LTIStrategy = require('passport-lti');
+const imsLti = require('ims-lti');
+const SAMLStrategy = require('passport-saml').Strategy;
+const LocalStrategy = require('passport-local').Strategy;
+const redis = require('redis');
+const redisClient = redis.createClient(config.redisOptions);
+const { Issuer, Strategy } = require('openid-client');
+const expressSession = require('express-session');
+const RedisStore = require('connect-redis')(expressSession);
+const sharedSession = require('express-socket.io-session');
+const interactiveServer = require('./lib/interactiveServer');
+const promExporter = require('./lib/promExporter');
+const { v4: uuidv4 } = require('uuid');
+
+/* eslint-disable no-console */
+console.log('- process.env.DEBUG:', process.env.DEBUG);
+console.log('- config.mediasoup.worker.logLevel:', config.mediasoup.worker.logLevel);
+console.log('- config.mediasoup.worker.logTags:', config.mediasoup.worker.logTags);
+/* eslint-enable no-console */
+
+const logger = new Logger();
+
+const queue = new AwaitQueue();
+
+let statusLogger = null;
+
+if ('StatusLogger' in config)
+ statusLogger = new config.StatusLogger();
+
+// mediasoup Workers.
+// @type {Array}
+const mediasoupWorkers = [];
+
+// Map of Room instances indexed by roomId.
+const rooms = new Map();
+
+// Map of Peer instances indexed by peerId.
+const peers = new Map();
+
+// TLS server configuration.
+const tls =
+{
+ cert : fs.readFileSync(config.tls.cert),
+ key : fs.readFileSync(config.tls.key),
+ secureOptions : 'tlsv12',
+ ciphers :
+ [
+ 'ECDHE-ECDSA-AES128-GCM-SHA256',
+ 'ECDHE-RSA-AES128-GCM-SHA256',
+ 'ECDHE-ECDSA-AES256-GCM-SHA384',
+ 'ECDHE-RSA-AES256-GCM-SHA384',
+ 'ECDHE-ECDSA-CHACHA20-POLY1305',
+ 'ECDHE-RSA-CHACHA20-POLY1305',
+ 'DHE-RSA-AES128-GCM-SHA256',
+ 'DHE-RSA-AES256-GCM-SHA384'
+ ].join(':'),
+ honorCipherOrder : true
+};
+
+const app = express();
+
+app.use(helmet.hsts());
+const sharedCookieParser=cookieParser();
+
+app.use(sharedCookieParser);
+app.use(bodyParser.json({ limit: '5mb' }));
+app.use(bodyParser.urlencoded({ limit: '5mb', extended: true }));
+
+const session = expressSession({
+ secret : config.cookieSecret,
+ name : config.cookieName,
+ resave : true,
+ saveUninitialized : true,
+ store : new RedisStore({ client: redisClient }),
+ cookie : {
+ secure : true,
+ httpOnly : true,
+ maxAge : 60 * 60 * 1000 // Expire after 1 hour since last request from user
+ }
+});
+
+if (config.trustProxy)
+{
+ app.set('trust proxy', config.trustProxy);
+}
+
+app.use(session);
+
+passport.serializeUser((user, done) =>
+{
+ done(null, user);
+});
+
+passport.deserializeUser((user, done) =>
+{
+ done(null, user);
+});
+
+let mainListener;
+let io;
+let oidcClient;
+let oidcStrategy;
+let samlStrategy;
+let localStrategy;
+
+async function run()
+{
+ try
+ {
+ // Open the interactive server.
+ await interactiveServer(rooms, peers);
+
+ // start Prometheus exporter
+ if (config.prometheus)
+ {
+ await promExporter(rooms, peers, config.prometheus);
+ }
+
+ if (typeof (config.auth) === 'undefined')
+ {
+ logger.warn('Auth is not configured properly!');
+ }
+ else
+ {
+ await setupAuth();
+ }
+
+ // Run a mediasoup Worker.
+ await runMediasoupWorkers();
+
+ // Run HTTPS server.
+ await runHttpsServer();
+
+ // Run WebSocketServer.
+ await runWebSocketServer();
+
+ // eslint-disable-next-line no-unused-vars
+ const errorHandler = (err, req, res, next) =>
+ {
+ const trackingId = uuidv4();
+
+ res.status(500).send(
+ `Internal Server Error
+ If you report this error, please also report this
+ tracking ID which makes it possible to locate your session
+ in the logs which are available to the system administrator:
+ ${trackingId}
`
+ );
+ logger.error(
+ 'Express error handler dump with tracking ID: %s, error dump: %o',
+ trackingId, err);
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ app.use(errorHandler);
+ }
+ catch (error)
+ {
+ logger.error('run() [error:"%o"]', error);
+ }
+}
+
+function statusLog()
+{
+ if (statusLogger)
+ {
+ statusLogger.log({
+ rooms : rooms,
+ peers : peers
+ });
+ }
+}
+
+function setupLTI(ltiConfig)
+{
+
+ // Add redis nonce store
+ ltiConfig.nonceStore = new imsLti.Stores.RedisStore(ltiConfig.consumerKey, redisClient);
+ ltiConfig.passReqToCallback = true;
+
+ const ltiStrategy = new LTIStrategy(
+ ltiConfig,
+ (req, lti, done) =>
+ {
+ // LTI launch parameters
+ if (lti)
+ {
+ const user = {};
+
+ if (lti.user_id && lti.custom_room)
+ {
+ user.id = lti.user_id;
+ user._userinfo = { 'lti': lti };
+ }
+
+ if (lti.custom_room)
+ {
+ user.room = lti.custom_room;
+ }
+ else
+ {
+ user.room = '';
+ }
+ if (lti.lis_person_name_full)
+ {
+ user.displayName = lti.lis_person_name_full;
+ }
+
+ // Perform local authentication if necessary
+ return done(null, user);
+
+ }
+ else
+ {
+ return done('LTI error');
+ }
+
+ }
+ );
+
+ passport.use('lti', ltiStrategy);
+}
+
+function setupSAML()
+{
+ samlStrategy = new SAMLStrategy(
+ config.auth.saml,
+ function(profile, done)
+ {
+ return done(null,
+ {
+ id : profile.uid,
+ _userinfo : profile
+ });
+ }
+ );
+
+ passport.use('saml', samlStrategy);
+}
+
+function setupLocal()
+{
+ localStrategy = new LocalStrategy(
+ function(username, plaintextPassword, done)
+ {
+ const found = config.auth.local.users.find((element) =>
+ {
+ // TODO use encrypted password
+ return element.username === username &&
+ bcrypt.compareSync(plaintextPassword, element.passwordHash);
+ });
+
+ if (found === undefined)
+ return done(null, null);
+ else
+ {
+ const userinfo = { ...found };
+
+ delete userinfo.password;
+
+ return done(null, { id: found.id, _userinfo: userinfo });
+ }
+ }
+ );
+
+ passport.use('local', localStrategy);
+}
+
+function setupOIDC(oidcIssuer)
+{
+
+ oidcClient = new oidcIssuer.Client(config.auth.oidc.clientOptions);
+
+ // ... any authorization request parameters go here
+ // client_id defaults to client.client_id
+ // redirect_uri defaults to client.redirect_uris[0]
+ // response type defaults to client.response_types[0], then 'code'
+ // scope defaults to 'openid'
+
+ /* eslint-disable camelcase */
+ const params = (({
+ client_id,
+ redirect_uri,
+ scope
+ }) => ({
+ client_id,
+ redirect_uri,
+ scope
+ }))(config.auth.oidc.clientOptions);
+ /* eslint-enable camelcase */
+
+ // optional, defaults to false, when true req is passed as a first
+ // argument to verify fn
+ const passReqToCallback = false;
+
+ // optional, defaults to false, when true the code_challenge_method will be
+ // resolved from the issuer configuration, instead of true you may provide
+ // any of the supported values directly, i.e. "S256" (recommended) or "plain"
+ const usePKCE = false;
+
+ oidcStrategy = new Strategy(
+ { client: oidcClient, params, passReqToCallback, usePKCE },
+ (tokenset, userinfo, done) =>
+ {
+ if (userinfo && tokenset)
+ {
+ // eslint-disable-next-line camelcase
+ userinfo._tokenset_claims = tokenset.claims();
+ }
+
+ const user =
+ {
+ id : tokenset.claims.sub,
+ provider : tokenset.claims.iss,
+ _userinfo : userinfo
+ };
+
+ return done(null, user);
+ }
+ );
+
+ passport.use('oidc', oidcStrategy);
+}
+
+async function setupAuth()
+{
+ // LTI
+ if (
+ typeof (config.auth.lti) !== 'undefined' &&
+ typeof (config.auth.lti.consumerKey) !== 'undefined' &&
+ typeof (config.auth.lti.consumerSecret) !== 'undefined'
+ ) setupLTI(config.auth.lti);
+
+ // OIDC
+ if (
+ typeof (config.auth) !== 'undefined' &&
+ (
+ (
+ typeof (config.auth.strategy) !== 'undefined' &&
+ config.auth.strategy === 'oidc'
+ )
+ // it is default strategy
+ || typeof (config.auth.strategy) === 'undefined'
+ ) &&
+ typeof (config.auth.oidc) !== 'undefined' &&
+ typeof (config.auth.oidc.issuerURL) !== 'undefined' &&
+ typeof (config.auth.oidc.clientOptions) !== 'undefined'
+ )
+ {
+ const oidcIssuer = await Issuer.discover(config.auth.oidc.issuerURL);
+
+ // Setup authentication
+ setupOIDC(oidcIssuer);
+
+ }
+
+ // SAML
+ if (
+ typeof (config.auth) !== 'undefined' &&
+ typeof (config.auth.strategy) !== 'undefined' &&
+ config.auth.strategy === 'saml' &&
+ typeof (config.auth.saml) !== 'undefined' &&
+ typeof (config.auth.saml.entryPoint) !== 'undefined' &&
+ typeof (config.auth.saml.issuer) !== 'undefined' &&
+ typeof (config.auth.saml.cert) !== 'undefined'
+ )
+ {
+ setupSAML();
+ }
+
+ // Local
+ if (
+ typeof (config.auth) !== 'undefined' &&
+ typeof (config.auth.strategy) !== 'undefined' &&
+ config.auth.strategy === 'local' &&
+ typeof (config.auth.local) !== 'undefined' &&
+ typeof (config.auth.local.users) !== 'undefined'
+ )
+ {
+ setupLocal();
+ }
+
+ app.use(passport.initialize());
+ app.use(passport.session());
+
+ // Auth strategy (by default oidc)
+ const authStrategy = (config.auth && config.auth.strategy) ? config.auth.strategy : 'oidc';
+
+ // loginparams
+ app.get('/auth/login', (req, res, next) =>
+ {
+ const state = {
+ peerId : req.query.peerId,
+ roomId : req.query.roomId
+ };
+
+ if (authStrategy== 'saml' || authStrategy=='local')
+ {
+ req.session.authState=state;
+ }
+
+ if (authStrategy === 'local' && !(req.user && req.password))
+ {
+ res.redirect('/login_dialog');
+ }
+ else
+ {
+ passport.authenticate(authStrategy, {
+ state : base64.encode(JSON.stringify(state))
+ }
+ )(req, res, next);
+ }
+ });
+
+ // lti launch
+ app.post('/auth/lti',
+ passport.authenticate('lti', { failureRedirect: '/' }),
+ (req, res) =>
+ {
+ res.redirect(`/${req.user.room}`);
+ }
+ );
+
+ // logout
+ app.get('/auth/logout', (req, res) =>
+ {
+ const { peerId } = req.session;
+
+ const peer = peers.get(peerId);
+
+ if (peer)
+ {
+ for (const role of peer.roles)
+ {
+ if (role.id !== userRoles.NORMAL.id)
+ peer.removeRole(role);
+ }
+ }
+
+ req.logout();
+ req.session.destroy(() => res.send(logoutHelper()));
+ });
+ // SAML metadata
+ app.get('/auth/metadata', (req, res) =>
+ {
+ if (config.auth && config.auth.saml &&
+ config.auth.saml.decryptionCert &&
+ config.auth.saml.signingCert)
+ {
+ const metadata = samlStrategy.generateServiceProviderMetadata(
+ config.auth.saml.decryptionCert,
+ config.auth.saml.signingCert
+ );
+
+ if (metadata)
+ {
+ res.set('Content-Type', 'text/xml');
+ res.send(metadata);
+ }
+ else
+ {
+ res.status('Error generating SAML metadata', 500);
+ }
+ }
+ else
+ res.status('Missing SAML decryptionCert or signingKey from config', 500);
+ });
+
+ // callback
+ app.all(
+ '/auth/callback',
+ passport.authenticate(authStrategy, { failureRedirect: '/auth/login', failureFlash: true }),
+ async (req, res, next) =>
+ {
+ try
+ {
+ let state;
+
+ if (authStrategy == 'saml' || authStrategy == 'local')
+ state=req.session.authState;
+ else
+ {
+ if (req.method === 'GET')
+ state = JSON.parse(base64.decode(req.query.state));
+ if (req.method === 'POST')
+ state = JSON.parse(base64.decode(req.body.state));
+ }
+ const { peerId, roomId } = state;
+
+ req.session.peerId = peerId;
+ req.session.roomId = roomId;
+
+ let peer = peers.get(peerId);
+ const room = rooms.get(roomId);
+
+ if (!peer) // User has no socket session yet, make temporary
+ peer = new Peer({ id: peerId, roomId });
+
+ if (peer.roomId !== roomId) // The peer is mischievous
+ throw new Error('peer authenticated with wrong room');
+
+ if (typeof config.userMapping === 'function')
+ {
+ await config.userMapping({
+ peer,
+ room,
+ roomId,
+ userinfo : req.user._userinfo
+ });
+ }
+
+ peer.authenticated = true;
+
+ res.send(loginHelper({
+ displayName : peer.displayName,
+ picture : peer.picture
+ }));
+ }
+ catch (error)
+ {
+ return next(error);
+ }
+ }
+ );
+}
+
+async function runHttpsServer()
+{
+ app.use(compression());
+
+ app.use('/.well-known/acme-challenge', express.static('public/.well-known/acme-challenge'));
+
+ app.all('*', async (req, res, next) =>
+ {
+ if (req.secure || config.httpOnly)
+ {
+ let ltiURL;
+
+ try
+ {
+ ltiURL = new URL(`${req.protocol}://${req.get('host')}${req.originalUrl}`);
+ }
+ catch (error)
+ {
+ logger.error('Error parsing LTI url: %o', error);
+ }
+
+ if (
+ req.isAuthenticated &&
+ req.user &&
+ req.user.displayName &&
+ !ltiURL.searchParams.get('displayName') &&
+ !isPathAlreadyTaken(req.url)
+ )
+ {
+
+ ltiURL.searchParams.append('displayName', req.user.displayName);
+
+ res.redirect(ltiURL);
+ }
+ else
+ {
+ const specialChars = "<>@!^*()[]{}:;|'\"\\,~`";
+
+ for (let i = 0; i < specialChars.length; i++)
+ {
+ if (req.url.substring(1).indexOf(specialChars[i]) > -1)
+ {
+ req.url = `/${encodeURIComponent(encodeURI(req.url.substring(1)))}`;
+ res.redirect(`${req.url}`);
+ }
+ }
+
+ return next();
+ }
+ }
+ else
+ res.redirect(`https://${req.hostname}${req.url}`);
+
+ });
+
+ // Serve all files in the public folder as static files.
+ app.use(express.static('public'));
+
+ app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`));
+
+ if (config.httpOnly === true)
+ {
+ // http
+ mainListener = http.createServer(app);
+ }
+ else
+ {
+ // https
+ mainListener = spdy.createServer(tls, app);
+
+ // http
+ const redirectListener = http.createServer(app);
+
+ if (config.listeningHost)
+ redirectListener.listen(config.listeningRedirectPort, config.listeningHost);
+ else
+ redirectListener.listen(config.listeningRedirectPort);
+ }
+
+ // https or http
+ if (config.listeningHost)
+ mainListener.listen(config.listeningPort, config.listeningHost);
+ else
+ mainListener.listen(config.listeningPort);
+}
+
+function isPathAlreadyTaken(actualUrl)
+{
+ const alreadyTakenPath =
+ [
+ '/config/',
+ '/static/',
+ '/images/',
+ '/sounds/',
+ '/favicon.',
+ '/auth/'
+ ];
+
+ alreadyTakenPath.forEach((path) =>
+ {
+ if (actualUrl.toString().startsWith(path))
+ return true;
+ });
+
+ return false;
+}
+
+/**
+ * Create a WebSocketServer to allow WebSocket connections from browsers.
+ */
+async function runWebSocketServer()
+{
+ io = require('socket.io')(mainListener, { cookie: false });
+
+ io.use(
+ sharedSession(session, sharedCookieParser, { autoSave: true })
+ );
+
+ // Handle connections from clients.
+ io.on('connection', (socket) =>
+ {
+ const { roomId, peerId } = socket.handshake.query;
+
+ if (!roomId || !peerId)
+ {
+ logger.warn('connection request without roomId and/or peerId');
+
+ socket.disconnect(true);
+
+ return;
+ }
+
+ logger.info(
+ 'connection request [roomId:"%s", peerId:"%s"]', roomId, peerId);
+
+ queue.push(async () =>
+ {
+ const { token } = socket.handshake.session;
+
+ const room = await getOrCreateRoom({ roomId });
+
+ let peer = peers.get(peerId);
+ let returning = false;
+
+ if (peer && !token)
+ { // Don't allow hijacking sessions
+ socket.disconnect(true);
+
+ return;
+ }
+ else if (token && room.verifyPeer({ id: peerId, token }))
+ { // Returning user, remove if old peer exists
+ if (peer)
+ peer.close();
+
+ returning = true;
+ }
+
+ peer = new Peer({ id: peerId, roomId, socket });
+
+ peers.set(peerId, peer);
+
+ peer.on('close', () =>
+ {
+ peers.delete(peerId);
+
+ statusLog();
+ });
+
+ if (
+ Boolean(socket.handshake.session.passport) &&
+ Boolean(socket.handshake.session.passport.user)
+ )
+ {
+ const {
+ id,
+ displayName,
+ picture,
+ email,
+ _userinfo
+ } = socket.handshake.session.passport.user;
+
+ peer.authId = id;
+ peer.displayName = displayName;
+ peer.picture = picture;
+ peer.email = email;
+ peer.authenticated = true;
+
+ if (typeof config.userMapping === 'function')
+ {
+ await config.userMapping({ peer, room, roomId, userinfo: _userinfo });
+ }
+ }
+
+ room.handlePeer({ peer, returning });
+
+ statusLog();
+ })
+ .catch((error) =>
+ {
+ logger.error('room creation or room joining failed [error:"%o"]', error);
+
+ if (socket)
+ socket.disconnect(true);
+
+ return;
+ });
+ });
+}
+
+/**
+ * Launch as many mediasoup Workers as given in the configuration file.
+ */
+async function runMediasoupWorkers()
+{
+ const { numWorkers } = config.mediasoup;
+
+ logger.info('running %d mediasoup Workers...', numWorkers);
+
+ for (let i = 0; i < numWorkers; ++i)
+ {
+ const worker = await mediasoup.createWorker(
+ {
+ logLevel : config.mediasoup.worker.logLevel,
+ logTags : config.mediasoup.worker.logTags,
+ rtcMinPort : config.mediasoup.worker.rtcMinPort,
+ rtcMaxPort : config.mediasoup.worker.rtcMaxPort
+ });
+
+ worker.on('died', () =>
+ {
+ logger.error(
+ 'mediasoup Worker died, exiting in 2 seconds... [pid:%d]', worker.pid);
+
+ setTimeout(() => process.exit(1), 2000);
+ });
+
+ mediasoupWorkers.push(worker);
+ }
+}
+
+/**
+ * Get a Room instance (or create one if it does not exist).
+ */
+async function getOrCreateRoom({ roomId })
+{
+ let room = rooms.get(roomId);
+
+ // If the Room does not exist create a new one.
+ if (!room)
+ {
+ logger.info('creating a new Room [roomId:"%s"]', roomId);
+
+ // const mediasoupWorker = getMediasoupWorker();
+
+ room = await Room.create({ mediasoupWorkers, roomId, peers });
+
+ rooms.set(roomId, room);
+
+ statusLog();
+
+ room.on('close', () =>
+ {
+ rooms.delete(roomId);
+
+ statusLog();
+ });
+ }
+
+ return room;
+}
+
+run();
diff --git a/meet/server/userRoles.js b/meet/server/userRoles.js
new file mode 100644
index 00000000..32226794
--- /dev/null
+++ b/meet/server/userRoles.js
@@ -0,0 +1,16 @@
+module.exports = {
+ // These can be changed, id must be unique.
+
+ // A person can give other peers any role that is promotable: true
+ // with a level up to and including their own highest role.
+ // Example: A MODERATOR can give other peers PRESENTER and MODERATOR
+ // roles (all peers always have NORMAL)
+ ADMIN : { id: 2529, label: 'admin', level: 50, promotable: true },
+ MODERATOR : { id: 5337, label: 'moderator', level: 40, promotable: true },
+ PRESENTER : { id: 9583, label: 'presenter', level: 30, promotable: true },
+ AUTHENTICATED : { id: 5714, label: 'authenticated', level: 20, promotable: false },
+ // Don't change anything after this point
+
+ // All users have this role by default, do not change or remove this role
+ NORMAL : { id: 4261, label: 'normal', level: 10, promotable: false }
+};
\ No newline at end of file
diff --git a/meet/server/utils/password_encode.js b/meet/server/utils/password_encode.js
new file mode 100644
index 00000000..cacd8c45
--- /dev/null
+++ b/meet/server/utils/password_encode.js
@@ -0,0 +1,10 @@
+const bcrypt = require('bcrypt');
+const saltRounds=10;
+
+if (process.argv.length == 3)
+{
+ const cleartextPassword = process.argv[2];
+
+ // eslint-disable-next-line no-console
+ console.log(bcrypt.hashSync(cleartextPassword, saltRounds));
+}