├── .npmignore ├── src ├── api │ ├── index.ts │ └── http-api.ts ├── database │ ├── index.ts │ ├── database-driver.ts │ ├── database.ts │ ├── redis.ts │ └── sqlite.ts ├── index.ts ├── subscribers │ ├── index.ts │ ├── subscriber.ts │ ├── redis-subscriber.ts │ └── http-subscriber.ts ├── channels │ ├── index.ts │ ├── private-channel.ts │ ├── channel.ts │ └── presence-channel.ts ├── cli │ ├── index.ts │ └── cli.ts ├── log.ts ├── server.ts └── echo-server.ts ├── .gitignore ├── examples ├── server.js └── server_stop.js ├── bin └── server.js ├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── tsconfig.json ├── LICENSE ├── package.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── README.md └── yarn.lock /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | examples/ 3 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export { HttpApi } from './http-api'; 2 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './database-driver'; 2 | export * from './database'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | examples/ 4 | npm-debug.log 5 | yarn-error.log 6 | .idea/ 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { EchoServer } from './echo-server'; 2 | 3 | module.exports = new EchoServer; 4 | -------------------------------------------------------------------------------- /src/subscribers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redis-subscriber'; 2 | export * from './http-subscriber'; 3 | export * from './subscriber'; 4 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | var echo = require('../dist/index.js'); 2 | 3 | var options = { 4 | host: 'http://example.dev', 5 | }; 6 | 7 | echo.run(options); 8 | -------------------------------------------------------------------------------- /src/channels/index.ts: -------------------------------------------------------------------------------- 1 | export { PresenceChannel } from './presence-channel'; 2 | export { PrivateChannel } from './private-channel'; 3 | export { Channel } from './channel'; 4 | -------------------------------------------------------------------------------- /examples/server_stop.js: -------------------------------------------------------------------------------- 1 | var echo = require('../dist/index.js'); 2 | 3 | var options = require('../laravel-echo-server'); 4 | 5 | echo.run(options).then(echo => { 6 | echo.stop(); 7 | }); 8 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | /** 4 | * Laravel Echo Server 5 | * 6 | * This file starts the socket.io server and loads configuration from a 7 | * echo-server.json file if available. 8 | * 9 | */ 10 | var LaravelEchoServerCli = require('../dist/cli'); 11 | 12 | process.title = 'laravel-echo-server'; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [{.babelrc,package.json}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /src/database/database-driver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for key/value data stores. 3 | */ 4 | export interface DatabaseDriver { 5 | /** 6 | * Get a value from the database. 7 | */ 8 | get(key: string): Promise; 9 | 10 | /** 11 | * Set a value to the database. 12 | */ 13 | set(key: string, value: any): void; 14 | } 15 | -------------------------------------------------------------------------------- /src/subscribers/subscriber.ts: -------------------------------------------------------------------------------- 1 | export interface Subscriber { 2 | /** 3 | * Subscribe to incoming events. 4 | * 5 | * @param {Function} callback 6 | * @return {void} 7 | */ 8 | subscribe(callback: Function): Promise; 9 | 10 | /** 11 | * Unsubscribe from incoming events 12 | * 13 | * @return {Promise} 14 | */ 15 | unsubscribe(): Promise; 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Only report bugs with this package 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project, ask questions on sites like stackoverflow 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": false, 7 | "outDir": "./dist", 8 | "pretty": true, 9 | "preserveConstEnums": true, 10 | "removeComments": true, 11 | "target": "es5", 12 | "lib": ["es2015", "dom"], 13 | "types": ["node"] 14 | }, 15 | "files": [ 16 | "src/index.ts", 17 | "src/cli/index.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ], 22 | "compileOnSave": false, 23 | "buildOnSave": false, 24 | "atom": { 25 | "formatOnSave": false, 26 | "rewriteTsconfig": false 27 | }, 28 | "typeRoots": [ 29 | "../node_modules/@types" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import { Cli } from './cli'; 2 | 3 | let cli = new Cli(); 4 | 5 | let yargs = require('yargs') 6 | .usage("Usage: laravel-echo-server [options]") 7 | .command("start", "Starts the server.", yargs => cli.start(yargs)) 8 | .command("stop", "Stops the server.", yargs => cli.stop(yargs)) 9 | .command(["configure", "init"], "Creates a custom config file.", yargs => cli.configure(yargs)) // Has an alias of "init" for backwards compatibility, remove in next version 10 | .command("client:add [id]", "Register a client that can make api requests.", yargs => cli.clientAdd(yargs)) 11 | .command("client:remove [id]", "Remove a registered client.", yargs => cli.clientRemove(yargs)) 12 | .demandCommand(1, "Please provide a valid command.") 13 | .help("help") 14 | .alias("help", "h"); 15 | 16 | yargs.$0 = ''; 17 | 18 | var argv = yargs.argv; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Thiery Laverdure 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-echo-server", 3 | "version": "1.6.3", 4 | "description": "Laravel Echo Node JS Server for Socket.io", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/tlaverdure/Laravel-Echo-Server.git" 8 | }, 9 | "main": "dist/index.js", 10 | "keywords": [ 11 | "laravel", 12 | "socket.io" 13 | ], 14 | "author": "Thiery Laverdure", 15 | "license": "MIT", 16 | "jshintConfig": { 17 | "esversion": 6 18 | }, 19 | "scripts": { 20 | "build": "tsc", 21 | "dev": "tsc -w", 22 | "prepublish": "npm run build" 23 | }, 24 | "dependencies": { 25 | "colors": "1.4.0", 26 | "dotenv": "^8.2.0", 27 | "express": "^4.17.1", 28 | "inquirer": "^7.1.0", 29 | "ioredis": "^4.16.0", 30 | "lodash": "^4.17.15", 31 | "request": "^2.88.2", 32 | "socket.io": "^2.3.0", 33 | "yargs": "^15.3.1" 34 | }, 35 | "devDependencies": { 36 | "@types/lodash": "^4.14.149", 37 | "@types/node": "^13.9.1", 38 | "typescript": "^3.8.3" 39 | }, 40 | "peerDependecies": { 41 | "sqlite3": "^3.1.13" 42 | }, 43 | "bin": { 44 | "laravel-echo-server": "bin/server.js" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/database/database.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseDriver } from './database-driver'; 2 | import { SQLiteDatabase } from './sqlite'; 3 | import { RedisDatabase } from './redis'; 4 | import { Log } from './../log'; 5 | 6 | /** 7 | * Class that controls the key/value data store. 8 | */ 9 | export class Database implements DatabaseDriver { 10 | /** 11 | * Database driver. 12 | */ 13 | private driver: DatabaseDriver; 14 | 15 | /** 16 | * Create a new database instance. 17 | */ 18 | constructor(private options: any) { 19 | if (options.database == 'redis') { 20 | this.driver = new RedisDatabase(options); 21 | } else if (options.database == 'sqlite') { 22 | this.driver = new SQLiteDatabase(options); 23 | } else { 24 | Log.error('Database driver not set.'); 25 | } 26 | } 27 | 28 | /** 29 | * Get a value from the database. 30 | */ 31 | get(key: string): Promise { 32 | return this.driver.get(key) 33 | }; 34 | 35 | /** 36 | * Set a value to the database. 37 | */ 38 | set(key: string, value: any): void { 39 | this.driver.set(key, value); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/database/redis.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseDriver } from './database-driver'; 2 | var Redis = require('ioredis'); 3 | 4 | export class RedisDatabase implements DatabaseDriver { 5 | /** 6 | * Redis client. 7 | */ 8 | private _redis: any; 9 | 10 | /** 11 | * Create a new cache instance. 12 | */ 13 | constructor(private options) { 14 | this._redis = new Redis(options.databaseConfig.redis); 15 | } 16 | 17 | /** 18 | * Retrieve data from redis. 19 | */ 20 | get(key: string): Promise { 21 | return new Promise((resolve, reject) => { 22 | this._redis.get(key).then(value => resolve(JSON.parse(value))); 23 | }); 24 | } 25 | 26 | /** 27 | * Store data to cache. 28 | */ 29 | set(key: string, value: any): void { 30 | this._redis.set(key, JSON.stringify(value)); 31 | if (this.options.databaseConfig.publishPresence === true && /^presence-.*:members$/.test(key)) { 32 | this._redis.publish('PresenceChannelUpdated', JSON.stringify({ 33 | "event": { 34 | "channel": key, 35 | "members": value 36 | } 37 | })); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/database/sqlite.ts: -------------------------------------------------------------------------------- 1 | let sqlite3; 2 | import { DatabaseDriver } from './database-driver'; 3 | try { 4 | sqlite3 = require('sqlite3'); 5 | } catch (e) { } 6 | 7 | export class SQLiteDatabase implements DatabaseDriver { 8 | /** 9 | * SQLite client. 10 | */ 11 | private _sqlite: any; 12 | 13 | /** 14 | * Create a new cache instance. 15 | */ 16 | constructor(private options) { 17 | if (!sqlite3) return; 18 | 19 | let path = process.cwd() + options.databaseConfig.sqlite.databasePath; 20 | this._sqlite = new sqlite3.cached.Database(path); 21 | this._sqlite.serialize(() => { 22 | this._sqlite.run('CREATE TABLE IF NOT EXISTS key_value (key VARCHAR(255), value TEXT)'); 23 | this._sqlite.run('CREATE UNIQUE INDEX IF NOT EXISTS key_index ON key_value (key)'); 24 | }); 25 | } 26 | 27 | /** 28 | * Retrieve data from redis. 29 | */ 30 | get(key: string): Promise { 31 | return new Promise((resolve, reject) => { 32 | this._sqlite.get("SELECT value FROM key_value WHERE key = $key", { 33 | $key: key, 34 | }, (error, row) => { 35 | if (error) { 36 | reject(error); 37 | } 38 | 39 | let result = row ? JSON.parse(row.value) : null; 40 | 41 | resolve(result); 42 | }); 43 | }); 44 | } 45 | 46 | /** 47 | * Store data to cache. 48 | */ 49 | set(key: string, value: any): void { 50 | this._sqlite.run("INSERT OR REPLACE INTO key_value (key, value) VALUES ($key, $value)", { 51 | $key: key, 52 | $value: JSON.stringify(value) 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | var colors = require('colors'); 2 | 3 | colors.setTheme({ 4 | silly: 'rainbow', 5 | input: 'grey', 6 | verbose: 'cyan', 7 | prompt: 'grey', 8 | info: 'cyan', 9 | data: 'grey', 10 | help: 'cyan', 11 | warn: 'yellow', 12 | debug: 'blue', 13 | error: 'red', 14 | h1: 'grey', 15 | h2: 'yellow' 16 | }); 17 | 18 | export class Log { 19 | /** 20 | * Console log heading 1. 21 | * 22 | * @param {string|object} message 23 | * @return {void} 24 | */ 25 | static title(message: any): void { 26 | console.log(colors.bold(message)); 27 | } 28 | 29 | /** 30 | * Console log heaing 2. 31 | * 32 | * @param {string|object} message 33 | * @return {void} 34 | */ 35 | static subtitle(message: any): void { 36 | console.log(colors.h2.bold(message)); 37 | } 38 | 39 | /** 40 | * Console log info. 41 | * 42 | * @param {string|object} message 43 | * @return {void} 44 | */ 45 | static info(message: any): void { 46 | console.log(colors.info(message)); 47 | } 48 | 49 | /** 50 | * Console log success. 51 | * 52 | * @param {string|object} message 53 | * @return {void} 54 | */ 55 | static success(message: any): void { 56 | console.log(colors.green('\u2714 '), message); 57 | } 58 | 59 | /** 60 | * 61 | * 62 | * Console log info. 63 | * 64 | * @param {string|object} message 65 | * @return {void} 66 | */ 67 | static error(message: any): void { 68 | console.log(colors.error(message)); 69 | } 70 | 71 | /** 72 | * Console log warning. 73 | * 74 | * @param {string|object} message 75 | * @return {void} 76 | */ 77 | static warning(message: any): void { 78 | console.log(colors.warn('\u26A0 ' + message)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.6.3 2 | 3 | ## Fixed 4 | 5 | - Security patch - update dependencies 6 | 7 | # 1.6.2 8 | 9 | ## Added 10 | 11 | - Add method to stop the server (#502)[https://github.com/tlaverdure/laravel-echo-server/pull/502] 12 | - Document how to use Redis Sentinel (#437)[https://github.com/tlaverdure/laravel-echo-server/pull/437] 13 | - Add Apache proxt example tp docs (#361)[https://github.com/tlaverdure/laravel-echo-server/pull/361] 14 | - Expose user member user info in API. (#356)[https://github.com/tlaverdure/laravel-echo-server/pull/356] 15 | 16 | ## Fixed 17 | 18 | - Fix crash when invalid referer is sent (#513)[https://github.com/tlaverdure/laravel-echo-server/pull/513] 19 | 20 | # 1.6.1 21 | 22 | - Update dependencies for security reasons. 23 | 24 | # 1.6.0 25 | 26 | Add support for Redis prefixing. 27 | 28 | # 1.5.0 29 | 30 | Add `stop` command 31 | 32 | # 1.3.7 33 | 34 | Allow variables in .env file to set options in the server configuration. 35 | 36 | ### Updates 37 | 38 | - Auth Host: `LARAVEL_ECHO_SERVER_AUTH_HOST` _Note_: This option will fall back to the `LARAVEL_ECHO_SERVER_HOST` option as the default if that is set in the .env file. 39 | 40 | - _Host_: `LARAVEL_ECHO_SERVER_HOST` 41 | 42 | - _Port_: `LARAVEL_ECHO_SERVER_PORT` 43 | 44 | - _Debug_: `LARAVEL_ECHO_SERVER_DEBUG` 45 | 46 | # 1.3.3 47 | 48 | Return a better error when member data is not present when joining presence channels. 49 | 50 | # 1.3.2 51 | 52 | Added CORS support to the HTTP API. 53 | 54 | # 1.2.9 55 | 56 | Updated to socket.io v2 57 | 58 | # 1.2.0 59 | 60 | ## Upgrade Guide 61 | 62 | - Re-install laravel-echo-server globally using the command. 63 | 64 | ``` 65 | npm install -g laravel-echo-server 66 | ``` 67 | 68 | - In your `laravel-echo-server.json` file, remove the section named `referrers`. Then follow the [instructions](https://github.com/tlaverdure/laravel-echo-server#api-clients) to generate an app id and key. The `referrers` section has been replaced with `clients`. 69 | -------------------------------------------------------------------------------- /src/subscribers/redis-subscriber.ts: -------------------------------------------------------------------------------- 1 | var Redis = require('ioredis'); 2 | import { Log } from './../log'; 3 | import { Subscriber } from './subscriber'; 4 | 5 | export class RedisSubscriber implements Subscriber { 6 | /** 7 | * Redis pub/sub client. 8 | * 9 | * @type {object} 10 | */ 11 | private _redis: any; 12 | 13 | /** 14 | * 15 | * KeyPrefix for used in the redis Connection 16 | * 17 | * @type {String} 18 | */ 19 | private _keyPrefix: string; 20 | 21 | /** 22 | * Create a new instance of subscriber. 23 | * 24 | * @param {any} options 25 | */ 26 | constructor(private options) { 27 | this._keyPrefix = options.databaseConfig.redis.keyPrefix || ''; 28 | this._redis = new Redis(options.databaseConfig.redis); 29 | } 30 | 31 | /** 32 | * Subscribe to events to broadcast. 33 | * 34 | * @return {Promise} 35 | */ 36 | subscribe(callback): Promise { 37 | 38 | return new Promise((resolve, reject) => { 39 | this._redis.on('pmessage', (subscribed, channel, message) => { 40 | try { 41 | message = JSON.parse(message); 42 | 43 | if (this.options.devMode) { 44 | Log.info("Channel: " + channel); 45 | Log.info("Event: " + message.event); 46 | } 47 | 48 | callback(channel.substring(this._keyPrefix.length), message); 49 | } catch (e) { 50 | if (this.options.devMode) { 51 | Log.info("No JSON message"); 52 | } 53 | } 54 | }); 55 | 56 | this._redis.psubscribe(`${this._keyPrefix}*`, (err, count) => { 57 | if (err) { 58 | reject('Redis could not subscribe.') 59 | } 60 | 61 | Log.success('Listening for redis events...'); 62 | 63 | resolve(); 64 | }); 65 | }); 66 | } 67 | 68 | /** 69 | * Unsubscribe from events to broadcast. 70 | * 71 | * @return {Promise} 72 | */ 73 | unsubscribe(): Promise { 74 | return new Promise((resolve, reject) => { 75 | try { 76 | this._redis.disconnect(); 77 | resolve(); 78 | } catch(e) { 79 | reject('Could not disconnect from redis -> ' + e); 80 | } 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hello@spacestud.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/subscribers/http-subscriber.ts: -------------------------------------------------------------------------------- 1 | import { Log } from './../log'; 2 | import { Subscriber } from './subscriber'; 3 | var url = require('url'); 4 | 5 | export class HttpSubscriber implements Subscriber { 6 | /** 7 | * Create new instance of http subscriber. 8 | * 9 | * @param {any} express 10 | */ 11 | constructor(private express, private options) { } 12 | 13 | /** 14 | * Subscribe to events to broadcast. 15 | * 16 | * @return {void} 17 | */ 18 | subscribe(callback): Promise { 19 | return new Promise((resolve, reject) => { 20 | // Broadcast a message to a channel 21 | this.express.post('/apps/:appId/events', (req, res) => { 22 | let body: any = []; 23 | res.on('error', (error) => { 24 | if (this.options.devMode) { 25 | Log.error(error); 26 | } 27 | }); 28 | 29 | req.on('data', (chunk) => body.push(chunk)) 30 | .on('end', () => this.handleData(req, res, body, callback)); 31 | }); 32 | 33 | Log.success('Listening for http events...'); 34 | 35 | resolve(); 36 | }); 37 | } 38 | 39 | /** 40 | * Unsubscribe from events to broadcast. 41 | * 42 | * @return {Promise} 43 | */ 44 | unsubscribe(): Promise { 45 | return new Promise((resolve, reject) => { 46 | try { 47 | this.express.post('/apps/:appId/events', (req, res) => { 48 | res.status(404).send(); 49 | }); 50 | resolve(); 51 | } catch(e) { 52 | reject('Could not overwrite the event endpoint -> ' + e); 53 | } 54 | }); 55 | } 56 | 57 | /** 58 | * Handle incoming event data. 59 | * 60 | * @param {any} req 61 | * @param {any} res 62 | * @param {any} body 63 | * @param {Function} broadcast 64 | * @return {boolean} 65 | */ 66 | handleData(req, res, body, broadcast): boolean { 67 | body = JSON.parse(Buffer.concat(body).toString()); 68 | 69 | if ((body.channels || body.channel) && body.name && body.data) { 70 | 71 | var data = body.data; 72 | try { 73 | data = JSON.parse(data); 74 | } catch (e) { } 75 | 76 | var message = { 77 | event: body.name, 78 | data: data, 79 | socket: body.socket_id 80 | } 81 | var channels = body.channels || [body.channel]; 82 | 83 | if (this.options.devMode) { 84 | Log.info("Channel: " + channels.join(', ')); 85 | Log.info("Event: " + message.event); 86 | } 87 | 88 | channels.forEach(channel => broadcast(channel, message)); 89 | } else { 90 | return this.badResponse( 91 | req, 92 | res, 93 | 'Event must include channel, event name and data' 94 | ); 95 | } 96 | 97 | res.json({ message: 'ok' }) 98 | } 99 | 100 | /** 101 | * Handle bad requests. 102 | * 103 | * @param {any} req 104 | * @param {any} res 105 | * @param {string} message 106 | * @return {boolean} 107 | */ 108 | badResponse(req: any, res: any, message: string): boolean { 109 | res.statusCode = 400; 110 | res.json({ error: message }); 111 | 112 | return false; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/channels/private-channel.ts: -------------------------------------------------------------------------------- 1 | let request = require('request'); 2 | let url = require('url'); 3 | import { Channel } from './channel'; 4 | import { Log } from './../log'; 5 | 6 | export class PrivateChannel { 7 | /** 8 | * Create a new private channel instance. 9 | */ 10 | constructor(private options: any) { 11 | this.request = request; 12 | } 13 | 14 | /** 15 | * Request client. 16 | */ 17 | private request: any; 18 | 19 | /** 20 | * Send authentication request to application server. 21 | */ 22 | authenticate(socket: any, data: any): Promise { 23 | let options = { 24 | url: this.authHost(socket) + this.options.authEndpoint, 25 | form: { channel_name: data.channel }, 26 | headers: (data.auth && data.auth.headers) ? data.auth.headers : {}, 27 | rejectUnauthorized: false 28 | }; 29 | 30 | if (this.options.devMode) { 31 | Log.info(`[${new Date().toISOString()}] - Sending auth request to: ${options.url}\n`); 32 | } 33 | 34 | return this.serverRequest(socket, options); 35 | } 36 | 37 | /** 38 | * Get the auth host based on the Socket. 39 | */ 40 | protected authHost(socket: any): string { 41 | let authHosts = (this.options.authHost) ? 42 | this.options.authHost : this.options.host; 43 | 44 | if (typeof authHosts === "string") { 45 | authHosts = [authHosts]; 46 | } 47 | 48 | let authHostSelected = authHosts[0] || 'http://localhost'; 49 | 50 | if (socket.request.headers.referer) { 51 | let referer = url.parse(socket.request.headers.referer); 52 | 53 | for (let authHost of authHosts) { 54 | authHostSelected = authHost; 55 | 56 | if (this.hasMatchingHost(referer, authHost)) { 57 | authHostSelected = `${referer.protocol}//${referer.host}`; 58 | break; 59 | } 60 | }; 61 | } 62 | 63 | if (this.options.devMode) { 64 | Log.error(`[${new Date().toISOString()}] - Preparing authentication request to: ${authHostSelected}`); 65 | } 66 | 67 | return authHostSelected; 68 | } 69 | 70 | /** 71 | * Check if there is a matching auth host. 72 | */ 73 | protected hasMatchingHost(referer: any, host: any): boolean { 74 | return (referer.hostname && referer.hostname.substr(referer.hostname.indexOf('.')) === host) || 75 | `${referer.protocol}//${referer.host}` === host || 76 | referer.host === host; 77 | } 78 | 79 | /** 80 | * Send a request to the server. 81 | */ 82 | protected serverRequest(socket: any, options: any): Promise { 83 | return new Promise((resolve, reject) => { 84 | options.headers = this.prepareHeaders(socket, options); 85 | let body; 86 | 87 | this.request.post(options, (error, response, body, next) => { 88 | if (error) { 89 | if (this.options.devMode) { 90 | Log.error(`[${new Date().toISOString()}] - Error authenticating ${socket.id} for ${options.form.channel_name}`); 91 | Log.error(error); 92 | } 93 | 94 | reject({ reason: 'Error sending authentication request.', status: 0 }); 95 | } else if (response.statusCode !== 200) { 96 | if (this.options.devMode) { 97 | Log.warning(`[${new Date().toISOString()}] - ${socket.id} could not be authenticated to ${options.form.channel_name}`); 98 | Log.error(response.body); 99 | } 100 | 101 | reject({ reason: 'Client can not be authenticated, got HTTP status ' + response.statusCode, status: response.statusCode }); 102 | } else { 103 | if (this.options.devMode) { 104 | Log.info(`[${new Date().toISOString()}] - ${socket.id} authenticated for: ${options.form.channel_name}`); 105 | } 106 | 107 | try { 108 | body = JSON.parse(response.body); 109 | } catch (e) { 110 | body = response.body 111 | } 112 | 113 | resolve(body); 114 | } 115 | }); 116 | }); 117 | } 118 | 119 | /** 120 | * Prepare headers for request to app server. 121 | */ 122 | protected prepareHeaders(socket: any, options: any): any { 123 | options.headers['Cookie'] = options.headers['Cookie'] || socket.request.headers.cookie; 124 | options.headers['X-Requested-With'] = 'XMLHttpRequest'; 125 | 126 | return options.headers; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/channels/channel.ts: -------------------------------------------------------------------------------- 1 | import { PresenceChannel } from './presence-channel'; 2 | import { PrivateChannel } from './private-channel'; 3 | import { Log } from './../log'; 4 | 5 | export class Channel { 6 | /** 7 | * Channels and patters for private channels. 8 | */ 9 | protected _privateChannels: string[] = ['private-*', 'presence-*']; 10 | 11 | /** 12 | * Allowed client events 13 | */ 14 | protected _clientEvents: string[] = ['client-*']; 15 | 16 | /** 17 | * Private channel instance. 18 | */ 19 | private: PrivateChannel; 20 | 21 | /** 22 | * Presence channel instance. 23 | */ 24 | presence: PresenceChannel; 25 | 26 | /** 27 | * Create a new channel instance. 28 | */ 29 | constructor(private io, private options) { 30 | this.private = new PrivateChannel(options); 31 | this.presence = new PresenceChannel(io, options); 32 | 33 | if (this.options.devMode) { 34 | Log.success('Channels are ready.'); 35 | } 36 | } 37 | 38 | /** 39 | * Join a channel. 40 | */ 41 | join(socket, data): void { 42 | if (data.channel) { 43 | if (this.isPrivate(data.channel)) { 44 | this.joinPrivate(socket, data); 45 | } else { 46 | socket.join(data.channel); 47 | this.onJoin(socket, data.channel); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Trigger a client message 54 | */ 55 | clientEvent(socket, data): void { 56 | try { 57 | data = JSON.parse(data); 58 | } catch (e) { 59 | data = data; 60 | } 61 | 62 | if (data.event && data.channel) { 63 | if (this.isClientEvent(data.event) && 64 | this.isPrivate(data.channel) && 65 | this.isInChannel(socket, data.channel)) { 66 | this.io.sockets.connected[socket.id] 67 | .broadcast.to(data.channel) 68 | .emit(data.event, data.channel, data.data); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Leave a channel. 75 | */ 76 | leave(socket: any, channel: string, reason: string): void { 77 | if (channel) { 78 | if (this.isPresence(channel)) { 79 | this.presence.leave(socket, channel) 80 | } 81 | 82 | socket.leave(channel); 83 | 84 | if (this.options.devMode) { 85 | Log.info(`[${new Date().toISOString()}] - ${socket.id} left channel: ${channel} (${reason})`); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * Check if the incoming socket connection is a private channel. 92 | */ 93 | isPrivate(channel: string): boolean { 94 | let isPrivate = false; 95 | 96 | this._privateChannels.forEach(privateChannel => { 97 | let regex = new RegExp(privateChannel.replace('\*', '.*')); 98 | if (regex.test(channel)) isPrivate = true; 99 | }); 100 | 101 | return isPrivate; 102 | } 103 | 104 | /** 105 | * Join private channel, emit data to presence channels. 106 | */ 107 | joinPrivate(socket: any, data: any): void { 108 | this.private.authenticate(socket, data).then(res => { 109 | socket.join(data.channel); 110 | 111 | if (this.isPresence(data.channel)) { 112 | var member = res.channel_data; 113 | try { 114 | member = JSON.parse(res.channel_data); 115 | } catch (e) { } 116 | 117 | this.presence.join(socket, data.channel, member); 118 | } 119 | 120 | this.onJoin(socket, data.channel); 121 | }, error => { 122 | if (this.options.devMode) { 123 | Log.error(error.reason); 124 | } 125 | 126 | this.io.sockets.to(socket.id) 127 | .emit('subscription_error', data.channel, error.status); 128 | }); 129 | } 130 | 131 | /** 132 | * Check if a channel is a presence channel. 133 | */ 134 | isPresence(channel: string): boolean { 135 | return channel.lastIndexOf('presence-', 0) === 0; 136 | } 137 | 138 | /** 139 | * On join a channel log success. 140 | */ 141 | onJoin(socket: any, channel: string): void { 142 | if (this.options.devMode) { 143 | Log.info(`[${new Date().toISOString()}] - ${socket.id} joined channel: ${channel}`); 144 | } 145 | } 146 | 147 | /** 148 | * Check if client is a client event 149 | */ 150 | isClientEvent(event: string): boolean { 151 | let isClientEvent = false; 152 | 153 | this._clientEvents.forEach(clientEvent => { 154 | let regex = new RegExp(clientEvent.replace('\*', '.*')); 155 | if (regex.test(event)) isClientEvent = true; 156 | }); 157 | 158 | return isClientEvent; 159 | } 160 | 161 | /** 162 | * Check if a socket has joined a channel. 163 | */ 164 | isInChannel(socket: any, channel: string): boolean { 165 | return !!socket.rooms[channel]; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/channels/presence-channel.ts: -------------------------------------------------------------------------------- 1 | import { Database } from './../database'; 2 | import { Log } from './../log'; 3 | var _ = require("lodash"); 4 | 5 | export class PresenceChannel { 6 | /** 7 | * Database instance. 8 | */ 9 | db: Database; 10 | 11 | /** 12 | * Create a new Presence channel instance. 13 | */ 14 | constructor(private io, private options: any) { 15 | this.db = new Database(options); 16 | } 17 | 18 | /** 19 | * Get the members of a presence channel. 20 | */ 21 | getMembers(channel: string): Promise { 22 | return this.db.get(channel + ":members"); 23 | } 24 | 25 | /** 26 | * Check if a user is on a presence channel. 27 | */ 28 | isMember(channel: string, member: any): Promise { 29 | return new Promise((resolve, reject) => { 30 | this.getMembers(channel).then( 31 | (members) => { 32 | this.removeInactive(channel, members, member).then( 33 | (members: any) => { 34 | let search = members.filter( 35 | (m) => m.user_id == member.user_id 36 | ); 37 | 38 | if (search && search.length) { 39 | resolve(true); 40 | } 41 | 42 | resolve(false); 43 | } 44 | ); 45 | }, 46 | (error) => Log.error(error) 47 | ); 48 | }); 49 | } 50 | 51 | /** 52 | * Remove inactive channel members from the presence channel. 53 | */ 54 | removeInactive(channel: string, members: any[], member: any): Promise { 55 | return new Promise((resolve, reject) => { 56 | this.io 57 | .of("/") 58 | .in(channel) 59 | .clients((error, clients) => { 60 | members = members || []; 61 | members = members.filter((member) => { 62 | return clients.indexOf(member.socketId) >= 0; 63 | }); 64 | 65 | this.db.set(channel + ":members", members); 66 | 67 | resolve(members); 68 | }); 69 | }); 70 | } 71 | 72 | /** 73 | * Join a presence channel and emit that they have joined only if it is the 74 | * first instance of their presence. 75 | */ 76 | join(socket: any, channel: string, member: any) { 77 | if (!member) { 78 | if (this.options.devMode) { 79 | Log.error( 80 | "Unable to join channel. Member data for presence channel missing" 81 | ); 82 | } 83 | 84 | return; 85 | } 86 | 87 | this.isMember(channel, member).then( 88 | (is_member) => { 89 | this.getMembers(channel).then( 90 | (members) => { 91 | members = members || []; 92 | member.socketId = socket.id; 93 | members.push(member); 94 | 95 | this.db.set(channel + ":members", members); 96 | 97 | members = _.uniqBy(members.reverse(), "user_id"); 98 | 99 | this.onSubscribed(socket, channel, members); 100 | 101 | if (!is_member) { 102 | this.onJoin(socket, channel, member); 103 | } 104 | }, 105 | (error) => Log.error(error) 106 | ); 107 | }, 108 | () => { 109 | Log.error("Error retrieving pressence channel members."); 110 | } 111 | ); 112 | } 113 | 114 | /** 115 | * Remove a member from a presenece channel and broadcast they have left 116 | * only if not other presence channel instances exist. 117 | */ 118 | leave(socket: any, channel: string): void { 119 | this.getMembers(channel).then( 120 | (members) => { 121 | members = members || []; 122 | let member = members.find( 123 | (member) => member.socketId == socket.id 124 | ); 125 | members = members.filter((m) => m.socketId != member.socketId); 126 | 127 | this.db.set(channel + ":members", members); 128 | 129 | this.isMember(channel, member).then((is_member) => { 130 | if (!is_member) { 131 | delete member.socketId; 132 | this.onLeave(channel, member); 133 | } 134 | }); 135 | }, 136 | (error) => Log.error(error) 137 | ); 138 | } 139 | 140 | /** 141 | * On join event handler. 142 | */ 143 | onJoin(socket: any, channel: string, member: any): void { 144 | this.io.sockets.connected[socket.id].broadcast 145 | .to(channel) 146 | .emit("presence:joining", channel, member); 147 | } 148 | 149 | /** 150 | * On leave emitter. 151 | */ 152 | onLeave(channel: string, member: any): void { 153 | this.io.to(channel).emit("presence:leaving", channel, member); 154 | } 155 | 156 | /** 157 | * On subscribed event emitter. 158 | */ 159 | onSubscribed(socket: any, channel: string, members: any[]) { 160 | this.io.to(socket.id).emit("presence:subscribed", channel, members); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/api/http-api.ts: -------------------------------------------------------------------------------- 1 | import { Log } from './../log'; 2 | let url = require('url'); 3 | import * as _ from 'lodash'; 4 | 5 | export class HttpApi { 6 | /** 7 | * Create new instance of http subscriber. 8 | * 9 | * @param {any} io 10 | * @param {any} channel 11 | * @param {any} express 12 | * @param {object} options object 13 | */ 14 | constructor(private io, private channel, private express, private options) { } 15 | 16 | /** 17 | * Initialize the API. 18 | */ 19 | init(): void { 20 | this.corsMiddleware(); 21 | 22 | this.express.get( 23 | '/', 24 | (req, res) => this.getRoot(req, res), 25 | ); 26 | 27 | this.express.get( 28 | '/apps/:appId/status', 29 | (req, res) => this.getStatus(req, res) 30 | ); 31 | 32 | this.express.get( 33 | '/apps/:appId/channels', 34 | (req, res) => this.getChannels(req, res) 35 | ); 36 | 37 | this.express.get( 38 | '/apps/:appId/channels/:channelName', 39 | (req, res) => this.getChannel(req, res) 40 | ); 41 | 42 | this.express.get( 43 | '/apps/:appId/channels/:channelName/users', 44 | (req, res) => this.getChannelUsers(req, res) 45 | ); 46 | } 47 | 48 | /** 49 | * Add CORS middleware if applicable. 50 | */ 51 | corsMiddleware(): void { 52 | if (this.options.allowCors) { 53 | this.express.use((req, res, next) => { 54 | res.header('Access-Control-Allow-Origin', this.options.allowOrigin); 55 | res.header('Access-Control-Allow-Methods', this.options.allowMethods); 56 | res.header('Access-Control-Allow-Headers', this.options.allowHeaders); 57 | next(); 58 | }); 59 | } 60 | } 61 | 62 | /** 63 | * Outputs a simple message to show that the server is running. 64 | * 65 | * @param {any} req 66 | * @param {any} res 67 | */ 68 | getRoot(req: any, res: any): void { 69 | res.send('OK'); 70 | } 71 | 72 | /** 73 | * Get the status of the server. 74 | * 75 | * @param {any} req 76 | * @param {any} res 77 | */ 78 | getStatus(req: any, res: any): void { 79 | res.json({ 80 | subscription_count: this.io.engine.clientsCount, 81 | uptime: process.uptime(), 82 | memory_usage: process.memoryUsage(), 83 | }); 84 | } 85 | 86 | /** 87 | * Get a list of the open channels on the server. 88 | * 89 | * @param {any} req 90 | * @param {any} res 91 | */ 92 | getChannels(req: any, res: any): void { 93 | let prefix = url.parse(req.url, true).query.filter_by_prefix; 94 | let rooms = this.io.sockets.adapter.rooms; 95 | let channels = {}; 96 | 97 | Object.keys(rooms).forEach(function(channelName) { 98 | if (rooms[channelName].sockets[channelName]) { 99 | return; 100 | } 101 | 102 | if (prefix && !channelName.startsWith(prefix)) { 103 | return; 104 | } 105 | 106 | channels[channelName] = { 107 | subscription_count: rooms[channelName].length, 108 | occupied: true 109 | }; 110 | }); 111 | 112 | res.json({ channels: channels }); 113 | } 114 | 115 | /** 116 | * Get a information about a channel. 117 | * 118 | * @param {any} req 119 | * @param {any} res 120 | */ 121 | getChannel(req: any, res: any): void { 122 | let channelName = req.params.channelName; 123 | let room = this.io.sockets.adapter.rooms[channelName]; 124 | let subscriptionCount = room ? room.length : 0; 125 | 126 | let result = { 127 | subscription_count: subscriptionCount, 128 | occupied: !!subscriptionCount 129 | }; 130 | 131 | if (this.channel.isPresence(channelName)) { 132 | this.channel.presence.getMembers(channelName).then(members => { 133 | result['user_count'] = _.uniqBy(members, 'user_id').length; 134 | 135 | res.json(result); 136 | }); 137 | } else { 138 | res.json(result); 139 | } 140 | } 141 | 142 | /** 143 | * Get the users of a channel. 144 | * 145 | * @param {any} req 146 | * @param {any} res 147 | * @return {boolean} 148 | */ 149 | getChannelUsers(req: any, res: any): boolean { 150 | let channelName = req.params.channelName; 151 | 152 | if (!this.channel.isPresence(channelName)) { 153 | return this.badResponse( 154 | req, 155 | res, 156 | 'User list is only possible for Presence Channels' 157 | ); 158 | } 159 | 160 | this.channel.presence.getMembers(channelName).then(members => { 161 | let users = []; 162 | 163 | _.uniqBy(members, 'user_id').forEach((member: any) => { 164 | users.push({ id: member.user_id, user_info: member.user_info }); 165 | }); 166 | 167 | res.json({ users: users }); 168 | }, error => Log.error(error)); 169 | } 170 | 171 | /** 172 | * Handle bad requests. 173 | * 174 | * @param {any} req 175 | * @param {any} res 176 | * @param {string} message 177 | * @return {boolean} 178 | */ 179 | badResponse(req: any, res: any, message: string): boolean { 180 | res.statusCode = 400; 181 | res.json({ error: message }); 182 | 183 | return false; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var http = require('http'); 3 | var https = require('https'); 4 | var express = require('express'); 5 | var url = require('url'); 6 | var io = require('socket.io'); 7 | import { Log } from './log'; 8 | 9 | export class Server { 10 | /** 11 | * The http server. 12 | * 13 | * @type {any} 14 | */ 15 | public express: any; 16 | 17 | /** 18 | * Socket.io client. 19 | * 20 | * @type {object} 21 | */ 22 | public io: any; 23 | 24 | /** 25 | * Create a new server instance. 26 | */ 27 | constructor(private options) { } 28 | 29 | /** 30 | * Start the Socket.io server. 31 | * 32 | * @return {void} 33 | */ 34 | init(): Promise { 35 | return new Promise((resolve, reject) => { 36 | this.serverProtocol().then(() => { 37 | let host = this.options.host || 'localhost'; 38 | Log.success(`Running at ${host} on port ${this.getPort()}`); 39 | 40 | resolve(this.io); 41 | }, error => reject(error)); 42 | }); 43 | } 44 | 45 | /** 46 | * Sanitize the port number from any extra characters 47 | * 48 | * @return {number} 49 | */ 50 | getPort() { 51 | let portRegex = /([0-9]{2,5})[\/]?$/; 52 | let portToUse = String(this.options.port).match(portRegex); // index 1 contains the cleaned port number only 53 | return Number(portToUse[1]); 54 | } 55 | 56 | /** 57 | * Select the http protocol to run on. 58 | * 59 | * @return {Promise} 60 | */ 61 | serverProtocol(): Promise { 62 | return new Promise((resolve, reject) => { 63 | if (this.options.protocol == 'https') { 64 | this.secure().then(() => { 65 | resolve(this.httpServer(true)); 66 | }, error => reject(error)); 67 | } else { 68 | resolve(this.httpServer(false)); 69 | } 70 | }); 71 | } 72 | 73 | /** 74 | * Load SSL 'key' & 'cert' files if https is enabled. 75 | * 76 | * @return {void} 77 | */ 78 | secure(): Promise { 79 | return new Promise((resolve, reject) => { 80 | if (!this.options.sslCertPath || !this.options.sslKeyPath) { 81 | reject('SSL paths are missing in server config.'); 82 | } 83 | 84 | Object.assign(this.options, { 85 | cert: fs.readFileSync(this.options.sslCertPath), 86 | key: fs.readFileSync(this.options.sslKeyPath), 87 | ca: (this.options.sslCertChainPath) ? fs.readFileSync(this.options.sslCertChainPath) : '', 88 | passphrase: this.options.sslPassphrase, 89 | }); 90 | 91 | resolve(this.options); 92 | }); 93 | } 94 | 95 | /** 96 | * Create a socket.io server. 97 | * 98 | * @return {any} 99 | */ 100 | httpServer(secure: boolean) { 101 | this.express = express(); 102 | this.express.use((req, res, next) => { 103 | for (var header in this.options.headers) { 104 | res.setHeader(header, this.options.headers[header]); 105 | } 106 | next(); 107 | }); 108 | 109 | if (secure) { 110 | var httpServer = https.createServer(this.options, this.express); 111 | } else { 112 | var httpServer = http.createServer(this.express); 113 | } 114 | 115 | httpServer.listen(this.getPort(), this.options.host); 116 | 117 | this.authorizeRequests(); 118 | 119 | return this.io = io(httpServer, this.options.socketio); 120 | } 121 | 122 | /** 123 | * Attach global protection to HTTP routes, to verify the API key. 124 | */ 125 | authorizeRequests(): void { 126 | this.express.param('appId', (req, res, next) => { 127 | if (!this.canAccess(req)) { 128 | return this.unauthorizedResponse(req, res); 129 | } 130 | 131 | next(); 132 | }); 133 | } 134 | 135 | /** 136 | * Check is an incoming request can access the api. 137 | * 138 | * @param {any} req 139 | * @return {boolean} 140 | */ 141 | canAccess(req: any): boolean { 142 | let appId = this.getAppId(req); 143 | let key = this.getAuthKey(req); 144 | 145 | if (key && appId) { 146 | let client = this.options.clients.find((client) => { 147 | return client.appId === appId; 148 | }); 149 | 150 | if (client) { 151 | return client.key === key; 152 | } 153 | } 154 | 155 | return false; 156 | } 157 | 158 | /** 159 | * Get the appId from the URL 160 | * 161 | * @param {any} req 162 | * @return {string|boolean} 163 | */ 164 | getAppId(req: any): (string | boolean) { 165 | if (req.params.appId) { 166 | return req.params.appId; 167 | } 168 | 169 | return false; 170 | } 171 | 172 | /** 173 | * Get the api token from the request. 174 | * 175 | * @param {any} req 176 | * @return {string|boolean} 177 | */ 178 | getAuthKey(req: any): (string | boolean) { 179 | if (req.headers.authorization) { 180 | return req.headers.authorization.replace('Bearer ', ''); 181 | } 182 | 183 | if (url.parse(req.url, true).query.auth_key) { 184 | return url.parse(req.url, true).query.auth_key 185 | } 186 | 187 | return false; 188 | 189 | } 190 | 191 | /** 192 | * Handle unauthorized requests. 193 | * 194 | * @param {any} req 195 | * @param {any} res 196 | * @return {boolean} 197 | */ 198 | unauthorizedResponse(req: any, res: any): boolean { 199 | res.statusCode = 403; 200 | res.json({ error: 'Unauthorized' }); 201 | 202 | return false; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/echo-server.ts: -------------------------------------------------------------------------------- 1 | import { HttpSubscriber, RedisSubscriber, Subscriber } from './subscribers'; 2 | import { Channel } from './channels'; 3 | import { Server } from './server'; 4 | import { HttpApi } from './api'; 5 | import { Log } from './log'; 6 | import * as fs from 'fs'; 7 | const packageFile = require('../package.json'); 8 | const { constants } = require('crypto'); 9 | 10 | /** 11 | * Echo server class. 12 | */ 13 | export class EchoServer { 14 | /** 15 | * Default server options. 16 | */ 17 | public defaultOptions: any = { 18 | authHost: 'http://localhost', 19 | authEndpoint: '/broadcasting/auth', 20 | clients: [], 21 | database: 'redis', 22 | databaseConfig: { 23 | redis: {}, 24 | sqlite: { 25 | databasePath: '/database/laravel-echo-server.sqlite' 26 | } 27 | }, 28 | devMode: false, 29 | host: null, 30 | port: 6001, 31 | protocol: "http", 32 | socketio: {}, 33 | secureOptions: constants.SSL_OP_NO_TLSv1, 34 | sslCertPath: '', 35 | sslKeyPath: '', 36 | sslCertChainPath: '', 37 | sslPassphrase: '', 38 | subscribers: { 39 | http: true, 40 | redis: true 41 | }, 42 | apiOriginAllow: { 43 | allowCors: false, 44 | allowOrigin: '', 45 | allowMethods: '', 46 | allowHeaders: '' 47 | } 48 | }; 49 | 50 | /** 51 | * Configurable server options. 52 | */ 53 | public options: any; 54 | 55 | /** 56 | * Socket.io server instance. 57 | */ 58 | private server: Server; 59 | 60 | /** 61 | * Channel instance. 62 | */ 63 | private channel: Channel; 64 | 65 | /** 66 | * Subscribers 67 | */ 68 | private subscribers: Subscriber[]; 69 | 70 | /** 71 | * Http api instance. 72 | */ 73 | private httpApi: HttpApi; 74 | 75 | /** 76 | * Create a new instance. 77 | */ 78 | constructor() { } 79 | 80 | /** 81 | * Start the Echo Server. 82 | */ 83 | run(options: any): Promise { 84 | return new Promise((resolve, reject) => { 85 | this.options = Object.assign(this.defaultOptions, options); 86 | this.startup(); 87 | this.server = new Server(this.options); 88 | 89 | this.server.init().then(io => { 90 | this.init(io).then(() => { 91 | Log.info('\nServer ready!\n'); 92 | resolve(this); 93 | }, error => Log.error(error)); 94 | }, error => Log.error(error)); 95 | }); 96 | } 97 | 98 | /** 99 | * Initialize the class 100 | */ 101 | init(io: any): Promise { 102 | return new Promise((resolve, reject) => { 103 | this.channel = new Channel(io, this.options); 104 | 105 | this.subscribers = []; 106 | if (this.options.subscribers.http) 107 | this.subscribers.push(new HttpSubscriber(this.server.express, this.options)); 108 | if (this.options.subscribers.redis) 109 | this.subscribers.push(new RedisSubscriber(this.options)); 110 | 111 | this.httpApi = new HttpApi(io, this.channel, this.server.express, this.options.apiOriginAllow); 112 | this.httpApi.init(); 113 | 114 | this.onConnect(); 115 | this.listen().then(() => resolve(), err => Log.error(err)); 116 | }); 117 | } 118 | 119 | /** 120 | * Text shown at startup. 121 | */ 122 | startup(): void { 123 | Log.title(`\nL A R A V E L E C H O S E R V E R\n`); 124 | Log.info(`version ${packageFile.version}\n`); 125 | 126 | if (this.options.devMode) { 127 | Log.warning('Starting server in DEV mode...\n'); 128 | } else { 129 | Log.info('Starting server...\n') 130 | } 131 | } 132 | 133 | /** 134 | * Stop the echo server. 135 | */ 136 | stop(): Promise { 137 | console.log('Stopping the LARAVEL ECHO SERVER') 138 | let promises = []; 139 | this.subscribers.forEach(subscriber => { 140 | promises.push(subscriber.unsubscribe()); 141 | }); 142 | promises.push(this.server.io.close()); 143 | return Promise.all(promises).then(() => { 144 | this.subscribers = []; 145 | console.log('The LARAVEL ECHO SERVER server has been stopped.'); 146 | }); 147 | } 148 | 149 | /** 150 | * Listen for incoming event from subscibers. 151 | */ 152 | listen(): Promise { 153 | return new Promise((resolve, reject) => { 154 | let subscribePromises = this.subscribers.map(subscriber => { 155 | return subscriber.subscribe((channel, message) => { 156 | return this.broadcast(channel, message); 157 | }); 158 | }); 159 | 160 | Promise.all(subscribePromises).then(() => resolve()); 161 | }); 162 | } 163 | 164 | /** 165 | * Return a channel by its socket id. 166 | */ 167 | find(socket_id: string): any { 168 | return this.server.io.sockets.connected[socket_id]; 169 | } 170 | 171 | /** 172 | * Broadcast events to channels from subscribers. 173 | */ 174 | broadcast(channel: string, message: any): boolean { 175 | if (message.socket && this.find(message.socket)) { 176 | return this.toOthers(this.find(message.socket), channel, message); 177 | } else { 178 | return this.toAll(channel, message); 179 | } 180 | } 181 | 182 | /** 183 | * Broadcast to others on channel. 184 | */ 185 | toOthers(socket: any, channel: string, message: any): boolean { 186 | socket.broadcast.to(channel) 187 | .emit(message.event, channel, message.data); 188 | 189 | return true 190 | } 191 | 192 | /** 193 | * Broadcast to all members on channel. 194 | */ 195 | toAll(channel: string, message: any): boolean { 196 | this.server.io.to(channel) 197 | .emit(message.event, channel, message.data); 198 | 199 | return true 200 | } 201 | 202 | /** 203 | * On server connection. 204 | */ 205 | onConnect(): void { 206 | this.server.io.on('connection', socket => { 207 | this.onSubscribe(socket); 208 | this.onUnsubscribe(socket); 209 | this.onDisconnecting(socket); 210 | this.onClientEvent(socket); 211 | }); 212 | } 213 | 214 | /** 215 | * On subscribe to a channel. 216 | */ 217 | onSubscribe(socket: any): void { 218 | socket.on('subscribe', data => { 219 | this.channel.join(socket, data); 220 | }); 221 | } 222 | 223 | /** 224 | * On unsubscribe from a channel. 225 | */ 226 | onUnsubscribe(socket: any): void { 227 | socket.on('unsubscribe', data => { 228 | this.channel.leave(socket, data.channel, 'unsubscribed'); 229 | }); 230 | } 231 | 232 | /** 233 | * On socket disconnecting. 234 | */ 235 | onDisconnecting(socket: any): void { 236 | socket.on('disconnecting', (reason) => { 237 | Object.keys(socket.rooms).forEach(room => { 238 | if (room !== socket.id) { 239 | this.channel.leave(socket, room, reason); 240 | } 241 | }); 242 | }); 243 | } 244 | 245 | /** 246 | * On client events. 247 | */ 248 | onClientEvent(socket: any): void { 249 | socket.on('client event', data => { 250 | this.channel.clientEvent(socket, data); 251 | }); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Echo Server 2 | 3 | NodeJs server for Laravel Echo broadcasting with Socket.io. 4 | 5 | ## System Requirements 6 | 7 | The following are required to function properly. 8 | 9 | * Laravel 5.3 10 | * Node 6.0+ 11 | * Redis 3+ 12 | 13 | Additional information on broadcasting with Laravel can be found on the 14 | official docs: 15 | 16 | ## Getting Started 17 | 18 | Install npm package globally with the following command: 19 | 20 | ``` shell 21 | $ npm install -g laravel-echo-server 22 | ``` 23 | 24 | ### Initialize with CLI Tool 25 | 26 | Run the init command in your project directory: 27 | 28 | ``` shell 29 | $ laravel-echo-server init 30 | ``` 31 | 32 | The cli tool will help you setup a **laravel-echo-server.json** file in the root directory of your project. This file will be loaded by the server during start up. You may edit this file later on to manage the configuration of your server. 33 | 34 | #### API Clients 35 | 36 | The Laravel Echo Server exposes a light http API to perform broadcasting functionality. For security purposes, access to these endpoints from http referrers must be authenticated with an APP id and key. This can be generated using the cli command: 37 | 38 | ``` shell 39 | $ laravel-echo-server client:add APP_ID 40 | ``` 41 | 42 | If you run `client:add` without an app id argument, one will be generated for you. After running this command, the client id and key will be displayed and stored in the **laravel-echo-server.json** file. 43 | 44 | In this example, requests will be allowed as long as the app id and key are both provided with http requests. 45 | 46 | ``` http 47 | Request Headers 48 | 49 | Authorization: Bearer skti68i... 50 | 51 | or 52 | 53 | http://app.dev:6001/apps/APP_ID/channels?auth_key=skti68i... 54 | ``` 55 | 56 | You can remove clients with `laravel-echo-server client:remove APP_ID` 57 | 58 | #### Run The Server 59 | 60 | in your project root directory, run 61 | 62 | ``` shell 63 | $ laravel-echo-server start 64 | ``` 65 | 66 | #### Stop The Server 67 | 68 | in your project root directory, run 69 | 70 | ``` shell 71 | $ laravel-echo-server stop 72 | ``` 73 | 74 | ### Configurable Options 75 | 76 | Edit the default configuration of the server by adding options to your **laravel-echo-server.json** file. 77 | 78 | 79 | | Title | Default | Description | 80 | | :------------------| :------------------- | :---------------------------| 81 | | `apiOriginAllow` | `{}` | Configuration to allow API be accessed over CORS. [Example](#cross-domain-access-to-api) | 82 | | `authEndpoint` | `/broadcasting/auth` | The route that authenticates private channels | 83 | | `authHost` | `http://localhost` | The host of the server that authenticates private and presence channels | 84 | | `database` | `redis` | Database used to store data that should persist, like presence channel members. Options are currently `redis` and `sqlite` | 85 | | `databaseConfig` | `{}` | Configurations for the different database drivers [Example](#database) | 86 | | `devMode` | `false` | Adds additional logging for development purposes | 87 | | `host` | `null` | The host of the socket.io server ex.`app.dev`. `null` will accept connections on any IP-address | 88 | | `port` | `6001` | The port that the socket.io server should run on | 89 | | `protocol` | `http` | Must be either `http` or `https` | 90 | | `sslCertPath` | `''` | The path to your server's ssl certificate | 91 | | `sslKeyPath` | `''` | The path to your server's ssl key | 92 | | `sslCertChainPath` | `''` | The path to your server's ssl certificate chain | 93 | | `sslPassphrase` | `''` | The pass phrase to use for the certificate (if applicable) | 94 | | `socketio` | `{}` | Options to pass to the socket.io instance ([available options](https://github.com/socketio/engine.io#methods-1)) | 95 | | `subscribers` | `{"http": true, "redis": true}` | Allows to disable subscribers individually. Available subscribers: `http` and `redis` | 96 | 97 | ### DotEnv 98 | If a .env file is found in the same directory as the laravel-echo-server.json 99 | file, the following options can be overridden: 100 | 101 | - `authHost`: `LARAVEL_ECHO_SERVER_AUTH_HOST` *Note*: This option will fall back to the `LARAVEL_ECHO_SERVER_HOST` option as the default if that is set in the .env file. 102 | - `host`: `LARAVEL_ECHO_SERVER_HOST` 103 | - `port`: `LARAVEL_ECHO_SERVER_PORT` 104 | - `devMode`: `LARAVEL_ECHO_SERVER_DEBUG` 105 | - `databaseConfig.redis.host`: `LARAVEL_ECHO_SERVER_REDIS_HOST` 106 | - `databaseConfig.redis.port`: `LARAVEL_ECHO_SERVER_REDIS_PORT` 107 | - `databaseConfig.redis.password`: `LARAVEL_ECHO_SERVER_REDIS_PASSWORD` 108 | - `protocol`: `LARAVEL_ECHO_SERVER_PROTO` 109 | - `sslKeyPath`: `LARAVEL_ECHO_SERVER_SSL_KEY` 110 | - `sslCertPath`: `LARAVEL_ECHO_SERVER_SSL_CERT` 111 | - `sslPassphrase`: `LARAVEL_ECHO_SERVER_SSL_PASS` 112 | - `sslCertChainPath`: `LARAVEL_ECHO_SERVER_SSL_CHAIN` 113 | 114 | 115 | ### Running with SSL 116 | 117 | * Your client side implementation must access the socket.io client from https. 118 | * The server configuration must set the server host to use https. 119 | * The server configuration should include paths to both your ssl certificate and key located on your server. 120 | 121 | *Note: This library currently only supports serving from either http or https, not both.* 122 | 123 | #### Alternative SSL implementation 124 | If you are struggling to get SSL implemented with this package, you could look at using a proxy module within Apache or NginX. Essentially, instead of connecting your websocket traffic to https://yourserver.dev:6001/socket.io?..... and trying to secure it, you can connect your websocket traffic to https://yourserver.dev/socket.io. Behind the scenes, the proxy module of Apache or NginX will be configured to intercept requests for /socket.io, and internally redirect those to your echo server over non-ssl on port 6001. This keeps all of the traffic encrypted between browser and web server, as your web server will still do the SSL encryption/decryption. The only thing that is left unsecured is the traffic between your webserver and your Echo server, which might be acceptable in many cases. 125 | ##### Sample NginX proxy config 126 | ``` 127 | #the following would go within the server{} block of your web server config 128 | location /socket.io { 129 | proxy_pass http://laravel-echo-server:6001; #could be localhost if Echo and NginX are on the same box 130 | proxy_http_version 1.1; 131 | proxy_set_header Upgrade $http_upgrade; 132 | proxy_set_header Connection "Upgrade"; 133 | } 134 | ``` 135 | 136 | #### Sample Apache proxy config 137 | 138 | ``` 139 | RewriteCond %{REQUEST_URI} ^/socket.io [NC] 140 | RewriteCond %{QUERY_STRING} transport=websocket [NC] 141 | RewriteRule /(.*) ws://localhost:6001/$1 [P,L] 142 | 143 | ProxyPass /socket.io http://localhost:6001/socket.io 144 | ProxyPassReverse /socket.io http://localhost:6001/socket.io 145 | ``` 146 | 147 | ### Setting the working directory 148 | The working directory in which `laravel-echo-server` will look for the configuration file `laravel-echo-server.json` can be passed to the `start` command through the `--dir` parameter like so: `laravel-echo-server start --dir=/var/www/html/example.com/configuration` 149 | 150 | ## Subscribers 151 | The Laravel Echo Server subscribes to incoming events with two methods: Redis & Http. 152 | 153 | ### Redis 154 | 155 | Your core application can use Redis to publish events to channels. The Laravel Echo Server will subscribe to those channels and broadcast those messages via socket.io. 156 | 157 | ### Http 158 | 159 | Using Http, you can also publish events to the Laravel Echo Server in the same fashion you would with Redis by submitting a `channel` and `message` to the broadcast endpoint. You need to generate an API key as described in the [API Clients](#api-clients) section and provide the correct API key. 160 | 161 | **Request Endpoint** 162 | 163 | ``` http 164 | POST http://app.dev:6001/apps/your-app-id/events?auth_key=skti68i... 165 | 166 | ``` 167 | 168 | **Request Body** 169 | 170 | ``` json 171 | 172 | { 173 | "channel": "channel-name", 174 | "name": "event-name", 175 | "data": { 176 | "key": "value" 177 | }, 178 | "socket_id": "h3nAdb134tbvqwrg" 179 | } 180 | 181 | ``` 182 | 183 | **channel** - The name of the channel to broadcast an event to. For private or presence channels prepend `private-` or `presence-`. 184 | **channels** - Instead of a single channel, you can broadcast to an array of channels with 1 request. 185 | **name** - A string that represents the event key within your app. 186 | **data** - Data you would like to broadcast to channel. 187 | **socket_id (optional)** - The socket id of the user that initiated the event. When present, the server will only "broadcast to others". 188 | 189 | ### Pusher 190 | 191 | The HTTP subscriber is compatible with the Laravel Pusher subscriber. Just configure the host and port for your Socket.IO server and set the app id and key in config/broadcasting.php. Secret is not required. 192 | 193 | ```php 194 | 'pusher' => [ 195 | 'driver' => 'pusher', 196 | 'key' => env('PUSHER_KEY'), 197 | 'secret' => null, 198 | 'app_id' => env('PUSHER_APP_ID'), 199 | 'options' => [ 200 | 'host' => 'localhost', 201 | 'port' => 6001, 202 | 'scheme' => 'http' 203 | ], 204 | ], 205 | ``` 206 | 207 | You can now send events using HTTP, without using Redis. This also allows you to use the Pusher API to list channels/users as described in the [Pusher PHP library](https://github.com/pusher/pusher-http-php) 208 | 209 | ## HTTP API 210 | The HTTP API exposes endpoints that allow you to gather information about your running server and channels. 211 | 212 | **Status** 213 | Get total number of clients, uptime of the server, and memory usage. 214 | 215 | ``` http 216 | GET /apps/:APP_ID/status 217 | ``` 218 | **Channels** 219 | List of all channels. 220 | 221 | ``` http 222 | GET /apps/:APP_ID/channels 223 | ``` 224 | **Channel** 225 | Get information about a particular channel. 226 | 227 | ``` http 228 | GET /apps/:APP_ID/channels/:CHANNEL_NAME 229 | ``` 230 | **Channel Users** 231 | List of users on a channel. 232 | ``` http 233 | GET /apps/:APP_ID/channels/:CHANNEL_NAME/users 234 | ``` 235 | 236 | ## Cross Domain Access To API 237 | Cross domain access can be specified in the laravel-echo-server.json file by changing `allowCors` in `apiOriginAllow` to `true`. You can then set the CORS Access-Control-Allow-Origin, Access-Control-Allow-Methods as a comma separated string (GET and POST are enabled by default) and the Access-Control-Allow-Headers that the API can receive. 238 | 239 | Example below: 240 | 241 | ``` json 242 | { 243 | "apiOriginAllow":{ 244 | "allowCors" : true, 245 | "allowOrigin" : "http://127.0.0.1", 246 | "allowMethods" : "GET, POST", 247 | "allowHeaders" : "Origin, Content-Type, X-Auth-Token, X-Requested-With, Accept, Authorization, X-CSRF-TOKEN, X-Socket-Id" 248 | } 249 | } 250 | 251 | ``` 252 | This allows you to send requests to the API via AJAX from an app that may be running on the same domain but a different port or an entirely different domain. 253 | 254 | ## Database 255 | 256 | To persist presence channel data, there is support for use of Redis or SQLite as a key/value store. The key being the channel name, and the value being the list of presence channel members. 257 | 258 | Each database driver may be configured in the **laravel-echo-server.json** file under the `databaseConfig` property. The options get passed through to the database provider, so developers are free to set these up as they wish. 259 | 260 | ### Redis 261 | For example, if you wanted to pass a custom configuration to Redis: 262 | 263 | ``` json 264 | 265 | { 266 | "databaseConfig" : { 267 | "redis" : { 268 | "port": "3001", 269 | "host": "redis.app.dev", 270 | "keyPrefix": "my-redis-prefix" 271 | } 272 | } 273 | } 274 | 275 | ``` 276 | *Note: No scheme (http/https etc) should be used for the host address* 277 | 278 | *A full list of Redis options can be found [here](https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options).* 279 | 280 | ### Redis sentinel 281 | For example, if you wanted to use redis-sentinel, you need to pass a custom configuration : 282 | 283 | ``` json 284 | "databaseConfig": { 285 | "redis": { 286 | "sentinels": [ 287 | { 288 | "host": "redis-sentinel-0", 289 | "port": 26379 290 | }, 291 | { 292 | "host": "redis-sentinel-1", 293 | "port": 26379 294 | } 295 | { 296 | "host": "redis-sentinel-2", 297 | "port": 26379 298 | } 299 | ], 300 | "name": "mymaster", 301 | "sentinelPassword": "redis-password" 302 | }, 303 | }, 304 | ``` 305 | *For more information about redis sentinel configuration you can check [this](https://github.com/luin/ioredis#sentinel)* 306 | ### SQLite 307 | 308 | With SQLite you may be interested in changing the path where the database is stored. 309 | 310 | ``` json 311 | { 312 | "databaseConfig" : { 313 | "sqlite" : { 314 | "databasePath": "/path/to/laravel-echo-server.sqlite" 315 | } 316 | } 317 | } 318 | ``` 319 | 320 | ***Note 1:*** The path is relative to the root of your application, not your system. 321 | 322 | ***Note 2:*** [node-sqlite3](https://github.com/mapbox/node-sqlite3) is required for this database. Please install before using. 323 | 324 | ``` 325 | npm install sqlite3 -g 326 | ``` 327 | 328 | ## Presence Channels 329 | 330 | When users join a presence channel, their presence channel authentication data is stored using Redis. 331 | 332 | While presence channels contain a list of users, there will be instances where a user joins a presence channel multiple times. For example, this would occur when opening multiple browser tabs. In this situation "joining" and "leaving" events are only emitted to the first and last instance of the user. 333 | 334 | Optionally, you can configure laravel-echo-server to publish an event on each update to a presence channel, by setting `databaseConfig.publishPresence` to `true`: 335 | 336 | ```json 337 | { 338 | "database": "redis", 339 | "databaseConfig": { 340 | "redis" : { 341 | "port": "6379", 342 | "host": "localhost" 343 | }, 344 | "publishPresence": true 345 | } 346 | } 347 | ``` 348 | You can use Laravel's Redis integration, to trigger Application code from there: 349 | ```php 350 | Redis::subscribe(['PresenceChannelUpdated'], function ($message) { 351 | var_dump($message); 352 | }); 353 | ``` 354 | 355 | 356 | ## Client Side Configuration 357 | 358 | See the official Laravel documentation for more information. 359 | 360 | ### Tips 361 | #### Socket.io client library 362 | You can include the socket.io client library from your running server. For example, if your server is running at `app.dev:6001` you should be able to 363 | add a script tag to your html like so: 364 | 365 | ``` 366 | 367 | ``` 368 | 369 | _Note: When using the socket.io client library from your running server, remember to check that the `io` global variable is defined before subscribing to events._ 370 | 371 | #### µWebSockets deprecation 372 | 373 | µWebSockets has been [officially deprecated](https://www.npmjs.com/package/uws). Currently there is no support for µWebSockets in Socket.IO, but it may have the new [ClusterWS](https://www.npmjs.com/package/@clusterws/cws) support incoming. Meanwhile Laravel Echo Server will use [`ws` engine](https://www.npmjs.com/package/ws) by default until there is another option. 374 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const colors = require("colors"); 4 | const echo = require("./../../dist"); 5 | const inquirer = require("inquirer"); 6 | const crypto = require("crypto"); 7 | 8 | import ErrnoException = NodeJS.ErrnoException; 9 | 10 | /** 11 | * Laravel Echo Server CLI 12 | */ 13 | export class Cli { 14 | /** 15 | * Create new CLI instance. 16 | */ 17 | constructor() { 18 | this.defaultOptions = echo.defaultOptions; 19 | } 20 | 21 | /** 22 | * Default configuration options. 23 | */ 24 | defaultOptions: any; 25 | 26 | /** 27 | * Allowed environment variables. 28 | */ 29 | envVariables: any = { 30 | LARAVEL_ECHO_SERVER_AUTH_HOST: "authHost", 31 | LARAVEL_ECHO_SERVER_DEBUG: "devMode", 32 | LARAVEL_ECHO_SERVER_HOST: "host", 33 | LARAVEL_ECHO_SERVER_PORT: "port", 34 | LARAVEL_ECHO_SERVER_REDIS_HOST: "databaseConfig.redis.host", 35 | LARAVEL_ECHO_SERVER_REDIS_PORT: "databaseConfig.redis.port", 36 | LARAVEL_ECHO_SERVER_REDIS_PASSWORD: "databaseConfig.redis.password", 37 | LARAVEL_ECHO_SERVER_PROTO: "protocol", 38 | LARAVEL_ECHO_SERVER_SSL_CERT: "sslCertPath", 39 | LARAVEL_ECHO_SERVER_SSL_KEY: "sslKeyPath", 40 | LARAVEL_ECHO_SERVER_SSL_CHAIN: "sslCertChainPath", 41 | LARAVEL_ECHO_SERVER_SSL_PASS: "sslPassphrase" 42 | }; 43 | 44 | /** 45 | * Create a configuration file. 46 | */ 47 | configure(yargs: any): void { 48 | yargs.option({ 49 | config: { 50 | type: "string", 51 | default: "laravel-echo-server.json", 52 | describe: "The name of the config file to create." 53 | } 54 | }); 55 | 56 | this.setupConfig(yargs.argv.config).then( 57 | options => { 58 | options = Object.assign({}, this.defaultOptions, options); 59 | 60 | if (options.addClient) { 61 | const client = { 62 | appId: this.createAppId(), 63 | key: this.createApiKey() 64 | }; 65 | options.clients.push(client); 66 | 67 | console.log("appId: " + colors.magenta(client.appId)); 68 | console.log("key: " + colors.magenta(client.key)); 69 | } 70 | 71 | if (options.corsAllow) { 72 | options.apiOriginAllow.allowCors = true; 73 | options.apiOriginAllow.allowOrigin = options.allowOrigin; 74 | options.apiOriginAllow.allowMethods = options.allowMethods; 75 | options.apiOriginAllow.allowHeaders = options.allowHeaders; 76 | } 77 | 78 | this.saveConfig(options).then( 79 | file => { 80 | console.log( 81 | "Configuration file saved. Run " + 82 | colors.magenta.bold( 83 | "laravel-echo-server start" + 84 | (file != "laravel-echo-server.json" 85 | ? ' --config="' + file + '"' 86 | : "") 87 | ) + 88 | " to run server." 89 | ); 90 | 91 | process.exit(); 92 | }, 93 | error => { 94 | console.error(colors.error(error)); 95 | } 96 | ); 97 | }, 98 | error => console.error(error) 99 | ); 100 | } 101 | 102 | /** 103 | * Inject the .env vars into options if they exist. 104 | */ 105 | resolveEnvFileOptions(options: any): any { 106 | require("dotenv").config(); 107 | 108 | for (let key in this.envVariables) { 109 | let value = (process.env[key] || "").toString(); 110 | let replacementVar; 111 | 112 | if (value) { 113 | const path = this.envVariables[key].split("."); 114 | let modifier = options; 115 | 116 | while (path.length > 1) { 117 | modifier = modifier[path.shift()]; 118 | } 119 | 120 | if ((replacementVar = value.match(/\${(.*?)}/))) { 121 | value = (process.env[replacementVar[1]] || "").toString(); 122 | } 123 | 124 | modifier[path.shift()] = value; 125 | } 126 | } 127 | 128 | return options; 129 | } 130 | 131 | /** 132 | * Setup configuration with questions. 133 | */ 134 | setupConfig(defaultFile) { 135 | return inquirer.prompt([ 136 | { 137 | name: "devMode", 138 | default: false, 139 | message: "Do you want to run this server in development mode?", 140 | type: "confirm" 141 | }, 142 | { 143 | name: "port", 144 | default: "6001", 145 | message: "Which port would you like to serve from?" 146 | }, 147 | { 148 | name: "database", 149 | message: 150 | "Which database would you like to use to store presence channel members?", 151 | type: "list", 152 | choices: ["redis", "sqlite"] 153 | }, 154 | { 155 | name: "authHost", 156 | default: "http://localhost", 157 | message: "Enter the host of your Laravel authentication server." 158 | }, 159 | { 160 | name: "protocol", 161 | message: "Will you be serving on http or https?", 162 | type: "list", 163 | choices: ["http", "https"] 164 | }, 165 | { 166 | name: "sslCertPath", 167 | message: "Enter the path to your SSL cert file.", 168 | when: function(options) { 169 | return options.protocol == "https"; 170 | } 171 | }, 172 | { 173 | name: "sslKeyPath", 174 | message: "Enter the path to your SSL key file.", 175 | when: function(options) { 176 | return options.protocol == "https"; 177 | } 178 | }, 179 | { 180 | name: "addClient", 181 | default: false, 182 | message: 183 | "Do you want to generate a client ID/Key for HTTP API?", 184 | type: "confirm" 185 | }, 186 | { 187 | name: "corsAllow", 188 | default: false, 189 | message: "Do you want to setup cross domain access to the API?", 190 | type: "confirm" 191 | }, 192 | { 193 | name: "allowOrigin", 194 | default: "http://localhost:80", 195 | message: "Specify the URI that may access the API:", 196 | when: function(options) { 197 | return options.corsAllow == true; 198 | } 199 | }, 200 | { 201 | name: "allowMethods", 202 | default: "GET, POST", 203 | message: "Enter the HTTP methods that are allowed for CORS:", 204 | when: function(options) { 205 | return options.corsAllow == true; 206 | } 207 | }, 208 | { 209 | name: "allowHeaders", 210 | default: 211 | "Origin, Content-Type, X-Auth-Token, X-Requested-With, Accept, Authorization, X-CSRF-TOKEN, X-Socket-Id", 212 | message: "Enter the HTTP headers that are allowed for CORS:", 213 | when: function(options) { 214 | return options.corsAllow == true; 215 | } 216 | }, 217 | { 218 | name: "file", 219 | default: defaultFile, 220 | message: "What do you want this config to be saved as?" 221 | } 222 | ]); 223 | } 224 | 225 | /** 226 | * Save configuration file. 227 | */ 228 | saveConfig(options): Promise { 229 | const opts = {}; 230 | 231 | Object.keys(options) 232 | .filter(k => { 233 | return Object.keys(this.defaultOptions).indexOf(k) >= 0; 234 | }) 235 | .forEach(option => (opts[option] = options[option])); 236 | 237 | return new Promise((resolve, reject) => { 238 | if (opts) { 239 | fs.writeFile( 240 | this.getConfigFile(options.file), 241 | JSON.stringify(opts, null, "\t"), 242 | error => (error ? reject(error) : resolve(options.file)) 243 | ); 244 | } else { 245 | reject("No options provided."); 246 | } 247 | }); 248 | } 249 | 250 | /** 251 | * Start the Laravel Echo server. 252 | */ 253 | start(yargs: any): void { 254 | yargs.option({ 255 | config: { 256 | type: "string", 257 | describe: "The config file to use." 258 | }, 259 | 260 | dir: { 261 | type: "string", 262 | describe: "The working directory to use." 263 | }, 264 | 265 | force: { 266 | type: "boolean", 267 | describe: "If a server is already running, stop it." 268 | }, 269 | 270 | dev: { 271 | type: "boolean", 272 | describe: "Run in dev mode." 273 | } 274 | }); 275 | 276 | const configFile = this.getConfigFile( 277 | yargs.argv.config, 278 | yargs.argv.dir 279 | ); 280 | 281 | fs.access(configFile, fs.F_OK, error => { 282 | if (error) { 283 | console.error( 284 | colors.error("Error: The config file could not be found.") 285 | ); 286 | 287 | return false; 288 | } 289 | 290 | const options = this.readConfigFile(configFile); 291 | 292 | options.devMode = 293 | `${yargs.argv.dev || options.devMode || false}` === "true"; 294 | 295 | const lockFile = path.join( 296 | path.dirname(configFile), 297 | path.basename(configFile, ".json") + ".lock" 298 | ); 299 | 300 | if (fs.existsSync(lockFile)) { 301 | let lockProcess; 302 | 303 | try { 304 | lockProcess = parseInt( 305 | JSON.parse(fs.readFileSync(lockFile, "utf8")).process 306 | ); 307 | } catch { 308 | console.error( 309 | colors.error( 310 | "Error: There was a problem reading the existing lock file." 311 | ) 312 | ); 313 | } 314 | 315 | if (lockProcess) { 316 | try { 317 | process.kill(lockProcess, 0); 318 | 319 | if (yargs.argv.force) { 320 | process.kill(lockProcess); 321 | 322 | console.log( 323 | colors.yellow( 324 | "Warning: Closing process " + 325 | lockProcess + 326 | " because you used the '--force' option." 327 | ) 328 | ); 329 | } else { 330 | console.error( 331 | colors.error( 332 | "Error: There is already a server running! Use the option '--force' to stop it and start another one." 333 | ) 334 | ); 335 | 336 | return false; 337 | } 338 | } catch { 339 | // The process in the lock file doesn't exist, so continue 340 | } 341 | } 342 | } 343 | 344 | fs.writeFile( 345 | lockFile, 346 | JSON.stringify({ process: process.pid }, null, "\t"), 347 | error => { 348 | if (error) { 349 | console.error( 350 | colors.error("Error: Cannot write lock file.") 351 | ); 352 | 353 | return false; 354 | } 355 | 356 | process.on("exit", () => { 357 | try { 358 | fs.unlinkSync(lockFile); 359 | } catch {} 360 | }); 361 | 362 | process.on("SIGINT", () => process.exit()); 363 | process.on("SIGHUP", () => process.exit()); 364 | process.on("SIGTERM", () => process.exit()); 365 | 366 | echo.run(options); 367 | } 368 | ); 369 | }); 370 | } 371 | 372 | /** 373 | * Stop the Laravel Echo server. 374 | */ 375 | stop(yargs: any): void { 376 | yargs.option({ 377 | config: { 378 | type: "string", 379 | describe: "The config file to use." 380 | }, 381 | 382 | dir: { 383 | type: "string", 384 | describe: "The working directory to use." 385 | } 386 | }); 387 | 388 | const configFile = this.getConfigFile( 389 | yargs.argv.config, 390 | yargs.argv.dir 391 | ); 392 | const lockFile = path.join( 393 | path.dirname(configFile), 394 | path.basename(configFile, ".json") + ".lock" 395 | ); 396 | 397 | if (fs.existsSync(lockFile)) { 398 | let lockProcess; 399 | 400 | try { 401 | lockProcess = parseInt( 402 | JSON.parse(fs.readFileSync(lockFile, "utf8")).process 403 | ); 404 | } catch { 405 | console.error( 406 | colors.error( 407 | "Error: There was a problem reading the lock file." 408 | ) 409 | ); 410 | } 411 | 412 | if (lockProcess) { 413 | try { 414 | fs.unlinkSync(lockFile); 415 | 416 | process.kill(lockProcess); 417 | 418 | console.log(colors.green("Closed the running server.")); 419 | } catch (e) { 420 | console.error(e); 421 | console.log(colors.error("No running servers to close.")); 422 | } 423 | } 424 | } else { 425 | console.log(colors.error("Error: Could not find any lock file.")); 426 | } 427 | } 428 | 429 | /** 430 | * Create an app key for server. 431 | */ 432 | getRandomString(bytes: number): string { 433 | return crypto.randomBytes(bytes).toString("hex"); 434 | } 435 | 436 | /** 437 | * Create an api key for the HTTP API. 438 | */ 439 | createApiKey(): string { 440 | return this.getRandomString(16); 441 | } 442 | 443 | /** 444 | * Create an api key for the HTTP API. 445 | */ 446 | createAppId(): string { 447 | return this.getRandomString(8); 448 | } 449 | 450 | /** 451 | * Add a registered referrer. 452 | */ 453 | clientAdd(yargs: any): void { 454 | yargs.option({ 455 | config: { 456 | type: "string", 457 | describe: "The config file to use." 458 | }, 459 | 460 | dir: { 461 | type: "string", 462 | describe: "The working directory to use." 463 | } 464 | }); 465 | 466 | const options = this.readConfigFile( 467 | this.getConfigFile(yargs.argv.config, yargs.argv.dir) 468 | ); 469 | const appId = yargs.argv._[1] || this.createAppId(); 470 | options.clients = options.clients || []; 471 | 472 | if (appId) { 473 | let index = null; 474 | let client = options.clients.find((client, i) => { 475 | index = i; 476 | return client.appId == appId; 477 | }); 478 | 479 | if (client) { 480 | client.key = this.createApiKey(); 481 | 482 | options.clients[index] = client; 483 | 484 | console.log(colors.green("API Client updated!")); 485 | } else { 486 | client = { 487 | appId: appId, 488 | key: this.createApiKey() 489 | }; 490 | 491 | options.clients.push(client); 492 | 493 | console.log(colors.green("API Client added!")); 494 | } 495 | 496 | console.log(colors.magenta("appId: " + client.appId)); 497 | console.log(colors.magenta("key: " + client.key)); 498 | 499 | this.saveConfig(options); 500 | } 501 | } 502 | 503 | /** 504 | * Remove a registered referrer. 505 | */ 506 | clientRemove(yargs: any): void { 507 | yargs.option({ 508 | config: { 509 | type: "string", 510 | describe: "The config file to use." 511 | }, 512 | 513 | dir: { 514 | type: "string", 515 | describe: "The working directory to use." 516 | } 517 | }); 518 | 519 | const options = this.readConfigFile( 520 | this.getConfigFile(yargs.argv.config, yargs.argv.dir) 521 | ); 522 | const appId = yargs.argv._[1] || null; 523 | options.clients = options.clients || []; 524 | 525 | let index = null; 526 | 527 | const client = options.clients.find((client, i) => { 528 | index = i; 529 | return client.appId == appId; 530 | }); 531 | 532 | if (index >= 0) { 533 | options.clients.splice(index, 1); 534 | } 535 | 536 | console.log(colors.green("Client removed: " + appId)); 537 | 538 | this.saveConfig(options); 539 | } 540 | 541 | /** 542 | * Gets the config file with the provided args 543 | */ 544 | getConfigFile(file: string = null, dir: string = null): string { 545 | const filePath = path.join( 546 | dir || "", 547 | file || "laravel-echo-server.json" 548 | ); 549 | 550 | return path.isAbsolute(filePath) 551 | ? filePath 552 | : path.join(process.cwd(), filePath); 553 | } 554 | 555 | /** 556 | * Tries to read a config file 557 | */ 558 | readConfigFile(file: string): any { 559 | let data = {}; 560 | 561 | try { 562 | data = JSON.parse(fs.readFileSync(file, "utf8")); 563 | } catch { 564 | console.error( 565 | colors.error( 566 | "Error: There was a problem reading the config file." 567 | ) 568 | ); 569 | process.exit(); 570 | } 571 | 572 | return this.resolveEnvFileOptions(data); 573 | } 574 | } 575 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/color-name@^1.1.1": 6 | version "1.1.1" 7 | resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" 8 | 9 | "@types/lodash@^4.14.149": 10 | version "4.14.157" 11 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8" 12 | 13 | "@types/node@^13.9.1": 14 | version "13.13.14" 15 | resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.14.tgz#20cd7d2a98f0c3b08d379f4ea9e6b315d2019529" 16 | 17 | accepts@~1.3.4: 18 | version "1.3.5" 19 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" 20 | dependencies: 21 | mime-types "~2.1.18" 22 | negotiator "0.6.1" 23 | 24 | accepts@~1.3.7: 25 | version "1.3.7" 26 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" 27 | dependencies: 28 | mime-types "~2.1.24" 29 | negotiator "0.6.2" 30 | 31 | after@0.8.2: 32 | version "0.8.2" 33 | resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" 34 | 35 | ajv@^6.5.5: 36 | version "6.10.2" 37 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" 38 | dependencies: 39 | fast-deep-equal "^2.0.1" 40 | fast-json-stable-stringify "^2.0.0" 41 | json-schema-traverse "^0.4.1" 42 | uri-js "^4.2.2" 43 | 44 | ansi-escapes@^4.2.1: 45 | version "4.3.1" 46 | resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" 47 | dependencies: 48 | type-fest "^0.11.0" 49 | 50 | ansi-regex@^5.0.0: 51 | version "5.0.0" 52 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" 53 | 54 | ansi-styles@^4.0.0, ansi-styles@^4.1.0: 55 | version "4.2.1" 56 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" 57 | dependencies: 58 | "@types/color-name" "^1.1.1" 59 | color-convert "^2.0.1" 60 | 61 | array-flatten@1.1.1: 62 | version "1.1.1" 63 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 64 | 65 | arraybuffer.slice@~0.0.7: 66 | version "0.0.7" 67 | resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" 68 | 69 | asn1@~0.2.3: 70 | version "0.2.3" 71 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" 72 | 73 | assert-plus@1.0.0, assert-plus@^1.0.0: 74 | version "1.0.0" 75 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 76 | 77 | async-limiter@^1.0.0: 78 | version "1.0.1" 79 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" 80 | 81 | async-limiter@~1.0.0: 82 | version "1.0.0" 83 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" 84 | 85 | asynckit@^0.4.0: 86 | version "0.4.0" 87 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 88 | 89 | aws-sign2@~0.7.0: 90 | version "0.7.0" 91 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 92 | 93 | aws4@^1.8.0: 94 | version "1.8.0" 95 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" 96 | 97 | backo2@1.0.2: 98 | version "1.0.2" 99 | resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" 100 | 101 | base64-arraybuffer@0.1.5: 102 | version "0.1.5" 103 | resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" 104 | 105 | base64id@2.0.0: 106 | version "2.0.0" 107 | resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" 108 | 109 | bcrypt-pbkdf@^1.0.0: 110 | version "1.0.1" 111 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" 112 | dependencies: 113 | tweetnacl "^0.14.3" 114 | 115 | better-assert@~1.0.0: 116 | version "1.0.2" 117 | resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" 118 | dependencies: 119 | callsite "1.0.0" 120 | 121 | blob@0.0.5: 122 | version "0.0.5" 123 | resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" 124 | 125 | body-parser@1.19.0: 126 | version "1.19.0" 127 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" 128 | dependencies: 129 | bytes "3.1.0" 130 | content-type "~1.0.4" 131 | debug "2.6.9" 132 | depd "~1.1.2" 133 | http-errors "1.7.2" 134 | iconv-lite "0.4.24" 135 | on-finished "~2.3.0" 136 | qs "6.7.0" 137 | raw-body "2.4.0" 138 | type-is "~1.6.17" 139 | 140 | bytes@3.1.0: 141 | version "3.1.0" 142 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" 143 | 144 | callsite@1.0.0: 145 | version "1.0.0" 146 | resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" 147 | 148 | camelcase@^5.0.0: 149 | version "5.3.1" 150 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" 151 | 152 | caseless@~0.12.0: 153 | version "0.12.0" 154 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 155 | 156 | chalk@^4.1.0: 157 | version "4.1.0" 158 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" 159 | dependencies: 160 | ansi-styles "^4.1.0" 161 | supports-color "^7.1.0" 162 | 163 | chardet@^0.7.0: 164 | version "0.7.0" 165 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" 166 | 167 | cli-cursor@^3.1.0: 168 | version "3.1.0" 169 | resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" 170 | dependencies: 171 | restore-cursor "^3.1.0" 172 | 173 | cli-width@^3.0.0: 174 | version "3.0.0" 175 | resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" 176 | 177 | cliui@^6.0.0: 178 | version "6.0.0" 179 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" 180 | dependencies: 181 | string-width "^4.2.0" 182 | strip-ansi "^6.0.0" 183 | wrap-ansi "^6.2.0" 184 | 185 | cluster-key-slot@^1.1.0: 186 | version "1.1.0" 187 | resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" 188 | 189 | color-convert@^2.0.1: 190 | version "2.0.1" 191 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 192 | dependencies: 193 | color-name "~1.1.4" 194 | 195 | color-name@~1.1.4: 196 | version "1.1.4" 197 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 198 | 199 | colors@^1.4.0: 200 | version "1.4.0" 201 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" 202 | 203 | combined-stream@^1.0.6, combined-stream@~1.0.6: 204 | version "1.0.8" 205 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 206 | dependencies: 207 | delayed-stream "~1.0.0" 208 | 209 | component-bind@1.0.0: 210 | version "1.0.0" 211 | resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" 212 | 213 | component-emitter@1.2.1: 214 | version "1.2.1" 215 | resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" 216 | 217 | component-inherit@0.0.3: 218 | version "0.0.3" 219 | resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" 220 | 221 | content-disposition@0.5.3: 222 | version "0.5.3" 223 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" 224 | dependencies: 225 | safe-buffer "5.1.2" 226 | 227 | content-type@~1.0.4: 228 | version "1.0.4" 229 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 230 | 231 | cookie-signature@1.0.6: 232 | version "1.0.6" 233 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 234 | 235 | cookie@0.3.1: 236 | version "0.3.1" 237 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 238 | 239 | cookie@0.4.0: 240 | version "0.4.0" 241 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" 242 | 243 | core-util-is@1.0.2: 244 | version "1.0.2" 245 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 246 | 247 | dashdash@^1.12.0: 248 | version "1.14.1" 249 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 250 | dependencies: 251 | assert-plus "^1.0.0" 252 | 253 | debug@2.6.9: 254 | version "2.6.9" 255 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 256 | dependencies: 257 | ms "2.0.0" 258 | 259 | debug@^4.1.1, debug@~4.1.0: 260 | version "4.1.1" 261 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 262 | dependencies: 263 | ms "^2.1.1" 264 | 265 | debug@~3.1.0: 266 | version "3.1.0" 267 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 268 | dependencies: 269 | ms "2.0.0" 270 | 271 | decamelize@^1.2.0: 272 | version "1.2.0" 273 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 274 | 275 | delayed-stream@~1.0.0: 276 | version "1.0.0" 277 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 278 | 279 | denque@^1.1.0: 280 | version "1.4.1" 281 | resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" 282 | 283 | depd@~1.1.2: 284 | version "1.1.2" 285 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 286 | 287 | destroy@~1.0.4: 288 | version "1.0.4" 289 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 290 | 291 | dotenv@^8.2.0: 292 | version "8.2.0" 293 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" 294 | 295 | ecc-jsbn@~0.1.1: 296 | version "0.1.1" 297 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" 298 | dependencies: 299 | jsbn "~0.1.0" 300 | 301 | ee-first@1.1.1: 302 | version "1.1.1" 303 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 304 | 305 | emoji-regex@^8.0.0: 306 | version "8.0.0" 307 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 308 | 309 | encodeurl@~1.0.2: 310 | version "1.0.2" 311 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 312 | 313 | engine.io-client@~3.4.0: 314 | version "3.4.0" 315 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700" 316 | dependencies: 317 | component-emitter "1.2.1" 318 | component-inherit "0.0.3" 319 | debug "~4.1.0" 320 | engine.io-parser "~2.2.0" 321 | has-cors "1.1.0" 322 | indexof "0.0.1" 323 | parseqs "0.0.5" 324 | parseuri "0.0.5" 325 | ws "~6.1.0" 326 | xmlhttprequest-ssl "~1.5.4" 327 | yeast "0.1.2" 328 | 329 | engine.io-parser@~2.2.0: 330 | version "2.2.0" 331 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" 332 | dependencies: 333 | after "0.8.2" 334 | arraybuffer.slice "~0.0.7" 335 | base64-arraybuffer "0.1.5" 336 | blob "0.0.5" 337 | has-binary2 "~1.0.2" 338 | 339 | engine.io@~3.4.0: 340 | version "3.4.0" 341 | resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3" 342 | dependencies: 343 | accepts "~1.3.4" 344 | base64id "2.0.0" 345 | cookie "0.3.1" 346 | debug "~4.1.0" 347 | engine.io-parser "~2.2.0" 348 | ws "^7.1.2" 349 | 350 | escape-html@~1.0.3: 351 | version "1.0.3" 352 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 353 | 354 | escape-string-regexp@^1.0.5: 355 | version "1.0.5" 356 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 357 | 358 | etag@~1.8.1: 359 | version "1.8.1" 360 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 361 | 362 | express@^4.17.1: 363 | version "4.17.1" 364 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" 365 | dependencies: 366 | accepts "~1.3.7" 367 | array-flatten "1.1.1" 368 | body-parser "1.19.0" 369 | content-disposition "0.5.3" 370 | content-type "~1.0.4" 371 | cookie "0.4.0" 372 | cookie-signature "1.0.6" 373 | debug "2.6.9" 374 | depd "~1.1.2" 375 | encodeurl "~1.0.2" 376 | escape-html "~1.0.3" 377 | etag "~1.8.1" 378 | finalhandler "~1.1.2" 379 | fresh "0.5.2" 380 | merge-descriptors "1.0.1" 381 | methods "~1.1.2" 382 | on-finished "~2.3.0" 383 | parseurl "~1.3.3" 384 | path-to-regexp "0.1.7" 385 | proxy-addr "~2.0.5" 386 | qs "6.7.0" 387 | range-parser "~1.2.1" 388 | safe-buffer "5.1.2" 389 | send "0.17.1" 390 | serve-static "1.14.1" 391 | setprototypeof "1.1.1" 392 | statuses "~1.5.0" 393 | type-is "~1.6.18" 394 | utils-merge "1.0.1" 395 | vary "~1.1.2" 396 | 397 | extend@~3.0.2: 398 | version "3.0.2" 399 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 400 | 401 | external-editor@^3.0.3: 402 | version "3.1.0" 403 | resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" 404 | dependencies: 405 | chardet "^0.7.0" 406 | iconv-lite "^0.4.24" 407 | tmp "^0.0.33" 408 | 409 | extsprintf@1.3.0: 410 | version "1.3.0" 411 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 412 | 413 | extsprintf@^1.2.0: 414 | version "1.4.0" 415 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 416 | 417 | fast-deep-equal@^2.0.1: 418 | version "2.0.1" 419 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" 420 | 421 | fast-json-stable-stringify@^2.0.0: 422 | version "2.0.0" 423 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" 424 | 425 | figures@^3.0.0: 426 | version "3.2.0" 427 | resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" 428 | dependencies: 429 | escape-string-regexp "^1.0.5" 430 | 431 | finalhandler@~1.1.2: 432 | version "1.1.2" 433 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" 434 | dependencies: 435 | debug "2.6.9" 436 | encodeurl "~1.0.2" 437 | escape-html "~1.0.3" 438 | on-finished "~2.3.0" 439 | parseurl "~1.3.3" 440 | statuses "~1.5.0" 441 | unpipe "~1.0.0" 442 | 443 | find-up@^4.1.0: 444 | version "4.1.0" 445 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" 446 | dependencies: 447 | locate-path "^5.0.0" 448 | path-exists "^4.0.0" 449 | 450 | forever-agent@~0.6.1: 451 | version "0.6.1" 452 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 453 | 454 | form-data@~2.3.2: 455 | version "2.3.3" 456 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" 457 | dependencies: 458 | asynckit "^0.4.0" 459 | combined-stream "^1.0.6" 460 | mime-types "^2.1.12" 461 | 462 | forwarded@~0.1.2: 463 | version "0.1.2" 464 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 465 | 466 | fresh@0.5.2: 467 | version "0.5.2" 468 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 469 | 470 | get-caller-file@^2.0.1: 471 | version "2.0.5" 472 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 473 | 474 | getpass@^0.1.1: 475 | version "0.1.7" 476 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 477 | dependencies: 478 | assert-plus "^1.0.0" 479 | 480 | har-schema@^2.0.0: 481 | version "2.0.0" 482 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 483 | 484 | har-validator@~5.1.3: 485 | version "5.1.3" 486 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" 487 | dependencies: 488 | ajv "^6.5.5" 489 | har-schema "^2.0.0" 490 | 491 | has-binary2@~1.0.2: 492 | version "1.0.2" 493 | resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.2.tgz#e83dba49f0b9be4d026d27365350d9f03f54be98" 494 | dependencies: 495 | isarray "2.0.1" 496 | 497 | has-cors@1.1.0: 498 | version "1.1.0" 499 | resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" 500 | 501 | has-flag@^4.0.0: 502 | version "4.0.0" 503 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 504 | 505 | http-errors@1.7.2: 506 | version "1.7.2" 507 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" 508 | dependencies: 509 | depd "~1.1.2" 510 | inherits "2.0.3" 511 | setprototypeof "1.1.1" 512 | statuses ">= 1.5.0 < 2" 513 | toidentifier "1.0.0" 514 | 515 | http-errors@~1.7.2: 516 | version "1.7.3" 517 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" 518 | dependencies: 519 | depd "~1.1.2" 520 | inherits "2.0.4" 521 | setprototypeof "1.1.1" 522 | statuses ">= 1.5.0 < 2" 523 | toidentifier "1.0.0" 524 | 525 | http-signature@~1.2.0: 526 | version "1.2.0" 527 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 528 | dependencies: 529 | assert-plus "^1.0.0" 530 | jsprim "^1.2.2" 531 | sshpk "^1.7.0" 532 | 533 | iconv-lite@0.4.24, iconv-lite@^0.4.24: 534 | version "0.4.24" 535 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 536 | dependencies: 537 | safer-buffer ">= 2.1.2 < 3" 538 | 539 | indexof@0.0.1: 540 | version "0.0.1" 541 | resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 542 | 543 | inherits@2.0.3: 544 | version "2.0.3" 545 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 546 | 547 | inherits@2.0.4: 548 | version "2.0.4" 549 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 550 | 551 | inquirer@^7.1.0: 552 | version "7.3.2" 553 | resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.2.tgz#25245d2e32dc9f33dbe26eeaada231daa66e9c7c" 554 | dependencies: 555 | ansi-escapes "^4.2.1" 556 | chalk "^4.1.0" 557 | cli-cursor "^3.1.0" 558 | cli-width "^3.0.0" 559 | external-editor "^3.0.3" 560 | figures "^3.0.0" 561 | lodash "^4.17.16" 562 | mute-stream "0.0.8" 563 | run-async "^2.4.0" 564 | rxjs "^6.6.0" 565 | string-width "^4.1.0" 566 | strip-ansi "^6.0.0" 567 | through "^2.3.6" 568 | 569 | ioredis@^4.16.0: 570 | version "4.17.3" 571 | resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.17.3.tgz#9938c60e4ca685f75326337177bdc2e73ae9c9dc" 572 | dependencies: 573 | cluster-key-slot "^1.1.0" 574 | debug "^4.1.1" 575 | denque "^1.1.0" 576 | lodash.defaults "^4.2.0" 577 | lodash.flatten "^4.4.0" 578 | redis-commands "1.5.0" 579 | redis-errors "^1.2.0" 580 | redis-parser "^3.0.0" 581 | standard-as-callback "^2.0.1" 582 | 583 | ipaddr.js@1.9.1: 584 | version "1.9.1" 585 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 586 | 587 | is-fullwidth-code-point@^3.0.0: 588 | version "3.0.0" 589 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 590 | 591 | is-typedarray@~1.0.0: 592 | version "1.0.0" 593 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 594 | 595 | isarray@2.0.1: 596 | version "2.0.1" 597 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" 598 | 599 | isstream@~0.1.2: 600 | version "0.1.2" 601 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 602 | 603 | jsbn@~0.1.0: 604 | version "0.1.1" 605 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 606 | 607 | json-schema-traverse@^0.4.1: 608 | version "0.4.1" 609 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 610 | 611 | json-schema@0.2.3: 612 | version "0.2.3" 613 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 614 | 615 | json-stringify-safe@~5.0.1: 616 | version "5.0.1" 617 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 618 | 619 | jsprim@^1.2.2: 620 | version "1.4.1" 621 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 622 | dependencies: 623 | assert-plus "1.0.0" 624 | extsprintf "1.3.0" 625 | json-schema "0.2.3" 626 | verror "1.10.0" 627 | 628 | locate-path@^5.0.0: 629 | version "5.0.0" 630 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" 631 | dependencies: 632 | p-locate "^4.1.0" 633 | 634 | lodash.defaults@^4.2.0: 635 | version "4.2.0" 636 | resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" 637 | 638 | lodash.flatten@^4.4.0: 639 | version "4.4.0" 640 | resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" 641 | 642 | lodash@^4.17.15, lodash@^4.17.16: 643 | version "4.17.19" 644 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" 645 | 646 | media-typer@0.3.0: 647 | version "0.3.0" 648 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 649 | 650 | merge-descriptors@1.0.1: 651 | version "1.0.1" 652 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 653 | 654 | methods@~1.1.2: 655 | version "1.1.2" 656 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 657 | 658 | mime-db@1.40.0: 659 | version "1.40.0" 660 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" 661 | 662 | mime-db@1.44.0: 663 | version "1.44.0" 664 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" 665 | 666 | mime-db@~1.33.0: 667 | version "1.33.0" 668 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 669 | 670 | mime-types@^2.1.12, mime-types@~2.1.18: 671 | version "2.1.18" 672 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" 673 | dependencies: 674 | mime-db "~1.33.0" 675 | 676 | mime-types@~2.1.19: 677 | version "2.1.24" 678 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" 679 | dependencies: 680 | mime-db "1.40.0" 681 | 682 | mime-types@~2.1.24: 683 | version "2.1.27" 684 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" 685 | dependencies: 686 | mime-db "1.44.0" 687 | 688 | mime@1.6.0: 689 | version "1.6.0" 690 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 691 | 692 | mimic-fn@^2.1.0: 693 | version "2.1.0" 694 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" 695 | 696 | ms@2.0.0: 697 | version "2.0.0" 698 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 699 | 700 | ms@2.1.1: 701 | version "2.1.1" 702 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 703 | 704 | ms@^2.1.1: 705 | version "2.1.2" 706 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 707 | 708 | mute-stream@0.0.8: 709 | version "0.0.8" 710 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" 711 | 712 | negotiator@0.6.1: 713 | version "0.6.1" 714 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 715 | 716 | negotiator@0.6.2: 717 | version "0.6.2" 718 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 719 | 720 | oauth-sign@~0.9.0: 721 | version "0.9.0" 722 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" 723 | 724 | object-component@0.0.3: 725 | version "0.0.3" 726 | resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" 727 | 728 | on-finished@~2.3.0: 729 | version "2.3.0" 730 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 731 | dependencies: 732 | ee-first "1.1.1" 733 | 734 | onetime@^5.1.0: 735 | version "5.1.0" 736 | resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" 737 | dependencies: 738 | mimic-fn "^2.1.0" 739 | 740 | os-tmpdir@~1.0.2: 741 | version "1.0.2" 742 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 743 | 744 | p-limit@^2.2.0: 745 | version "2.3.0" 746 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" 747 | dependencies: 748 | p-try "^2.0.0" 749 | 750 | p-locate@^4.1.0: 751 | version "4.1.0" 752 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" 753 | dependencies: 754 | p-limit "^2.2.0" 755 | 756 | p-try@^2.0.0: 757 | version "2.2.0" 758 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" 759 | 760 | parseqs@0.0.5: 761 | version "0.0.5" 762 | resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" 763 | dependencies: 764 | better-assert "~1.0.0" 765 | 766 | parseuri@0.0.5: 767 | version "0.0.5" 768 | resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" 769 | dependencies: 770 | better-assert "~1.0.0" 771 | 772 | parseurl@~1.3.3: 773 | version "1.3.3" 774 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 775 | 776 | path-exists@^4.0.0: 777 | version "4.0.0" 778 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 779 | 780 | path-to-regexp@0.1.7: 781 | version "0.1.7" 782 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 783 | 784 | performance-now@^2.1.0: 785 | version "2.1.0" 786 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 787 | 788 | proxy-addr@~2.0.5: 789 | version "2.0.6" 790 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" 791 | dependencies: 792 | forwarded "~0.1.2" 793 | ipaddr.js "1.9.1" 794 | 795 | psl@^1.1.28: 796 | version "1.8.0" 797 | resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" 798 | 799 | punycode@^2.1.0, punycode@^2.1.1: 800 | version "2.1.1" 801 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 802 | 803 | qs@6.7.0: 804 | version "6.7.0" 805 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" 806 | 807 | qs@~6.5.2: 808 | version "6.5.2" 809 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 810 | 811 | range-parser@~1.2.1: 812 | version "1.2.1" 813 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 814 | 815 | raw-body@2.4.0: 816 | version "2.4.0" 817 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" 818 | dependencies: 819 | bytes "3.1.0" 820 | http-errors "1.7.2" 821 | iconv-lite "0.4.24" 822 | unpipe "1.0.0" 823 | 824 | redis-commands@1.5.0: 825 | version "1.5.0" 826 | resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" 827 | 828 | redis-errors@^1.0.0, redis-errors@^1.2.0: 829 | version "1.2.0" 830 | resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" 831 | 832 | redis-parser@^3.0.0: 833 | version "3.0.0" 834 | resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" 835 | dependencies: 836 | redis-errors "^1.0.0" 837 | 838 | request@^2.88.2: 839 | version "2.88.2" 840 | resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" 841 | dependencies: 842 | aws-sign2 "~0.7.0" 843 | aws4 "^1.8.0" 844 | caseless "~0.12.0" 845 | combined-stream "~1.0.6" 846 | extend "~3.0.2" 847 | forever-agent "~0.6.1" 848 | form-data "~2.3.2" 849 | har-validator "~5.1.3" 850 | http-signature "~1.2.0" 851 | is-typedarray "~1.0.0" 852 | isstream "~0.1.2" 853 | json-stringify-safe "~5.0.1" 854 | mime-types "~2.1.19" 855 | oauth-sign "~0.9.0" 856 | performance-now "^2.1.0" 857 | qs "~6.5.2" 858 | safe-buffer "^5.1.2" 859 | tough-cookie "~2.5.0" 860 | tunnel-agent "^0.6.0" 861 | uuid "^3.3.2" 862 | 863 | require-directory@^2.1.1: 864 | version "2.1.1" 865 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 866 | 867 | require-main-filename@^2.0.0: 868 | version "2.0.0" 869 | resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" 870 | 871 | restore-cursor@^3.1.0: 872 | version "3.1.0" 873 | resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" 874 | dependencies: 875 | onetime "^5.1.0" 876 | signal-exit "^3.0.2" 877 | 878 | run-async@^2.4.0: 879 | version "2.4.1" 880 | resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" 881 | 882 | rxjs@^6.6.0: 883 | version "6.6.0" 884 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84" 885 | dependencies: 886 | tslib "^1.9.0" 887 | 888 | safe-buffer@5.1.2: 889 | version "5.1.2" 890 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 891 | 892 | safe-buffer@^5.0.1: 893 | version "5.1.1" 894 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 895 | 896 | safe-buffer@^5.1.2: 897 | version "5.2.0" 898 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" 899 | 900 | "safer-buffer@>= 2.1.2 < 3": 901 | version "2.1.2" 902 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 903 | 904 | send@0.17.1: 905 | version "0.17.1" 906 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" 907 | dependencies: 908 | debug "2.6.9" 909 | depd "~1.1.2" 910 | destroy "~1.0.4" 911 | encodeurl "~1.0.2" 912 | escape-html "~1.0.3" 913 | etag "~1.8.1" 914 | fresh "0.5.2" 915 | http-errors "~1.7.2" 916 | mime "1.6.0" 917 | ms "2.1.1" 918 | on-finished "~2.3.0" 919 | range-parser "~1.2.1" 920 | statuses "~1.5.0" 921 | 922 | serve-static@1.14.1: 923 | version "1.14.1" 924 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" 925 | dependencies: 926 | encodeurl "~1.0.2" 927 | escape-html "~1.0.3" 928 | parseurl "~1.3.3" 929 | send "0.17.1" 930 | 931 | set-blocking@^2.0.0: 932 | version "2.0.0" 933 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 934 | 935 | setprototypeof@1.1.1: 936 | version "1.1.1" 937 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" 938 | 939 | signal-exit@^3.0.2: 940 | version "3.0.3" 941 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" 942 | 943 | socket.io-adapter@~1.1.0: 944 | version "1.1.1" 945 | resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b" 946 | 947 | socket.io-client@2.3.0: 948 | version "2.3.0" 949 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" 950 | dependencies: 951 | backo2 "1.0.2" 952 | base64-arraybuffer "0.1.5" 953 | component-bind "1.0.0" 954 | component-emitter "1.2.1" 955 | debug "~4.1.0" 956 | engine.io-client "~3.4.0" 957 | has-binary2 "~1.0.2" 958 | has-cors "1.1.0" 959 | indexof "0.0.1" 960 | object-component "0.0.3" 961 | parseqs "0.0.5" 962 | parseuri "0.0.5" 963 | socket.io-parser "~3.3.0" 964 | to-array "0.1.4" 965 | 966 | socket.io-parser@~3.3.0: 967 | version "3.3.0" 968 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" 969 | dependencies: 970 | component-emitter "1.2.1" 971 | debug "~3.1.0" 972 | isarray "2.0.1" 973 | 974 | socket.io-parser@~3.4.0: 975 | version "3.4.0" 976 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.0.tgz#370bb4a151df2f77ce3345ff55a7072cc6e9565a" 977 | dependencies: 978 | component-emitter "1.2.1" 979 | debug "~4.1.0" 980 | isarray "2.0.1" 981 | 982 | socket.io@^2.3.0: 983 | version "2.3.0" 984 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb" 985 | dependencies: 986 | debug "~4.1.0" 987 | engine.io "~3.4.0" 988 | has-binary2 "~1.0.2" 989 | socket.io-adapter "~1.1.0" 990 | socket.io-client "2.3.0" 991 | socket.io-parser "~3.4.0" 992 | 993 | sshpk@^1.7.0: 994 | version "1.14.1" 995 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb" 996 | dependencies: 997 | asn1 "~0.2.3" 998 | assert-plus "^1.0.0" 999 | dashdash "^1.12.0" 1000 | getpass "^0.1.1" 1001 | optionalDependencies: 1002 | bcrypt-pbkdf "^1.0.0" 1003 | ecc-jsbn "~0.1.1" 1004 | jsbn "~0.1.0" 1005 | tweetnacl "~0.14.0" 1006 | 1007 | standard-as-callback@^2.0.1: 1008 | version "2.0.1" 1009 | resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126" 1010 | 1011 | "statuses@>= 1.5.0 < 2", statuses@~1.5.0: 1012 | version "1.5.0" 1013 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 1014 | 1015 | string-width@^4.1.0, string-width@^4.2.0: 1016 | version "4.2.0" 1017 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" 1018 | dependencies: 1019 | emoji-regex "^8.0.0" 1020 | is-fullwidth-code-point "^3.0.0" 1021 | strip-ansi "^6.0.0" 1022 | 1023 | strip-ansi@^6.0.0: 1024 | version "6.0.0" 1025 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" 1026 | dependencies: 1027 | ansi-regex "^5.0.0" 1028 | 1029 | supports-color@^7.1.0: 1030 | version "7.1.0" 1031 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" 1032 | dependencies: 1033 | has-flag "^4.0.0" 1034 | 1035 | through@^2.3.6: 1036 | version "2.3.8" 1037 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 1038 | 1039 | tmp@^0.0.33: 1040 | version "0.0.33" 1041 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" 1042 | dependencies: 1043 | os-tmpdir "~1.0.2" 1044 | 1045 | to-array@0.1.4: 1046 | version "0.1.4" 1047 | resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" 1048 | 1049 | toidentifier@1.0.0: 1050 | version "1.0.0" 1051 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 1052 | 1053 | tough-cookie@~2.5.0: 1054 | version "2.5.0" 1055 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" 1056 | dependencies: 1057 | psl "^1.1.28" 1058 | punycode "^2.1.1" 1059 | 1060 | tslib@^1.9.0: 1061 | version "1.13.0" 1062 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" 1063 | 1064 | tunnel-agent@^0.6.0: 1065 | version "0.6.0" 1066 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 1067 | dependencies: 1068 | safe-buffer "^5.0.1" 1069 | 1070 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 1071 | version "0.14.5" 1072 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 1073 | 1074 | type-fest@^0.11.0: 1075 | version "0.11.0" 1076 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" 1077 | 1078 | type-is@~1.6.17, type-is@~1.6.18: 1079 | version "1.6.18" 1080 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 1081 | dependencies: 1082 | media-typer "0.3.0" 1083 | mime-types "~2.1.24" 1084 | 1085 | typescript@^3.8.3: 1086 | version "3.9.6" 1087 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" 1088 | 1089 | unpipe@1.0.0, unpipe@~1.0.0: 1090 | version "1.0.0" 1091 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1092 | 1093 | uri-js@^4.2.2: 1094 | version "4.2.2" 1095 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" 1096 | dependencies: 1097 | punycode "^2.1.0" 1098 | 1099 | utils-merge@1.0.1: 1100 | version "1.0.1" 1101 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1102 | 1103 | uuid@^3.3.2: 1104 | version "3.3.3" 1105 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" 1106 | 1107 | vary@~1.1.2: 1108 | version "1.1.2" 1109 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1110 | 1111 | verror@1.10.0: 1112 | version "1.10.0" 1113 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 1114 | dependencies: 1115 | assert-plus "^1.0.0" 1116 | core-util-is "1.0.2" 1117 | extsprintf "^1.2.0" 1118 | 1119 | which-module@^2.0.0: 1120 | version "2.0.0" 1121 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 1122 | 1123 | wrap-ansi@^6.2.0: 1124 | version "6.2.0" 1125 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" 1126 | dependencies: 1127 | ansi-styles "^4.0.0" 1128 | string-width "^4.1.0" 1129 | strip-ansi "^6.0.0" 1130 | 1131 | ws@^7.1.2: 1132 | version "7.2.0" 1133 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.0.tgz#422eda8c02a4b5dba7744ba66eebbd84bcef0ec7" 1134 | dependencies: 1135 | async-limiter "^1.0.0" 1136 | 1137 | ws@~6.1.0: 1138 | version "6.1.4" 1139 | resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" 1140 | dependencies: 1141 | async-limiter "~1.0.0" 1142 | 1143 | xmlhttprequest-ssl@~1.5.4: 1144 | version "1.5.5" 1145 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" 1146 | 1147 | y18n@^4.0.0: 1148 | version "4.0.0" 1149 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" 1150 | 1151 | yargs-parser@^18.1.2: 1152 | version "18.1.3" 1153 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" 1154 | dependencies: 1155 | camelcase "^5.0.0" 1156 | decamelize "^1.2.0" 1157 | 1158 | yargs@^15.3.1: 1159 | version "15.4.1" 1160 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" 1161 | dependencies: 1162 | cliui "^6.0.0" 1163 | decamelize "^1.2.0" 1164 | find-up "^4.1.0" 1165 | get-caller-file "^2.0.1" 1166 | require-directory "^2.1.1" 1167 | require-main-filename "^2.0.0" 1168 | set-blocking "^2.0.0" 1169 | string-width "^4.2.0" 1170 | which-module "^2.0.0" 1171 | y18n "^4.0.0" 1172 | yargs-parser "^18.1.2" 1173 | 1174 | yeast@0.1.2: 1175 | version "0.1.2" 1176 | resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" 1177 | --------------------------------------------------------------------------------